feat(taker): Improve maker discovery (#692)

* mvp

* only redial if DisconnectedAndNotDialing

* progress

* progress

* progress

* some progress

* progress

* extract common logic into behaviour_util::ConnectionTracker

* extract common logic into BackoffTracker helper struct

* add comment in quote::background behaviour

* BackoffTracker rename get_backoff to get(...)

* cleanup, fix some things that got lost during rebase

* properly propagate quote::background ToSwarm events

* actually persist event loop, add quotes and rendezvous::discovery to cli behaviour

* some progress, cleanup, comments

* progress

* redial all peers that we dont know dont support quote, use quotes_cached behaviour in example, add remove_peer(...) to redial behaviour, don't redial discovered rendezvous peers

* remove old todo

* quotes_cached.rs: cache last connected endpoint

* rename: add_peer_address -> queue_peer_address

* extract p2p defaults into swap-p2p/defaults.rs

* split rendezvous.rs into two sub-modules

* remove unused bob::BackgroundQuoteReceived

* replace usage of list_sellers with event loop

* prune: remove list_sellers command

* use backoff helper in swap-p2p/src/protocols/quotes.rs

* refactor rendezvous::register behaviour, getting some unit tests working again

* add all peer addresses to the swarm on init

* less agressive redials

* extract magic backoff numbers

* proof of concept: drill tracing span into event loop through channels

* add BackoffTracker::increment, re-schedule register when we lose connection to rendezvous point

* fetch identify version and propagate into UI

* forbid private/local/loopback ip addresses to be shared/accepted through Identify

* remove legacy list_sellers code

* ensure uuids are unique for alice during swap_setup

* formatting and nitpicks

* fix: allow multiple swap setup requests over the same connection handler

* small cleanups

* fix: protocols/quotes.rs unit tests

* revert: listen on 0.0.0.0 for asb p2p

* propagate handle_pending_inbound_connection and handle_pending_outbound_connection to identify patch source

* replace loop with repeated return Poll::Ready in discovery.rs

* format

* MultiAddrVecExt trait, emit rendezvous addresses to rendezvous-node swarm

* fix: strictly disallow concurrent swap setup requests for the same swap on the same connection

* fix tests etc

* remove slop from futures_util.rs

* address some comments

* behaviour_util.rs: track inflight dials, add tests, return Some(peer_id) if internal state was changed

* replace boring-avatars with jidenticon

* feat: add peer discovery status dialog

* remove buy-xmr cli command, remove "sellers" arg for BuyXmrArgs, add changelog

* disable body overscroll

* add changelog for jidenticon

* increase quote fetch interval to 45s

* fix rendezvous::register_and_discover_together test
This commit is contained in:
Mohan 2025-12-01 18:03:13 +01:00 committed by GitHub
parent a54b7618d5
commit 801ce8fd9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
86 changed files with 4470 additions and 3800 deletions

3
.gitignore vendored
View file

@ -21,3 +21,6 @@ swap-orchestrator/config.toml
release-build.sh
cn_macos
libp2p-rendezvous-node/rendezvous-data
rust-electrum-client/
rust-libp2p/
bdk/

View file

@ -0,0 +1,18 @@
{
"db_name": "SQLite",
"query": "\n SELECT 1 as found\n FROM swap_states\n WHERE swap_id = ?\n LIMIT 1\n ",
"describe": {
"columns": [
{
"name": "found",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [false]
},
"hash": "60462ce4f45f174eb4603a2d94e67cf98eb7d6176515e6a28c4e8ce9fda6ef15"
}

View file

@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- GUI: Display detailed information about which peers we are connected to.
- GUI: Change the unique avatars of makers to be distinctly different from each other to allow for recognizability.
- GUI + SWAP + ASB: Massivly improved the reliability of all parts of the P2P networking stack.
- ASB: Fix bugs where we would not properly reconnect to rendezvous servers which could negatively impact peer discovery
- CLI: Removed the `list-sellers` and `buy-xmr` CLI commands. They were cumbersome to use and confusing. They will be re-added once the CLI is more flexible to allow for interactivity.
## [3.3.4] - 2025-11-14
## [3.3.8] - 2025-11-30
- ASB + CONTROLLER: Add the `peer-id` command to the controller shell which can be used to obtain the Peer ID of your ASB instance.

1
CLAUDE.md Normal file
View file

@ -0,0 +1 @@
- When asked about libp2p, check if a rust-libp2p folder exists which contains the cloned rust libp2p codebase. Read through to figure out what the best response it. If its a question about best practice when implementing protocols read @rust-libp2p/protocols/ specificially.

7
Cargo.lock generated
View file

@ -11341,6 +11341,7 @@ name = "swap-p2p"
version = "0.1.0"
dependencies = [
"anyhow",
"arti-client",
"async-trait",
"asynchronous-codec 0.7.0",
"backoff",
@ -11349,8 +11350,10 @@ dependencies = [
"bmrng",
"futures",
"libp2p",
"libp2p-tor",
"monero",
"rand 0.8.5",
"semver",
"serde",
"serde_cbor",
"swap-core",
@ -11360,7 +11363,9 @@ dependencies = [
"swap-serde",
"thiserror 1.0.69",
"tokio",
"tor-rtcompat",
"tracing",
"tracing-subscriber",
"typeshare",
"unsigned-varint 0.8.0",
"uuid",
@ -13982,11 +13987,13 @@ version = "3.3.8"
dependencies = [
"anyhow",
"dfx-swiss-sdk",
"libp2p",
"monero-rpc-pool",
"rustls 0.23.35",
"serde",
"serde_json",
"swap",
"swap-p2p",
"tauri",
"tauri-build",
"tauri-plugin-cli",

View file

@ -49,6 +49,7 @@ sigma_fun = { version = "0.7", default-features = false }
async-trait = "0.1"
# Serialization
semver = { version = "1.0" }
serde_cbor = "0.11"
strum = { version = "0.26" }

View file

@ -30,7 +30,7 @@ test-ffi-address:
# Start the Tauri app
tauri:
cd src-tauri && cargo tauri dev --no-watch -- --verbose -- --testnet
cd src-tauri && cargo tauri dev --no-watch -- -vv -- --testnet
tauri-mainnet:
cd src-tauri && cargo tauri dev --no-watch -- -vv

View file

@ -1,6 +1,6 @@
use anyhow::{Context, Result};
use anyhow::Result;
use libp2p::swarm::NetworkBehaviour;
use libp2p::{identity, Multiaddr, PeerId};
use libp2p::{identity, PeerId};
use swap_p2p::protocols::rendezvous::{register, XmrBtcNamespace};
/// Acts as both a rendezvous server and registers at other rendezvous points
@ -13,50 +13,15 @@ pub struct Behaviour {
impl Behaviour {
pub fn new(
identity: identity::Keypair,
rendezvous_addrs: Vec<Multiaddr>,
rendezvous_nodes: Vec<PeerId>,
namespace: XmrBtcNamespace,
registration_ttl: Option<u64>,
) -> Result<Self> {
let server = libp2p::rendezvous::server::Behaviour::new(
libp2p::rendezvous::server::Config::default(),
);
let rendezvous_nodes =
build_rendezvous_nodes(rendezvous_addrs, namespace, registration_ttl)?;
let register = register::Behaviour::new(identity, rendezvous_nodes);
let register = register::Behaviour::new(identity, rendezvous_nodes, namespace.into());
Ok(Self { server, register })
}
}
/// Builds a list of RendezvousNode from multiaddrs and namespace
fn build_rendezvous_nodes(
addrs: Vec<Multiaddr>,
namespace: XmrBtcNamespace,
registration_ttl: Option<u64>,
) -> Result<Vec<register::RendezvousNode>> {
addrs
.into_iter()
.map(|addr| {
let peer_id = extract_peer_id(&addr)?;
Ok(register::RendezvousNode::new(
&addr,
peer_id,
namespace,
registration_ttl,
))
})
.collect()
}
fn extract_peer_id(addr: &Multiaddr) -> Result<PeerId> {
addr.iter()
.find_map(|protocol| {
if let libp2p::multiaddr::Protocol::P2p(peer_id) = protocol {
Some(peer_id)
} else {
None
}
})
.context("No peer_id found in multiaddr")
}

View file

@ -107,24 +107,19 @@ async fn main() -> Result<()> {
tracing::info!(peer=%enquirer, "Discovery served");
}
SwarmEvent::Behaviour(behaviour::BehaviourEvent::Register(
register::InnerBehaviourEvent::Rendezvous(rendezvous::client::Event::Registered {
rendezvous_node,
ttl,
namespace,
}),
register::Event::Registered { peer_id },
)) => {
tracing::info!(%rendezvous_node, %namespace, ttl, "Registered at rendezvous point");
tracing::info!(%peer_id, "Registered at rendezvous point");
}
SwarmEvent::Behaviour(behaviour::BehaviourEvent::Register(
register::InnerBehaviourEvent::Rendezvous(
rendezvous::client::Event::RegisterFailed {
rendezvous_node,
namespace,
error,
},
),
register::Event::RegisterRequestFailed { peer_id, error },
)) => {
tracing::warn!(%rendezvous_node, %namespace, ?error, "Failed to register at rendezvous point");
tracing::warn!(%peer_id, ?error, "Failed to register at rendezvous point");
}
SwarmEvent::Behaviour(behaviour::BehaviourEvent::Register(
register::Event::RegisterDispatchFailed { peer_id, error },
)) => {
tracing::warn!(%peer_id, ?error, "Failed to dispatch register request at rendezvous point");
}
SwarmEvent::NewListenAddr { address, .. } => {
tracing::info!(%address, "New listening address reported");

View file

@ -10,6 +10,7 @@ use libp2p::{dns, noise, Multiaddr, PeerId, Swarm, Transport};
use libp2p_tor::{AddressConversion, TorTransport};
use std::fmt;
use std::path::Path;
use swap_p2p::libp2p_ext::MultiAddrVecExt;
use tor_hsservice::config::OnionServiceConfigBuilder;
use crate::behaviour::Behaviour;
@ -26,48 +27,23 @@ mod defaults {
pub const HIDDEN_SERVICE_NUM_INTRO_POINTS: u8 = 5;
pub const MULTIPLEX_TIMEOUT: Duration = Duration::from_secs(60);
pub const REGISTRATION_TTL: Option<u64> = None;
}
pub fn create_swarm(
identity: identity::Keypair,
rendezvous_addrs: Vec<Multiaddr>,
rendezvous_nodes: Vec<Multiaddr>,
) -> Result<Swarm<Behaviour>> {
let rendezvous_nodes = rendezvous_nodes.extract_peer_addresses();
let rendezvous_nodes_peer_ids = rendezvous_nodes
.iter()
.map(|(peer_id, _)| *peer_id)
.collect();
let transport = create_transport(&identity).context("Failed to create transport")?;
let behaviour = Behaviour::new(
identity.clone(),
rendezvous_addrs,
rendezvous_nodes_peer_ids,
swap_p2p::protocols::rendezvous::XmrBtcNamespace::RendezvousPoint,
defaults::REGISTRATION_TTL,
)?;
let swarm = SwarmBuilder::with_existing_identity(identity)
.with_tokio()
.with_other_transport(|_| transport)?
.with_behaviour(|_| behaviour)?
.with_swarm_config(|cfg| {
cfg.with_idle_connection_timeout(defaults::IDLE_CONNECTION_TIMEOUT)
})
.build();
Ok(swarm)
}
pub async fn create_swarm_with_onion(
identity: identity::Keypair,
onion_port: u16,
data_dir: &Path,
rendezvous_addrs: Vec<Multiaddr>,
) -> Result<Swarm<Behaviour>> {
let (transport, onion_address) = create_transport_with_onion(&identity, onion_port, data_dir)
.await
.context("Failed to create transport with onion")?;
let behaviour = Behaviour::new(
identity.clone(),
rendezvous_addrs,
swap_p2p::protocols::rendezvous::XmrBtcNamespace::RendezvousPoint,
defaults::REGISTRATION_TTL,
)?;
let mut swarm = SwarmBuilder::with_existing_identity(identity)
@ -79,6 +55,54 @@ pub async fn create_swarm_with_onion(
})
.build();
// Add all the addresses of the rendezvous nodes to the Swarm
for (peer_id, addresses) in rendezvous_nodes {
for address in addresses {
swarm.add_peer_address(peer_id.clone(), address.clone());
}
}
Ok(swarm)
}
pub async fn create_swarm_with_onion(
identity: identity::Keypair,
onion_port: u16,
data_dir: &Path,
rendezvous_nodes: Vec<Multiaddr>,
) -> Result<Swarm<Behaviour>> {
let rendezvous_nodes = rendezvous_nodes.extract_peer_addresses();
let rendezvous_nodes_peer_ids = rendezvous_nodes
.iter()
.map(|(peer_id, _)| *peer_id)
.collect();
let (transport, onion_address) = create_transport_with_onion(&identity, onion_port, data_dir)
.await
.context("Failed to create transport with onion")?;
let behaviour = Behaviour::new(
identity.clone(),
rendezvous_nodes_peer_ids,
swap_p2p::protocols::rendezvous::XmrBtcNamespace::RendezvousPoint,
)?;
let mut swarm = SwarmBuilder::with_existing_identity(identity)
.with_tokio()
.with_other_transport(|_| transport)?
.with_behaviour(|_| behaviour)?
.with_swarm_config(|cfg| {
cfg.with_idle_connection_timeout(defaults::IDLE_CONNECTION_TIMEOUT)
})
.build();
// Add all the addresses of the rendezvous nodes to the Swarm
for (peer_id, addresses) in rendezvous_nodes {
for address in addresses {
swarm.add_peer_address(peer_id, address.clone());
}
}
// Listen on the onion address
swarm
.listen_on(onion_address.clone())

View file

@ -27,6 +27,7 @@
height: 100%;
margin: 0;
overflow: auto;
overscroll-behavior: none;
}
</style>
</body>

View file

@ -34,9 +34,9 @@
"@tauri-apps/plugin-shell": "^2.3.0",
"@tauri-apps/plugin-store": "^2.4.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"boring-avatars": "^1.11.2",
"dayjs": "^1.11.13",
"humanize-duration": "^3.32.1",
"jdenticon": "^3.3.0",
"lodash": "^4.17.21",
"multiaddr": "^10.0.1",
"notistack": "^3.0.1",

View file

@ -23,10 +23,6 @@ import {
} from "store/features/ratesSlice";
import { FiatCurrency } from "store/features/settingsSlice";
import { setAlerts } from "store/features/alertsSlice";
import {
registryConnectionFailed,
setRegistryMakers,
} from "store/features/makersSlice";
import logger from "utils/logger";
import { setConversation } from "store/features/conversationsSlice";
@ -185,19 +181,6 @@ export async function updateRates(): Promise<void> {
}
}
/**
* Update public registry
*/
export async function updatePublicRegistry(): Promise<void> {
try {
const providers = await fetchMakersViaHttp();
store.dispatch(setRegistryMakers(providers));
} catch (error) {
store.dispatch(registryConnectionFailed());
logger.error(error, "Error fetching providers");
}
}
/**
* Fetch all alerts
*/

View file

@ -11,19 +11,17 @@ import { setBitcoinBalance } from "store/features/bitcoinWalletSlice";
import { receivedCliLog } from "store/features/logsSlice";
import { poolStatusReceived } from "store/features/poolSlice";
import { swapProgressEventReceived } from "store/features/swapSlice";
import logger from "utils/logger";
import {
fetchAllConversations,
updateAlerts,
updatePublicRegistry,
updateRates,
} from "./api";
quotesProgressReceived,
connectionChangeReceived,
} from "store/features/p2pSlice";
import logger from "utils/logger";
import { fetchAllConversations, updateAlerts, updateRates } from "./api";
import {
checkContextStatus,
getSwapInfo,
getSwapTimelock,
initializeContext,
listSellersAtRendezvousPoint,
refreshApprovals,
updateAllNodeStatuses,
} from "./rpc";
@ -80,15 +78,9 @@ export async function setupBackgroundTasks(): Promise<void> {
);
// Setup periodic fetch tasks
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
setIntervalImmediate(
() =>
listSellersAtRendezvousPoint(store.getState().settings.rendezvousPoints),
DISCOVER_PEERS_INTERVAL,
);
setIntervalImmediate(refreshApprovals, FETCH_PENDING_APPROVALS_INTERVAL);
// Fetch all alerts
@ -195,6 +187,15 @@ listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
}
break;
case "P2P":
if (eventData.type === "ConnectionChange") {
store.dispatch(connectionChangeReceived(eventData.content));
}
if (eventData.type === "QuotesProgress") {
store.dispatch(quotesProgressReceived(eventData.content.peers));
}
break;
default:
exhaustiveGuard(channelName);
}

View file

@ -185,9 +185,9 @@ export default function SeedSelectionDialog() {
const isDisabled =
selectedOption === "FromSeed"
? customSeed.trim().length === 0 ||
!isSeedValid ||
isBlockheightInvalid ||
!isPasswordValid
!isSeedValid ||
isBlockheightInvalid ||
!isPasswordValid
: selectedOption === "FromWalletPath"
? !walletPath
: selectedOption === "RandomSeed"

View file

@ -0,0 +1,42 @@
import { Box, Tooltip } from "@mui/material";
import { ReactNode, useState } from "react";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
type Props = {
content: string;
children: ReactNode;
showTooltip?: boolean;
};
export default function ClickToCopy({
content,
children,
showTooltip = true,
}: Props) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const wrapper = (
<Box
onClick={handleCopy}
sx={{ cursor: "pointer", display: "inline-block" }}
>
{children}
</Box>
);
if (!showTooltip) {
return wrapper;
}
return (
<Tooltip title={copied ? "Copied!" : "Click to copy"} arrow>
{wrapper}
</Tooltip>
);
}

View file

@ -0,0 +1,39 @@
import { useEffect, useRef } from "react";
import { update } from "jdenticon";
interface JdenticonProps {
value: string;
size: number;
className?: string;
style?: React.CSSProperties;
}
/**
* React wrapper component for jdenticon
* Generates a unique identicon based on the provided value
*/
export default function Jdenticon({
value,
size,
className,
style,
}: JdenticonProps) {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (svgRef.current) {
update(svgRef.current, value);
}
}, [value]);
return (
<svg
ref={svgRef}
width={size}
height={size}
data-jdenticon-value={value}
className={className}
style={style}
/>
);
}

View file

@ -1,58 +0,0 @@
import { Box, Typography, styled } from "@mui/material";
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
import { useSettings } from "store/hooks";
import { Search } from "@mui/icons-material";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { listSellersAtRendezvousPoint } from "renderer/rpc";
import { useAppDispatch } from "store/hooks";
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
import { useSnackbar } from "notistack";
const StyledPromiseButton = styled(PromiseInvokeButton)(({ theme }) => ({
marginTop: theme.spacing(2),
}));
export default function DiscoveryBox() {
const rendezvousPoints = useSettings((s) => s.rendezvousPoints);
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const handleDiscovery = async () => {
const { sellers } = await listSellersAtRendezvousPoint(rendezvousPoints);
dispatch(discoveredMakersByRendezvous(sellers));
};
return (
<InfoBox
title={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
Discover Makers
</Box>
}
mainContent={
<Typography variant="subtitle2">
By connecting to rendezvous points run by volunteers, you can discover
makers and then connect and swap with them in a decentralized manner.
You have {rendezvousPoints.length} stored rendezvous{" "}
{rendezvousPoints.length === 1 ? "point" : "points"} which we will
connect to. We will also attempt to connect to peers which you have
previously connected to.
</Typography>
}
additionalContent={
<StyledPromiseButton
variant="contained"
color="primary"
onInvoke={handleDiscovery}
disabled={rendezvousPoints.length === 0}
startIcon={<Search />}
displayErrorSnackbar
>
Discover Makers
</StyledPromiseButton>
}
icon={null}
loading={false}
/>
);
}

View file

@ -2,7 +2,6 @@ import { Box } from "@mui/material";
import DonateInfoBox from "./DonateInfoBox";
import DaemonControlBox from "./DaemonControlBox";
import SettingsBox from "./SettingsBox";
import DiscoveryBox from "./DiscoveryBox";
import MoneroPoolHealthBox from "./MoneroPoolHealthBox";
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
@ -30,7 +29,6 @@ export default function SettingsPage() {
<SettingsBox />
<DaemonControlBox />
<MoneroPoolHealthBox />
<DiscoveryBox />
</Box>
);
}

View file

@ -109,7 +109,7 @@ export default function HistoryRowExpanded({
</TableCell>
</TableRow>
<TableRow>
<TableCell>Monero receive pool</TableCell>
<TableCell>Monero Redeem Addresses</TableCell>
<TableCell>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{swap.monero_receive_pool.map((pool, index) => (

View file

@ -1,122 +1,412 @@
import { Box, Typography, LinearProgress, Paper } from "@mui/material";
import { usePendingBackgroundProcesses } from "store/hooks";
import {
Box,
Typography,
LinearProgress,
Paper,
IconButton,
Dialog,
DialogTitle,
DialogContent,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TablePagination,
Chip,
CircularProgress,
Stack,
} from "@mui/material";
import { Info as InfoIcon, Close as CloseIcon } from "@mui/icons-material";
import { useEffect, useState, useMemo } from "react";
import { useAppSelector } from "store/hooks";
import { QuoteStatus, ConnectionStatus } from "models/tauriModel";
import { selectPeers } from "store/selectors";
import TorIcon from "renderer/components/icons/TorIcon";
import TruncatedText from "renderer/components/other/TruncatedText";
import ClickToCopy from "renderer/components/other/ClickToCopy";
import Jdenticon from "renderer/components/other/Jdenticon";
type Peer = ReturnType<typeof selectPeers>[number];
export default function MakerDiscoveryStatus() {
const backgroundProcesses = usePendingBackgroundProcesses();
// Find active ListSellers processes
const listSellersProcesses = backgroundProcesses.filter(
([, status]) =>
status.componentName === "ListSellers" &&
status.progress.type === "Pending",
const [dialogOpen, setDialogOpen] = useState(false);
const [everConnectedPeers, setEverConnectedPeers] = useState<Set<string>>(
new Set(),
);
const peers = useAppSelector(selectPeers);
const isActive = listSellersProcesses.length > 0;
// Track peers that have ever been connected
useEffect(() => {
const connectedPeerIds = peers
.filter((p) => p.connection === ConnectionStatus.Connected)
.map((p) => p.peer_id);
// Default values for inactive state
let progress = {
rendezvous_points_total: 0,
peers_discovered: 0,
rendezvous_points_connected: 0,
quotes_received: 0,
quotes_failed: 0,
};
let progressValue = 0;
if (isActive) {
// Use the first ListSellers process for display
const [, status] = listSellersProcesses[0];
// Type guard to ensure we have ListSellers progress
if (
status.componentName === "ListSellers" &&
status.progress.type === "Pending"
) {
progress = status.progress.content;
const totalExpected =
progress.rendezvous_points_total + progress.peers_discovered;
const totalCompleted =
progress.rendezvous_points_connected +
progress.quotes_received +
progress.quotes_failed;
progressValue =
totalExpected > 0 ? (totalCompleted / totalExpected) * 100 : 0;
if (connectedPeerIds.length > 0) {
setEverConnectedPeers((prev) => {
const updated = new Set(prev);
connectedPeerIds.forEach((id) => updated.add(id));
return updated;
});
}
}
}, [peers]);
const totalPeers = peers.length;
const quotesReceived = peers.filter(
(p) => p.quote === QuoteStatus.Received,
).length;
const quotesFailed = peers.filter(
(p) =>
p.quote === QuoteStatus.Failed || p.quote === QuoteStatus.NotSupported,
).length;
const quotesInflight = peers.filter(
(p) => p.quote === QuoteStatus.Inflight,
).length;
const isActive = quotesInflight > 0 || totalPeers === 0;
const totalCompleted = quotesReceived + quotesFailed;
const progressValue =
totalPeers > 0 ? (totalCompleted / totalPeers) * 100 : 0;
return (
<Paper
variant="outlined"
sx={{
width: "100%",
mb: 2,
p: 2,
border: "1px solid",
borderColor: isActive ? "success.main" : "divider",
borderRadius: 1,
opacity: isActive ? 1 : 0.6,
}}
>
<Box
<>
<Paper
variant="outlined"
onClick={() => setDialogOpen(true)}
sx={{
display: "flex",
flexDirection: "column",
gap: 1.5,
width: "100%",
mb: 2,
p: 2,
borderColor: isActive ? "success.main" : "divider",
opacity: isActive ? 1 : 0.6,
cursor: "pointer",
transition: "background-color 0.2s",
"&:hover": {
bgcolor: "action.hover",
},
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<Typography
variant="body2"
sx={{
fontWeight: "medium",
color: isActive ? "info.main" : "text.disabled",
}}
<Stack gap={1.5}>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
>
{isActive
? "Getting offers..."
: "Waiting a few seconds before refreshing offers"}
</Typography>
<Box sx={{ display: "flex", gap: 2 }}>
<Typography
variant="caption"
variant="body2"
sx={{
color: isActive ? "success.main" : "text.disabled",
fontWeight: "medium",
color: isActive ? "info.main" : "text.disabled",
}}
>
{progress.quotes_received} online
{isActive
? "Getting offers..."
: "Waiting a few seconds before refreshing offers"}
</Typography>
<Typography
variant="caption"
sx={{
color: isActive ? "error.main" : "text.disabled",
fontWeight: "medium",
}}
>
{progress.quotes_failed} offline
</Typography>
</Box>
</Box>
<LinearProgress
variant="determinate"
value={Math.min(progressValue, 100)}
sx={{
width: "100%",
height: 8,
borderRadius: 4,
opacity: isActive ? 1 : 0.4,
}}
/>
</Box>
</Paper>
<Stack direction="row" alignItems="center" gap={2}>
<Stack direction="row" gap={2}>
<Typography
variant="caption"
sx={{
color: isActive ? "success.main" : "text.disabled",
fontWeight: "medium",
}}
>
{quotesReceived} online
</Typography>
<Typography
variant="caption"
sx={{
color: isActive ? "error.main" : "text.disabled",
fontWeight: "medium",
}}
>
{quotesFailed} offline
</Typography>
</Stack>
<InfoIcon
fontSize="small"
sx={{ opacity: 0.7, color: "action.active" }}
/>
</Stack>
</Stack>
<LinearProgress
variant="determinate"
value={Math.min(progressValue, 100)}
sx={{
width: "100%",
height: 8,
borderRadius: 4,
opacity: isActive ? 1 : 0.4,
}}
/>
</Stack>
</Paper>
<PeerDetailsDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
peers={peers}
everConnectedPeers={everConnectedPeers}
/>
</>
);
}
function QuoteStatusChip({ status }: { status: QuoteStatus | null }) {
switch (status) {
case QuoteStatus.Received:
return <Chip label="Got quote" color="success" size="small" />;
case QuoteStatus.Inflight:
return (
<Chip
label="Requesting"
color="info"
size="small"
icon={<CircularProgress size={12} color="inherit" />}
/>
);
case QuoteStatus.Failed:
return <Chip label="Failed" color="error" size="small" />;
case QuoteStatus.NotSupported:
return <Chip label="No offers" color="warning" size="small" />;
case QuoteStatus.Nothing:
case null:
return <Chip label="--" size="small" />;
default:
return null;
}
}
function ConnectionStatusChip({ status }: { status: ConnectionStatus | null }) {
switch (status) {
case ConnectionStatus.Connected:
return <Chip label="Connected" color="success" size="small" />;
case ConnectionStatus.Disconnected:
case null:
return <Chip label="Disconnected" color="default" size="small" />;
case ConnectionStatus.Dialing:
return (
<Chip
label="Dialing"
color="info"
size="small"
icon={<CircularProgress size={12} color="inherit" />}
/>
);
default:
return null;
}
}
/**
* Sorts peers based on connection history, quote status (optional), and peer ID.
*/
function sortPeers(
peers: Peer[],
everConnectedPeers: Set<string>,
checkQuoteStatus: boolean,
): Peer[] {
return [...peers].sort((a, b) => {
// 1. Put peers that have never connected at the bottom
const aEverConnected = everConnectedPeers.has(a.peer_id);
const bEverConnected = everConnectedPeers.has(b.peer_id);
if (aEverConnected !== bEverConnected) {
return aEverConnected ? -1 : 1;
}
// 2. Put NotSupported quotes at the bottom
if (checkQuoteStatus) {
const aNotSupported = a.quote === QuoteStatus.NotSupported;
const bNotSupported = b.quote === QuoteStatus.NotSupported;
if (aNotSupported !== bNotSupported) {
return aNotSupported ? 1 : -1;
}
}
// 3. Sort alphabetically by peer_id
return a.peer_id.localeCompare(b.peer_id);
});
}
interface PeerTableProps {
peers: Peer[];
page: number;
rowsPerPage: number;
}
function PeerTable({ peers, page, rowsPerPage }: PeerTableProps) {
const paginatedPeers = useMemo(() => {
const startIndex = page * rowsPerPage;
return peers.slice(startIndex, startIndex + rowsPerPage);
}, [peers, page, rowsPerPage]);
const emptyRows = rowsPerPage - paginatedPeers.length;
return (
<TableContainer sx={{ maxHeight: "70vh" }}>
<Table
size="small"
stickyHeader
sx={{
tableLayout: "fixed",
"& .MuiTableRow-root": {
height: "3rem",
},
"& .MuiTableCell-root": {
verticalAlign: "middle",
},
}}
>
<TableHead>
<TableRow>
<TableCell sx={{ width: "25%" }}>Peer ID</TableCell>
<TableCell sx={{ width: "35%" }}>Address</TableCell>
<TableCell sx={{ width: "20%" }}>Connection</TableCell>
<TableCell sx={{ width: "20%" }}>Quote</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedPeers.map((entry) => (
<TableRow key={entry.peer_id}>
<TableCell sx={{ textAlign: "center" }}>
<ClickToCopy content={entry.peer_id}>
<Stack
direction="row"
alignItems="center"
justifyContent="center"
gap={1}
>
<Jdenticon value={entry.peer_id} size={24} />
<Typography
variant="body2"
sx={{ fontFamily: "monospace", fontSize: "0.75rem" }}
>
<TruncatedText limit={16} truncateMiddle>
{entry.peer_id}
</TruncatedText>
</Typography>
</Stack>
</ClickToCopy>
</TableCell>
<TableCell>
<ClickToCopy
content={entry.last_address ?? ""}
showTooltip={!!entry.last_address}
>
<Stack direction="row" alignItems="center" gap={0.5}>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: "0.7rem",
maxWidth: 200,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{entry.last_address ?? "--"}
</Typography>
{entry.last_address?.includes("/onion3/") && (
<TorIcon
sx={{ fontSize: "0.9rem", color: "text.secondary" }}
/>
)}
</Stack>
</ClickToCopy>
</TableCell>
<TableCell>
<ConnectionStatusChip status={entry.connection} />
</TableCell>
<TableCell>
<QuoteStatusChip status={entry.quote} />
</TableCell>
</TableRow>
))}
{emptyRows > 0 &&
Array.from({ length: emptyRows }).map((_, index) => (
<TableRow key={`empty-${index}`}>
<TableCell colSpan={4} />
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
interface PeerDetailsDialogProps {
open: boolean;
onClose: () => void;
peers: Peer[];
everConnectedPeers: Set<string>;
}
function PeerDetailsDialog({
open,
onClose,
peers,
everConnectedPeers,
}: PeerDetailsDialogProps) {
const [page, setPage] = useState(0);
const rowsPerPage = 10;
const sortedPeers = useMemo(() => {
return sortPeers(peers, everConnectedPeers, true);
}, [peers, everConnectedPeers]);
const handleChangePage = (_event: unknown, newPage: number) => {
setPage(newPage);
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
Peers
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</Stack>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
{peers.length === 0 ? (
<Box sx={{ p: 3 }}>
<Typography color="text.secondary">
No peers discovered yet.
</Typography>
</Box>
) : (
<Stack>
<PeerTable
peers={sortedPeers}
page={page}
rowsPerPage={rowsPerPage}
/>
<TablePagination
component="div"
count={sortedPeers.length}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[]}
sx={{
borderTop: 1,
borderColor: "divider",
}}
/>
</Stack>
)}
</DialogContent>
</Dialog>
);
}

View file

@ -1,5 +1,5 @@
import { Box, Button, Chip, Paper, Tooltip, Typography } from "@mui/material";
import Avatar from "boring-avatars";
import Jdenticon from "renderer/components/other/Jdenticon";
import { QuoteWithAddress } from "models/tauriModel";
import {
MoneroSatsExchangeRate,
@ -42,12 +42,7 @@ export default function MakerOfferItem({
gap: 2,
}}
>
<Avatar
size={40}
name={peer_id}
variant="marble"
colors={["#92A1C6", "#146A7C", "#F0AB3D", "#C271B4", "#C20D90"]}
/>
<Jdenticon value={peer_id} size={40} />
<Box
sx={{
display: "flex",

View file

@ -3,11 +3,9 @@ import {
BalanceArgs,
BalanceResponse,
BuyXmrArgs,
BuyXmrResponse,
GetLogsArgs,
GetLogsResponse,
GetSwapInfoResponse,
ListSellersArgs,
MoneroRecoveryArgs,
ResumeSwapArgs,
ResumeSwapResponse,
@ -56,7 +54,6 @@ import {
import {
rpcSetSwapInfo,
approvalRequestsReplaced,
contextInitializationFailed,
timelockChangeEventReceived,
} from "store/features/rpcSlice";
import { selectAllSwapIds } from "store/selectors";
@ -69,15 +66,11 @@ import {
setRestoreHeight,
} from "store/features/walletSlice";
import { store } from "./store/storeRenderer";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
import { MoneroRecoveryResponse } from "models/rpcModel";
import { ListSellersResponse } from "../models/tauriModel";
import logger from "utils/logger";
import { getNetwork, isTestnet } from "store/config";
import { DonateToDevelopmentTip } from "store/features/settingsSlice";
import { Blockchain, Network } from "store/types";
import { setStatus } from "store/features/nodesSlice";
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
import { CliLog } from "models/cliModel";
import { logsToRawString, parseLogsFromString } from "utils/parseUtils";
import { DEFAULT_RENDEZVOUS_POINTS } from "store/defaults";
@ -128,19 +121,6 @@ async function invokeNoArgs<RESPONSE>(command: string): Promise<RESPONSE> {
return invokeUnsafe(command) as Promise<RESPONSE>;
}
export async function fetchSellersAtPresetRendezvousPoints() {
await Promise.all(
DEFAULT_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => {
const response = await listSellersAtRendezvousPoint([rendezvousPoint]);
store.dispatch(discoveredMakersByRendezvous(response.sellers));
logger.info(
`Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`,
);
}),
);
}
export async function checkBitcoinBalance() {
// If we are already syncing, don't start a new sync
if (
@ -180,17 +160,6 @@ export async function buyXmr() {
const donationPercentage = state.settings.donateToDevelopment;
// Get all available makers from the Redux store
const allMakers = [
...(state.makers.registry.makers || []),
...state.makers.rendezvous.makers,
];
// Convert all makers to multiaddr format
const sellers = allMakers.map((maker) =>
providerToConcatenatedMultiAddr(maker),
);
const address_pool: LabeledMoneroAddress[] = [];
if (donationPercentage !== false && donationPercentage > 0) {
const donation_address = isTestnet()
@ -229,9 +198,7 @@ export async function buyXmr() {
});
}
await invoke<BuyXmrArgs, BuyXmrResponse>("buy_xmr", {
rendezvous_points: DEFAULT_RENDEZVOUS_POINTS,
sellers,
await invoke<BuyXmrArgs, void>("buy_xmr", {
monero_receive_pool: address_pool,
// We convert null to undefined because typescript
// expects undefined if the field is optional and does not accept null here
@ -253,6 +220,12 @@ export async function initializeContext() {
const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool;
const useMoneroTor = store.getState().settings.enableMoneroTor;
const rendezvousPoints = Array.from(
new Set([
...store.getState().settings.rendezvousPoints,
...DEFAULT_RENDEZVOUS_POINTS,
]),
);
const moneroNodeUrl =
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
@ -276,6 +249,7 @@ export async function initializeContext() {
monero_node_config: moneroNodeConfig,
use_tor: useTor,
enable_monero_tor: useMoneroTor,
rendezvous_points: rendezvousPoints,
};
logger.info({ tauriSettings }, "Initializing context with settings");
@ -444,14 +418,6 @@ export async function redactLogs(
return parseLogsFromString(response.text);
}
export async function listSellersAtRendezvousPoint(
rendezvousPointAddresses: string[],
): Promise<ListSellersResponse> {
return await invoke<ListSellersArgs, ListSellersResponse>("list_sellers", {
rendezvous_points: rendezvousPointAddresses,
});
}
export async function getWalletDescriptor() {
return await invokeNoArgs<ExportBitcoinWalletResponse>(
"get_wallet_descriptor",

View file

@ -1,5 +1,4 @@
import alertsSlice from "./features/alertsSlice";
import makersSlice from "./features/makersSlice";
import ratesSlice from "./features/ratesSlice";
import rpcSlice from "./features/rpcSlice";
import swapReducer from "./features/swapSlice";
@ -10,11 +9,12 @@ import poolSlice from "./features/poolSlice";
import walletSlice from "./features/walletSlice";
import bitcoinWalletSlice from "./features/bitcoinWalletSlice";
import logsSlice from "./features/logsSlice";
import p2pSlice from "./features/p2pSlice";
export const reducers = {
swap: swapReducer,
makers: makersSlice,
rpc: rpcSlice,
p2p: p2pSlice,
alerts: alertsSlice,
rates: ratesSlice,
settings: settingsSlice,

View file

@ -1,79 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
import { SellerStatus } from "models/tauriModel";
import { getStubTestnetMaker } from "store/config";
import { rendezvousSellerToMakerStatus } from "utils/conversionUtils";
import { isMakerOutdated } from "utils/multiAddrUtils";
const stubTestnetMaker = getStubTestnetMaker();
export interface MakersSlice {
rendezvous: {
makers: (ExtendedMakerStatus | MakerStatus)[];
};
registry: {
makers: ExtendedMakerStatus[] | null;
// This counts how many failed connections attempts we have counted since the last successful connection
connectionFailsCount: number;
};
selectedMaker: ExtendedMakerStatus | null;
}
const initialState: MakersSlice = {
rendezvous: {
makers: [],
},
registry: {
makers: stubTestnetMaker ? [stubTestnetMaker] : null,
connectionFailsCount: 0,
},
selectedMaker: null,
};
export const makersSlice = createSlice({
name: "providers",
initialState,
reducers: {
discoveredMakersByRendezvous(slice, action: PayloadAction<SellerStatus[]>) {
action.payload.forEach((discoveredSeller) => {
const discoveredMakerStatus =
rendezvousSellerToMakerStatus(discoveredSeller);
// If the seller has a status of "Unreachable" the provider is not added to the list
if (discoveredMakerStatus === null) {
return;
}
// If the provider was already discovered via the public registry, don't add it again
const indexOfExistingMaker = slice.rendezvous.makers.findIndex(
(prov) =>
prov.peerId === discoveredMakerStatus.peerId &&
prov.multiAddr === discoveredMakerStatus.multiAddr,
);
// Avoid duplicate entries, replace them instead
if (indexOfExistingMaker !== -1) {
slice.rendezvous.makers[indexOfExistingMaker] = discoveredMakerStatus;
} else {
slice.rendezvous.makers.push(discoveredMakerStatus);
}
});
},
setRegistryMakers(slice, action: PayloadAction<ExtendedMakerStatus[]>) {
if (stubTestnetMaker) {
action.payload.push(stubTestnetMaker);
}
},
registryConnectionFailed(slice) {
slice.registry.connectionFailsCount += 1;
},
},
});
export const {
discoveredMakersByRendezvous,
setRegistryMakers,
registryConnectionFailed,
} = makersSlice.actions;
export default makersSlice.reducer;

View file

@ -0,0 +1,54 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import {
PeerQuoteProgress,
ConnectionChange,
ConnectionStatus,
QuoteStatus,
} from "models/tauriModel";
import { exhaustiveGuard } from "utils/typescriptUtils";
interface P2PSlice {
connectionStatus: Record<string, ConnectionStatus>;
lastAddress: Record<string, string>;
quoteStatus: Record<string, QuoteStatus>;
}
const initialState: P2PSlice = {
connectionStatus: {},
lastAddress: {},
quoteStatus: {},
};
export const p2pSlice = createSlice({
name: "p2p",
initialState,
reducers: {
quotesProgressReceived(slice, action: PayloadAction<PeerQuoteProgress[]>) {
action.payload.forEach((entry) => {
slice.quoteStatus[entry.peer_id] = entry.quote_status;
});
},
connectionChangeReceived(
slice,
action: PayloadAction<{ peer_id: string; change: ConnectionChange }>,
) {
const { peer_id, change } = action.payload;
switch (change.type) {
case "Connection":
slice.connectionStatus[peer_id] = change.content;
break;
case "LastAddress":
slice.lastAddress[peer_id] = change.content;
break;
default:
exhaustiveGuard(change);
}
},
},
});
export const { quotesProgressReceived, connectionChangeReceived } =
p2pSlice.actions;
export default p2pSlice.reducer;

View file

@ -16,7 +16,6 @@ import logger from "utils/logger";
interface State {
withdrawTxId: string | null;
rendezvousDiscoveredSellers: (ExtendedMakerStatus | MakerStatus)[];
swapInfos: {
[swapId: string]: GetSwapInfoResponseExt;
};
@ -58,7 +57,6 @@ const initialState: RPCSlice = {
status: null,
state: {
withdrawTxId: null,
rendezvousDiscoveredSellers: [],
swapInfos: {},
swapTimelocks: {},
moneroRecovery: null,
@ -97,12 +95,6 @@ export const rpcSlice = createSlice({
rpcSetWithdrawTxId(slice, action: PayloadAction<string>) {
slice.state.withdrawTxId = action.payload;
},
rpcSetRendezvousDiscoveredMakers(
slice,
action: PayloadAction<(ExtendedMakerStatus | MakerStatus)[]>,
) {
slice.state.rendezvousDiscoveredSellers = action.payload;
},
rpcResetWithdrawTxId(slice) {
slice.state.withdrawTxId = null;
},
@ -175,7 +167,6 @@ export const {
contextInitializationFailed,
rpcSetWithdrawTxId,
rpcResetWithdrawTxId,
rpcSetRendezvousDiscoveredMakers,
rpcSetSwapInfo,
rpcSetMoneroRecoveryKeys,
rpcResetMoneroRecoveryKeys,

View file

@ -153,14 +153,6 @@ export function useActiveSwapLogs() {
}, [logs, swapId]);
}
export function useAllMakers() {
return useAppSelector((state) => {
const registryMakers = state.makers.registry.makers || [];
const listSellersMakers = state.makers.rendezvous.makers || [];
return [...registryMakers, ...listSellersMakers];
});
}
/// This hook returns the all swap infos, as an array
/// Excluding those who are in a state where it's better to hide them from the user
export function useSaneSwapInfos() {

View file

@ -6,7 +6,6 @@ import {
checkBitcoinBalance,
getBitcoinAddress,
updateAllNodeStatuses,
fetchSellersAtPresetRendezvousPoints,
getSwapInfo,
getSwapTimelock,
initializeMoneroWallet,
@ -139,17 +138,6 @@ export function createMainListeners() {
await getAllSwapInfos();
await getAllSwapTimelocks();
}
// If the database just became availiable, fetch sellers at preset rendezvous points
if (
status.database_available &&
!previousContextStatus?.database_available
) {
logger.info(
"Database just became available, fetching sellers at preset rendezvous points...",
);
await fetchSellersAtPresetRendezvousPoints();
}
},
});

View file

@ -1,9 +1,14 @@
import { createSelector } from "@reduxjs/toolkit";
import { RootState } from "renderer/store/storeRenderer";
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
import { ExpiredTimelocks } from "models/tauriModel";
import {
ConnectionStatus,
ExpiredTimelocks,
QuoteStatus,
} from "models/tauriModel";
const selectRpcState = (state: RootState) => state.rpc.state;
const selectP2pState = (state: RootState) => state.p2p;
export const selectAllSwapIds = createSelector([selectRpcState], (rpcState) =>
Object.keys(rpcState.swapInfos),
@ -47,3 +52,19 @@ export const selectPendingApprovals = createSelector(
(c) => c.request_status.state === "Pending",
),
);
// TODO: This should be split into multiple selectors/hooks to avoid excessive re-rendering
export const selectPeers = createSelector([selectP2pState], (p2p) => {
const peerIds = new Set([
...Object.keys(p2p.connectionStatus),
...Object.keys(p2p.lastAddress),
...Object.keys(p2p.quoteStatus),
]);
return Array.from(peerIds).map((peerId) => ({
peer_id: peerId,
connection: p2p.connectionStatus[peerId] ?? null,
last_address: p2p.lastAddress[peerId] ?? null,
quote: p2p.quoteStatus[peerId] ?? null,
}));
});

View file

@ -1,8 +1,3 @@
import { MakerStatus, ExtendedMakerStatus } from "models/apiModel";
import { SellerStatus } from "models/tauriModel";
import { isTestnet } from "store/config";
import { splitPeerIdFromMultiAddress } from "./parseUtils";
export function satsToBtc(sats: number): number {
return sats / 100000000;
}
@ -48,27 +43,6 @@ export function secondsToDays(seconds: number): number {
return seconds / 86400;
}
// Convert the "Seller" object returned by the list sellers tauri endpoint to a "MakerStatus" object
// which we use internally to represent the status of a provider. This provides consistency between
// the models returned by the public registry and the models used internally.
export function rendezvousSellerToMakerStatus(
seller: SellerStatus,
): ExtendedMakerStatus | null {
if (seller.type === "Unreachable") {
return null;
}
return {
maxSwapAmount: seller.content.quote.max_quantity,
minSwapAmount: seller.content.quote.min_quantity,
price: seller.content.quote.price,
peerId: seller.content.peer_id,
multiAddr: seller.content.multiaddr,
testnet: isTestnet(),
version: seller.content.version,
};
}
export function bytesToMb(bytes: number): number {
return bytes / (1024 * 1024);
}

View file

@ -8,12 +8,6 @@ import { isTestnet } from "store/config";
// const MIN_ASB_VERSION = "2.0.0-beta.1"; // First version with support for tx_early_refund
const MIN_ASB_VERSION = "3.2.0-rc.1";
export function providerToConcatenatedMultiAddr(provider: Maker) {
return new Multiaddr(provider.multiAddr)
.encapsulate(`/p2p/${provider.peerId}`)
.toString();
}
export function isMakerOnCorrectNetwork(
provider: ExtendedMakerStatus,
): boolean {

View file

@ -1142,6 +1142,13 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93"
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==
"@types/node@*":
version "24.10.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01"
integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==
dependencies:
undici-types "~7.16.0"
"@types/node@^22.15.29":
version "22.17.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.17.2.tgz#47a93d6f4b79327da63af727e7c54e8cab8c4d33"
@ -1568,11 +1575,6 @@ bl@^1.2.1:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
boring-avatars@^1.11.2:
version "1.11.2"
resolved "https://registry.yarnpkg.com/boring-avatars/-/boring-avatars-1.11.2.tgz#365e0b765fb0065ca0cb2fd20c200674d0a9ded6"
integrity sha512-3+wkwPeObwS4R37FGXMYViqc4iTrIRj5yzfX9Qy4mnpZ26sX41dGMhsAgmKks1r/uufY1pl4vpgzMWHYfJRb2A==
brace-expansion@^1.1.7:
version "1.1.12"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
@ -1659,6 +1661,13 @@ caniuse-lite@^1.0.30001735:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz#8292bb7591932ff09e9a765f12fdf5629a241ccc"
integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==
canvas-renderer@~2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/canvas-renderer/-/canvas-renderer-2.2.1.tgz#c1d131f78a9799aca8af9679ad0a005052b65550"
integrity sha512-RrBgVL5qCEDIXpJ6NrzyRNoTnXxYarqm/cS/W6ERhUJts5UQtt/XPEosGN3rqUkZ4fjBArlnCbsISJ+KCFnIAg==
dependencies:
"@types/node" "*"
chai@^5.1.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.2.tgz#e2c35570b8fa23b5b7129b4114d5dc03b3fd3401"
@ -2842,6 +2851,13 @@ iterator.prototype@^1.1.4:
has-symbols "^1.1.0"
set-function-name "^2.0.2"
jdenticon@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/jdenticon/-/jdenticon-3.3.0.tgz#64bae9f9b3cf5c2a210e183648117afe3a89b367"
integrity sha512-DhuBRNRIybGPeAjMjdHbkIfiwZCCmf8ggu7C49jhp6aJ7DYsZfudnvnTY5/1vgUhrGA7JaDAx1WevnpjCPvaGg==
dependencies:
canvas-renderer "~2.2.0"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -4084,6 +4100,11 @@ undici-types@~6.21.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
undici-types@~7.16.0:
version "7.16.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"

View file

@ -15,6 +15,7 @@ tauri-build = { version = "2.*", features = ["config-json5"] }
[dependencies]
monero-rpc-pool = { path = "../monero-rpc-pool" }
swap = { path = "../swap", features = [ "tauri" ] }
swap-p2p = { path = "../swap-p2p" }
dfx-swiss-sdk = { workspace = true }
anyhow = "1"
@ -40,6 +41,9 @@ tauri-plugin-updater = "2.*"
tokio = { workspace = true }
tokio-util = { workspace = true }
# LibP2P
libp2p = { workspace = true }
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-cli = "2.*"
tauri-plugin-single-instance = "2.*"

View file

@ -12,7 +12,7 @@ use swap::cli::{
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs,
GetMoneroHistoryArgs, GetMoneroMainAddressArgs, GetMoneroSeedArgs,
GetMoneroSyncProgressArgs, GetPendingApprovalsResponse, GetRestoreHeightArgs,
GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
GetSwapInfoArgs, GetSwapInfosAllArgs, MoneroRecoveryArgs, RedactArgs,
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
SendMoneroArgs, SetMoneroWalletPasswordArgs, SetRestoreHeightArgs,
SuspendCurrentSwapArgs, WithdrawBtcArgs,
@ -22,6 +22,7 @@ use swap::cli::{
},
command::Bitcoin,
};
use swap_p2p::libp2p_ext::MultiAddrVecExt;
use tauri_plugin_dialog::DialogExt;
use zip::{write::SimpleFileOptions, ZipWriter};
@ -46,7 +47,6 @@ macro_rules! generate_command_handlers {
get_history,
monero_recovery,
get_logs,
list_sellers,
suspend_current_swap,
cancel_and_refund,
initialize_context,
@ -168,6 +168,9 @@ pub async fn initialize_context(
// Get tauri handle from the state
let tauri_handle = state.handle.clone();
// Parse rendeuvous points
let rendezvous_points = settings.rendezvous_points.extract_peer_addresses();
// Now populate the context in the background
let context_result = ContextBuilder::new(testnet)
.with_bitcoin(Bitcoin {
@ -178,6 +181,7 @@ pub async fn initialize_context(
.with_json(false)
.with_tor(settings.use_tor)
.with_enable_monero_tor(settings.enable_monero_tor)
.with_rendezvous_points(rendezvous_points)
.with_tauri(tauri_handle.clone())
.build(state.context())
.await;
@ -435,7 +439,6 @@ tauri_command!(resume_swap, ResumeSwapArgs);
tauri_command!(withdraw_btc, WithdrawBtcArgs);
tauri_command!(monero_recovery, MoneroRecoveryArgs);
tauri_command!(get_logs, GetLogsArgs);
tauri_command!(list_sellers, ListSellersArgs);
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
tauri_command!(redact, RedactArgs);
tauri_command!(send_monero, SendMoneroArgs);

View file

@ -40,21 +40,21 @@ pub struct ActiveConnectionsResponse {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RendezvousConnectionStatus {
Disconnected,
Dialling,
Connected,
Disconnected,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RendezvousRegistrationStatus {
RegisterOnNextConnection,
Pending,
Registered,
WillRegisterAfterDelay,
RegisterOnceConnected,
RequestInflight,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegistrationStatusItem {
pub address: String,
pub address: Option<String>,
pub connection: RendezvousConnectionStatus,
pub registration: RendezvousRegistrationStatus,
}

View file

@ -95,9 +95,10 @@ async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> {
println!("No rendezvous points configured");
} else {
for item in response.registrations {
let address = item.address.as_deref().unwrap_or("?");
println!(
"Connection status to rendezvous point at \"{}\" is \"{:?}\". Registration status is \"{:?}\"",
item.address, item.connection, item.registration
address, item.connection, item.registration
);
}
}

View file

@ -46,7 +46,6 @@ pub fn default_rendezvous_points() -> Vec<Multiaddr> {
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw".parse().unwrap(),
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU".parse().unwrap(),
"/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT".parse().unwrap(),
"/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp".parse().unwrap(),
"/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa".parse().unwrap(),
"/dns4/aswap.click/tcp/8888/p2p/12D3KooWQzW52mdsLHTMu1EPiz3APumG6vGwpCuyy494MAQoEa5X".parse().unwrap(),
"/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk".parse().unwrap()
@ -141,7 +140,7 @@ impl GetDefaults for Testnet {
.join("testnet")
.join("config.toml"),
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/9839")?,
electrum_rpc_urls: default_electrum_servers_testnet(),
price_ticker_ws_url: Url::parse(KRAKEN_PRICE_TICKER_WS_URL)?,
bitcoin_confirmation_target: 1,

View file

@ -166,4 +166,5 @@ pub trait Database {
&self,
swap_id: Uuid,
) -> Result<Option<monero::TransferProof>>;
async fn has_swap(&self, swap_id: Uuid) -> Result<bool>;
}

View file

@ -14,7 +14,7 @@ swap-serde = { path = "../swap-serde" }
# Networking
async-trait = { workspace = true }
libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "identify", "ping"] }
libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "ping", "identify"] }
# Serialization
asynchronous-codec = "0.7.0"
@ -31,6 +31,7 @@ rand = { workspace = true }
# Utils
anyhow = { workspace = true }
backoff = { workspace = true }
semver = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
void = "1"
@ -43,8 +44,21 @@ tokio = { workspace = true }
# Logging
tracing = { workspace = true }
[[example]]
name = "fetch_quotes"
required-features = ["fetch-quotes-example"]
[features]
fetch-quotes-example = []
test-support = ["libp2p/noise", "libp2p/tcp", "libp2p/yamux", "libp2p/tokio", "libp2p/dns"]
[dev-dependencies]
libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "ping", "identify", "noise", "tcp", "yamux", "tokio"] }
arti-client = { workspace = true }
async-trait = { workspace = true }
libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "ping", "identify", "noise", "tcp", "yamux", "tokio", "dns"] }
libp2p-tor = { path = "../libp2p-tor" }
tor-rtcompat = { workspace = true }
tracing-subscriber = { workspace = true }
[lints]
workspace = true

View file

@ -0,0 +1,157 @@
use anyhow::Result;
use arti_client::{config::TorClientConfigBuilder, TorClient};
use futures::StreamExt;
use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::Boxed;
use libp2p::core::upgrade::Version;
use libp2p::multiaddr::Protocol;
use libp2p::swarm::dial_opts::DialOpts;
use libp2p::swarm::NetworkBehaviour;
use libp2p::{dns, tcp};
use libp2p::{identify, noise, ping, request_response};
use libp2p::{identity, yamux, Multiaddr, PeerId, SwarmBuilder, Transport};
use libp2p_tor::{AddressConversion, TorTransport};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::time::Duration;
use swap_p2p::libp2p_ext::MultiAddrExt;
use swap_p2p::protocols::quote::BidQuote;
use swap_p2p::protocols::{quote, quotes_cached, rendezvous};
use tor_rtcompat::tokio::TokioRustlsRuntime;
const USE_TOR: bool = true;
#[derive(NetworkBehaviour)]
struct Behaviour {
rendezvous: rendezvous::discovery::Behaviour,
ping: ping::Behaviour,
quote: quotes_cached::Behaviour,
}
fn create_transport(
identity: &identity::Keypair,
tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
) -> Result<Boxed<(PeerId, StreamMuxerBox)>> {
let auth_upgrade = noise::Config::new(identity)?;
let multiplex_upgrade = yamux::Config::default();
if let Some(tor_client) = tor_client {
let transport = TorTransport::from_client(tor_client, AddressConversion::IpAndDns)
.upgrade(Version::V1)
.authenticate(auth_upgrade)
.multiplex(multiplex_upgrade)
.timeout(Duration::from_secs(60))
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed();
Ok(transport)
} else {
// TCP with system DNS
let tcp = tcp::tokio::Transport::new(tcp::Config::default().nodelay(true));
let tcp_dns = dns::tokio::Transport::system(tcp)?;
let transport = tcp_dns
.upgrade(Version::V1)
.authenticate(auth_upgrade)
.multiplex(multiplex_upgrade)
.timeout(Duration::from_secs(60))
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed();
Ok(transport)
}
}
#[tokio::main]
async fn main() -> Result<()> {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::new(
"info,swap_p2p=trace,fetch_quotes=trace,libp2p_request_response=trace,libp2p_swarm=debug",
))
.try_init();
let identity = identity::Keypair::generate_ed25519();
let tor_client_opt = if USE_TOR {
let config = TorClientConfigBuilder::default().build()?;
let runtime = TokioRustlsRuntime::current()?;
let tor_client = TorClient::with_runtime(runtime)
.config(config)
.create_bootstrapped()
.await?;
Some(Arc::new(tor_client))
} else {
None
};
let rendezvous_nodes = swap_env::defaults::default_rendezvous_points();
let rendezvous_nodes_peer_ids = rendezvous_nodes
.iter()
.map(|addr| {
addr.extract_peer_id()
.expect("Rendezvous node address must contain peer ID")
})
.collect();
let namespace = rendezvous::XmrBtcNamespace::Mainnet;
let behaviour = Behaviour {
rendezvous: rendezvous::discovery::Behaviour::new(
identity.clone(),
rendezvous_nodes_peer_ids,
namespace.into(),
),
ping: ping::Behaviour::new(ping::Config::new().with_interval(Duration::from_secs(1))),
quote: quotes_cached::Behaviour::new(identify::Config::new(
"fetch_quotes/1.0.0".to_string(),
identity.public(),
)),
};
let transport = create_transport(&identity, tor_client_opt)?;
let mut swarm = SwarmBuilder::with_existing_identity(identity)
.with_tokio()
.with_other_transport(|_| transport)?
.with_behaviour(|_| behaviour)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(60)))
.build();
for rendezvous_node_addr in rendezvous_nodes {
swarm.add_peer_address(
rendezvous_node_addr
.extract_peer_id()
.expect("Rendezvous node address must contain peer ID"),
rendezvous_node_addr,
);
}
loop {
let event = swarm.select_next_some().await;
// println!("Event: {:?}", event);
match event {
libp2p::swarm::SwarmEvent::Behaviour(event) => match event {
BehaviourEvent::Rendezvous(event) => match event {
rendezvous::discovery::Event::DiscoveredPeer { peer_id } => {}
},
BehaviourEvent::Quote(quotes_cached::Event::CachedQuotes { quotes }) => {
println!("================");
println!("================");
println!("================");
println!("==== !!!! GOT CACHED QUOTES SNAPSHOT !!!! ====");
println!("All quotes:");
for (peer, addr, quote, agent_version) in quotes {
println!("- {peer} @ {addr}:");
if let Some(version) = agent_version {
println!(" - Agent Version: {version}");
}
println!(" - {:?}", quote);
println!("================");
}
// panic!("Got quote from peer, stopping");
}
_ => {}
},
libp2p::swarm::SwarmEvent::ConnectionEstablished { peer_id, .. } => {}
_ => {}
}
}
}

View file

@ -0,0 +1,379 @@
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use backoff::backoff::Backoff;
use backoff::ExponentialBackoff;
use libp2p::core::ConnectedPoint;
use libp2p::Multiaddr;
use libp2p::{
swarm::{ConnectionId, FromSwarm},
PeerId,
};
/// Used inside of a Behaviour to track connections to peers
// TODO: Track inflight dial attempts
pub struct ConnectionTracker {
connections: HashMap<PeerId, HashSet<ConnectionId>>,
inflight_dials: HashMap<PeerId, HashSet<ConnectionId>>,
}
impl ConnectionTracker {
pub fn new() -> Self {
Self {
connections: HashMap::new(),
inflight_dials: HashMap::new(),
}
}
pub fn is_connected(&self, peer_id: &PeerId) -> bool {
self.connections
.get(peer_id)
.map(|connections| !connections.is_empty())
.unwrap_or(false)
}
pub fn has_inflight_dial(&self, peer_id: &PeerId) -> bool {
self.inflight_dials
.get(peer_id)
.map(|dials| !dials.is_empty())
.unwrap_or(false)
}
pub fn peers(&self) -> impl Iterator<Item = &PeerId> {
self.connections.keys()
}
/// Any behaviour that uses the ConnectionTracker MUST call this method on every [`NetworkBehaviour::on_swarm_event`]
///
/// Returns the peer id if the calling of this method resulted in a change of the internal state of that peer
pub fn handle_swarm_event(&mut self, event: FromSwarm<'_>) -> Option<PeerId> {
match event {
FromSwarm::ConnectionEstablished(connection_established) => {
self.connections
.entry(connection_established.peer_id)
.or_insert_with(HashSet::new)
.insert(connection_established.connection_id);
// This dial attempts is no longer inflight because it has been established
if let Some(inflight_dials) =
self.inflight_dials.get_mut(&connection_established.peer_id)
{
if inflight_dials.remove(&connection_established.connection_id) {
return Some(connection_established.peer_id);
}
}
}
FromSwarm::ConnectionClosed(connection_closed) => {
self.connections
.entry(connection_closed.peer_id)
.and_modify(|connections| {
connections.remove(&connection_closed.connection_id);
});
return Some(connection_closed.peer_id);
}
FromSwarm::DialFailure(dial_failure) => {
// This dial attempts is no longer inflight because it has failed
if let Some(peer_id) = dial_failure.peer_id {
if let Some(inflight_dials) = self.inflight_dials.get_mut(&peer_id) {
if inflight_dials.remove(&dial_failure.connection_id) {
return Some(peer_id);
}
}
}
}
_ => {}
}
None
}
/// Any behaviour that uses the ConnectionTracker MUST call this method on every [`NetworkBehaviour::handle_pending_outbound_connection`]
pub fn handle_pending_outbound_connection(
&mut self,
connection_id: ConnectionId,
maybe_peer: Option<PeerId>,
) -> Option<PeerId> {
if let Some(peer_id) = maybe_peer {
if self
.inflight_dials
.entry(peer_id)
.or_insert_with(HashSet::new)
.insert(connection_id)
{
return Some(peer_id);
}
}
None
}
}
/// Used inside of a Behaviour to track exponential backoff states for each peer.
pub struct BackoffTracker {
backoffs: HashMap<PeerId, ExponentialBackoff>,
initial_interval: Duration,
max_interval: Duration,
multiplier: f64,
}
impl BackoffTracker {
pub fn new(initial: Duration, max: Duration, multiplier: f64) -> Self {
Self {
backoffs: HashMap::new(),
initial_interval: initial,
max_interval: max,
multiplier,
}
}
/// Get the backoff for a given peer.
pub fn get(&mut self, peer: &PeerId) -> &mut ExponentialBackoff {
self.backoffs
.entry(*peer)
.or_insert_with(|| ExponentialBackoff {
initial_interval: self.initial_interval,
current_interval: self.initial_interval,
max_interval: self.max_interval,
multiplier: self.multiplier,
// Never give up
max_elapsed_time: None,
..ExponentialBackoff::default()
})
}
/// Reset the backoff state the given peer.
pub fn reset(&mut self, peer: &PeerId) {
if let Some(b) = self.backoffs.get_mut(peer) {
b.reset();
}
}
/// Increments the backoff for the given peer and returns the new backoff
pub fn increment(&mut self, peer: &PeerId) -> Duration {
self.get(peer)
.next_backoff()
.expect("backoff should never run out")
}
}
/// Used inside of a Behaviour to track the last successful address for a peer
/// TODO: Track success/failure rates for each address
pub struct AddressTracker {
addresses: HashMap<PeerId, Multiaddr>,
}
impl AddressTracker {
pub fn new() -> Self {
Self {
addresses: HashMap::new(),
}
}
/// Any behaviour that uses the AddressTracker MUST call this method on every [`NetworkBehaviour::on_swarm_event`]
///
/// Returns the peer id if the calling of this method resulted in a change of the internal state of that peer
pub fn handle_swarm_event(&mut self, event: FromSwarm<'_>) -> Option<PeerId> {
match event {
// If we connected as a dialer, record the address we connected to them at
FromSwarm::ConnectionEstablished(connection_established) => {
if let ConnectedPoint::Dialer { address, .. } = connection_established.endpoint {
let old_address = self
.addresses
.insert(connection_established.peer_id, address.clone());
// Return the peer id if the address was changed
if old_address.as_ref() != Some(address) {
return Some(connection_established.peer_id);
}
}
}
FromSwarm::NewExternalAddrOfPeer(new_external_addr_of_peer) => {
// If we have never successfully connected to any address of the peer, we record the first announced address
if !self
.addresses
.contains_key(&new_external_addr_of_peer.peer_id)
{
self.addresses.insert(
new_external_addr_of_peer.peer_id,
new_external_addr_of_peer.addr.clone(),
);
// Always return the peer id because the entry was previously empty
return Some(new_external_addr_of_peer.peer_id);
}
}
_ => (),
}
None
}
pub fn peers(&self) -> impl Iterator<Item = &PeerId> {
self.addresses.keys()
}
pub fn last_seen_address(&self, peer_id: &PeerId) -> Option<Multiaddr> {
self.addresses.get(peer_id).cloned()
}
}
/// Extracts the semver version from a user agent string.
/// Example input: "asb/2.0.0 (xmr-btc-swap-mainnet)"
/// Returns None if the version cannot be parsed.
pub fn extract_semver_from_agent_str(agent_str: &str) -> Option<semver::Version> {
// Split on '/' and take the second part
let version_str = agent_str.split('/').nth(1)?;
// Split on whitespace and take the first part
let version_str = version_str.split_whitespace().next()?;
// Parse the version string
semver::Version::parse(version_str).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use libp2p::core::{ConnectedPoint, Endpoint, Multiaddr};
use libp2p::swarm::behaviour::{
ConnectionClosed, ConnectionEstablished, DialFailure, NewExternalAddrOfPeer,
};
use libp2p::swarm::{ConnectionId, DialError, FromSwarm};
use libp2p::PeerId;
#[test]
fn test_connection_tracker_basic() {
let mut tracker = ConnectionTracker::new();
let peer_id = PeerId::random();
let conn_id = ConnectionId::new_unchecked(1);
let endpoint = ConnectedPoint::Dialer {
address: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
role_override: Endpoint::Dialer,
};
// Verify initially not connected
assert!(!tracker.is_connected(&peer_id));
// Simulate connection established
let event = FromSwarm::ConnectionEstablished(ConnectionEstablished {
peer_id,
connection_id: conn_id,
endpoint: &endpoint,
failed_addresses: &[],
other_established: 0,
});
tracker.handle_swarm_event(event);
assert!(tracker.is_connected(&peer_id));
// Simulate connection closed
let event = FromSwarm::ConnectionClosed(ConnectionClosed {
peer_id,
connection_id: conn_id,
endpoint: &endpoint,
remaining_established: 0,
});
tracker.handle_swarm_event(event);
assert!(!tracker.is_connected(&peer_id));
}
#[test]
fn test_connection_tracker_inflight() {
let mut tracker = ConnectionTracker::new();
let peer_id = PeerId::random();
let conn_id = ConnectionId::new_unchecked(1);
// Add pending outbound
tracker.handle_pending_outbound_connection(conn_id, Some(peer_id));
assert!(tracker.has_inflight_dial(&peer_id));
// Simulate dial failure
let error = DialError::Aborted;
let event = FromSwarm::DialFailure(DialFailure {
peer_id: Some(peer_id),
error: &error,
connection_id: conn_id,
});
tracker.handle_swarm_event(event);
assert!(!tracker.has_inflight_dial(&peer_id));
}
#[test]
fn test_backoff_tracker() {
let mut tracker = BackoffTracker::new(Duration::from_secs(1), Duration::from_secs(10), 2.0);
let peer_id = PeerId::random();
// Initial increment
let backoff1 = tracker.increment(&peer_id);
// With default randomization factor 0.5, it can be down to 0.5 * initial
assert!(backoff1 >= Duration::from_millis(500));
// Next increment increases
let backoff2 = tracker.increment(&peer_id);
assert!(backoff2 > backoff1);
// Reset
tracker.reset(&peer_id);
// After reset, it should start over
let backoff_after_reset = tracker.increment(&peer_id);
assert!(backoff_after_reset < backoff2);
}
#[test]
fn test_address_tracker() {
let mut tracker = AddressTracker::new();
let peer_id = PeerId::random();
let addr: Multiaddr = "/ip4/127.0.0.1/tcp/1234".parse().unwrap();
let endpoint = ConnectedPoint::Dialer {
address: addr.clone(),
role_override: Endpoint::Dialer,
};
let conn_id = ConnectionId::new_unchecked(1);
// Connection established
let event = FromSwarm::ConnectionEstablished(ConnectionEstablished {
peer_id,
connection_id: conn_id,
endpoint: &endpoint,
failed_addresses: &[],
other_established: 0,
});
tracker.handle_swarm_event(event);
assert_eq!(tracker.last_seen_address(&peer_id), Some(addr.clone()));
}
#[test]
fn test_address_tracker_external_addr() {
let mut tracker = AddressTracker::new();
let peer_id = PeerId::random();
let addr: Multiaddr = "/ip4/127.0.0.1/tcp/8080".parse().unwrap();
let event = FromSwarm::NewExternalAddrOfPeer(NewExternalAddrOfPeer {
peer_id,
addr: &addr,
});
tracker.handle_swarm_event(event);
assert_eq!(tracker.last_seen_address(&peer_id), Some(addr));
}
#[test]
fn test_extract_semver() {
let agent = "asb/2.0.0 (xmr-btc-swap-mainnet)";
let version = extract_semver_from_agent_str(agent).unwrap();
assert_eq!(version, semver::Version::new(2, 0, 0));
let invalid = "invalid";
assert!(extract_semver_from_agent_str(invalid).is_none());
let agent_v3 = "asb/3.1.4-rc1 other-info";
let version_v3 = extract_semver_from_agent_str(agent_v3).unwrap();
assert_eq!(version_v3.major, 3);
assert_eq!(version_v3.minor, 1);
assert_eq!(version_v3.patch, 4);
}
}

34
swap-p2p/src/defaults.rs Normal file
View file

@ -0,0 +1,34 @@
use std::time::Duration;
pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
pub const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15 * 60); // 15 minutes
pub const BACKOFF_MULTIPLIER: f64 = 1.5;
// Redial
pub const REDIAL_INITIAL_INTERVAL: Duration = Duration::from_secs(1);
pub const REDIAL_MAX_INTERVAL: Duration = Duration::from_secs(10);
// Rendezvous
pub const RENDEZVOUS_REDIAL_MAX_INTERVAL: Duration = Duration::from_secs(60);
// Rendezvous discovery
pub const DISCOVERY_INITIAL_INTERVAL: Duration = Duration::from_secs(1);
pub const DISCOVERY_MAX_INTERVAL: Duration = Duration::from_secs(60 * 3);
pub const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60);
// Rendezvous register
pub const RENDEZVOUS_RETRY_INITIAL_INTERVAL: Duration = Duration::from_secs(1);
pub const RENDEZVOUS_RETRY_MAX_INTERVAL: Duration = Duration::from_secs(60);
// Quote
pub const CACHED_QUOTE_EXPIRY: Duration = Duration::from_secs(120);
pub const QUOTE_INTERVAL: Duration = Duration::from_secs(45);
pub const QUOTE_REDIAL_INTERVAL: Duration = Duration::from_secs(1);
pub const QUOTE_REDIAL_MAX_INTERVAL: Duration = Duration::from_secs(30);
pub const QUOTE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
// Swap setup
pub const NEGOTIATION_TIMEOUT: Duration = Duration::from_secs(120);
pub const SWAP_SETUP_KEEP_ALIVE: Duration = Duration::from_secs(30);
pub const SWAP_SETUP_CHANNEL_TIMEOUT: Duration = Duration::from_secs(60);

View file

@ -1,66 +1,100 @@
use libp2p::futures::future::BoxFuture;
use libp2p::futures::future::{self, AbortHandle, Abortable, BoxFuture};
use libp2p::futures::stream::{FuturesUnordered, StreamExt};
use std::collections::HashSet;
use std::collections::HashMap;
use std::hash::Hash;
use std::task::{Context, Poll};
/// A collection of futures with associated keys that can be checked for presence
/// before completion.
///
/// This combines a HashSet for key tracking with FuturesUnordered for efficient polling.
/// This combines a HashMap for key tracking and cancellation with FuturesUnordered for efficient polling.
/// The key is provided during insertion; the future only needs to yield the value.
/// If a future with the same key is inserted, the previous one is aborted/replaced.
pub struct FuturesHashSet<K, V> {
keys: HashSet<K>,
futures: FuturesUnordered<BoxFuture<'static, (K, V)>>,
futures: FuturesUnordered<BoxFuture<'static, Result<(K, V), future::Aborted>>>,
handles: HashMap<K, AbortHandle>,
}
impl<K: Hash + Eq + Clone + Send + 'static, V: 'static> FuturesHashSet<K, V> {
pub fn new() -> Self {
Self {
keys: HashSet::new(),
futures: FuturesUnordered::new(),
handles: HashMap::new(),
}
}
/// Check if a future with the given key is already pending
pub fn contains_key(&self, key: &K) -> bool {
self.keys.contains(key)
self.handles.contains_key(key)
}
/// Insert a new future with the given key.
/// The future should yield V; the key will be paired with it when it completes.
/// Returns true if the key was newly inserted, false if it was already present.
/// If false is returned, the future is not added.
/// If a future with the same key already exists, it returns false and does NOT replace it.
pub fn insert(&mut self, key: K, future: BoxFuture<'static, V>) -> bool {
if self.keys.insert(key.clone()) {
let key_clone = key;
let wrapped = async move {
let value = future.await;
(key_clone, value)
};
self.futures.push(Box::pin(wrapped));
true
} else {
false
if self.handles.contains_key(&key) {
return false;
}
let (handle, registration) = AbortHandle::new_pair();
self.handles.insert(key.clone(), handle);
let key_clone = key;
let wrapped = async move {
let value = future.await;
(key_clone, value)
};
let abortable = Abortable::new(Box::pin(wrapped), registration);
self.futures.push(Box::pin(abortable));
true
}
/// Removes a future with the given key, aborting it if it exists.
pub fn remove(&mut self, key: &K) -> bool {
if let Some(handle) = self.handles.remove(key) {
handle.abort();
return true;
}
false
}
/// Insert a new future with the given key.
/// If a future with the same key already exists, it is aborted and replaced.
///
/// Returns true if a future was replaced, false if no future was replaced.
pub fn replace(&mut self, key: K, future: BoxFuture<'static, V>) -> bool {
let did_remove_existing = self.remove(&key);
self.insert(key, future);
did_remove_existing
}
/// Poll for the next completed future.
/// When a future completes, its key is automatically removed from the tracking set.
pub fn poll_next_unpin(&mut self, cx: &mut Context) -> Poll<Option<(K, V)>> {
match self.futures.poll_next_unpin(cx) {
Poll::Ready(Some((k, v))) => {
self.keys.remove(&k);
Poll::Ready(Some((k, v)))
loop {
match self.futures.poll_next_unpin(cx) {
Poll::Ready(Some(Ok((k, v)))) => {
let did_remove_handle = self.handles.remove(&k).is_some();
// TODO: Make this a production assert
debug_assert!(did_remove_handle, "A future returned Ok but the key is not in handles. This should never happen.");
// Still return the value to avoid panicking
return Poll::Ready(Some((k, v)));
}
Poll::Ready(Some(Err(future::Aborted))) => {
// Future was aborted, ignore and continue polling
continue;
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
other => other,
}
}
pub fn len(&self) -> usize {
assert_eq!(self.keys.len(), self.futures.len());
self.keys.len()
self.handles.len()
}
}

View file

@ -1,6 +1,12 @@
pub mod behaviour_util;
pub mod defaults;
pub mod futures_util;
pub mod impl_from_rr_event;
pub mod libp2p_ext;
pub mod observe;
pub mod out_event;
pub mod patches;
pub mod protocols;
#[cfg(any(test, feature = "test-support"))]
pub mod test;

View file

@ -0,0 +1,80 @@
use libp2p::multiaddr::Protocol;
use libp2p::{Multiaddr, PeerId};
use std::collections::HashMap;
pub trait MultiAddrExt {
fn extract_peer_id(&self) -> Option<PeerId>;
fn split_peer_id(&self) -> Option<(PeerId, Multiaddr)>;
fn is_local(&self) -> bool;
}
impl MultiAddrExt for Multiaddr {
fn extract_peer_id(&self) -> Option<PeerId> {
match self.iter().last()? {
Protocol::P2p(peer_id) => Some(peer_id),
_ => None,
}
}
// Takes a peer id like /ip4/192.168.178.64/tcp/9939/p2p/12D3KooWQsqsCyJ9ae1YEAJZAfoVdVFZdDdUq3yvZ92btq7hSv9f
// and returns the peer id and the original address *with* the peer id
fn split_peer_id(&self) -> Option<(PeerId, Multiaddr)> {
let peer_id = self.extract_peer_id()?;
let address = self.clone();
Some((peer_id, address))
}
// Returns true if the multi address contains a local address which should not be advertised to the global internet
fn is_local(&self) -> bool {
self.iter().any(|p| match p {
Protocol::Ip4(addr) => {
addr.is_private()
|| addr.is_loopback()
|| addr.is_link_local()
|| addr.is_unspecified()
}
Protocol::Ip6(addr) => {
addr.is_unique_local()
|| addr.is_loopback()
|| addr.is_unicast_link_local()
|| addr.is_unspecified()
}
_ => false,
})
}
}
pub trait MultiAddrVecExt {
/// Takes multiaddresses where each multiaddress contains a peer id
/// and returns a vector of peer ids and their respective addresses
fn extract_peer_addresses(&self) -> Vec<(PeerId, Vec<Multiaddr>)>;
}
impl MultiAddrVecExt for Vec<String> {
fn extract_peer_addresses(&self) -> Vec<(PeerId, Vec<Multiaddr>)> {
let addresses = self
.iter()
.filter_map(|addr| addr.parse::<Multiaddr>().ok())
.collect::<Vec<_>>();
parse_strings_to_multiaddresses(&addresses)
}
}
impl MultiAddrVecExt for Vec<Multiaddr> {
fn extract_peer_addresses(&self) -> Vec<(PeerId, Vec<Multiaddr>)> {
parse_strings_to_multiaddresses(self)
}
}
pub fn parse_strings_to_multiaddresses(addresses: &[Multiaddr]) -> Vec<(PeerId, Vec<Multiaddr>)> {
let mut map: HashMap<PeerId, Vec<Multiaddr>> = HashMap::new();
for addr in addresses {
if let Some(peer_id) = addr.extract_peer_id() {
map.entry(peer_id).or_default().push(addr.clone());
}
}
map.into_iter().collect()
}

163
swap-p2p/src/observe.rs Normal file
View file

@ -0,0 +1,163 @@
//! A network behaviour that observes our connections to peers and emits events which can
//! then be used to be (among other things) emitted to the UI to display which peers we are connected to.
use libp2p::{
swarm::{NetworkBehaviour, ToSwarm},
Multiaddr, PeerId,
};
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, task::Poll};
use typeshare::typeshare;
use crate::behaviour_util::{AddressTracker, ConnectionTracker};
pub struct Behaviour {
connections: ConnectionTracker,
addresses: AddressTracker,
/// Queue of events to be sent to the swarm
to_swarm: VecDeque<Event>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[typeshare]
pub struct Event {
#[typeshare(serialized_as = "string")]
pub peer_id: PeerId,
pub update: ConnectionChange,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum ConnectionChange {
/// Emitted when the connection status of a peer changes
Connection(ConnectionStatus),
/// Emitted when the address changes that we display to the user
LastAddress(#[typeshare(serialized_as = "string")] Multiaddr),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
pub enum ConnectionStatus {
Connected,
Disconnected,
Dialing,
}
impl Behaviour {
pub fn new() -> Self {
Self {
connections: ConnectionTracker::new(),
addresses: AddressTracker::new(),
to_swarm: VecDeque::new(),
}
}
// TODO: We could extract this into the trackers. They could have an internal queue which we can pop in our poll function.
fn emit_connection_status(&mut self, peer_id: PeerId) {
let status = if self.connections.has_inflight_dial(&peer_id) {
ConnectionStatus::Dialing
} else if self.connections.is_connected(&peer_id) {
ConnectionStatus::Connected
} else {
ConnectionStatus::Disconnected
};
self.to_swarm.push_back(Event {
peer_id,
update: ConnectionChange::Connection(status),
});
}
fn emit_last_seen_address(&mut self, peer_id: PeerId) {
let Some(address) = self.addresses.last_seen_address(&peer_id) else {
return;
};
self.to_swarm.push_back(Event {
peer_id,
update: ConnectionChange::LastAddress(address),
});
}
}
impl NetworkBehaviour for Behaviour {
type ConnectionHandler = libp2p::swarm::dummy::ConnectionHandler;
type ToSwarm = Event;
fn handle_established_inbound_connection(
&mut self,
_connection_id: libp2p::swarm::ConnectionId,
_peer: libp2p::PeerId,
_local_addr: &libp2p::Multiaddr,
_remote_addr: &libp2p::Multiaddr,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
Ok(libp2p::swarm::dummy::ConnectionHandler)
}
fn handle_established_outbound_connection(
&mut self,
_connection_id: libp2p::swarm::ConnectionId,
_peer: libp2p::PeerId,
_addr: &libp2p::Multiaddr,
_role_override: libp2p::core::Endpoint,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
Ok(libp2p::swarm::dummy::ConnectionHandler)
}
fn on_swarm_event(&mut self, event: libp2p::swarm::FromSwarm) {
if let Some(peer_id) = self.connections.handle_swarm_event(event) {
self.emit_connection_status(peer_id);
}
if let Some(peer_id) = self.addresses.handle_swarm_event(event) {
self.emit_last_seen_address(peer_id);
}
}
fn on_connection_handler_event(
&mut self,
_peer_id: libp2p::PeerId,
_connection_id: libp2p::swarm::ConnectionId,
_event: libp2p::swarm::THandlerOutEvent<Self>,
) {
unreachable!("No event will be produced by a dummy handler.");
}
fn poll(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, libp2p::swarm::THandlerInEvent<Self>>> {
if let Some(event) = self.to_swarm.pop_front() {
return Poll::Ready(ToSwarm::GenerateEvent(event));
}
Poll::Pending
}
fn handle_pending_inbound_connection(
&mut self,
_connection_id: libp2p::swarm::ConnectionId,
_local_addr: &Multiaddr,
_remote_addr: &Multiaddr,
) -> Result<(), libp2p::swarm::ConnectionDenied> {
Ok(())
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
maybe_peer: Option<PeerId>,
_addresses: &[Multiaddr],
_effective_role: libp2p::core::Endpoint,
) -> Result<Vec<Multiaddr>, libp2p::swarm::ConnectionDenied> {
if let Some(peer_id) = self
.connections
.handle_pending_outbound_connection(connection_id, maybe_peer)
{
self.emit_connection_status(peer_id);
}
Ok(std::vec![])
}
}

View file

@ -7,6 +7,7 @@ use libp2p::{
};
use uuid::Uuid;
use crate::protocols::rendezvous;
use crate::protocols::{
cooperative_xmr_redeem_after_punish, encrypted_signature, quote::BidQuote, swap_setup,
};
@ -45,7 +46,7 @@ pub enum OutEvent {
swap_id: Uuid,
peer: PeerId,
},
Rendezvous(libp2p::rendezvous::client::Event),
Rendezvous(rendezvous::register::Event),
OutboundRequestResponseFailure {
peer: PeerId,
error: OutboundFailure,
@ -96,21 +97,8 @@ impl From<identify::Event> for OutEvent {
}
}
impl From<libp2p::rendezvous::client::Event> for OutEvent {
fn from(e: libp2p::rendezvous::client::Event) -> Self {
OutEvent::Rendezvous(e)
}
}
impl From<crate::protocols::rendezvous::register::InnerBehaviourEvent> for OutEvent {
fn from(e: crate::protocols::rendezvous::register::InnerBehaviourEvent) -> Self {
match e {
crate::protocols::rendezvous::register::InnerBehaviourEvent::Rendezvous(ev) => {
OutEvent::from(ev)
}
crate::protocols::rendezvous::register::InnerBehaviourEvent::Redial(_) => {
OutEvent::Other
}
}
impl From<rendezvous::register::Event> for OutEvent {
fn from(event: rendezvous::register::Event) -> Self {
OutEvent::Rendezvous(event)
}
}

View file

@ -3,14 +3,15 @@ use libp2p::{
request_response::{
InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel,
},
PeerId,
Multiaddr, PeerId,
};
use crate::protocols::redial;
use crate::observe;
use crate::protocols::{
cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason, quote::BidQuote,
transfer_proof,
quotes_cached::QuoteStatus, transfer_proof,
};
use crate::protocols::{redial, rendezvous};
#[derive(Debug)]
pub enum OutEvent {
@ -18,6 +19,13 @@ pub enum OutEvent {
id: OutboundRequestId,
response: BidQuote,
},
CachedQuotes {
quotes: Vec<(PeerId, Multiaddr, BidQuote, Option<semver::Version>)>,
},
CachedQuotesProgress {
peers: Vec<(PeerId, QuoteStatus)>,
},
Observe(observe::Event),
SwapSetupCompleted {
peer: PeerId,
swap_id: uuid::Uuid,
@ -93,6 +101,18 @@ impl From<identify::Event> for OutEvent {
}
}
impl From<rendezvous::discovery::Event> for OutEvent {
fn from(_: rendezvous::discovery::Event) -> Self {
OutEvent::Other
}
}
impl From<observe::Event> for OutEvent {
fn from(event: observe::Event) -> Self {
OutEvent::Observe(event)
}
}
impl From<()> for OutEvent {
fn from(_: ()) -> Self {
OutEvent::Other

1
swap-p2p/src/patches.rs Normal file
View file

@ -0,0 +1 @@
pub mod identify;

View file

@ -0,0 +1,136 @@
use std::task::Poll;
use libp2p::identify;
use libp2p::PeerId;
use crate::libp2p_ext::MultiAddrExt;
/// This wraps libp2p::identify::Behaviour, and:
/// 1. Blocks Identify from sharing local addresses with other peers
/// 2. Blocks Identify from sharing addresses of other peers with the Swarm
///
/// This helps with:
/// 1. privacy (by avoiding to share local addresses with other peers)
/// 2. preventing the Swarm from trying to dial addresses that we probably cannot reach anyway
///
/// TODO: Add a clipply rule to forbid the normal identify behaviour from being used in the codebase
pub struct Behaviour {
inner: identify::Behaviour,
}
impl Behaviour {
pub fn new(config: identify::Config) -> Self {
Self {
inner: identify::Behaviour::new(config),
}
}
}
impl libp2p::swarm::NetworkBehaviour for Behaviour {
type ConnectionHandler =
<identify::Behaviour as libp2p::swarm::NetworkBehaviour>::ConnectionHandler;
type ToSwarm = <identify::Behaviour as libp2p::swarm::NetworkBehaviour>::ToSwarm;
fn handle_established_inbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
peer: PeerId,
local_addr: &libp2p::Multiaddr,
remote_addr: &libp2p::Multiaddr,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
self.inner.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)
}
fn handle_established_outbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
peer: PeerId,
addr: &libp2p::Multiaddr,
role_override: libp2p::core::Endpoint,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
self.inner
.handle_established_outbound_connection(connection_id, peer, addr, role_override)
}
fn on_swarm_event(&mut self, event: libp2p::swarm::FromSwarm) {
match event {
libp2p::swarm::FromSwarm::NewListenAddr(new_listen_addr)
if new_listen_addr.addr.is_local() =>
{
tracing::trace!(?new_listen_addr, "Blocking attempt by Swarm to tell Identify to share local address with other peers (FromSwarm::NewListenAddr)");
}
libp2p::swarm::FromSwarm::NewExternalAddrCandidate(new_external_addr_candidate)
if new_external_addr_candidate.addr.is_local() =>
{
tracing::trace!(?new_external_addr_candidate, "Blocking attempt by Swarm to tell Identify to share a local address with the Swarm (FromSwarm::NewExternalAddrCandidate)");
}
libp2p::swarm::FromSwarm::NewExternalAddrCandidate(new_external_addr_candidate)
if new_external_addr_candidate.addr.is_local() =>
{
tracing::trace!(?new_external_addr_candidate, "Blocking attempt by Swarm to tell Identify to share a local address of another peer (FromSwarm::NewExternalAddrCandidate)");
}
other => self.inner.on_swarm_event(other),
}
}
fn on_connection_handler_event(
&mut self,
peer_id: PeerId,
connection_id: libp2p::swarm::ConnectionId,
event: libp2p::swarm::THandlerOutEvent<Self>,
) {
self.inner
.on_connection_handler_event(peer_id, connection_id, event);
}
fn poll(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<libp2p::swarm::ToSwarm<Self::ToSwarm, libp2p::swarm::THandlerInEvent<Self>>>
{
while let Poll::Ready(event) = self.inner.poll(cx) {
match event {
// We ignore the private addresses that other peers tell us through Identify
libp2p::swarm::ToSwarm::NewExternalAddrOfPeer { peer_id, address }
if address.is_local() =>
{
tracing::trace!(?peer_id, ?address, "Blocking attempt by Identify to share a local address of another peer with the Swarm");
continue;
}
_ => return Poll::Ready(event),
}
}
Poll::Pending
}
fn handle_pending_inbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
local_addr: &libp2p::Multiaddr,
remote_addr: &libp2p::Multiaddr,
) -> Result<(), libp2p::swarm::ConnectionDenied> {
self.inner
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[libp2p::Multiaddr],
effective_role: libp2p::core::Endpoint,
) -> Result<Vec<libp2p::Multiaddr>, libp2p::swarm::ConnectionDenied> {
self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)
}
}

View file

@ -1,6 +1,9 @@
pub mod cooperative_xmr_redeem_after_punish;
pub mod encrypted_signature;
pub mod notice;
pub mod quote;
pub mod quotes;
pub mod quotes_cached;
pub mod redial;
pub mod rendezvous;
pub mod swap_setup;

View file

@ -2,7 +2,6 @@ use crate::out_event;
use libp2p::request_response::ProtocolSupport;
use libp2p::{request_response, PeerId, StreamProtocol};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use swap_core::monero::{Scalar, TransferProof};
use uuid::Uuid;
@ -55,7 +54,8 @@ pub fn alice() -> Behaviour {
StreamProtocol::new(CooperativeXmrRedeemProtocol.as_ref()),
ProtocolSupport::Inbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::DEFAULT_REQUEST_TIMEOUT),
)
}
@ -65,7 +65,8 @@ pub fn bob() -> Behaviour {
StreamProtocol::new(CooperativeXmrRedeemProtocol.as_ref()),
ProtocolSupport::Outbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::DEFAULT_REQUEST_TIMEOUT),
)
}

View file

@ -2,7 +2,6 @@ use crate::out_event;
use libp2p::request_response::{self};
use libp2p::{PeerId, StreamProtocol};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use uuid::Uuid;
const PROTOCOL: &str = "/comit/xmr/btc/encrypted_signature/1.0.0";
@ -32,7 +31,8 @@ pub fn alice() -> Behaviour {
StreamProtocol::new(EncryptedSignatureProtocol.as_ref()),
request_response::ProtocolSupport::Inbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::DEFAULT_REQUEST_TIMEOUT),
)
}
@ -42,7 +42,8 @@ pub fn bob() -> Behaviour {
StreamProtocol::new(EncryptedSignatureProtocol.as_ref()),
request_response::ProtocolSupport::Outbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::DEFAULT_REQUEST_TIMEOUT),
)
}

View file

@ -0,0 +1,162 @@
//! A behaviour that emits a Event to the Swarm when it notices that a specific peer supports a specific protocol.
use std::collections::VecDeque;
use libp2p::{
core::upgrade,
swarm::{handler::ProtocolsChange, ConnectionHandler, NetworkBehaviour, SubstreamProtocol},
PeerId, StreamProtocol,
};
pub struct Behaviour {
interesting_protocol: StreamProtocol,
to_swarm: VecDeque<Event>,
}
impl Behaviour {
pub fn new(interesting_protocol: StreamProtocol) -> Self {
Self {
interesting_protocol,
to_swarm: VecDeque::new(),
}
}
}
impl NetworkBehaviour for Behaviour {
type ConnectionHandler = NoticeProtocolSupportConnectionHandler;
type ToSwarm = Event;
fn handle_established_inbound_connection(
&mut self,
_connection_id: libp2p::swarm::ConnectionId,
_peer: libp2p::PeerId,
_local_addr: &libp2p::Multiaddr,
_remote_addr: &libp2p::Multiaddr,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
Ok(NoticeProtocolSupportConnectionHandler::new(
self.interesting_protocol.clone(),
))
}
fn handle_established_outbound_connection(
&mut self,
_connection_id: libp2p::swarm::ConnectionId,
_peer: libp2p::PeerId,
_addr: &libp2p::Multiaddr,
_role_override: libp2p::core::Endpoint,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
Ok(NoticeProtocolSupportConnectionHandler::new(
self.interesting_protocol.clone(),
))
}
fn on_swarm_event(&mut self, _event: libp2p::swarm::FromSwarm) {
// nothing to do here
}
fn on_connection_handler_event(
&mut self,
peer_id: libp2p::PeerId,
_connection_id: libp2p::swarm::ConnectionId,
_event: libp2p::swarm::THandlerOutEvent<Self>,
) {
self.to_swarm
.push_back(Event::SupportsProtocol { peer: peer_id });
}
fn poll(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<libp2p::swarm::ToSwarm<Self::ToSwarm, libp2p::swarm::THandlerInEvent<Self>>>
{
if let Some(event) = self.to_swarm.pop_front() {
return std::task::Poll::Ready(libp2p::swarm::ToSwarm::GenerateEvent(event));
}
std::task::Poll::Pending
}
}
pub struct NoticeProtocolSupportConnectionHandler {
interesting_protocol: StreamProtocol,
to_behaviour: VecDeque<ToBehaviour>,
}
impl NoticeProtocolSupportConnectionHandler {
fn new(interesting_protocol: StreamProtocol) -> Self {
Self {
interesting_protocol,
to_behaviour: VecDeque::new(),
}
}
}
#[derive(Debug)]
pub enum Event {
SupportsProtocol { peer: PeerId },
}
#[derive(Debug)]
pub enum ToBehaviour {
SupportsProtocol,
}
impl ConnectionHandler for NoticeProtocolSupportConnectionHandler {
type FromBehaviour = ();
type ToBehaviour = ToBehaviour;
type InboundProtocol = upgrade::DeniedUpgrade;
type OutboundProtocol = upgrade::DeniedUpgrade;
type InboundOpenInfo = ();
type OutboundOpenInfo = ();
fn listen_protocol(
&self,
) -> libp2p::swarm::SubstreamProtocol<Self::InboundProtocol, Self::InboundOpenInfo> {
SubstreamProtocol::new(upgrade::DeniedUpgrade, ())
}
fn poll(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<
libp2p::swarm::ConnectionHandlerEvent<
Self::OutboundProtocol,
Self::OutboundOpenInfo,
Self::ToBehaviour,
>,
> {
if let Some(to_behaviour) = self.to_behaviour.pop_front() {
return std::task::Poll::Ready(libp2p::swarm::ConnectionHandlerEvent::NotifyBehaviour(
to_behaviour,
));
}
std::task::Poll::Pending
}
fn on_behaviour_event(&mut self, _event: Self::FromBehaviour) {
unreachable!("This connection handler should not receive events");
}
fn on_connection_event(
&mut self,
event: libp2p::swarm::handler::ConnectionEvent<
Self::InboundProtocol,
Self::OutboundProtocol,
Self::InboundOpenInfo,
Self::OutboundOpenInfo,
>,
) {
if let libp2p::swarm::handler::ConnectionEvent::RemoteProtocolsChange(protocols) = event {
if let ProtocolsChange::Added(protocols) = protocols {
for protocol in protocols {
if protocol == &self.interesting_protocol {
self.to_behaviour.push_back(ToBehaviour::SupportsProtocol);
}
}
}
}
}
}

View file

@ -1,5 +1,3 @@
use std::time::Duration;
use crate::out_event;
use libp2p::request_response::{self, ProtocolSupport};
use libp2p::{PeerId, StreamProtocol};
@ -7,7 +5,7 @@ use serde::{Deserialize, Serialize};
use swap_core::bitcoin;
use typeshare::typeshare;
const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0";
pub(crate) const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0";
pub type OutEvent = request_response::Event<(), BidQuote>;
pub type Message = request_response::Message<(), BidQuote>;
@ -58,10 +56,11 @@ pub struct ZeroQuoteReceived;
///
/// The ASB is always listening and only supports inbound connections, i.e.
/// handing out quotes.
pub fn asb() -> Behaviour {
pub fn alice() -> Behaviour {
Behaviour::new(
vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Inbound)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::QUOTE_REQUEST_TIMEOUT),
)
}
@ -69,10 +68,11 @@ pub fn asb() -> Behaviour {
///
/// The CLI is always dialing and only supports outbound connections, i.e.
/// requesting quotes.
pub fn cli() -> Behaviour {
pub fn bob() -> Behaviour {
Behaviour::new(
vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Outbound)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::QUOTE_REQUEST_TIMEOUT),
)
}

View file

@ -0,0 +1,502 @@
use libp2p::{
core::Endpoint,
identify,
request_response::{self, OutboundFailure},
swarm::{
ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandlerInEvent,
THandlerOutEvent, ToSwarm,
},
Multiaddr, PeerId, StreamProtocol,
};
use crate::{
behaviour_util::{extract_semver_from_agent_str, BackoffTracker, ConnectionTracker},
futures_util::FuturesHashSet,
patches,
protocols::{
notice,
quote::{self, BidQuote},
redial,
},
};
use std::{
collections::{HashSet, VecDeque},
task::Poll,
time::Duration,
};
// We initially assume all peers support our protocol.
pub struct Behaviour {
inner: InnerBehaviour,
/// Track connected peers
connection_tracker: ConnectionTracker,
/// Peers which have explictly told us that they do not support our protocol
does_not_support: HashSet<PeerId>,
/// Peers to dispatch a quote request to as soon as we are connected to them
to_dispatch: VecDeque<PeerId>,
/// Peers to request a quote from once the future resolves
to_request: FuturesHashSet<PeerId, ()>,
/// Store backoffs for each peer
backoff: BackoffTracker,
// Queue of events to be sent to the swarm
to_swarm: VecDeque<Event>,
}
impl Behaviour {
pub fn new(identify_config: identify::Config) -> Self {
Self {
inner: InnerBehaviour {
quote: quote::bob(),
notice: notice::Behaviour::new(StreamProtocol::new(quote::PROTOCOL)),
redial: redial::Behaviour::new(
"quotes",
crate::defaults::QUOTE_REDIAL_INTERVAL,
crate::defaults::QUOTE_REDIAL_MAX_INTERVAL,
),
identify: patches::identify::Behaviour::new(identify_config),
},
connection_tracker: ConnectionTracker::new(),
does_not_support: HashSet::new(),
to_dispatch: VecDeque::new(),
to_request: FuturesHashSet::new(),
backoff: BackoffTracker::new(
crate::defaults::QUOTE_REDIAL_INTERVAL,
crate::defaults::QUOTE_REDIAL_MAX_INTERVAL,
crate::defaults::BACKOFF_MULTIPLIER,
),
to_swarm: VecDeque::new(),
}
}
fn is_connected(&self, peer_id: &PeerId) -> bool {
self.connection_tracker.is_connected(peer_id)
}
fn schedule_quote_request_after(&mut self, peer: PeerId, duration: Duration) -> Duration {
// TODO: Handle if there already was a future for this peer
self.to_request
.insert(peer, Box::pin(tokio::time::sleep(duration)));
duration
}
fn schedule_quote_request_with_backoff(&mut self, peer: PeerId) -> Duration {
let duration = self.backoff.get(&peer).current_interval;
self.schedule_quote_request_after(peer, duration)
}
// Called whenever we hear about a new peer, this can be called multiple times for the same peer
fn handle_discovered_peer(&mut self, peer: PeerId) {
// Instruct to redial unless we know that the peer does not support the protocol
if !self.does_not_support.contains(&peer) {
self.inner.redial.add_peer(peer);
}
}
fn handle_does_not_support_protocol(&mut self, peer: PeerId) {
tracing::trace!(%peer, "Peer does not support the quote protocol");
self.does_not_support.insert(peer);
self.inner.redial.remove_peer(&peer);
self.to_swarm
.push_back(Event::DoesNotSupportProtocol { peer });
}
}
#[derive(NetworkBehaviour)]
pub struct InnerBehaviour {
quote: quote::Behaviour,
notice: notice::Behaviour,
redial: redial::Behaviour,
identify: patches::identify::Behaviour,
}
#[derive(Debug)]
pub enum Event {
QuoteReceived {
peer: PeerId,
quote: BidQuote,
},
QuoteInflight {
peer: PeerId,
},
QuoteFailed {
peer: PeerId,
},
VersionReceived {
peer: PeerId,
version: semver::Version,
},
DoesNotSupportProtocol {
peer: PeerId,
},
}
impl libp2p::swarm::NetworkBehaviour for Behaviour {
type ConnectionHandler = <InnerBehaviour as libp2p::swarm::NetworkBehaviour>::ConnectionHandler;
type ToSwarm = Event;
fn poll(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
while let Poll::Ready(Some((peer, ()))) = self.to_request.poll_next_unpin(cx) {
self.to_dispatch.push_back(peer);
}
// Only dispatch to connected peers, keep non-connected ones in queue
// Take ownership of the queue to avoid borrow checker issues
let to_dispatch = std::mem::take(&mut self.to_dispatch);
self.to_dispatch = to_dispatch
.into_iter()
.filter(|peer| {
if self.is_connected(peer) {
let outbound_request_id = self.inner.quote.send_request(peer, ());
tracing::trace!(
%peer,
%outbound_request_id,
"Dispatching outgoing quote request to peer"
);
self.to_swarm
.push_back(Event::QuoteInflight { peer: *peer });
false
} else {
true
}
})
.collect();
while let Poll::Ready(ready_to_swarm) = self.inner.poll(cx) {
match ready_to_swarm {
ToSwarm::GenerateEvent(event) => {
match event {
InnerBehaviourEvent::Notice(notice::Event::SupportsProtocol { peer }) => {
self.does_not_support.remove(&peer);
}
InnerBehaviourEvent::Quote(request_response::Event::Message {
peer,
message,
}) => {
if let request_response::Message::Response { response, .. } = message {
self.to_swarm.push_back(Event::QuoteReceived {
peer,
quote: response,
});
// We got a successful response, so we reset the backoff
self.backoff.reset(&peer);
// Schedule a new quote request after backoff
self.schedule_quote_request_after(
peer,
crate::defaults::QUOTE_INTERVAL,
);
}
}
InnerBehaviourEvent::Quote(request_response::Event::OutboundFailure {
peer,
request_id,
error,
}) => {
// We got an outbound failure, so we increment the backoff
self.backoff.increment(&peer);
if let OutboundFailure::UnsupportedProtocols = error {
self.handle_does_not_support_protocol(peer);
// Only schedule a new quote request if we are not sure that the peer does not support the protocol
} else if !self.does_not_support.contains(&peer) {
// We schedule a new quote request
let next_request_in =
self.schedule_quote_request_with_backoff(peer);
tracing::trace!(%peer, %request_id, %error, next_request_in = %next_request_in.as_secs(), "Queuing quote request to peer after outbound failure");
self.to_swarm.push_back(Event::QuoteFailed { peer });
}
}
InnerBehaviourEvent::Identify(identify::Event::Received {
peer_id,
info,
}) => match extract_semver_from_agent_str(info.agent_version.as_str()) {
Some(version) => {
tracing::trace!(%peer_id, %version, "Received version from peer via identify");
self.to_swarm.push_back(Event::VersionReceived {
peer: peer_id,
version,
});
}
None => {
tracing::warn!(%peer_id, ?info, "Received identify info from peer but failed to extract semver version");
}
},
_other => {
// TODO: Do we need to handle other events?
}
}
}
_ => {
return Poll::Ready(ready_to_swarm.map_out(|_| {
unreachable!("we handle all generate events in the arm above")
}));
}
}
}
while let Some(event) = self.to_swarm.pop_front() {
return Poll::Ready(ToSwarm::GenerateEvent(event));
}
Poll::Pending
}
fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
self.connection_tracker.handle_swarm_event(event);
match event {
FromSwarm::ConnectionEstablished(connection_established) => {
// When we connected to a peer where we are not certain that they do not support the protocol, we schedule a quote request
if !self
.does_not_support
.contains(&connection_established.peer_id)
{
self.schedule_quote_request_with_backoff(connection_established.peer_id);
}
}
FromSwarm::NewExternalAddrOfPeer(event) => {
self.handle_discovered_peer(event.peer_id);
}
_ => {}
}
self.inner.on_swarm_event(event)
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[Multiaddr],
effective_role: libp2p::core::Endpoint,
) -> Result<Vec<Multiaddr>, libp2p::swarm::ConnectionDenied> {
self.connection_tracker
.handle_pending_outbound_connection(connection_id, maybe_peer);
self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)
}
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_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 handle_pending_inbound_connection(
&mut self,
connection_id: ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.inner
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{new_swarm, SwarmExt};
use futures::StreamExt;
use libp2p::swarm::{Swarm, SwarmEvent};
use tokio::task::JoinHandle;
#[tokio::test]
async fn receive_quote_from_alice() {
// Create the swarm for Bob
let mut bob =
new_swarm(|identity| Behaviour::new(identify_config(identity, "quotes", "1.0.0")));
// Create the swarm for Alice
// Let her listen on a random memory address
// Let her respond to requests
let alice = new_swarm(|_| quote::alice());
let (alice_peer_id, alice_addr, alice_handle) = serve_quotes(alice).await;
// Tell Bob about Alice's address
// This should be enough to get the `quotes` behaviour to dial Alice
bob.add_peer_address(alice_peer_id, alice_addr);
let bob_handle = tokio::spawn(async move {
loop {
if let SwarmEvent::Behaviour(Event::QuoteReceived { peer, quote }) =
bob.select_next_some().await
{
if peer == alice_peer_id && quote == quote::BidQuote::ZERO {
return;
}
}
}
});
tokio::select! {
_ = bob_handle => {}
_ = tokio::time::sleep(Duration::from_secs(10)) => {
panic!("Test timed out");
}
}
alice_handle.abort();
}
#[tokio::test]
async fn receive_does_not_support_protocol_from_alice() {
// Create the swarm for Bob
let mut bob =
new_swarm(|identity| Behaviour::new(identify_config(identity, "quotes", "1.0.0")));
// Use quote::bob() so Alice doesn't support inbound requests
let mut alice = new_swarm(|_| quote::bob());
let alice_peer_id = *alice.local_peer_id();
let alice_addr = alice.listen_on_random_memory_address().await;
// Let Alice run but she doesn't need to do anything
let alice_handle = tokio::spawn(async move {
loop {
alice.select_next_some().await;
}
});
// Tell Bob about Alice's address
bob.add_peer_address(alice_peer_id, alice_addr);
// Wait for DoesNotSupportProtocol
let timeout = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(timeout);
loop {
tokio::select! {
event = bob.select_next_some() => {
if let SwarmEvent::Behaviour(Event::DoesNotSupportProtocol { peer }) = event {
if peer == alice_peer_id {
break;
}
}
}
_ = &mut timeout => {
panic!("Timeout waiting for DoesNotSupportProtocol");
}
}
}
// Kill Alice which should kill the connection
// Bob should not attempt to redial Alice because he has noticed that she does not support the protocol
alice_handle.abort();
let mut connection_closed = false;
let timeout = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(timeout);
loop {
tokio::select! {
event = bob.select_next_some() => {
match event {
SwarmEvent::ConnectionClosed { peer_id, .. } if peer_id == alice_peer_id => {
connection_closed = true;
}
SwarmEvent::OutgoingConnectionError { peer_id: Some(peer_id), .. } if peer_id == alice_peer_id => {
panic!("Bob attempted to redial Alice!");
}
_ => {}
}
}
_ = &mut timeout => {
break;
}
}
}
assert!(
connection_closed,
"Bob should have noticed connection closed"
);
}
/// Ensures that Alice responds with a zero quote when requested.
async fn serve_quotes(
mut alice: Swarm<quote::Behaviour>,
) -> (PeerId, Multiaddr, JoinHandle<()>) {
let alice_peer_id = *alice.local_peer_id();
let alice_addr = alice.listen_on_random_memory_address().await;
let alice_handle = tokio::spawn(async move {
loop {
match alice.select_next_some().await {
SwarmEvent::Behaviour(libp2p::request_response::Event::Message {
message: libp2p::request_response::Message::Request { channel, .. },
..
}) => {
alice
.behaviour_mut()
.send_response(channel, quote::BidQuote::ZERO)
.unwrap();
}
_ => {}
}
}
});
(alice_peer_id, alice_addr, alice_handle)
}
fn identify_config(
identity: libp2p::identity::Keypair,
protocol: &str,
version: &str,
) -> identify::Config {
identify::Config::new(format!("/quotes/test/{}", protocol), identity.public())
.with_agent_version(format!("{} ({})", protocol, version))
}
}

View file

@ -0,0 +1,263 @@
use crate::behaviour_util::{AddressTracker};
use crate::futures_util::FuturesHashSet;
use crate::out_event;
use crate::protocols::quote::BidQuote;
use crate::protocols::quotes;
use libp2p::identify;
use libp2p::swarm::{
ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent,
THandlerOutEvent, ToSwarm,
};
use libp2p::{Multiaddr, PeerId};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
use std::task::{Context, Poll};
use typeshare::typeshare;
pub struct Behaviour {
inner: quotes::Behaviour,
/// For each peer, cache the address we last connected to them at
// TODO: Technically this is not required. The UI gets the address from the observe behaviour.
address_tracker: AddressTracker,
/// For every peer track the last semver version we received from them
// TODO: Maybe let these expire after a certain time?
versions: HashMap<PeerId, semver::Version>,
// Caches quotes
// TODO: Maybe move the identify logic from quotes to quotes_cached?
cache: HashMap<PeerId, BidQuote>,
quote_status: HashMap<PeerId, QuoteStatus>,
expiry: FuturesHashSet<PeerId, ()>,
// Queue of events to be sent to the swarm
to_swarm: VecDeque<Event>,
}
impl Behaviour {
pub fn new(identify_config: identify::Config) -> Self {
Self {
inner: quotes::Behaviour::new(identify_config),
address_tracker: AddressTracker::new(),
versions: HashMap::new(),
cache: HashMap::new(),
quote_status: HashMap::new(),
expiry: FuturesHashSet::new(),
to_swarm: VecDeque::new(),
}
}
fn emit_cached_quotes(&mut self) {
// Attach the address we last connected to the peer at to the quote
//
// Ignores those peers where we don't have an address cached
let quotes: Vec<(PeerId, Multiaddr, BidQuote, Option<semver::Version>)> = self
.cache
.iter()
.filter_map(|(peer_id, quote)| {
self.address_tracker.last_seen_address(peer_id).map(|addr| {
let version = self.versions.get(peer_id).cloned();
(*peer_id, addr, quote.clone(), version)
})
})
.collect();
// There is no way to receive a quote from a peer without ever hearing about any address from them
debug_assert_eq!(quotes.len(), self.cache.len());
self.to_swarm.push_back(Event::CachedQuotes { quotes });
}
fn emit_progress(&mut self) {
let mut peers = Vec::new();
let all_peers: HashSet<PeerId> = self
.address_tracker
.peers()
.chain(self.quote_status.keys())
.cloned()
.collect();
for peer in all_peers {
let quote = self
.quote_status
.get(&peer)
.cloned()
.unwrap_or(QuoteStatus::Nothing);
peers.push((peer, quote));
}
self.to_swarm.push_back(Event::Progress { peers });
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
pub enum QuoteStatus {
Received,
NotSupported,
Inflight,
Failed,
Nothing,
}
#[derive(Debug)]
pub enum Event {
CachedQuotes {
// Peer ID, Address, Quote, Version
quotes: Vec<(PeerId, Multiaddr, BidQuote, Option<semver::Version>)>,
},
Progress {
peers: Vec<(PeerId, QuoteStatus)>,
},
}
impl NetworkBehaviour for Behaviour {
type ConnectionHandler = <quotes::Behaviour as NetworkBehaviour>::ConnectionHandler;
type ToSwarm = Event;
fn poll(
&mut self,
cx: &mut Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
while let Poll::Ready(Some((peer, ()))) = self.expiry.poll_next_unpin(cx) {
self.cache.remove(&peer);
self.emit_cached_quotes();
}
while let Poll::Ready(event) = self.inner.poll(cx) {
match event {
ToSwarm::GenerateEvent(event) => {
match event {
quotes::Event::QuoteReceived { peer, quote } => {
self.cache.insert(peer, quote);
self.quote_status.insert(peer, QuoteStatus::Received);
self.expiry.replace(
peer,
Box::pin(tokio::time::sleep(crate::defaults::CACHED_QUOTE_EXPIRY)),
);
self.emit_cached_quotes();
self.emit_progress();
}
quotes::Event::QuoteInflight { peer } => {
self.quote_status.insert(peer, QuoteStatus::Inflight);
self.emit_progress();
}
quotes::Event::QuoteFailed { peer } => {
self.quote_status.insert(peer, QuoteStatus::Failed);
self.emit_progress();
}
quotes::Event::VersionReceived { peer, version } => {
self.versions.insert(peer, version);
// TODO: Only emit if the version is different from the cached one?
self.emit_cached_quotes();
self.emit_progress();
}
quotes::Event::DoesNotSupportProtocol { peer } => {
self.quote_status.insert(peer, QuoteStatus::NotSupported);
self.emit_progress();
}
}
}
_ => {
return Poll::Ready(event.map_out(|_| {
unreachable!("we handle all generate events in the arm above")
}));
}
}
}
if let Some(event) = self.to_swarm.pop_front() {
return Poll::Ready(ToSwarm::GenerateEvent(event));
}
Poll::Pending
}
fn handle_established_inbound_connection(
&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 handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
addr: &Multiaddr,
role_override: libp2p::core::Endpoint,
) -> Result<THandler<Self>, ConnectionDenied> {
self.inner
.handle_established_outbound_connection(connection_id, peer, addr, role_override)
}
fn handle_pending_inbound_connection(
&mut self,
connection_id: ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.inner
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[Multiaddr],
effective_role: libp2p::core::Endpoint,
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)
}
fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
self.address_tracker.handle_swarm_event(event);
self.emit_progress(); // TODO: this will emit quite frequently
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);
}
}
impl From<Event> for out_event::bob::OutEvent {
fn from(event: Event) -> Self {
match event {
Event::CachedQuotes { quotes } => Self::CachedQuotes { quotes },
Event::Progress { peers } => Self::CachedQuotesProgress { peers },
}
}
}
impl From<Event> for out_event::alice::OutEvent {
fn from(_: Event) -> Self {
unreachable!("Alice should not use the cached quotes behaviour");
}
}

View file

@ -1,7 +1,6 @@
use crate::behaviour_util::{BackoffTracker, ConnectionTracker};
use crate::futures_util::FuturesHashSet;
use crate::out_event;
use backoff::backoff::Backoff;
use backoff::ExponentialBackoff;
use libp2p::core::Multiaddr;
use libp2p::swarm::dial_opts::{DialOpts, PeerCondition};
use libp2p::swarm::{DialError, FromSwarm, NetworkBehaviour, ToSwarm};
@ -18,27 +17,30 @@ use void::Void;
/// Note: Make sure that when using this as an inner behaviour for a `NetworkBehaviour` that you
/// call all the NetworkBehaviour methods (including `handle_pending_outbound_connection`) to ensure
/// that the addresses are cached correctly.
/// TODO: Allow removing peers from the set after we are done with them.
pub struct Behaviour {
/// An identifier for this redial behaviour instance (for logging/tracing).
name: &'static str,
/// The peers we are interested in.
peers: HashSet<PeerId>,
connections: ConnectionTracker,
/// Store address for all peers (even those we are not interested in)
/// because we might be interested in them later on
// TODO: Sort these by how often we were able to connect to them
// TODO: Use the behaviour_util::AddressTracker instead
addresses: HashMap<PeerId, HashSet<Multiaddr>>,
/// Tracks sleep timers for each peer waiting to redial.
/// Futures in here yield the PeerId and when a Future completes we dial that peer
to_dial: FuturesHashSet<PeerId, ()>,
/// Tracks the current backoff state for each peer.
backoff: HashMap<PeerId, ExponentialBackoff>,
/// Initial interval for backoff.
initial_interval: Duration,
/// Maximum interval for backoff.
max_interval: Duration,
backoff: BackoffTracker,
/// A queue of events to be sent to the swarm.
to_swarm: VecDeque<ToSwarm<Event, Void>>,
/// An identifier for this redial behaviour instance (for logging/tracing).
name: &'static str,
}
impl Behaviour {
@ -47,9 +49,12 @@ impl Behaviour {
peers: HashSet::default(),
addresses: HashMap::default(),
to_dial: FuturesHashSet::new(),
backoff: HashMap::new(),
initial_interval: interval,
max_interval,
connections: ConnectionTracker::new(),
backoff: BackoffTracker::new(
interval,
max_interval,
crate::defaults::BACKOFF_MULTIPLIER,
),
to_swarm: VecDeque::new(),
name,
}
@ -64,12 +69,25 @@ impl Behaviour {
if newly_added {
self.schedule_redial(&peer, Duration::ZERO);
tracing::trace!("Added a new peer to the set of peers we want to contineously redial");
tracing::trace!("Started tracking peer");
}
newly_added
}
/// Removes a peer from the set of peers to track. Returns true if the peer was removed.
#[tracing::instrument(level = "trace", name = "redial::remove_peer", skip(self, peer), fields(redial_type = %self.name, peer = %peer))]
pub fn remove_peer(&mut self, peer: &PeerId) -> bool {
if self.peers.remove(peer) {
self.to_dial.remove(peer);
tracing::trace!("Stopped tracking peer");
return true;
}
false
}
/// Adds a peer to the set of peers to track with a specific address. Returns true if the peer was newly added.
#[tracing::instrument(level = "trace", name = "redial::add_peer_with_address", skip(self, peer, address), fields(redial_type = %self.name, peer = %peer, address = %address))]
pub fn add_peer_with_address(&mut self, peer: PeerId, address: Multiaddr) -> bool {
@ -79,31 +97,20 @@ impl Behaviour {
if newly_added {
self.schedule_redial(&peer, Duration::ZERO);
tracing::trace!(?address, "Added a new peer to the set of peers we want to contineously redial with a specific address");
tracing::trace!(
?address,
"Started tracking peer and added a specific address"
);
}
self.to_swarm.push_back(ToSwarm::NewExternalAddrOfPeer {
peer_id: peer,
address: address.clone(),
});
self.insert_address(&peer, address);
newly_added
}
fn get_backoff(&mut self, peer: &PeerId) -> &mut ExponentialBackoff {
self.backoff.entry(*peer).or_insert_with(|| {
ExponentialBackoff {
initial_interval: self.initial_interval,
current_interval: self.initial_interval,
max_interval: self.max_interval,
// We never give up on re-dialling
max_elapsed_time: None,
..ExponentialBackoff::default()
}
})
}
#[tracing::instrument(level = "trace", name = "redial::schedule_redial", skip(self, peer, override_next_dial_in), fields(redial_type = %self.name, peer = %peer))]
fn schedule_redial(
&mut self,
@ -118,11 +125,10 @@ impl Behaviour {
// How long should we wait before we redial the peer?
// If an override is provided, use that, otherwise use the backoff
let next_dial_in = override_next_dial_in.into().unwrap_or_else(|| {
self.get_backoff(peer)
.next_backoff()
.expect("redial backoff should never run out of attempts")
});
// TODO: Instead only increment on errors
let next_dial_in = override_next_dial_in
.into()
.unwrap_or_else(|| self.backoff.increment(peer));
let did_queue_new_dial = self.to_dial.insert(
peer.clone(),
@ -153,16 +159,18 @@ impl Behaviour {
self.to_dial.contains_key(peer)
}
pub fn insert_address(&mut self, peer: &PeerId, address: Multiaddr) {
pub fn insert_address(&mut self, peer: &PeerId, address: Multiaddr) -> bool {
self.addresses
.entry(peer.clone())
.or_default()
.insert(address);
.insert(address)
}
}
#[derive(Debug)]
pub enum Event {
// TODO: This should emit useful events like Connected, Disconnected, etc. for the peers we are interested in.
// This could prevent having to use the ConnectionTracker in parent behaviours (essentiually duplicating the code here)
ScheduledRedial {
peer: PeerId,
next_dial_in: Duration,
@ -175,13 +183,15 @@ impl NetworkBehaviour for Behaviour {
#[tracing::instrument(level = "trace", name = "redial::on_swarm_event", skip(self, event), fields(redial_type = %self.name))]
fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
self.connections.handle_swarm_event(event);
let peer_to_redial = match event {
// Check if we discovered a new address for some peer
FromSwarm::NewExternalAddrOfPeer(event) => {
// TOOD: Ensure that if the address contains a peer id it matches the peer id in the event
self.insert_address(&event.peer_id, event.addr.clone());
tracing::trace!(peer = %event.peer_id, address = %event.addr, "Cached an address for a peer");
if self.insert_address(&event.peer_id, event.addr.clone()) {
tracing::trace!(peer = %event.peer_id, address = %event.addr, "Cached an address for a peer");
}
None
}
@ -189,14 +199,20 @@ impl NetworkBehaviour for Behaviour {
// - a failed dial
// - a closed connection
//
// We will then schedule a redial for the peer
FromSwarm::ConnectionClosed(event) if self.peers.contains(&event.peer_id) => {
tracing::trace!(peer = %event.peer_id, "A connection was closed for a peer we want to contineously redial. We will schedule a redial.");
// We will then schedule a redial for the peer. We only do this if we are not already connected to the peer.
FromSwarm::ConnectionClosed(event)
if self.peers.contains(&event.peer_id)
&& !self.connections.is_connected(&event.peer_id) =>
{
tracing::trace!(peer = %event.peer_id, "Connection closed. We will schedule a redial for this peer.");
Some(event.peer_id)
}
FromSwarm::DialFailure(event) => match event.peer_id {
Some(peer_id) if self.peers.contains(&peer_id) => {
Some(peer_id)
if self.peers.contains(&peer_id)
&& !self.connections.is_connected(&peer_id) =>
{
match event.error {
DialError::DialPeerConditionFalse(_) => {
// TODO: Can this lead to a condition where we will not redial the peer ever again? I don't think so...
@ -205,11 +221,11 @@ impl NetworkBehaviour for Behaviour {
// We always dial with `PeerCondition::DisconnectedAndNotDialing`.
// If we not disconnected, we don't need to redial.
// If we are already dialing, another event will be emitted if that dial fails.
tracing::trace!(peer = %peer_id, dial_error = ?event.error, "A dial failure occurred for a peer we want to contineously redial, but this was due to a dial condition failure. We are not treating this as a failure. We will not schedule a redial.");
// tracing::trace!(peer = %peer_id, dial_error = ?event.error, "A dial failure occurred for a peer we want to contineously redial, but this was due to a dial condition failure. We are not treating this as a failure. We will not schedule a redial.");
None
}
_ => {
tracing::trace!(peer = %peer_id, dial_error = ?event.error, "A dial failure occurred for a peer we want to contineously redial. We will schedule a redial.");
tracing::trace!(peer = %peer_id, dial_error = ?event.error, "Dial failure occurred. We will schedule a redial for this peer.");
Some(peer_id)
}
}
@ -223,8 +239,6 @@ impl NetworkBehaviour for Behaviour {
// We will then reset the backoff state for the peer
let peer_to_reset = match event {
FromSwarm::ConnectionEstablished(e) if self.peers.contains(&e.peer_id) => {
tracing::trace!(peer = %e.peer_id, "A connection was established for a peer we want to contineously redial, resetting backoff state");
Some(e.peer_id)
}
_ => None,
@ -232,9 +246,7 @@ impl NetworkBehaviour for Behaviour {
// Reset the backoff state for the peer if needed
if let Some(peer) = peer_to_reset {
if let Some(backoff) = self.backoff.get_mut(&peer) {
backoff.reset();
}
self.backoff.reset(&peer);
}
// Schedule a redial if needed
@ -253,9 +265,6 @@ impl NetworkBehaviour for Behaviour {
// Check if any peer's sleep timer has completed
// If it has, dial that peer
if let Poll::Ready(Some((peer, _))) = self.to_dial.poll_next_unpin(cx) {
tracing::trace!(peer = %peer, "Instructing swarm to redial a peer we want to contineously redial after the sleep timer completed");
// Actually dial the peer
return Poll::Ready(ToSwarm::Dial {
opts: DialOpts::peer_id(peer)
.condition(PeerCondition::DisconnectedAndNotDialing)
@ -295,23 +304,32 @@ impl NetworkBehaviour for Behaviour {
Ok(Self::ConnectionHandler {})
}
#[tracing::instrument(level = "trace", name = "redial::handle_pending_outbound_connection", skip(self, _connection_id, _addresses, maybe_peer, _effective_role), fields(redial_type = %self.name))]
#[tracing::instrument(level = "trace", name = "redial::handle_pending_outbound_connection", skip(self, connection_id, _addresses, maybe_peer, _effective_role), fields(redial_type = %self.name))]
fn handle_pending_outbound_connection(
&mut self,
_connection_id: libp2p::swarm::ConnectionId,
connection_id: libp2p::swarm::ConnectionId,
maybe_peer: Option<PeerId>,
_addresses: &[Multiaddr],
_effective_role: libp2p::core::Endpoint,
) -> Result<Vec<Multiaddr>, libp2p::swarm::ConnectionDenied> {
self.connections
.handle_pending_outbound_connection(connection_id, maybe_peer);
// If we don't know the peer id, we cannot contribute any addresses
let Some(peer_id) = maybe_peer else {
return Ok(vec![]);
};
// TODO: Uncomment this if we only want to contribute addresses for peers we are instructed to redial
// if !self.peers.contains(&peer_id) {
// return Ok(vec![]);
// }
// We only want to contribute addresses for peers we are instructed to redial
if !self.peers.contains(&peer_id) {
return Ok(vec![]);
}
// Cancel all pending dials for this peer in this behaviour
// Another Behaviour already schedules a dial before we could
if self.to_dial.remove(&peer_id) {
tracing::trace!(peer = %peer_id, "Cancelled a pending dial for a peer because something else already scheduled a dial");
}
// Check if we have any addresses cached for the peer
// TODO: Sort these by how often we were able to connect to them
@ -321,7 +339,7 @@ impl NetworkBehaviour for Behaviour {
.map(|addrs| addrs.iter().cloned().collect())
.unwrap_or_default();
tracing::trace!(peer = %peer_id, contributed_addresses = ?addresses, "Contributing our cached addresses for a peer to the dial attempt");
// tracing::trace!(peer = %peer_id, contributed_addresses = ?addresses, "Contributing our cached addresses for a peer to the dial attempt");
Ok(addresses)
}

View file

@ -1,5 +1,5 @@
use libp2p::rendezvous::Namespace;
use std::{fmt, time::Duration};
use std::fmt;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum XmrBtcNamespace {
@ -32,9 +32,6 @@ impl From<XmrBtcNamespace> for Namespace {
}
}
const REDIAL_INITIAL_INTERVAL: Duration = Duration::from_secs(1);
const REDIAL_MAX_INTERVAL: Duration = Duration::from_secs(60);
impl XmrBtcNamespace {
pub fn from_is_testnet(testnet: bool) -> XmrBtcNamespace {
if testnet {
@ -46,530 +43,109 @@ impl XmrBtcNamespace {
}
/// A behaviour that periodically re-registers at multiple rendezvous points as a client
pub mod register {
use crate::protocols::redial;
pub mod register;
/// A behaviour that periodically discovers other peers at a given rendezvous point
///
/// The behaviour also internally attempts to dial any newly discovered peers
/// It uses the `redial` behaviour internally to do this
pub mod discovery;
#[cfg(test)]
mod tests {
use super::*;
use backoff::backoff::Backoff;
use backoff::ExponentialBackoff;
use futures::FutureExt;
use libp2p::rendezvous::client::RegisterError;
use libp2p::swarm::{
ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent,
THandlerOutEvent, ToSwarm,
};
use libp2p::{identity, rendezvous, Multiaddr, PeerId};
use std::collections::HashMap;
use std::pin::Pin;
use std::task::{Context, Poll};
use crate::test::{new_swarm, SwarmExt};
use futures::StreamExt;
use libp2p::rendezvous;
use libp2p::swarm::SwarmEvent;
use libp2p::{Multiaddr, PeerId};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionStatus {
Disconnected,
Connected,
}
#[tokio::test]
async fn register_and_discover_together() {
// Create rendezvous node
let (rendezvous_peer_id, rendezvous_addr, rendezvous_handle) =
spawn_rendezvous_node().await;
enum RegistrationStatus {
RegisterOnNextConnection,
Pending,
Registered {
re_register_in: Pin<Box<tokio::time::Sleep>>,
},
}
pub struct Behaviour {
inner: InnerBehaviour,
rendezvous_nodes: Vec<RendezvousNode>,
backoffs: HashMap<PeerId, ExponentialBackoff>,
}
#[derive(NetworkBehaviour)]
pub struct InnerBehaviour {
rendezvous: rendezvous::client::Behaviour,
redial: redial::Behaviour,
}
// Provide a read-only snapshot of rendezvous registrations
impl Behaviour {
/// Returns a snapshot of registration and connection status for all configured rendezvous nodes.
pub fn registrations(&self) -> Vec<RegistrationReport> {
self.rendezvous_nodes
.iter()
.map(|n| RegistrationReport {
address: n.address.clone(),
connection: n.connection_status,
registration: match &n.registration_status {
RegistrationStatus::RegisterOnNextConnection => {
RegistrationStatusReport::RegisterOnNextConnection
}
RegistrationStatus::Pending => RegistrationStatusReport::Pending,
RegistrationStatus::Registered { .. } => {
RegistrationStatusReport::Registered
}
},
})
.collect()
}
}
/// Public representation of a rendezvous node registration status
/// The raw `RegistrationStatus` cannot be exposed because it is not serializable
#[derive(Debug, Clone)]
pub struct RegistrationReport {
pub address: Multiaddr,
pub connection: ConnectionStatus,
pub registration: RegistrationStatusReport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegistrationStatusReport {
RegisterOnNextConnection,
Pending,
Registered,
}
/// A node running the rendezvous server protocol.
pub struct RendezvousNode {
pub address: Multiaddr,
connection_status: ConnectionStatus,
pub peer_id: PeerId,
registration_status: RegistrationStatus,
pub registration_ttl: Option<u64>,
pub namespace: XmrBtcNamespace,
}
impl RendezvousNode {
pub fn new(
address: &Multiaddr,
peer_id: PeerId,
namespace: XmrBtcNamespace,
registration_ttl: Option<u64>,
) -> Self {
Self {
address: address.to_owned(),
connection_status: ConnectionStatus::Disconnected,
namespace,
peer_id,
registration_status: RegistrationStatus::RegisterOnNextConnection,
registration_ttl,
}
}
fn set_connection(&mut self, status: ConnectionStatus) {
self.connection_status = status;
}
fn set_registration(&mut self, status: RegistrationStatus) {
self.registration_status = status;
}
}
impl Behaviour {
const RENDEZVOUS_RETRY_INITIAL_INTERVAL: Duration = Duration::from_secs(1);
const RENDEZVOUS_RETRY_MAX_INTERVAL: Duration = Duration::from_secs(60);
const REDIAL_IDENTIFIER: &str = "rendezvous-server-for-register";
pub fn new(identity: identity::Keypair, rendezvous_nodes: Vec<RendezvousNode>) -> Self {
let our_peer_id = identity.public().to_peer_id();
let rendezvous_nodes: Vec<RendezvousNode> = rendezvous_nodes
.into_iter()
.filter(|node| node.peer_id != our_peer_id)
.collect();
let mut backoffs = HashMap::new();
let mut redial = redial::Behaviour::new(
Self::REDIAL_IDENTIFIER,
REDIAL_INITIAL_INTERVAL,
REDIAL_MAX_INTERVAL,
);
// Initialize backoff for each rendezvous node
for node in &rendezvous_nodes {
redial.add_peer_with_address(node.peer_id, node.address.clone());
backoffs.insert(
node.peer_id,
ExponentialBackoff {
// Never give up
max_elapsed_time: None,
// We retry aggressively. We begin with 50ms and increase by 10% per retry.
multiplier: 1.1f64,
initial_interval: Self::RENDEZVOUS_RETRY_INITIAL_INTERVAL,
current_interval: Self::RENDEZVOUS_RETRY_INITIAL_INTERVAL,
max_interval: Self::RENDEZVOUS_RETRY_MAX_INTERVAL,
..ExponentialBackoff::default()
},
);
}
Self {
inner: InnerBehaviour {
rendezvous: rendezvous::client::Behaviour::new(identity),
redial,
},
rendezvous_nodes,
backoffs,
}
}
/// Registers the rendezvous node at the given index.
/// Also sets the registration status to [`RegistrationStatus::Pending`].
pub fn register(&mut self, node_index: usize) -> Result<(), RegisterError> {
let node = &mut self.rendezvous_nodes[node_index];
node.set_registration(RegistrationStatus::Pending);
let (namespace, peer_id, ttl) =
(node.namespace.into(), node.peer_id, node.registration_ttl);
self.inner.rendezvous.register(namespace, peer_id, ttl)
}
}
impl NetworkBehaviour for Behaviour {
type ConnectionHandler = <InnerBehaviour as NetworkBehaviour>::ConnectionHandler;
type ToSwarm = InnerBehaviourEvent;
fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
match event {
FromSwarm::ConnectionEstablished(connection) => {
let peer_id = connection.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);
// Reset backoff on successful connection
if let Some(backoff) = self.backoffs.get_mut(&peer_id) {
backoff.reset();
}
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");
});
}
}
}
FromSwarm::ConnectionClosed(connection) => {
let peer_id = connection.peer_id;
// Update the connection status of the rendezvous node that disconnected.
if let Some(node) = self
.rendezvous_nodes
.iter_mut()
.find(|node| node.peer_id == peer_id)
{
node.set_connection(ConnectionStatus::Disconnected);
}
}
FromSwarm::DialFailure(dial_failure) => {
// Update the connection status of the rendezvous node that failed to connect.
if let Some(peer_id) = dial_failure.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 poll(
&mut self,
cx: &mut Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
// Check the status of each rendezvous node
for i in 0..self.rendezvous_nodes.len() {
let connection_status = self.rendezvous_nodes[i].connection_status.clone();
match &mut self.rendezvous_nodes[i].registration_status {
RegistrationStatus::RegisterOnNextConnection => match connection_status {
ConnectionStatus::Disconnected => {}
ConnectionStatus::Connected => {
let _ = self.register(i);
}
},
RegistrationStatus::Registered { re_register_in } => {
if let Poll::Ready(()) = re_register_in.poll_unpin(cx) {
match connection_status {
ConnectionStatus::Connected => {
let _ = self.register(i).inspect_err(|err| {
tracing::error!(
error=%err,
rendezvous_node=%self.rendezvous_nodes[i].peer_id,
"Failed to register with rendezvous node");
});
}
ConnectionStatus::Disconnected => {
self.rendezvous_nodes[i].set_registration(
RegistrationStatus::RegisterOnNextConnection,
);
}
}
}
}
RegistrationStatus::Pending => {}
}
}
let inner_poll = self.inner.poll(cx);
// Reset the timer for the specific rendezvous node if we successfully registered
if let Poll::Ready(ToSwarm::GenerateEvent(InnerBehaviourEvent::Rendezvous(
rendezvous::client::Event::Registered {
ttl,
rendezvous_node,
..
},
))) = &inner_poll
{
if let Some(i) = self
.rendezvous_nodes
.iter()
.position(|n| &n.peer_id == rendezvous_node)
{
let half_of_ttl = Duration::from_secs(*ttl) / 2;
let re_register_in = Box::pin(tokio::time::sleep(half_of_ttl));
let status = RegistrationStatus::Registered { re_register_in };
self.rendezvous_nodes[i].set_registration(status);
}
}
inner_poll
}
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 handle_established_inbound_connection(
&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,
// Create peer that registers at the rendezvous node
let mut registrar = new_swarm(|identity| {
register::Behaviour::new(
identity,
vec![rendezvous_peer_id],
XmrBtcNamespace::Testnet.into(),
)
}
});
registrar.add_peer_address(rendezvous_peer_id, rendezvous_addr.clone());
registrar.listen_on_random_memory_address().await;
let registrar_id = *registrar.local_peer_id();
fn handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
addr: &Multiaddr,
role_override: libp2p::core::Endpoint,
) -> Result<THandler<Self>, ConnectionDenied> {
self.inner.handle_established_outbound_connection(
connection_id,
peer,
addr,
role_override,
// Create peer that discovers the
let mut discoverer = new_swarm(|identity| {
discovery::Behaviour::new(
identity,
vec![rendezvous_peer_id],
XmrBtcNamespace::Testnet.into(),
)
}
});
discoverer.add_peer_address(rendezvous_peer_id, rendezvous_addr);
fn handle_pending_outbound_connection(
&mut self,
connection_id: ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[Multiaddr],
effective_role: libp2p::core::Endpoint,
) -> std::result::Result<Vec<Multiaddr>, ConnectionDenied> {
self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)
}
let registrar_task = tokio::spawn(async move {
loop {
registrar.next().await;
}
});
fn handle_pending_inbound_connection(
&mut self,
connection_id: ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.inner
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
// Now wait until discovery wrapper discovers registrar and dials it.
let discovery_task = tokio::spawn(async move {
let mut saw_discovery = false;
let mut saw_address = false;
loop {
match discoverer.select_next_some().await {
SwarmEvent::Behaviour(discovery::Event::DiscoveredPeer { peer_id })
if peer_id == registrar_id =>
{
saw_discovery = true;
}
SwarmEvent::NewExternalAddrOfPeer { peer_id, .. }
if peer_id == registrar_id =>
{
saw_address = true;
}
_ => {}
}
if saw_discovery && saw_address {
break;
}
}
});
tokio::time::timeout(Duration::from_secs(10), discovery_task)
.await
.expect("discovery and direct connection to registrar timed out")
.unwrap();
registrar_task.abort();
rendezvous_handle.abort();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{new_swarm, SwarmExt};
use futures::StreamExt;
use libp2p::rendezvous;
use libp2p::swarm::SwarmEvent;
use std::collections::HashMap;
/// Spawns a rendezvous server that continuously processes events
async fn spawn_rendezvous_node() -> (PeerId, Multiaddr, tokio::task::JoinHandle<()>) {
let mut rendezvous_node = new_swarm(|_| {
rendezvous::server::Behaviour::new(
rendezvous::server::Config::default().with_min_ttl(2),
)
});
let address = rendezvous_node.listen_on_random_memory_address().await;
let peer_id = *rendezvous_node.local_peer_id();
#[tokio::test]
async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node(
) {
let mut rendezvous_node = new_swarm(|_| {
rendezvous::server::Behaviour::new(rendezvous::server::Config::default())
});
let address = rendezvous_node.listen_on_random_memory_address().await;
let rendezvous_point = RendezvousNode::new(
&address,
rendezvous_node.local_peer_id().to_owned(),
XmrBtcNamespace::Testnet,
None,
);
let mut asb =
new_swarm(|identity| super::Behaviour::new(identity, vec![rendezvous_point]));
asb.listen_on_random_memory_address().await;
tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
let asb_registered = tokio::spawn(async move {
loop {
if let SwarmEvent::Behaviour(InnerBehaviourEvent::Rendezvous(
rendezvous::client::Event::Registered { .. },
)) = asb.select_next_some().await
{
break;
}
}
});
tokio::time::timeout(Duration::from_secs(10), asb_registered)
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn asb_automatically_re_registers() {
let mut rendezvous_node = new_swarm(|_| {
rendezvous::server::Behaviour::new(
rendezvous::server::Config::default().with_min_ttl(2),
)
});
let address = rendezvous_node.listen_on_random_memory_address().await;
let rendezvous_point = RendezvousNode::new(
&address,
rendezvous_node.local_peer_id().to_owned(),
XmrBtcNamespace::Testnet,
Some(5),
);
let mut asb =
new_swarm(|identity| super::Behaviour::new(identity, vec![rendezvous_point]));
asb.listen_on_random_memory_address().await;
tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
let asb_registered_three_times = tokio::spawn(async move {
let mut number_of_registrations = 0;
loop {
if let SwarmEvent::Behaviour(InnerBehaviourEvent::Rendezvous(
rendezvous::client::Event::Registered { .. },
)) = asb.select_next_some().await
{
number_of_registrations += 1
}
if number_of_registrations == 3 {
break;
}
}
});
tokio::time::timeout(Duration::from_secs(30), asb_registered_three_times)
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn asb_registers_multiple() {
let registration_ttl = Some(10);
let mut rendezvous_nodes = Vec::new();
let mut registrations = HashMap::new();
// Register with 5 rendezvous nodes
for _ in 0..5 {
let mut rendezvous = new_swarm(|_| {
rendezvous::server::Behaviour::new(
rendezvous::server::Config::default().with_min_ttl(2),
)
});
let address = rendezvous.listen_on_random_memory_address().await;
let id = *rendezvous.local_peer_id();
registrations.insert(id, 0);
rendezvous_nodes.push(RendezvousNode::new(
&address,
*rendezvous.local_peer_id(),
XmrBtcNamespace::Testnet,
registration_ttl,
));
tokio::spawn(async move {
loop {
rendezvous.next().await;
}
});
let handle = tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
let mut asb =
new_swarm(|identity| register::Behaviour::new(identity, rendezvous_nodes));
asb.listen_on_random_memory_address().await; // this adds an external address
let handle = tokio::spawn(async move {
loop {
if let SwarmEvent::Behaviour(InnerBehaviourEvent::Rendezvous(
rendezvous::client::Event::Registered {
rendezvous_node, ..
},
)) = asb.select_next_some().await
{
registrations
.entry(rendezvous_node)
.and_modify(|counter| *counter += 1);
}
if registrations.iter().all(|(_, &count)| count >= 4) {
break;
}
}
});
tokio::time::timeout(Duration::from_secs(30), handle)
.await
.unwrap()
.unwrap();
}
(peer_id, address, handle)
}
}

View file

@ -0,0 +1,289 @@
use futures::future::{self};
use futures::FutureExt;
use libp2p::{
identity, rendezvous,
swarm::{NetworkBehaviour, THandlerInEvent, ToSwarm},
Multiaddr, PeerId,
};
use std::{
collections::{HashSet, VecDeque},
task::Poll,
};
use crate::{
behaviour_util::{BackoffTracker, ConnectionTracker},
futures_util::FuturesHashSet,
protocols::redial,
};
pub struct Behaviour {
inner: InnerBehaviour,
namespace: rendezvous::Namespace,
// Set of all (peer_id, address) pairs that we have discovered over the lifetime of the behaviour
// This is also used to avoid notifying the swarm about the same pair multiple times
discovered: HashSet<(PeerId, Multiaddr)>,
// Track all the connections internally
connection_tracker: ConnectionTracker,
// Backoff for each rendezvous node for discovery
backoff: BackoffTracker,
// Queue of discovery requests to send to a rendezvous node
// once the future resolves, the peer id is removed from the queue and a discovery request is sent
pending_to_discover: FuturesHashSet<PeerId, ()>,
// Queue of peers to send a request to as soon as we are connected to them
to_discover: VecDeque<PeerId>,
// Queue of events to be sent to the swarm
to_swarm: VecDeque<ToSwarm<Event, THandlerInEvent<Self>>>,
}
// This could use notice to recursively discover other rendezvous nodes
#[derive(NetworkBehaviour)]
pub struct InnerBehaviour {
rendezvous: libp2p::rendezvous::client::Behaviour,
redial: redial::Behaviour,
}
#[derive(Debug)]
pub enum Event {
DiscoveredPeer { peer_id: PeerId },
}
impl Behaviour {
pub fn new(
identity: identity::Keypair,
rendezvous_nodes: Vec<PeerId>,
namespace: rendezvous::Namespace,
) -> Self {
let mut redial = redial::Behaviour::new(
"rendezvous-discovery",
crate::defaults::REDIAL_INITIAL_INTERVAL,
crate::defaults::REDIAL_MAX_INTERVAL,
);
let rendezvous = libp2p::rendezvous::client::Behaviour::new(identity);
let backoff = BackoffTracker::new(
crate::defaults::DISCOVERY_INITIAL_INTERVAL,
crate::defaults::DISCOVERY_MAX_INTERVAL,
crate::defaults::BACKOFF_MULTIPLIER,
);
let mut to_discover = FuturesHashSet::new();
// Initialize backoff for each rendezvous node
for node in &rendezvous_nodes {
// We initially schedule a discovery request for each rendezvous node
to_discover.insert(node.clone(), future::ready(()).boxed());
// We instruct the redial behaviour to dial rendezvous nodes periodically
redial.add_peer(node.clone());
}
Self {
inner: InnerBehaviour { rendezvous, redial },
discovered: HashSet::new(),
backoff,
pending_to_discover: to_discover,
to_discover: VecDeque::new(),
connection_tracker: ConnectionTracker::new(),
namespace,
to_swarm: VecDeque::new(),
}
}
}
impl NetworkBehaviour for Behaviour {
// We use a dummy connection handler here as we don't need low level connection handling
// This is handled by the inner behaviour
type ConnectionHandler = <InnerBehaviour as NetworkBehaviour>::ConnectionHandler;
type ToSwarm = Event;
fn poll(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<libp2p::swarm::ToSwarm<Self::ToSwarm, libp2p::swarm::THandlerInEvent<Self>>>
{
// Check if a backoff timer for a peer has resolved
while let Poll::Ready(Some((peer_id, _))) = self.pending_to_discover.poll_next_unpin(cx) {
// Request is ready to be dispatched
self.to_discover.push_back(peer_id);
}
// Take ownership of the queue to avoid borrow checker issues
let to_discover = std::mem::take(&mut self.to_discover);
self.to_discover = to_discover
.into_iter()
.filter(|peer| {
// If we are not connected to the peer, keep it in the queue
if !self.connection_tracker.is_connected(peer) {
return true;
}
// If we are connected to the peer, send a discovery request
self.inner.rendezvous.discover(
Some(self.namespace.clone()),
None,
None,
peer.clone(),
);
false
})
.collect();
while let Poll::Ready(event) = self.inner.poll(cx) {
match event {
ToSwarm::GenerateEvent(InnerBehaviourEvent::Rendezvous(
libp2p::rendezvous::client::Event::Discovered {
rendezvous_node,
registrations,
..
},
)) => {
tracing::trace!(
?rendezvous_node,
num_registrations = %registrations.len(),
"Discovered peers at rendezvous node"
);
for registration in registrations {
for address in registration.record.addresses() {
let peer_id = registration.record.peer_id();
if self.discovered.insert((peer_id, address.clone())) {
self.to_swarm.push_back(ToSwarm::NewExternalAddrOfPeer {
peer_id,
address: address.clone(),
});
self.to_swarm.push_back(ToSwarm::GenerateEvent(
Event::DiscoveredPeer { peer_id },
));
tracing::trace!(
?rendezvous_node,
?peer_id,
?address,
"Discovered peer at rendezvous node"
);
}
self.pending_to_discover.insert(
rendezvous_node,
tokio::time::sleep(crate::defaults::DISCOVERY_INTERVAL).boxed(),
);
}
}
continue;
}
ToSwarm::GenerateEvent(InnerBehaviourEvent::Rendezvous(
libp2p::rendezvous::client::Event::DiscoverFailed {
rendezvous_node,
error,
namespace: _,
},
)) => {
let backoff = self.backoff.increment(&rendezvous_node);
self.pending_to_discover
.insert(rendezvous_node, tokio::time::sleep(backoff).boxed());
tracing::error!(
?rendezvous_node,
?error,
seconds_until_next_discovery_attempt = %backoff.as_secs(),
"Failed to discover peers at rendezvous node, scheduling retry after backoff"
);
continue;
}
ToSwarm::GenerateEvent(_) => {
// TODO: Do anything with these?
}
other => {
return Poll::Ready(other.map_out(|_| {
unreachable!("we handled all generated events in the arm above")
}))
}
}
}
// Check if we have any events to send to the swarm
if let Some(event) = self.to_swarm.pop_front() {
return Poll::Ready(event);
}
Poll::Pending
}
fn on_swarm_event(&mut self, event: libp2p::swarm::FromSwarm) {
self.connection_tracker.handle_swarm_event(event);
self.inner.on_swarm_event(event);
}
fn handle_established_inbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
peer: PeerId,
local_addr: &libp2p::Multiaddr,
remote_addr: &libp2p::Multiaddr,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
self.inner.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)
}
fn handle_established_outbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
peer: PeerId,
addr: &libp2p::Multiaddr,
role_override: libp2p::core::Endpoint,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
self.inner
.handle_established_outbound_connection(connection_id, peer, addr, role_override)
}
fn on_connection_handler_event(
&mut self,
peer_id: PeerId,
connection_id: libp2p::swarm::ConnectionId,
event: libp2p::swarm::THandlerOutEvent<Self>,
) {
self.inner
.on_connection_handler_event(peer_id, connection_id, event);
}
fn handle_pending_inbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), libp2p::swarm::ConnectionDenied> {
self.inner
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: libp2p::swarm::ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[Multiaddr],
effective_role: libp2p::core::Endpoint,
) -> Result<Vec<Multiaddr>, libp2p::swarm::ConnectionDenied> {
self.connection_tracker
.handle_pending_outbound_connection(connection_id, maybe_peer);
self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)
}
}

View file

@ -0,0 +1,445 @@
use crate::behaviour_util::{AddressTracker, BackoffTracker, ConnectionTracker};
use crate::futures_util::FuturesHashSet;
use crate::protocols::redial;
use futures::{future, FutureExt};
use libp2p::rendezvous::client::RegisterError;
use libp2p::rendezvous::ErrorCode;
use libp2p::swarm::{
ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent,
THandlerOutEvent, ToSwarm,
};
use libp2p::{identity, rendezvous, Multiaddr, PeerId};
use std::collections::{HashSet, VecDeque};
use std::task::{Context, Poll};
use std::time::Duration;
// TODO: This could use notice to recursively discover other rendezvous nodes
// TODO: Get the tests working again
pub struct Behaviour {
inner: InnerBehaviour,
rendezvous_nodes: Vec<PeerId>,
namespace: rendezvous::Namespace,
backoffs: BackoffTracker,
connections: ConnectionTracker,
address: AddressTracker,
// Set of all peers that we think we are registered at
registered: HashSet<PeerId>,
// Register at these as soon as we are connected to them
to_dispatch: VecDeque<PeerId>,
// Move these into `to_dispatch` once the future resolves
pending_to_dispatch: FuturesHashSet<PeerId, ()>,
to_swarm: VecDeque<Event>,
}
#[derive(NetworkBehaviour)]
pub struct InnerBehaviour {
rendezvous: rendezvous::client::Behaviour,
redial: redial::Behaviour,
}
#[derive(Debug)]
pub enum Event {
Registered {
peer_id: PeerId,
},
RegisterDispatchFailed {
peer_id: PeerId,
error: RegisterError,
},
RegisterRequestFailed {
peer_id: PeerId,
error: ErrorCode,
},
}
pub mod public {
use libp2p::{Multiaddr, PeerId};
#[derive(Debug, Clone)]
pub struct RendezvousNodeStatus {
pub peer_id: PeerId,
pub address: Option<Multiaddr>,
pub is_connected: bool,
pub registration: RegistrationStatus,
}
#[derive(Debug, Clone)]
pub enum RegistrationStatus {
Registered,
WillRegisterAfterDelay,
RegisterOnceConnected,
RequestInflight,
}
}
impl Behaviour {
const REDIAL_IDENTIFIER: &str = "rendezvous-register";
pub fn new(
identity: identity::Keypair,
rendezvous_nodes: Vec<PeerId>,
namespace: rendezvous::Namespace,
) -> Self {
let backoffs = BackoffTracker::new(
crate::defaults::RENDEZVOUS_RETRY_INITIAL_INTERVAL,
crate::defaults::RENDEZVOUS_RETRY_MAX_INTERVAL,
crate::defaults::BACKOFF_MULTIPLIER,
);
let mut redial = redial::Behaviour::new(
Self::REDIAL_IDENTIFIER,
crate::defaults::REDIAL_INITIAL_INTERVAL,
crate::defaults::REDIAL_MAX_INTERVAL,
);
let mut pending_to_dispatch = FuturesHashSet::new();
for &peer_id in &rendezvous_nodes {
// We want to redial all of the nodes periodically because we only dispatch requests once we are connected
redial.add_peer(peer_id.clone());
// Schedule an intitial register
pending_to_dispatch.insert(peer_id, Box::pin(future::ready(())));
}
Self {
inner: InnerBehaviour {
rendezvous: rendezvous::client::Behaviour::new(identity),
redial,
},
connections: ConnectionTracker::new(),
address: AddressTracker::new(),
registered: HashSet::new(),
rendezvous_nodes,
backoffs,
to_dispatch: VecDeque::new(),
pending_to_dispatch,
namespace,
to_swarm: VecDeque::new(),
}
}
pub fn status(&self) -> Vec<public::RendezvousNodeStatus> {
self.rendezvous_nodes
.iter()
.map(|peer_id| {
let registration = {
if self.registered.contains(peer_id) {
public::RegistrationStatus::Registered
} else if self.to_dispatch.contains(peer_id) {
public::RegistrationStatus::RegisterOnceConnected
} else {
public::RegistrationStatus::WillRegisterAfterDelay
}
};
public::RendezvousNodeStatus {
peer_id: peer_id.clone(),
address: self.address.last_seen_address(peer_id),
is_connected: self.connections.is_connected(peer_id),
registration,
}
})
.collect()
}
pub fn schedule_re_register_replace(&mut self, peer_id: PeerId) -> Duration {
let backoff = self.backoffs.get(&peer_id).current_interval;
// We replace any existing timeout
self.pending_to_dispatch
.replace(peer_id, Box::pin(tokio::time::sleep(backoff).boxed()));
backoff
}
}
impl NetworkBehaviour for Behaviour {
type ConnectionHandler = <InnerBehaviour as NetworkBehaviour>::ConnectionHandler;
type ToSwarm = Event;
fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
self.connections.handle_swarm_event(event);
self.address.handle_swarm_event(event);
self.inner.on_swarm_event(event);
if let FromSwarm::ConnectionClosed(connection_closed) = event {
// We disconnected from a node where we were registered, so we schedule a re-register
if self.registered.contains(&connection_closed.peer_id) {
let backoff = self.schedule_re_register_replace(connection_closed.peer_id);
tracing::info!(
?connection_closed.peer_id,
seconds_until_next_registration_attempt = %backoff.as_secs(),
"Disconnected from rendezvous node, scheduling re-register after backoff"
);
}
}
}
fn poll(
&mut self,
cx: &mut Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
// Check if any of the futures resolved
while let Poll::Ready(Some((peer_id, _))) = self.pending_to_dispatch.poll_next_unpin(cx) {
self.to_dispatch.push_back(peer_id);
// We assume that if we have queued a register to be dispatched, then we are not registed anymore
// because we only queue a register if we failed to register or the ttl expired
self.registered.remove(&peer_id);
}
// Take ownership of the queue to avoid borrow checker issues
let to_dispatch = std::mem::take(&mut self.to_dispatch);
self.to_dispatch = to_dispatch
.into_iter()
.filter(|peer| {
if !self.connections.is_connected(peer) {
return true;
}
// If we are connected to the peer, register with them
if let Err(err) = self.inner.rendezvous.register(self.namespace.clone(), peer.clone(), None) {
// We failed to dispatch the register, so we backoff
self.backoffs.increment(&peer);
// Schedule a re-register
let backoff = self.schedule_re_register_replace(*peer);
tracing::error!(
?peer,
?err,
seconds_until_next_registration_attempt = %backoff.as_secs(),
"Failed to dispatch register at rendezvous node, scheduling retry after backoff"
);
// Inform swarm
self.to_swarm.push_back(Event::RegisterDispatchFailed { peer_id: peer.clone(), error: err });
}
false
})
.collect();
while let Poll::Ready(event) = self.inner.poll(cx) {
match event {
ToSwarm::GenerateEvent(InnerBehaviourEvent::Rendezvous(
rendezvous::client::Event::Registered {
ttl,
rendezvous_node,
..
},
)) => {
// We successfully registered, so we reset the backoff
self.backoffs.reset(&rendezvous_node);
self.registered.insert(rendezvous_node.clone());
// Schedule a re-registration after half of the TTL
let half_of_ttl = Duration::from_secs(ttl) / 2;
self.pending_to_dispatch.insert(
rendezvous_node.clone(),
tokio::time::sleep(half_of_ttl).boxed(),
);
// Inform swarm
self.to_swarm.push_back(Event::Registered {
peer_id: rendezvous_node,
});
tracing::info!(
?rendezvous_node,
re_register_after_seconds = %half_of_ttl.as_secs(),
"Registered with rendezvous node"
);
}
ToSwarm::GenerateEvent(InnerBehaviourEvent::Rendezvous(
rendezvous::client::Event::RegisterFailed {
rendezvous_node,
namespace: _,
error,
},
)) => {
// We failed to register, so we backoff
let backoff = self.backoffs.increment(&rendezvous_node);
// Inform swarm
self.to_swarm.push_back(Event::RegisterRequestFailed {
peer_id: rendezvous_node,
error,
});
tracing::error!(
?rendezvous_node,
?error,
seconds_until_next_registration_attempt = %backoff.as_secs(),
"Failed to register with rendezvous node, scheduling retry after backoff"
);
// Schedule a retry after the backoff
self.pending_to_dispatch
.insert(rendezvous_node.clone(), tokio::time::sleep(backoff).boxed());
}
ToSwarm::GenerateEvent(_) => {
// swallow all other generated events by the inner swarm
// TODO: Do something with these
}
other => {
return Poll::Ready(other.map_out(|_| {
unreachable!("we handled all generated events in the arm above")
}))
}
}
}
while let Some(event) = self.to_swarm.pop_front() {
return Poll::Ready(ToSwarm::GenerateEvent(event));
}
Poll::Pending
}
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 handle_established_inbound_connection(
&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 handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
addr: &Multiaddr,
role_override: libp2p::core::Endpoint,
) -> Result<THandler<Self>, ConnectionDenied> {
self.inner
.handle_established_outbound_connection(connection_id, peer, addr, role_override)
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[Multiaddr],
effective_role: libp2p::core::Endpoint,
) -> std::result::Result<Vec<Multiaddr>, ConnectionDenied> {
self.connections
.handle_pending_outbound_connection(connection_id, maybe_peer);
self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)
}
fn handle_pending_inbound_connection(
&mut self,
connection_id: ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.inner
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
}
// TODO: Add a test that ensures we re-register after some time
#[cfg(test)]
mod tests {
use super::*;
use crate::protocols::rendezvous::XmrBtcNamespace;
use crate::test::{new_swarm, SwarmExt};
use futures::StreamExt;
use libp2p::rendezvous;
use libp2p::swarm::SwarmEvent;
#[tokio::test]
async fn registers_once_at_two_rendezvous_nodes() {
let (rendezvous_peer_id1, rendezvous_addr1, _) = spawn_rendezvous_server().await;
let (rendezvous_peer_id2, rendezvous_addr2, _) = spawn_rendezvous_server().await;
let mut asb = new_swarm(|identity| {
super::Behaviour::new(
identity,
vec![rendezvous_peer_id1, rendezvous_peer_id2],
XmrBtcNamespace::Testnet.into(),
)
});
asb.add_peer_address(rendezvous_peer_id1, rendezvous_addr1);
asb.add_peer_address(rendezvous_peer_id2, rendezvous_addr2);
// We need to listen on address because otherwise we cannot advertise an address at the rendezvous point
asb.listen_on_random_memory_address().await;
let mut registered = HashSet::new();
let asb_registered_three_times = tokio::spawn(async move {
loop {
if let SwarmEvent::Behaviour(Event::Registered { peer_id }) =
asb.select_next_some().await
{
assert!(peer_id == rendezvous_peer_id1 || peer_id == rendezvous_peer_id2);
registered.insert(peer_id);
}
if registered.contains(&rendezvous_peer_id1)
&& registered.contains(&rendezvous_peer_id2)
{
break;
}
}
});
tokio::time::timeout(Duration::from_secs(5), asb_registered_three_times)
.await
.unwrap()
.unwrap();
}
/// Spawns a rendezvous server that continuously processes events
async fn spawn_rendezvous_server() -> (PeerId, Multiaddr, tokio::task::JoinHandle<()>) {
let mut rendezvous_node = new_swarm(|_| {
rendezvous::server::Behaviour::new(
rendezvous::server::Config::default().with_min_ttl(2),
)
});
let address = rendezvous_node.listen_on_random_memory_address().await;
let peer_id = *rendezvous_node.local_peer_id();
let handle = tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
(peer_id, address, handle)
}
}

View file

@ -4,9 +4,11 @@ use crate::protocols::swap_setup::{
protocol, BlockchainNetwork, SpotPriceError, SpotPriceRequest, SpotPriceResponse,
};
use anyhow::{anyhow, Context, Result};
use futures::future::{BoxFuture, OptionFuture};
use futures::future::BoxFuture;
use futures::stream::FuturesUnordered;
use futures::AsyncWriteExt;
use futures::FutureExt;
use futures::StreamExt;
use libp2p::core::upgrade;
use libp2p::swarm::handler::ConnectionEvent;
use libp2p::swarm::{ConnectionHandler, ConnectionId};
@ -15,7 +17,7 @@ use libp2p::{Multiaddr, PeerId};
use std::collections::VecDeque;
use std::fmt::Debug;
use std::task::Poll;
use std::time::{Duration, Instant};
use std::time::Duration;
use swap_core::bitcoin;
use swap_env::env;
use swap_feed::LatestRate;
@ -218,10 +220,8 @@ where
}
}
type InboundStream = BoxFuture<'static, Result<(Uuid, State3)>>;
pub struct Handler<LR> {
inbound_stream: OptionFuture<InboundStream>,
inbound_streams: FuturesUnordered<BoxFuture<'static, Result<(Uuid, State3)>>>,
events: VecDeque<HandlerOutEvent>,
min_buy: bitcoin::Amount,
@ -233,10 +233,6 @@ pub struct Handler<LR> {
// This is the timeout for the negotiation phase where Alice and Bob exchange messages
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> {
@ -248,15 +244,14 @@ impl<LR> Handler<LR> {
resume_only: bool,
) -> Self {
Self {
inbound_stream: OptionFuture::from(None),
inbound_streams: FuturesUnordered::new(),
events: Default::default(),
min_buy,
max_buy,
env_config,
latest_rate,
resume_only,
negotiation_timeout: Duration::from_secs(120),
keep_alive_until: Some(Instant::now() + Duration::from_secs(30)),
negotiation_timeout: crate::defaults::NEGOTIATION_TIMEOUT,
}
}
}
@ -295,14 +290,13 @@ where
) {
match event {
ConnectionEvent::FullyNegotiatedInbound(substream) => {
self.keep_alive_until = None;
let mut substream = substream.protocol;
let (sender, receiver) = bmrng::channel_with_timeout::<
bitcoin::Amount,
WalletSnapshot,
>(1, Duration::from_secs(60));
let (sender, receiver) =
bmrng::channel_with_timeout::<bitcoin::Amount, WalletSnapshot>(
1,
crate::defaults::SWAP_SETUP_CHANNEL_TIMEOUT,
);
let resume_only = self.resume_only;
let min_buy = self.min_buy;
@ -448,14 +442,14 @@ where
});
let max_seconds = self.negotiation_timeout.as_secs();
self.inbound_stream = OptionFuture::from(Some(
self.inbound_streams.push(
async move {
protocol.await.with_context(|| {
format!("Failed to complete execution setup within {}s", max_seconds)
})?
}
.boxed(),
));
);
self.events.push_back(HandlerOutEvent::Initiated(receiver));
}
@ -474,12 +468,7 @@ where
}
fn connection_keep_alive(&self) -> bool {
// If keep_alive_until is None, we keep the connection alive indefinitely
// If keep_alive_until is Some, we keep the connection alive until the given instant
match self.keep_alive_until {
None => true,
Some(keep_alive_until) => Instant::now() < keep_alive_until,
}
!self.inbound_streams.is_empty()
}
#[allow(clippy::type_complexity)]
@ -495,10 +484,9 @@ where
return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(event));
}
if let Some(result) = futures::ready!(self.inbound_stream.poll_unpin(cx)) {
self.inbound_stream = OptionFuture::from(None);
if let Poll::Ready(Some(result)) = self.inbound_streams.poll_next_unpin(cx) {
// Notify the behaviour that the negotiation phase has been completed
// (either successfully or with an error)
return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(
HandlerOutEvent::Completed(result),
));

View file

@ -1,12 +1,11 @@
use crate::futures_util::FuturesHashSet;
use crate::out_event;
use crate::protocols::swap_setup::{
protocol, BlockchainNetwork, SpotPriceError, SpotPriceResponse,
};
use anyhow::{Context, Result};
use bitcoin_wallet::BitcoinWallet;
use futures::future::{BoxFuture, OptionFuture};
use futures::AsyncWriteExt;
use futures::FutureExt;
use libp2p::core::upgrade;
use libp2p::swarm::behaviour::ConnectionEstablished;
use libp2p::swarm::dial_opts::{DialOpts, PeerCondition};
@ -28,6 +27,8 @@ use uuid::Uuid;
use super::{read_cbor_message, write_cbor_message, SpotPriceRequest};
// TODO: This should use redial::Behaviour to keep connections alive for peers with queued requests
// TODO: Do not use swap_id as key inside the ConnectionHandler, use another key
#[allow(missing_debug_implementations)]
pub struct Behaviour {
env_config: env::Config,
@ -35,20 +36,18 @@ pub struct Behaviour {
// Queue of swap setup request that haven't been assigned to a connection handler yet
// (peer_id, swap_id, new_swap)
new_swaps: VecDeque<(PeerId, Uuid, NewSwap)>,
new_swaps: VecDeque<(PeerId, NewSwap)>,
// Maintains the list of connections handlers for a specific peer
//
// 0. List of connection handlers that are still active but haven't been assigned a swap setup request yet
// 1. List of connection handlers that have died. Once their death is acknowledged / processed, they are removed from the list
connection_handlers: HashMap<PeerId, (VecDeque<ConnectionId>, VecDeque<ConnectionId>)>,
// Maintains the set of all alive connections handlers for a specific peer
connection_handlers: HashMap<PeerId, HashSet<ConnectionId>>,
connection_handler_deaths: VecDeque<(PeerId, ConnectionId)>,
// Queue of completed swaps that we have assigned a connection handler to but where we haven't notified the ConnectionHandler yet
// We notify the ConnectionHandler by emitting a ConnectionHandlerEvent::NotifyBehaviour event
assigned_unnotified_swaps: VecDeque<(ConnectionId, PeerId, Uuid, NewSwap)>,
// Maintains the list of requests that we have sent to a connection handler but haven't yet received a response
inflight_requests: HashMap<ConnectionId, (Uuid, PeerId)>,
inflight_requests: HashMap<ConnectionId, HashSet<(Uuid, PeerId)>>,
// Queue of swap setup results that we want to notify the Swarm about
to_swarm: VecDeque<SwapSetupResult>,
@ -67,44 +66,26 @@ impl Behaviour {
assigned_unnotified_swaps: VecDeque::default(),
inflight_requests: HashMap::default(),
connection_handlers: HashMap::default(),
connection_handler_deaths: VecDeque::default(),
to_dial: VecDeque::default(),
}
}
pub async fn start(&mut self, alice_peer_id: PeerId, swap: NewSwap) {
pub fn queue_new_swap(&mut self, alice_peer_id: PeerId, swap: NewSwap) {
tracing::trace!(
%alice_peer_id,
?swap,
"Queuing new swap setup request inside the Behaviour",
);
// TODO: This is a bit redundant because we already have the swap_id in the NewSwap struct
self.new_swaps
.push_back((alice_peer_id, swap.swap_id, swap));
self.new_swaps.push_back((alice_peer_id, swap));
self.to_dial.push_back(alice_peer_id);
}
// Returns a mutable reference to the queues of the connection handlers for a specific peer
fn connection_handlers_mut(
&mut self,
peer_id: PeerId,
) -> &mut (VecDeque<ConnectionId>, VecDeque<ConnectionId>) {
fn connection_handlers_mut(&mut self, peer_id: PeerId) -> &mut HashSet<ConnectionId> {
self.connection_handlers.entry(peer_id).or_default()
}
// Returns a mutable reference to the queues of the connection handlers for a specific peer
fn alive_connection_handlers_mut(&mut self, peer_id: PeerId) -> &mut VecDeque<ConnectionId> {
&mut self.connection_handlers_mut(peer_id).0
}
// Returns a mutable reference to the queues of the connection handlers for a specific peer
fn dead_connection_handlers_mut(&mut self, peer_id: PeerId) -> &mut VecDeque<ConnectionId> {
&mut self.connection_handlers_mut(peer_id).1
}
fn known_peers(&self) -> HashSet<PeerId> {
self.connection_handlers.keys().copied().collect()
}
}
impl NetworkBehaviour for Behaviour {
@ -118,6 +99,13 @@ impl NetworkBehaviour for Behaviour {
_local_addr: &Multiaddr,
_remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
// This should never be called as Bob does not support inbound substreams
// TODO: Can this still be called somehow by libp2p? Can we forbid this?
debug_assert!(
false,
"Bob does not listen so he should never get an inbound connection"
);
Ok(Handler::new(self.env_config, self.bitcoin_wallet.clone()))
}
@ -146,8 +134,7 @@ impl NetworkBehaviour for Behaviour {
"A new connection handler has been established",
);
self.alive_connection_handlers_mut(peer_id)
.push_back(connection_id);
self.connection_handlers_mut(peer_id).insert(connection_id);
}
FromSwarm::ConnectionClosed(ConnectionClosed {
peer_id,
@ -157,11 +144,11 @@ impl NetworkBehaviour for Behaviour {
tracing::trace!(
peer = %peer_id,
connection_id = %connection_id,
"A swap setup connection handler has died",
"A connection handler has died",
);
self.dead_connection_handlers_mut(peer_id)
.push_back(connection_id);
self.connection_handler_deaths
.push_back((peer_id, connection_id));
}
_ => {}
}
@ -173,14 +160,21 @@ impl NetworkBehaviour for Behaviour {
connection_id: libp2p::swarm::ConnectionId,
result: THandlerOutEvent<Self>,
) {
if let Some((swap_id, peer)) = self.inflight_requests.remove(&connection_id) {
assert_eq!(peer, event_peer_id);
let (handler_swap_id, result) = result;
if self
.inflight_requests
.get_mut(&connection_id)
.map(|swap_ids| swap_ids.remove(&(handler_swap_id, event_peer_id)))
.unwrap_or(false)
{
self.to_swarm.push_back(SwapSetupResult {
peer,
swap_id,
peer: event_peer_id,
swap_id: handler_swap_id,
result,
});
} else {
debug_assert!(false, "Received a swap setup result from a connection handler for which we have no inflight request stored");
}
}
@ -200,6 +194,7 @@ impl NetworkBehaviour for Behaviour {
// Forward any peers that we want to dial to the Swarm
if let Some(peer) = self.to_dial.pop_front() {
// TODO: We need to redial here!!
tracing::trace!(
peer = %peer,
"Instructing swarm to dial a new connection handler for a swap setup request",
@ -212,59 +207,49 @@ impl NetworkBehaviour for Behaviour {
});
}
// Remove any unused already dead connection handlers that were never assigned a request
for peer in self.known_peers() {
let (alive_connection_handlers, dead_connection_handlers) =
self.connection_handlers_mut(peer);
// Create sets for efficient lookup
let alive_set: HashSet<_> = alive_connection_handlers.iter().copied().collect();
let dead_set: HashSet<_> = dead_connection_handlers.iter().copied().collect();
// Remove from alive any handlers that are also in dead
alive_connection_handlers.retain(|id| !dead_set.contains(id));
// Remove from dead any handlers that were in alive (the overlap we just processed)
dead_connection_handlers.retain(|id| !alive_set.contains(id));
}
// Go through our new_swaps and try to assign a request to a connection handler
//
// If we find a connection handler for the peer, it will be removed from new_swaps
// If we don't find a connection handler for the peer, it will remain in new_swaps
{
let new_swaps = &mut self.new_swaps;
let connection_handlers = &mut self.connection_handlers;
let assigned_unnotified_swaps = &mut self.assigned_unnotified_swaps;
let mut remaining = std::collections::VecDeque::new();
for (peer, swap_id, new_swap) in new_swaps.drain(..) {
if let Some(connection_id) =
connection_handlers.entry(peer).or_default().0.pop_front()
{
assigned_unnotified_swaps.push_back((connection_id, peer, swap_id, new_swap));
} else {
remaining.push_back((peer, swap_id, new_swap));
}
}
*new_swaps = remaining;
}
// If a connection handler died which had an assigned swap setup request,
// we need to notify the swarm that the request failed
for peer_id in self.known_peers() {
while let Some(connection_id) = self.dead_connection_handlers_mut(peer_id).pop_front() {
if let Some((swap_id, _)) = self.inflight_requests.remove(&connection_id) {
// Check for dead connection handlers
// Important: This must be done at the top of the function to avoid assigning new swaps to dead connection handlers
while let Some((peer_id, connection_id)) = self.connection_handler_deaths.pop_front() {
// Did the connection handler have any assigned swap setup request?
// If it did, we need to notify the swarm that the request failed
if let Some(swap_ids) = self.inflight_requests.remove(&connection_id) {
for (swap_id, peer_id) in swap_ids {
self.to_swarm.push_back(SwapSetupResult {
peer: peer_id,
swap_id,
result: Err(anyhow::anyhow!("Connection handler for peer {} has died after we notified it of the swap setup request", peer_id)),
result: Err(anyhow::anyhow!("Connection handler for peer died after we notified it of the swap setup request")),
});
}
}
// After handling inflight request, remove the connection handler from the list
self.connection_handlers
.get_mut(&peer_id)
.map(|connection_ids| connection_ids.remove(&connection_id));
}
self.new_swaps.retain(|(peer, new_swap)| {
// Check if we have any open connection handlers for this peer
if let Some(connection_ids) = self.connection_handlers.get(&peer) {
// Choose the first one and assign it to the new swap
if let Some(connection_id) = connection_ids.iter().next() {
// TODO: Double swap_id is useless
self.assigned_unnotified_swaps.push_back((
*connection_id,
peer.clone(),
new_swap.swap_id.clone(),
new_swap.clone(),
));
// Remove the swap from queue
return false;
}
}
// Keep in queue as we didn't find a connection handler for this peer
true
});
// Iterate through our assigned_unnotified_swaps queue (with popping)
if let Some((connection_id, peer_id, swap_id, new_swap)) =
self.assigned_unnotified_swaps.pop_front()
@ -276,66 +261,61 @@ impl NetworkBehaviour for Behaviour {
"Dispatching swap setup request from Behaviour to a specific connection handler",
);
// Check if the connection handler is still alive
if let Some(dead_connection_handler) = self
.dead_connection_handlers_mut(peer_id)
.iter()
.position(|id| *id == connection_id)
{
self.dead_connection_handlers_mut(peer_id)
.remove(dead_connection_handler);
// ConnectionHandler must still be alive
// If it wasn't we'd have removed it from the list at the start of poll(..)
tracing::trace!(
peer = %peer_id,
swap_id = %swap_id,
?new_swap,
"Notifying connection handler of the swap setup request. We are assuming it is still alive.",
);
self.to_swarm.push_back(SwapSetupResult {
peer: peer_id,
swap_id,
result: Err(anyhow::anyhow!("Connection handler for peer {} has died before we could notify it of the swap setup request", peer_id)),
});
} else {
// ConnectionHandler must still be alive, notify it of the swap setup request
tracing::trace!(
peer = %peer_id,
swap_id = %swap_id,
?new_swap,
"Notifying connection handler of the swap setup request. We are assuming it is still alive.",
);
self.inflight_requests
.entry(connection_id)
.or_default()
.insert((swap_id, peer_id));
self.inflight_requests
.insert(connection_id, (swap_id, peer_id));
return Poll::Ready(ToSwarm::NotifyHandler {
peer_id,
handler: libp2p::swarm::NotifyHandler::One(connection_id),
event: new_swap,
});
}
return Poll::Ready(ToSwarm::NotifyHandler {
peer_id,
handler: libp2p::swarm::NotifyHandler::One(connection_id),
event: new_swap,
});
}
Poll::Pending
}
}
type OutboundStream = BoxFuture<'static, Result<State2, Error>>;
pub struct Handler {
outbound_stream: OptionFuture<OutboundStream>,
// Configuration
env_config: env::Config,
timeout: Duration,
new_swaps: VecDeque<NewSwap>,
bitcoin_wallet: Arc<dyn BitcoinWallet>,
keep_alive: bool,
// Queue of swap setup requests that do not have an inflight substream negotiation
new_swaps: VecDeque<NewSwap>,
// When we have instructed the Behaviour to start a new outbound substream, we store the swap id here
// Eventually we will either get a fully negotiated outbound substream or a dial upgrade error
inflight_substream_negotiations: HashSet<Uuid>,
// Inflight swap setup requests that we have a fully negotiated outbound substream for
outbound_streams: FuturesHashSet<Uuid, Result<State2, Error>>,
// Queue of swap setup results that we want to notify the Behaviour about
to_behaviour: VecDeque<(Uuid, Result<State2>)>,
}
impl Handler {
fn new(env_config: env::Config, bitcoin_wallet: Arc<dyn BitcoinWallet>) -> Self {
Self {
env_config,
outbound_stream: OptionFuture::from(None),
timeout: Duration::from_secs(120),
new_swaps: VecDeque::default(),
timeout: crate::defaults::NEGOTIATION_TIMEOUT,
bitcoin_wallet,
// TODO: This will keep ALL connections alive indefinitely
// which is not optimal
keep_alive: true,
new_swaps: VecDeque::default(),
outbound_streams: FuturesHashSet::default(),
to_behaviour: VecDeque::default(),
inflight_substream_negotiations: HashSet::default(),
}
}
}
@ -359,7 +339,7 @@ pub struct SwapSetupResult {
impl ConnectionHandler for Handler {
type FromBehaviour = NewSwap;
type ToBehaviour = Result<State2>;
type ToBehaviour = (Uuid, Result<State2>);
type InboundProtocol = upgrade::DeniedUpgrade;
type OutboundProtocol = protocol::SwapSetup;
type InboundOpenInfo = ();
@ -381,24 +361,25 @@ impl ConnectionHandler for Handler {
>,
) {
match event {
libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedInbound(_) => {
// TODO: Maybe warn here as Bob does not support inbound substreams?
}
libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedOutbound(outbound) => {
let mut substream = outbound.protocol;
let new_swap_request = outbound.info;
libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedOutbound(
libp2p::swarm::handler::FullyNegotiatedOutbound {
protocol: mut substream,
info,
},
) => {
let swap_id = info.swap_id;
// We got the substream, so its no longer inflight
self.inflight_substream_negotiations.remove(&swap_id);
let bitcoin_wallet = self.bitcoin_wallet.clone();
let env_config = self.env_config;
// This runs runs the actual negotiation protocol
// It is wrapped in a timeout to protect against the case where the peer does not respond
let protocol = tokio::time::timeout(self.timeout, async move {
let result = run_swap_setup(
&mut substream,
new_swap_request,
env_config,
bitcoin_wallet,
)
.await;
let result =
run_swap_setup(&mut substream, info, env_config, bitcoin_wallet).await;
result.map_err(|err: anyhow::Error| {
tracing::error!(?err, "Error occurred during swap setup protocol");
@ -408,40 +389,59 @@ impl ConnectionHandler for Handler {
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));
let did_replace_existing_future = self.outbound_streams.replace(
swap_id,
Box::pin(async move {
protocol.await.map_err(|_| Error::Timeout {
seconds: max_seconds,
})?
}),
);
// In poll(..), we ensure that we never dispatch multiple concurrent swap setup requests for the same swap on the same ConnectionHandler
// This invariant should therefore never be violated
// TODO: Is this truly true?
assert!(!did_replace_existing_future, "Replacing an existing inflight swap setup request is not allowed. We should have checked for this invariant before instructing the Behaviour to start a substream.");
}
libp2p::swarm::handler::ConnectionEvent::AddressChange(address_change) => {
tracing::trace!(
?address_change,
"Connection address changed during swap setup"
libp2p::swarm::handler::ConnectionEvent::DialUpgradeError(
libp2p::swarm::handler::DialUpgradeError { info, error },
) => {
// We failed to get a fully negotiated outbound substream, so its no longer inflight
self.inflight_substream_negotiations.remove(&info.swap_id);
tracing::error!(%error, "Dial upgrade error during swap setup substream negotiation. Propagating error back to the Behaviour");
self.to_behaviour.push_back((
info.swap_id,
Err(anyhow::Error::from(error)
.context("Dial upgrade error during swap setup. The peer may not support the swap setup protocol.")),
));
}
libp2p::swarm::handler::ConnectionEvent::ListenUpgradeError(_)
| libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedInbound(_) => {
// This should never be called as Bob does not support inbound substreams
// TODO: Maybe warn here as Bob does not support inbound substreams?
debug_assert!(
false,
"Bob does not support inbound substreams for the swap setup protocol"
);
}
libp2p::swarm::handler::ConnectionEvent::DialUpgradeError(dial_upgrade_error) => {
tracing::trace!(error = %dial_upgrade_error.error, "Dial upgrade error during swap setup");
}
libp2p::swarm::handler::ConnectionEvent::ListenUpgradeError(listen_upgrade_error) => {
tracing::trace!(
?listen_upgrade_error,
"Listen upgrade error during swap setup"
);
}
_ => {
// We ignore the rest of events
}
_ => {}
}
}
fn on_behaviour_event(&mut self, new_swap: Self::FromBehaviour) {
tracing::trace!(
swap_id = %new_swap.swap_id,
"Received a new swap setup request from the Behaviour",
);
self.new_swaps.push_back(new_swap);
}
fn connection_keep_alive(&self) -> bool {
self.keep_alive
// Keep alive as long as there are queued swaps our inflight requests
!self.new_swaps.is_empty() || self.outbound_streams.len() > 0
}
fn poll(
@ -452,38 +452,67 @@ impl ConnectionHandler for Handler {
> {
// Check if there is a new swap to be started on this connection
// Has the Behaviour assigned us a new swap to be started on this connection?
if let Some(new_swap) = self.new_swaps.pop_front() {
while let Some(new_swap) = self.new_swaps.pop_front() {
// Check if we already have an inflight request for this swap
// We disallow multiple concurrent swap setup requests for the same swap on the same ConnectionHandler
if self.outbound_streams.contains_key(&new_swap.swap_id) {
tracing::error!(
swap_id = %new_swap.swap_id,
"Received a new swap setup request for a swap id that we already have an inflight request for. Ignoring request. The upstream behaviour may encounter bugs if its internal logic does not handle this correctly.",
);
// TODO: Potentially make this a production assert
debug_assert!(false, "Multiple concurrent swap setup requests with the same swap id are not allowed.");
continue;
}
// We disallow multiple concurrent substream negotiations for the same swap on the same ConnectionHandler
if self
.inflight_substream_negotiations
.contains(&new_swap.swap_id)
{
tracing::error!(
swap_id = %new_swap.swap_id,
"Received a new swap setup request for a swap id that we already have an inflight substream negotiation for. Ignoring. The upstream behaviour may encounter bugs if its internal logic does not handle this correctly.",
);
// TODO: Potentially make this a production assert
debug_assert!(false, "Multiple concurrent substream negotiations for the same swap id are not allowed.");
continue;
}
tracing::trace!(
?new_swap.swap_id,
"Instructing swarm to start a new outbound substream as part of swap setup",
);
// Keep the connection alive because we want to use it
self.keep_alive = true;
// We instruct the swarm to start a new outbound substream
self.inflight_substream_negotiations
.insert(new_swap.swap_id);
return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest {
protocol: SubstreamProtocol::new(protocol::new(), new_swap),
});
}
// Check if the outbound stream has completed
if let Poll::Ready(Some(result)) = self.outbound_stream.poll_unpin(cx) {
self.outbound_stream = None.into();
while let Poll::Ready(Some((swap_id, result))) = self.outbound_streams.poll_next_unpin(cx) {
self.to_behaviour
.push_back((swap_id, result.map_err(anyhow::Error::from)));
}
// 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(
result.map_err(anyhow::Error::from).into(),
));
// Notify the Behaviour about any swap setup results
if let Some(result) = self.to_behaviour.pop_front() {
return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(result));
}
Poll::Pending
}
}
// TODO: This is protocol and should be moved to another crate (probably swap-machine, swap-core or swap)
async fn run_swap_setup(
mut substream: &mut libp2p::swarm::Stream,
new_swap_request: NewSwap,
@ -624,7 +653,7 @@ pub enum Error {
asb: BlockchainNetwork,
},
#[error("Failed to complete swap setup within {seconds}s")]
#[error("Failed to complete swap setup back-and-forth within {seconds}s")]
Timeout { seconds: u64 },
/// Something went wrong during the swap setup protocol that is not covered by the other errors
@ -666,3 +695,9 @@ impl From<SwapSetupResult> for out_event::bob::OutEvent {
}
}
}
// TODO: Tests
// - Case where Alice does not support the protocol at all
// - Case where Connection dies before the swap setup is started
// - Case where Connection dies during the swap setup protocol
// TODO: Extract actualy protocol logic into a callback of sorts or some type of event/state system

View file

@ -1,7 +1,6 @@
use libp2p::request_response::{self, ProtocolSupport};
use libp2p::{PeerId, StreamProtocol};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use uuid::Uuid;
use crate::out_event;
@ -30,14 +29,16 @@ pub struct Request {
pub fn alice() -> Behaviour {
Behaviour::new(
vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Outbound)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::DEFAULT_REQUEST_TIMEOUT),
)
}
pub fn bob() -> Behaviour {
Behaviour::new(
vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Inbound)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
request_response::Config::default()
.with_request_timeout(crate::defaults::DEFAULT_REQUEST_TIMEOUT),
)
}

View file

@ -34,7 +34,7 @@ where
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed();
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2); // 2 hours
const IDLE_CONNECTION_TIMEOUT: Duration = crate::defaults::IDLE_CONNECTION_TIMEOUT;
SwarmBuilder::with_existing_identity(identity)
.with_tokio()
@ -52,17 +52,6 @@ fn get_rand_memory_address() -> Multiaddr {
format!("/memory/{}", address_port).parse().unwrap()
}
async fn get_local_tcp_address() -> Multiaddr {
let random_port = {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
listener.local_addr().unwrap().port()
};
format!("/ip4/127.0.0.1/tcp/{}", random_port)
.parse()
.unwrap()
}
/// An extension trait for [`Swarm`] that makes it easier to set up a network of
/// [`Swarm`]s for tests.
#[async_trait]
@ -77,10 +66,6 @@ pub trait SwarmExt {
/// Listens on a random memory address, polling the [`Swarm`] until the
/// transport is ready to accept connections.
async fn listen_on_random_memory_address(&mut self) -> Multiaddr;
/// Listens on a TCP port for localhost only, polling the [`Swarm`] until
/// the transport is ready to accept connections.
async fn listen_on_tcp_localhost(&mut self) -> Multiaddr;
}
#[async_trait]
@ -158,15 +143,6 @@ where
multiaddr
}
async fn listen_on_tcp_localhost(&mut self) -> Multiaddr {
let multiaddr = get_local_tcp_address().await;
self.listen_on(multiaddr.clone()).unwrap();
block_until_listening_on(self, &multiaddr).await;
multiaddr
}
}
async fn block_until_listening_on<B>(swarm: &mut Swarm<B>, multiaddr: &Multiaddr)

View file

@ -78,7 +78,7 @@ regex = "1.10"
reqwest = { workspace = true, features = ["http2", "rustls-tls-native-roots", "stream", "socks"] }
rust_decimal = { version = "1", features = ["serde-float"] }
rust_decimal_macros = "1"
semver = "1.0"
semver = { workspace = true }
structopt = "0.3"
swap-controller-api = { path = "../swap-controller-api" }
swap-core = { path = "../swap-core" }
@ -145,6 +145,7 @@ monero-harness = { path = "../monero-harness" }
proptest = "1"
serde_cbor = "0.11"
serial_test = "3.1"
swap-p2p = { path = "../swap-p2p", features = ["test-support"] }
tempfile = "3"
testcontainers = "0.15"

View file

@ -258,7 +258,9 @@ where
let _ = responder.respond(wallet_snapshot);
}
SwarmEvent::Behaviour(OutEvent::SwapSetupCompleted{peer_id, swap_id, state3}) => {
self.handle_execution_setup_done(peer_id, swap_id, state3).await;
if let Err(error) = self.handle_execution_setup_done(peer_id, swap_id, state3).await {
tracing::error!(%swap_id, ?error, "Failed to handle execution setup done");
}
}
SwarmEvent::Behaviour(OutEvent::SwapDeclined { peer, error }) => {
tracing::warn!(%peer, "Ignoring spot price request: {}", error);
@ -424,11 +426,14 @@ where
tracing::info!(swap_id = %swap_id, peer = %peer, "Fullfilled cooperative XMR redeem request");
}
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { rendezvous_node, ttl, namespace })) => {
tracing::trace!("Successfully registered with rendezvous node: {} with namespace: {} and TTL: {:?}", rendezvous_node, namespace, ttl);
SwarmEvent::Behaviour(OutEvent::Rendezvous(swap_p2p::protocols::rendezvous::register::Event::Registered { peer_id })) => {
tracing::trace!("Successfully registered with rendezvous node: {}", peer_id);
}
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::RegisterFailed { rendezvous_node, namespace, error })) => {
tracing::trace!("Registration with rendezvous node {} failed for namespace {}: {:?}", rendezvous_node, namespace, error);
SwarmEvent::Behaviour(OutEvent::Rendezvous(swap_p2p::protocols::rendezvous::register::Event::RegisterRequestFailed { peer_id, error })) => {
tracing::trace!("Registration with rendezvous node {} failed: {:?}", peer_id, error);
}
SwarmEvent::Behaviour(OutEvent::Rendezvous(swap_p2p::protocols::rendezvous::register::Event::RegisterDispatchFailed { peer_id, error })) => {
tracing::trace!("Failed to dispatch registration to rendezvous node {}: {:?}", peer_id, error);
}
SwarmEvent::Behaviour(OutEvent::OutboundRequestResponseFailure {peer, error, request_id, protocol}) => {
tracing::error!(
@ -529,7 +534,7 @@ where
.behaviour()
.rendezvous
.as_ref()
.map(|b| b.registrations())
.map(|b| b.status())
.unwrap_or_default(); // If rendezvous behaviour is disabled we report empty list
let _ = respond_to.send(registrations);
@ -611,7 +616,17 @@ where
bob_peer_id: PeerId,
swap_id: Uuid,
state3: State3,
) {
) -> Result<()> {
if self
.db
.has_swap(swap_id)
.await
.context("Failed to check if UUID is already in use")?
{
// TODO: We should ideally check this during swap setup, not after
return Err(anyhow::anyhow!("UUID is already in use"));
}
let handle = self.new_handle(bob_peer_id, swap_id);
let initial_state = AliceState::Started {
@ -629,16 +644,16 @@ where
developer_tip: self.developer_tip.clone(),
};
match self.db.insert_peer_id(swap_id, bob_peer_id).await {
Ok(_) => {
if let Err(error) = self.swap_sender.send(swap).await {
tracing::warn!(%swap_id, "Failed to start swap: {:?}", error);
}
}
Err(error) => {
tracing::warn!(%swap_id, "Unable to save peer-id in database: {}", error);
}
}
self.db
.insert_peer_id(swap_id, bob_peer_id)
.await
.context("Failed to save peer-id in database")?;
self.swap_sender
.send(swap)
.await
.context("Failed to send message to spawn swap state machine")?;
Ok(())
}
/// Create a new [`EventLoopHandle`] that is scoped for communication with
@ -841,7 +856,9 @@ mod service {
respond_to: oneshot::Sender<usize>,
},
GetRegistrationStatus {
respond_to: oneshot::Sender<Vec<crate::asb::register::RegistrationReport>>,
respond_to: oneshot::Sender<
Vec<swap_p2p::protocols::rendezvous::register::public::RendezvousNodeStatus>,
>,
},
}
@ -879,7 +896,8 @@ mod service {
/// Get the registration status at configured rendezvous points
pub async fn get_registration_status(
&self,
) -> anyhow::Result<Vec<crate::asb::register::RegistrationReport>> {
) -> anyhow::Result<Vec<crate::network::rendezvous::register::public::RendezvousNodeStatus>>
{
let (tx, rx) = oneshot::channel();
self.sender
.send(EventLoopRequest::GetRegistrationStatus { respond_to: tx })

View file

@ -93,9 +93,9 @@ pub mod transport {
pub mod behaviour {
use libp2p::{identify, identity, ping, swarm::behaviour::toggle::Toggle};
use swap_p2p::out_event::alice::OutEvent;
use swap_p2p::{out_event::alice::OutEvent, patches};
use super::{rendezvous::register, *};
use super::*;
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
#[derive(NetworkBehaviour)]
@ -111,7 +111,7 @@ pub mod behaviour {
pub transfer_proof: transfer_proof::Behaviour,
pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour,
pub encrypted_signature: encrypted_signature::Behaviour,
pub identify: identify::Behaviour,
pub identify: patches::identify::Behaviour,
/// Ping behaviour that ensures that the underlying network connection
/// is still alive. If the ping fails a connection close event
@ -130,7 +130,7 @@ pub mod behaviour {
resume_only: bool,
env_config: env::Config,
identify_params: (identity::Keypair, XmrBtcNamespace),
rendezvous_nodes: Vec<register::RendezvousNode>,
rendezvous_nodes: Vec<PeerId>,
) -> Self {
let (identity, namespace) = identify_params;
let agent_version = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), namespace);
@ -147,12 +147,13 @@ pub mod behaviour {
Some(rendezvous::register::Behaviour::new(
identity,
rendezvous_nodes,
namespace.into(),
))
};
Self {
rendezvous: Toggle::from(behaviour),
quote: quote::asb(),
quote: quote::alice(),
swap_setup: alice::Behaviour::new(
min_buy,
max_buy,
@ -164,7 +165,7 @@ pub mod behaviour {
encrypted_signature: encrypted_signature::alice(),
cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::alice(),
ping: ping::Behaviour::new(pingConfig),
identify: identify::Behaviour::new(identifyConfig),
identify: patches::identify::Behaviour::new(identifyConfig),
}
}
}

View file

@ -184,23 +184,23 @@ impl AsbApiServer for RpcImpl {
let registrations = regs
.into_iter()
.map(|r| RegistrationStatusItem {
address: r.address.to_string(),
connection: match r.connection {
crate::asb::register::ConnectionStatus::Disconnected => {
RendezvousConnectionStatus::Disconnected
}
crate::asb::register::ConnectionStatus::Connected => {
RendezvousConnectionStatus::Connected
}
address: r.address.map(|a| a.to_string()),
connection: if r.is_connected {
RendezvousConnectionStatus::Connected
} else {
RendezvousConnectionStatus::Disconnected
},
registration: match r.registration {
crate::asb::register::RegistrationStatusReport::RegisterOnNextConnection => {
RendezvousRegistrationStatus::RegisterOnNextConnection
crate::network::rendezvous::register::public::RegistrationStatus::RegisterOnceConnected => {
RendezvousRegistrationStatus::RegisterOnceConnected
}
crate::asb::register::RegistrationStatusReport::Pending => {
RendezvousRegistrationStatus::Pending
crate::network::rendezvous::register::public::RegistrationStatus::WillRegisterAfterDelay => {
RendezvousRegistrationStatus::WillRegisterAfterDelay
}
crate::asb::register::RegistrationStatusReport::Registered => {
crate::network::rendezvous::register::public::RegistrationStatus::RequestInflight => {
RendezvousRegistrationStatus::RequestInflight
}
crate::network::rendezvous::register::public::RegistrationStatus::Registered => {
RendezvousRegistrationStatus::Registered
}
},

View file

@ -44,8 +44,8 @@ mod tests {
use libp2p::PeerId;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use swap::cli::api::request::{determine_btc_to_swap, QuoteFetchFuture};
use swap::cli::{QuoteWithAddress, SellerStatus};
use swap::cli::api::request::determine_btc_to_swap;
use swap::cli::QuoteWithAddress;
use swap::network::quote::BidQuote;
use swap::tracing_ext::capture_logs;
use tracing::level_filters::LevelFilter;
@ -398,15 +398,18 @@ mod tests {
}
}
fn quote_with_max(btc: f64) -> impl Fn() -> QuoteFetchFuture {
fn quote_with_max(btc: f64) -> ::tokio::sync::watch::Receiver<Vec<QuoteWithAddress>> {
quote_minmax(None, Some(btc))
}
fn quote_with_min(btc: f64) -> impl Fn() -> QuoteFetchFuture {
fn quote_with_min(btc: f64) -> ::tokio::sync::watch::Receiver<Vec<QuoteWithAddress>> {
quote_minmax(Some(btc), None)
}
fn quote_minmax(min: Option<f64>, max: Option<f64>) -> impl Fn() -> QuoteFetchFuture {
fn quote_minmax(
min: Option<f64>,
max: Option<f64>,
) -> ::tokio::sync::watch::Receiver<Vec<QuoteWithAddress>> {
let max_quantity = max
.map(|m| Amount::from_btc(m).unwrap())
.unwrap_or(Amount::MAX_MONEY);
@ -414,23 +417,17 @@ mod tests {
.map(|m| Amount::from_btc(m).unwrap())
.unwrap_or(Amount::ZERO);
move || {
async move {
let (_, rx) =
::tokio::sync::watch::channel(vec![SellerStatus::Online(QuoteWithAddress {
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
peer_id: PeerId::random(),
quote: BidQuote {
price: Amount::from_btc(0.001).unwrap(),
max_quantity,
min_quantity,
},
version: "1.0.0".parse().unwrap(),
})]);
Ok((tokio::task::spawn(async {}), rx))
}
.boxed()
}
let (_, rx) = ::tokio::sync::watch::channel(vec![QuoteWithAddress {
peer_id: PeerId::random(),
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
quote: BidQuote {
price: Amount::from_btc(0.001).unwrap(),
max_quantity,
min_quantity,
},
version: Some("1.0.0".parse().unwrap()),
}]);
rx
}
async fn get_dummy_address() -> Result<bitcoin::Address> {

View file

@ -1,277 +1,14 @@
pub mod api;
mod behaviour;
pub mod cancel_and_refund;
pub mod command;
mod event_loop;
mod list_sellers;
pub mod transport;
pub mod watcher;
mod behaviour;
mod event_loop;
mod list_sellers;
pub use behaviour::{Behaviour, OutEvent};
pub use cancel_and_refund::{cancel, cancel_and_refund, refund};
pub use event_loop::{EventLoop, EventLoopHandle, SwapEventLoopHandle};
pub use list_sellers::{list_sellers, QuoteWithAddress, SellerStatus};
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::list_sellers::{QuoteWithAddress, SellerStatus};
use crate::network::quote;
use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace;
use futures::StreamExt;
use libp2p::core::Endpoint;
use libp2p::multiaddr::Protocol;
use libp2p::swarm::{
ConnectionDenied, ConnectionId, FromSwarm, THandlerInEvent, THandlerOutEvent, ToSwarm,
};
use libp2p::{identity, rendezvous, request_response, Multiaddr, PeerId};
use semver::Version;
use std::collections::HashSet;
use std::task::Poll;
use std::time::Duration;
use swap_p2p::protocols::rendezvous::register::{InnerBehaviourEvent, RendezvousNode};
use swap_p2p::test::{new_swarm, SwarmExt};
// Test-only struct for compatibility
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Seller {
multiaddr: Multiaddr,
status: SellerStatus,
}
#[tokio::test]
#[ignore]
// Due to an issue with the libp2p rendezvous library
// This needs to be fixed upstream and was
// introduced in our codebase by a libp2p refactor which bumped the version of libp2p:
//
// - The new bumped rendezvous client works, and can connect to an old rendezvous server
// - The new rendezvous has an issue, which is why these test (use the new mock server)
// do not work
//
// Ignore this test for now . This works in production :)
async fn list_sellers_should_report_all_registered_asbs_with_a_quote() {
let namespace = XmrBtcNamespace::Mainnet;
let (rendezvous_address, rendezvous_peer_id) = setup_rendezvous_point().await;
let expected_seller_1 = setup_asb(rendezvous_peer_id, &rendezvous_address, namespace).await;
let expected_seller_2 = setup_asb(rendezvous_peer_id, &rendezvous_address, namespace).await;
let list_sellers = list_sellers(
vec![(rendezvous_peer_id, rendezvous_address)],
namespace,
None,
identity::Keypair::generate_ed25519(),
None,
None,
);
let sellers = tokio::time::timeout(Duration::from_secs(15), list_sellers)
.await
.unwrap()
.unwrap();
// Convert SellerStatus to test Seller struct
let actual_sellers: Vec<Seller> = sellers
.into_iter()
.map(|status| Seller {
multiaddr: match &status {
SellerStatus::Online(quote_with_addr) => quote_with_addr.multiaddr.clone(),
SellerStatus::Unreachable(_) => "/ip4/0.0.0.0/tcp/0".parse().unwrap(), // placeholder
},
status,
})
.collect();
assert_eq!(
HashSet::<Seller>::from_iter(actual_sellers),
HashSet::<Seller>::from_iter([expected_seller_1, expected_seller_2])
)
}
async fn setup_rendezvous_point() -> (Multiaddr, PeerId) {
let mut rendezvous_node = new_swarm(|_| RendezvousPointBehaviour::default());
let rendezvous_address = rendezvous_node.listen_on_tcp_localhost().await;
let rendezvous_peer_id = *rendezvous_node.local_peer_id();
tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
(rendezvous_address, rendezvous_peer_id)
}
async fn setup_asb(
rendezvous_peer_id: PeerId,
rendezvous_address: &Multiaddr,
namespace: XmrBtcNamespace,
) -> Seller {
let static_quote = BidQuote {
price: bitcoin::Amount::from_sat(1337),
min_quantity: bitcoin::Amount::from_sat(42),
max_quantity: bitcoin::Amount::from_sat(9001),
};
let mut asb = new_swarm(|identity| {
let rendezvous_node =
RendezvousNode::new(rendezvous_address, rendezvous_peer_id, namespace, None);
let rendezvous = crate::asb::register::Behaviour::new(identity, vec![rendezvous_node]);
StaticQuoteAsbBehaviour {
inner: StaticQuoteAsbBehaviourInner {
rendezvous,
quote: quote::asb(),
},
static_quote,
registered: false,
}
});
let asb_address = asb.listen_on_tcp_localhost().await;
asb.add_external_address(asb_address.clone());
let asb_peer_id = *asb.local_peer_id();
// avoid race condition where `list_sellers` tries to discover before we are
// registered block this function until we are registered
while !asb.behaviour().registered {
asb.next().await;
}
tokio::spawn(async move {
loop {
asb.next().await;
}
});
let full_address = asb_address.with(Protocol::P2p(asb_peer_id));
Seller {
multiaddr: full_address.clone(),
status: SellerStatus::Online(QuoteWithAddress {
multiaddr: full_address,
peer_id: asb_peer_id,
quote: static_quote,
version: Version::parse("1.0.0").unwrap(),
}),
}
}
#[derive(libp2p::swarm::NetworkBehaviour)]
struct StaticQuoteAsbBehaviourInner {
rendezvous: crate::asb::register::Behaviour,
quote: quote::Behaviour,
}
struct StaticQuoteAsbBehaviour {
inner: StaticQuoteAsbBehaviourInner,
static_quote: BidQuote,
registered: bool,
}
impl libp2p::swarm::NetworkBehaviour for StaticQuoteAsbBehaviour {
type ConnectionHandler =
<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 InnerBehaviourEvent::Rendezvous(
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,
}
}
}
#[derive(libp2p::swarm::NetworkBehaviour)]
struct RendezvousPointBehaviour {
rendezvous: rendezvous::server::Behaviour,
}
impl Default for RendezvousPointBehaviour {
fn default() -> Self {
RendezvousPointBehaviour {
rendezvous: rendezvous::server::Behaviour::new(
rendezvous::server::Config::default(),
),
}
}
}
}
pub use list_sellers::QuoteWithAddress;

View file

@ -13,6 +13,7 @@ use crate::{bitcoin, common, monero};
use anyhow::{bail, Context as AnyContext, Error, Result};
use arti_client::TorClient;
use futures::future::try_join_all;
use libp2p::{Multiaddr, PeerId};
use std::fmt;
use std::future::Future;
use std::path::{Path, PathBuf};
@ -210,6 +211,21 @@ pub use swap_lock::{PendingTaskList, SwapLock};
mod context {
use super::*;
use crate::cli::EventLoopHandle;
/// Combined state for the EventLoop and its background task
/// These must always exist together
pub struct EventLoopState {
pub handle: EventLoopHandle,
#[allow(dead_code)]
pub task: JoinHandle<()>,
}
impl EventLoopState {
pub fn new(handle: EventLoopHandle, task: JoinHandle<()>) -> Self {
Self { handle, task }
}
}
/// Holds shared data for different parts of the CLI.
///
@ -229,6 +245,7 @@ mod context {
pub(super) tor_client: Arc<RwLock<Option<Arc<TorClient<TokioRustlsRuntime>>>>>,
#[allow(dead_code)]
pub(super) monero_rpc_pool_handle: Arc<RwLock<Option<Arc<monero_rpc_pool::PoolHandle>>>>,
pub(super) event_loop_state: Arc<RwLock<Option<EventLoopState>>>,
}
impl Context {
@ -252,6 +269,7 @@ mod context {
monero_manager: Arc::new(RwLock::new(None)),
tor_client: Arc::new(RwLock::new(None)),
monero_rpc_pool_handle: Arc::new(RwLock::new(None)),
event_loop_state: Arc::new(RwLock::new(None)),
}
}
@ -309,6 +327,16 @@ mod context {
.context("Tor client not initialized")
}
/// Get the event loop handle, returning an error if not initialized
pub async fn try_get_event_loop_handle(&self) -> Result<EventLoopHandle> {
self.event_loop_state
.read()
.await
.as_ref()
.map(|state| state.handle.clone())
.context("Event loop not initialized")
}
pub async fn for_harness(
seed: Seed,
env_config: EnvConfig,
@ -331,6 +359,7 @@ mod context {
tauri_handle: None,
tor_client: Arc::new(RwLock::new(None)),
monero_rpc_pool_handle: Arc::new(RwLock::new(None)),
event_loop_state: Arc::new(RwLock::new(None)),
}
}
@ -405,6 +434,7 @@ pub use context::Context;
mod builder {
use super::*;
use crate::cli::api::context::EventLoopState;
/// A conveniant builder struct for [`Context`].
#[must_use = "ContextBuilder must be built to be useful"]
@ -417,6 +447,7 @@ mod builder {
tor: bool,
enable_monero_tor: bool,
tauri_handle: Option<TauriHandle>,
rendezvous_points: Vec<(PeerId, Vec<Multiaddr>)>,
}
impl ContextBuilder {
@ -440,6 +471,7 @@ mod builder {
tor: false,
enable_monero_tor: false,
tauri_handle: None,
rendezvous_points: Vec::new(),
}
}
@ -492,6 +524,14 @@ mod builder {
self
}
pub fn with_rendezvous_points(
mut self,
rendezvous_points: Vec<(PeerId, Vec<Multiaddr>)>,
) -> Self {
self.rendezvous_points = rendezvous_points;
self
}
/// Initializes the context by populating it with all configured components.
///
/// Context fields are set as early as possible for availability to other parts of the system.
@ -671,6 +711,17 @@ mod builder {
let wallet_database = Some(Arc::new(wallet_database));
// Initialize config
*context.config.write().await = Some(Config {
namespace: XmrBtcNamespace::from_is_testnet(self.is_testnet),
env_config,
seed: seed.clone().into(),
json: self.json,
is_testnet: self.is_testnet,
data_dir: data_dir.clone(),
log_dir: log_dir.clone(),
});
// Initialize Monero wallet manager
async {
let manager = Arc::new(
@ -693,17 +744,6 @@ mod builder {
}
.await?;
// Initialize config
*context.config.write().await = Some(Config {
namespace: XmrBtcNamespace::from_is_testnet(self.is_testnet),
env_config,
seed: seed.clone().into(),
json: self.json,
is_testnet: self.is_testnet,
data_dir: data_dir.clone(),
log_dir: log_dir.clone(),
});
// Initialize swap database
let db = async {
let database_progress_handle = self
@ -778,6 +818,57 @@ mod builder {
}
}
// Initialize persistent EventLoop
if let Some(wallet) = bitcoin_wallet.clone() {
let namespace = XmrBtcNamespace::from_is_testnet(self.is_testnet);
let tor_client_for_swarm = unbootstrapped_tor_client.clone();
let db_for_swarm = db.clone();
let rendezvous_points = self.rendezvous_points;
let rendezvous_peer_ids: Vec<PeerId> =
rendezvous_points.iter().map(|(p, _)| *p).collect();
let behaviour = crate::cli::Behaviour::new(
env_config,
wallet.clone(),
seed.derive_libp2p_identity(),
namespace,
rendezvous_peer_ids,
);
let mut swarm = crate::network::swarm::cli(
seed.derive_libp2p_identity(),
tor_client_for_swarm,
behaviour,
)
.await?;
// Add addresses of rendezvous points to the swarm
for (peer_id, addrs) in rendezvous_points {
for addr in addrs {
swarm.add_peer_address(peer_id, addr);
}
}
// Add addresses of peers that we have previously connected to to the swarm
for (peer_id, addrs) in db.get_all_peer_addresses().await.context(
"Failed to retrieve peer addresses from database to insert into swarm",
)? {
for addr in addrs {
swarm.add_peer_address(peer_id, addr);
}
}
let (event_loop, event_loop_handle) =
crate::cli::EventLoop::new(swarm, db_for_swarm, self.tauri_handle.clone())?;
let event_loop_task = tokio::spawn(event_loop.run());
// TODO: We need to emit this to the frontend so that certain button can be disabled if the event loop is not yet running
*context.event_loop_state.write().await =
Some(EventLoopState::new(event_loop_handle, event_loop_task));
}
// Wait for Tor client to fully bootstrap
bootstrap_tor_client_task.await?;

View file

@ -5,28 +5,23 @@ use crate::cli::api::tauri_bindings::{
TauriSwapProgressEvent,
};
use crate::cli::api::Context;
use crate::cli::list_sellers::{list_sellers_init, QuoteWithAddress, UnreachableSeller};
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
use crate::cli::list_sellers::QuoteWithAddress;
use crate::common::{get_logs, redact};
use crate::libp2p_ext::MultiAddrExt;
use crate::monero::wallet_rpc::MoneroDaemon;
use crate::monero::MoneroAddressPool;
use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace;
use crate::network::swarm;
use crate::protocol::bob::{self, BobState, Swap};
use crate::protocol::{Database, State};
use crate::protocol::State;
use crate::{cli, monero};
use ::bitcoin::address::NetworkUnchecked;
use ::bitcoin::Txid;
use ::monero::Network;
use anyhow::{bail, Context as AnyContext, Result};
use arti_client::TorClient;
use futures::future::BoxFuture;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use libp2p::core::Multiaddr;
use libp2p::{identity, PeerId};
use libp2p::PeerId;
use monero_seed::{Language, Seed as MoneroSeed};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@ -41,7 +36,6 @@ use swap_core::bitcoin;
use swap_core::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock};
use thiserror::Error;
use tokio_util::task::AbortOnDropHandle;
use tor_rtcompat::tokio::TokioRustlsRuntime;
use tracing::debug_span;
use tracing::error;
use tracing::Instrument;
@ -69,25 +63,13 @@ fn get_swap_tracing_span(swap_id: Uuid) -> Span {
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct BuyXmrArgs {
#[typeshare(serialized_as = "Vec<string>")]
pub rendezvous_points: Vec<Multiaddr>,
#[typeshare(serialized_as = "Vec<string>")]
pub sellers: Vec<Multiaddr>,
#[typeshare(serialized_as = "Option<string>")]
pub bitcoin_change_address: Option<bitcoin::Address<NetworkUnchecked>>,
pub monero_receive_pool: MoneroAddressPool,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct BuyXmrResponse {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
pub quote: BidQuote,
}
impl Request for BuyXmrArgs {
type Response = BuyXmrResponse;
type Response = ();
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let swap_id = Uuid::new_v4();
@ -184,30 +166,6 @@ impl Request for WithdrawBtcArgs {
}
}
// ListSellers
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ListSellersArgs {
/// The rendezvous points to search for sellers
/// The address must contain a peer ID
#[typeshare(serialized_as = "Vec<string>")]
pub rendezvous_points: Vec<Multiaddr>,
}
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize)]
pub struct ListSellersResponse {
sellers: Vec<SellerStatus>,
}
impl Request for ListSellersArgs {
type Response = ListSellersResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
list_sellers(self, ctx).await
}
}
// GetSwapInfo
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
@ -982,12 +940,10 @@ pub async fn buy_xmr(
buy_xmr: BuyXmrArgs,
swap_id: Uuid,
context: Arc<Context>,
) -> Result<BuyXmrResponse, anyhow::Error> {
) -> Result<(), anyhow::Error> {
let _span = get_swap_tracing_span(swap_id);
let BuyXmrArgs {
rendezvous_points,
sellers,
bitcoin_change_address,
monero_receive_pool,
} = buy_xmr;
@ -1019,14 +975,14 @@ pub async fn buy_xmr(
let monero_wallet = context.try_get_monero_manager().await?;
let env_config = config.env_config;
let seed = config.seed.clone().context("Could not get seed")?;
// Prepare variables for the quote fetching process
let identity = seed.derive_libp2p_identity();
let namespace = config.namespace;
let tor_client = context.tor_client.read().await.clone();
let tauri_handle = context.tauri_handle.clone();
// Get the existing event loop handle from context
let mut event_loop_handle = context.try_get_event_loop_handle().await?;
let quotes_rx = event_loop_handle.cached_quotes();
// Wait for the user to approve a seller and to deposit coins
// Calling determine_btc_to_swap
let address_len = bitcoin_wallet.new_address().await?.script_pubkey().len();
@ -1035,13 +991,8 @@ pub async fn buy_xmr(
// Clone variables before moving them into closures
let bitcoin_change_address_for_spawn = bitcoin_change_address.clone();
let rendezvous_points_clone = rendezvous_points.clone();
let sellers_clone = sellers.clone();
let db_for_fetch = db.clone();
let tor_client_for_swarm = tor_client.clone();
// Clone tauri_handle for different closures
let tauri_handle_for_fetch = tauri_handle.clone();
let tauri_handle_for_determine = tauri_handle.clone();
let tauri_handle_for_selection = tauri_handle.clone();
let tauri_handle_for_suspension = tauri_handle.clone();
@ -1050,29 +1001,9 @@ pub async fn buy_xmr(
// because we need to be able to cancel the determine_btc_to_swap(..)
context.swap_lock.acquire_swap_lock(swap_id).await?;
let (seller_multiaddr, seller_peer_id, quote, tx_lock_amount, tx_lock_fee) = tokio::select! {
let select_offer_result = tokio::select! {
result = determine_btc_to_swap(
move || {
let rendezvous_points = rendezvous_points_clone.clone();
let sellers = sellers_clone.clone();
let namespace = namespace;
let identity = identity.clone();
let db = db_for_fetch.clone();
let tor_client = tor_client.clone();
let tauri_handle = tauri_handle_for_fetch.clone();
Box::pin(async move {
fetch_quotes_task(
rendezvous_points,
namespace,
sellers,
identity,
Some(db),
tor_client,
tauri_handle,
).await
})
},
quotes_rx,
bitcoin_wallet.new_address(),
{
let wallet = Arc::clone(&bitcoin_wallet_for_closures);
@ -1110,51 +1041,44 @@ pub async fn buy_xmr(
}) as Box<dyn Future<Output = Result<bool>> + Send>
},
) => {
result?
Some(result?)
}
_ = context.swap_lock.listen_for_swap_force_suspension() => {
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
if let Some(handle) = tauri_handle_for_suspension {
handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
}
bail!("Shutdown signal received");
None
},
};
let Some((seller_multiaddr, seller_peer_id, quote, tx_lock_amount, tx_lock_fee)) =
select_offer_result
else {
return Ok(());
};
// Insert the peer_id into the database
db.insert_peer_id(swap_id, seller_peer_id).await?;
db.insert_address(seller_peer_id, seller_multiaddr.clone())
.await?;
let behaviour = cli::Behaviour::new(
env_config,
bitcoin_wallet.clone(),
(seed.derive_libp2p_identity(), namespace),
);
let mut swarm = swarm::cli(
seed.derive_libp2p_identity(),
tor_client_for_swarm,
behaviour,
)
.await?;
swarm.add_peer_address(seller_peer_id, seller_multiaddr.clone());
db.insert_monero_address_pool(swap_id, monero_receive_pool.clone())
.await?;
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
// Add the seller's address to the swarm
event_loop_handle
.queue_peer_address(seller_peer_id, seller_multiaddr.clone())
.await?;
tauri_handle.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::ReceivedQuote(quote.clone()),
);
let (event_loop, mut event_loop_handle) = EventLoop::new(swarm, db.clone())?;
let event_loop = tokio::spawn(event_loop.run().in_current_span());
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(quote));
context.tasks.clone().spawn(async move {
@ -1169,16 +1093,6 @@ pub async fn buy_xmr(
bail!("Shutdown signal received");
},
event_loop_result = event_loop => {
match event_loop_result {
Ok(_) => {
tracing::debug!(%swap_id, "EventLoop completed")
}
Err(error) => {
tracing::error!(%swap_id, "EventLoop failed: {:#}", error)
}
}
},
swap_result = async {
let swap_event_loop_handle = event_loop_handle.swap_handle(seller_peer_id, swap_id).await?;
let swap = Swap::new(
@ -1219,7 +1133,7 @@ pub async fn buy_xmr(
Ok::<_, anyhow::Error>(())
}.in_current_span()).await;
Ok(BuyXmrResponse { swap_id, quote })
Ok(())
}
#[tracing::instrument(fields(method = "resume_swap"), skip(context))]
@ -1233,34 +1147,18 @@ pub async fn resume_swap(
let config = context.try_get_config().await?;
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
let monero_manager = context.try_get_monero_manager().await?;
let tor_client = context.tor_client.read().await.clone();
let seller_peer_id = db.get_peer_id(swap_id).await?;
let seller_addresses = db.get_addresses(seller_peer_id).await?;
let seed = config
.seed
.as_ref()
.context("Could not get seed")?
.derive_libp2p_identity();
let mut event_loop_handle = context.try_get_event_loop_handle().await?;
let behaviour = cli::Behaviour::new(
config.env_config,
bitcoin_wallet.clone(),
(seed.clone(), config.namespace),
);
let mut swarm = swarm::cli(seed.clone(), tor_client, behaviour).await?;
let our_peer_id = swarm.local_peer_id();
tracing::debug!(peer_id = %our_peer_id, "Network layer initialized");
// Fetch the seller's addresses from the database and add them to the swarm
for seller_address in seller_addresses {
swarm.add_peer_address(seller_peer_id, seller_address);
event_loop_handle
.queue_peer_address(seller_peer_id, seller_address)
.await?;
}
let (event_loop, mut event_loop_handle) = EventLoop::new(swarm, db.clone())?;
let monero_receive_pool = db.get_monero_address_pool(swap_id).await?;
let tauri_handle = context.tauri_handle.clone();
@ -1286,7 +1184,6 @@ pub async fn resume_swap(
context.tasks.clone().spawn(
async move {
let handle = tokio::spawn(event_loop.run().in_current_span());
tokio::select! {
biased;
_ = context.swap_lock.listen_for_swap_force_suspension() => {
@ -1298,16 +1195,6 @@ pub async fn resume_swap(
bail!("Shutdown signal received");
},
event_loop_result = handle => {
match event_loop_result {
Ok(_) => {
tracing::debug!(%swap_id, "EventLoop completed during swap resume")
}
Err(error) => {
tracing::error!(%swap_id, "EventLoop failed during swap resume: {:#}", error)
}
}
},
swap_result = bob::run(swap) => {
match swap_result {
Ok(state) => {
@ -1477,75 +1364,6 @@ pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<
})
}
#[tracing::instrument(fields(method = "list_sellers"), skip(context))]
pub async fn list_sellers(
list_sellers: ListSellersArgs,
context: Arc<Context>,
) -> Result<ListSellersResponse> {
let ListSellersArgs { rendezvous_points } = list_sellers;
let rendezvous_nodes: Vec<_> = rendezvous_points
.iter()
.filter_map(|rendezvous_point| rendezvous_point.split_peer_id())
.collect();
let config = context.try_get_config().await?;
let db = context.try_get_db().await?;
let tor_client = context.tor_client.read().await.clone();
let tauri_handle = context.tauri_handle.clone();
let identity = config
.seed
.as_ref()
.context("Cannot extract seed")?
.derive_libp2p_identity();
let sellers = list_sellers_impl(
rendezvous_nodes,
config.namespace,
tor_client,
identity,
Some(db.clone()),
tauri_handle,
)
.await?;
for seller in &sellers {
match seller {
SellerStatus::Online(QuoteWithAddress {
quote,
multiaddr,
peer_id,
version,
}) => {
tracing::trace!(
status = "Online",
price = %quote.price.to_string(),
min_quantity = %quote.min_quantity.to_string(),
max_quantity = %quote.max_quantity.to_string(),
address = %multiaddr.clone().to_string(),
peer_id = %peer_id,
version = %version,
"Fetched peer status"
);
// Add the peer as known to the database
// This'll allow us to later request a quote again
// without having to re-discover the peer at the rendezvous point
db.insert_address(*peer_id, multiaddr.clone()).await?;
}
SellerStatus::Unreachable(UnreachableSeller { peer_id }) => {
tracing::trace!(
status = "Unreachable",
peer_id = %peer_id.to_string(),
"Fetched peer status"
);
}
}
}
Ok(ListSellersResponse { sellers })
}
#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))]
pub async fn export_bitcoin_wallet(context: Arc<Context>) -> Result<serde_json::Value> {
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
@ -1600,49 +1418,6 @@ pub async fn get_current_swap(context: Arc<Context>) -> Result<GetCurrentSwapRes
Ok(GetCurrentSwapResponse { swap_id })
}
pub async fn fetch_quotes_task(
rendezvous_points: Vec<Multiaddr>,
namespace: XmrBtcNamespace,
sellers: Vec<Multiaddr>,
identity: identity::Keypair,
db: Option<Arc<dyn Database + Send + Sync>>,
tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
tauri_handle: Option<TauriHandle>,
) -> Result<(
tokio::task::JoinHandle<()>,
::tokio::sync::watch::Receiver<Vec<SellerStatus>>,
)> {
let (tx, rx) = ::tokio::sync::watch::channel(Vec::new());
let rendezvous_nodes: Vec<_> = rendezvous_points
.iter()
.filter_map(|addr| addr.split_peer_id())
.collect();
let fetch_fn = list_sellers_init(
rendezvous_nodes,
namespace,
tor_client,
identity,
db,
tauri_handle,
Some(tx.clone()),
sellers,
)
.await?;
let handle = tokio::task::spawn(async move {
loop {
let sellers = fetch_fn().await;
let _ = tx.send(sellers);
tokio::time::sleep(std::time::Duration::from_secs(90)).await;
}
});
Ok((handle, rx))
}
// TODO: Let this take a refresh interval as an argument
pub async fn refresh_wallet_task<FMG, TMG, FB, TB, FS, TS>(
max_giveable_fn: FMG,
@ -1679,17 +1454,9 @@ where
Ok((handle, rx))
}
pub type QuoteFetchFuture = BoxFuture<
'static,
Result<(
tokio::task::JoinHandle<()>,
::tokio::sync::watch::Receiver<Vec<SellerStatus>>,
)>,
>;
#[allow(clippy::too_many_arguments)]
pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS>(
quote_fetch_tasks: impl Fn() -> QuoteFetchFuture,
mut quotes_rx: ::tokio::sync::watch::Receiver<Vec<QuoteWithAddress>>,
// TODO: Shouldn't this be a function?
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
balance: FB,
@ -1714,17 +1481,12 @@ where
FS: Fn() -> TS + Send + 'static,
{
// Start background tasks with watch channels
let (quote_fetch_handle, mut quotes_rx): (
_,
::tokio::sync::watch::Receiver<Vec<SellerStatus>>,
) = quote_fetch_tasks().await?;
let (wallet_refresh_handle, mut balance_rx): (
_,
::tokio::sync::watch::Receiver<(bitcoin::Amount, bitcoin::Amount)>,
) = refresh_wallet_task(max_giveable_fn, balance, sync).await?;
// Get the abort handles to kill the background tasks when we exit the function
let quote_fetch_abort_handle = AbortOnDropHandle::new(quote_fetch_handle);
let wallet_refresh_abort_handle = AbortOnDropHandle::new(wallet_refresh_handle);
let mut pending_approvals = FuturesUnordered::new();
@ -1736,14 +1498,6 @@ where
let quotes = quotes_rx.borrow().clone();
let (balance, max_giveable) = *balance_rx.borrow();
let success_quotes = quotes
.iter()
.filter_map(|quote| match quote {
SellerStatus::Online(quote_with_address) => Some(quote_with_address.clone()),
SellerStatus::Unreachable(_) => None,
})
.collect::<Vec<_>>();
// Emit a Tauri event
event_emitter.emit_swap_progress_event(
swap_id,
@ -1751,12 +1505,12 @@ where
deposit_address: deposit_address.clone(),
max_giveable: max_giveable,
min_bitcoin_lock_tx_fee: balance - max_giveable,
known_quotes: success_quotes.clone(),
known_quotes: quotes.clone(),
},
);
// Iterate through quotes and find ones that match the balance and max_giveable
let matching_quotes = success_quotes
let matching_quotes = quotes
.iter()
.filter_map(|quote_with_address| {
let quote = quote_with_address.quote;
@ -1814,15 +1568,6 @@ where
});
}
tracing::trace!(
swap_id = ?swap_id,
pending_approvals = ?pending_approvals.len(),
balance = ?balance,
max_giveable = ?max_giveable,
quotes = ?quotes,
"Waiting for user to select an offer"
);
// Listen for approvals, balance changes, or quote changes
let result: Option<(
Multiaddr,
@ -1854,7 +1599,6 @@ where
// If user accepted an offer, return it to start the swap
if let Some((multiaddr, peer_id, quote, tx_lock_amount, tx_lock_fee)) = result {
quote_fetch_abort_handle.abort();
wallet_refresh_abort_handle.abort();
return Ok((multiaddr, peer_id, quote, tx_lock_amount, tx_lock_fee));

View file

@ -8,6 +8,7 @@ use crate::{monero, network::quote::BidQuote};
use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use bitcoin::Txid;
use libp2p::{Multiaddr, PeerId};
use monero_rpc_pool::pool::PoolStatus;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -18,6 +19,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use strum::Display;
use swap_core::bitcoin;
use swap_core::bitcoin::ExpiredTimelocks;
use swap_p2p::observe;
use swap_p2p::protocols::quotes_cached::QuoteStatus;
use tokio::sync::oneshot;
use typeshare::typeshare;
use uuid::Uuid;
@ -37,6 +40,29 @@ pub enum TauriEvent {
BackgroundProgress(TauriBackgroundProgressWrapper),
PoolStatusUpdate(PoolStatus),
MoneroWalletUpdate(MoneroWalletUpdate),
P2P(TauriP2PEvent),
}
#[typeshare]
#[derive(Clone, Serialize)]
#[serde(tag = "type", content = "content")]
pub enum TauriP2PEvent {
ConnectionChange {
#[typeshare(serialized_as = "string")]
peer_id: PeerId,
change: observe::ConnectionChange,
},
QuotesProgress {
peers: Vec<PeerQuoteProgress>,
},
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PeerQuoteProgress {
#[typeshare(serialized_as = "string")]
pub peer_id: PeerId,
pub quote_status: QuoteStatus,
}
#[typeshare]
@ -506,6 +532,27 @@ pub trait TauriEmitter {
self.emit_unified_event(TauriEvent::PoolStatusUpdate(status));
}
fn emit_peer_connection_change(&self, peer_id: PeerId, change: observe::ConnectionChange) {
self.emit_unified_event(TauriEvent::P2P(TauriP2PEvent::ConnectionChange {
peer_id,
change,
}));
}
fn emit_quotes_progress(&self, peers: Vec<(PeerId, QuoteStatus)>) {
let entries = peers
.into_iter()
.map(|(peer_id, quote_status)| PeerQuoteProgress {
peer_id,
quote_status,
})
.collect();
self.emit_unified_event(TauriEvent::P2P(TauriP2PEvent::QuotesProgress {
peers: entries,
}));
}
/// Create a new background progress handle for tracking a specific type of progress
fn new_background_process<T: Clone>(
&self,
@ -1022,6 +1069,8 @@ pub struct TauriSettings {
pub use_tor: bool,
/// Whether to route Monero wallet traffic through Tor
pub enable_monero_tor: bool,
/// The list of rendezvous points to connect to
pub rendezvous_points: Vec<String>,
}
#[typeshare]

View file

@ -1,15 +1,17 @@
use crate::network::rendezvous::XmrBtcNamespace;
use crate::network::swap_setup::bob;
use crate::network::{
cooperative_xmr_redeem_after_punish, encrypted_signature, quote, redial, transfer_proof,
cooperative_xmr_redeem_after_punish, encrypted_signature, quote, quotes_cached, redial,
rendezvous, transfer_proof,
};
use anyhow::Result;
use bitcoin_wallet::BitcoinWallet;
use libp2p::swarm::NetworkBehaviour;
use libp2p::{identify, identity, ping};
use libp2p::{identify, identity, ping, PeerId};
use std::sync::Arc;
use std::time::Duration;
use swap_env::env;
use swap_p2p::observe;
pub use swap_p2p::out_event::bob::OutEvent;
const PROTOCOL_VERSION: &str = "/comit/xmr/btc/1.0.0";
@ -22,13 +24,23 @@ const MAX_REDIAL_INTERVAL: Duration = Duration::from_secs(30);
#[behaviour(to_swarm = "OutEvent")]
#[allow(missing_debug_implementations)]
pub struct Behaviour {
pub quote: quote::Behaviour,
/// Fetch a quote from a specifc peer, usually before starting a swap
pub direct_quote: quote::Behaviour,
/// Periodically request quotes from any peers that might offer them
pub quotes: quotes_cached::Behaviour,
/// Periodically discover peers via rendezvous nodes
pub discovery: rendezvous::discovery::Behaviour,
/// Observes connection status and emits events
pub observe: observe::Behaviour,
/// Requires to actually do swaps
pub swap_setup: bob::Behaviour,
pub transfer_proof: transfer_proof::Behaviour,
pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour,
pub encrypted_signature: encrypted_signature::Behaviour,
/// Allows us to keep connections to specific peers alive
pub redial: redial::Behaviour,
pub identify: identify::Behaviour,
/// Ping behaviour that ensures that the underlying network connection is
/// still alive. If the ping fails a connection close event will be
@ -40,30 +52,37 @@ impl Behaviour {
pub fn new(
env_config: env::Config,
bitcoin_wallet: Arc<dyn BitcoinWallet>,
identify_params: (identity::Keypair, XmrBtcNamespace),
identity: identity::Keypair,
namespace: XmrBtcNamespace,
rendezvous_nodes: Vec<PeerId>,
) -> Self {
let agentVersion = format!("cli/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1);
let identifyConfig =
identify::Config::new(PROTOCOL_VERSION.to_string(), identify_params.0.public())
.with_agent_version(agentVersion);
let identifyConfig = identify::Config::new(PROTOCOL_VERSION.to_string(), identity.public())
.with_agent_version(agent_version(namespace));
let pingConfig = ping::Config::new().with_timeout(Duration::from_secs(60));
Self {
quote: quote::cli(),
direct_quote: quote::bob(),
quotes: quotes_cached::Behaviour::new(identifyConfig),
discovery: rendezvous::discovery::Behaviour::new(
identity,
rendezvous_nodes,
namespace.into(),
),
observe: observe::Behaviour::new(),
swap_setup: bob::Behaviour::new(env_config, bitcoin_wallet),
transfer_proof: transfer_proof::bob(),
encrypted_signature: encrypted_signature::bob(),
cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::bob(),
redial: redial::Behaviour::new(
// This redial behaviour is responsible for redialing all Alice peers during swaps
"multi-alice-redialer",
INITIAL_REDIAL_INTERVAL,
MAX_REDIAL_INTERVAL,
),
redial: redial::Behaviour::new("makers", INITIAL_REDIAL_INTERVAL, MAX_REDIAL_INTERVAL),
ping: ping::Behaviour::new(pingConfig),
identify: identify::Behaviour::new(identifyConfig),
}
}
}
fn agent_version(namespace: XmrBtcNamespace) -> String {
format!("cli/{} ({})", env!("CARGO_PKG_VERSION"), namespace)
}

View file

@ -1,6 +1,6 @@
use crate::cli::api::request::{
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs,
GetHistoryArgs, ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs,
BalanceArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs,
GetHistoryArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs,
};
use crate::cli::api::Context;
use crate::monero::{self, MoneroAddressPool};
@ -83,44 +83,6 @@ async fn apply_defaults(
(json, is_testnet, data): JsonTestnetData,
) -> Result<()> {
match cli_cmd {
CliCommand::BuyXmr {
seller: Seller { seller },
bitcoin,
bitcoin_change_address,
monero,
monero_receive_address,
tor,
} => {
let monero_receive_pool: MoneroAddressPool =
swap_serde::monero::address::validate_is_testnet(
monero_receive_address,
is_testnet,
)?
.into();
let bitcoin_change_address = bitcoin_change_address
.map(|address| bitcoin_address::validate(address, is_testnet))
.transpose()?
.map(|address| address.into_unchecked());
ContextBuilder::new(is_testnet)
.with_tor(tor.enable_tor)
.with_bitcoin(bitcoin)
.with_monero(monero)
.with_data_dir(data)
.with_json(json)
.build(context.clone())
.await?;
BuyXmrArgs {
rendezvous_points: vec![],
sellers: vec![seller],
bitcoin_change_address,
monero_receive_pool,
}
.request(context)
.await?;
}
CliCommand::History => {
ContextBuilder::new(is_testnet)
.with_data_dir(data)
@ -218,23 +180,6 @@ async fn apply_defaults(
CancelAndRefundArgs { swap_id }.request(context).await?;
}
CliCommand::ListSellers {
rendezvous_point,
tor,
} => {
ContextBuilder::new(is_testnet)
.with_tor(tor.enable_tor)
.with_data_dir(data)
.with_json(json)
.build(context.clone())
.await?;
ListSellersArgs {
rendezvous_points: vec![rendezvous_point],
}
.request(context)
.await?;
}
CliCommand::ExportBitcoinWallet { bitcoin } => {
ContextBuilder::new(is_testnet)
.with_bitcoin(bitcoin)
@ -296,33 +241,34 @@ struct Arguments {
#[derive(structopt::StructOpt, Debug, PartialEq)]
enum CliCommand {
/// Start a BTC for XMR swap
BuyXmr {
#[structopt(flatten)]
seller: Seller,
// TODO: Add this back once our CLI is more flexible
// Start a BTC for XMR swap
// BuyXmr {
// #[structopt(flatten)]
// seller: Seller,
#[structopt(flatten)]
bitcoin: Bitcoin,
// #[structopt(flatten)]
// bitcoin: Bitcoin,
#[structopt(
long = "change-address",
help = "The bitcoin address where any form of change or excess funds should be sent to. If omitted they will be sent to the internal wallet.",
parse(try_from_str = bitcoin_address::parse)
)]
bitcoin_change_address: Option<bitcoin::Address<NetworkUnchecked>>,
// #[structopt(
// long = "change-address",
// help = "The bitcoin address where any form of change or excess funds should be sent to. If omitted they will be sent to the internal wallet.",
// parse(try_from_str = bitcoin_address::parse)
// )]
// bitcoin_change_address: Option<bitcoin::Address<NetworkUnchecked>>,
#[structopt(flatten)]
monero: Monero,
// #[structopt(flatten)]
// monero: Monero,
#[structopt(long = "receive-address",
help = "The monero address where you would like to receive monero",
parse(try_from_str = swap_serde::monero::address::parse)
)]
monero_receive_address: monero::Address,
// #[structopt(long = "receive-address",
// help = "The monero address where you would like to receive monero",
// parse(try_from_str = swap_serde::monero::address::parse)
// )]
// monero_receive_address: monero::Address,
#[structopt(flatten)]
tor: Tor,
},
// #[structopt(flatten)]
// tor: Tor,
// },
/// Show a list of past, ongoing and completed swaps
History,
/// Output all logging messages that have been issued.
@ -391,17 +337,6 @@ enum CliCommand {
#[structopt(flatten)]
bitcoin: Bitcoin,
},
/// Discover and list sellers (i.e. ASB providers)
ListSellers {
#[structopt(
long,
help = "Address of the rendezvous point you want to use to discover ASBs"
)]
rendezvous_point: Multiaddr,
#[structopt(flatten)]
tor: Tor,
},
/// Print the internal bitcoin wallet descriptor
ExportBitcoinWallet {
#[structopt(flatten)]

View file

@ -1,8 +1,11 @@
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle};
use crate::cli::behaviour::{Behaviour, OutEvent};
use crate::cli::list_sellers::QuoteWithAddress;
use crate::monero;
use crate::network::cooperative_xmr_redeem_after_punish::{self, Request, Response};
use crate::network::encrypted_signature;
use crate::network::quote::BidQuote;
use crate::network::quotes_cached::QuoteStatus;
use crate::network::swap_setup::bob::NewSwap;
use crate::protocol::bob::swap::has_already_processed_transfer_proof;
use crate::protocol::bob::{BobState, State2};
@ -18,7 +21,9 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use swap_core::bitcoin::EncryptedSignature;
use swap_p2p::observe;
use swap_p2p::protocols::redial;
use tracing::Instrument;
use uuid::Uuid;
// Timeout for the execution setup protocol within the event loop.
@ -53,6 +58,7 @@ pub struct EventLoop {
Uuid,
PeerId,
bmrng::unbounded::UnboundedRequestSender<monero::TransferProof, ()>,
tracing::Span,
),
(),
>,
@ -61,27 +67,32 @@ pub struct EventLoop {
(
PeerId,
bmrng::unbounded::UnboundedRequestSender<monero::TransferProof, ()>,
tracing::Span,
),
>,
// These streams represents outgoing requests that we have to make (queues)
//
// Requests are keyed by the PeerId because they do not correspond to an existing swap yet
quote_requests:
bmrng::unbounded::UnboundedRequestReceiverStream<PeerId, Result<BidQuote, OutboundFailure>>,
quote_requests: bmrng::unbounded::UnboundedRequestReceiverStream<
(PeerId, tracing::Span),
Result<BidQuote, OutboundFailure>,
>,
// TODO: technically NewSwap.swap_id already contains the id of the swap
execution_setup_requests:
bmrng::unbounded::UnboundedRequestReceiverStream<(PeerId, NewSwap), Result<State2>>,
execution_setup_requests: bmrng::unbounded::UnboundedRequestReceiverStream<
(PeerId, NewSwap, tracing::Span),
Result<State2>,
>,
// These streams represents outgoing requests that we have to make (queues)
//
// Requests are keyed by the swap_id because they correspond to a specific swap
cooperative_xmr_redeem_requests: bmrng::unbounded::UnboundedRequestReceiverStream<
(PeerId, Uuid),
(PeerId, Uuid, tracing::Span),
Result<cooperative_xmr_redeem_after_punish::Response, OutboundFailure>,
>,
encrypted_signatures_requests: bmrng::unbounded::UnboundedRequestReceiverStream<
(PeerId, Uuid, EncryptedSignature),
(PeerId, Uuid, EncryptedSignature, tracing::Span),
Result<(), OutboundFailure>,
>,
@ -91,19 +102,33 @@ pub struct EventLoop {
// response.
inflight_quote_requests: HashMap<
OutboundRequestId,
bmrng::unbounded::UnboundedResponder<Result<BidQuote, OutboundFailure>>,
(
bmrng::unbounded::UnboundedResponder<Result<BidQuote, OutboundFailure>>,
tracing::Span,
),
>,
inflight_encrypted_signature_requests: HashMap<
OutboundRequestId,
bmrng::unbounded::UnboundedResponder<Result<(), OutboundFailure>>,
(
bmrng::unbounded::UnboundedResponder<Result<(), OutboundFailure>>,
tracing::Span,
),
>,
inflight_swap_setup: HashMap<
(PeerId, Uuid),
(
bmrng::unbounded::UnboundedResponder<Result<State2>>,
tracing::Span,
),
>,
inflight_swap_setup:
HashMap<(PeerId, Uuid), bmrng::unbounded::UnboundedResponder<Result<State2>>>,
inflight_cooperative_xmr_redeem_requests: HashMap<
OutboundRequestId,
bmrng::unbounded::UnboundedResponder<
Result<cooperative_xmr_redeem_after_punish::Response, OutboundFailure>,
>,
(
bmrng::unbounded::UnboundedResponder<
Result<cooperative_xmr_redeem_after_punish::Response, OutboundFailure>,
>,
tracing::Span,
),
>,
/// The future representing the successful handling of an incoming transfer proof (by the state machine)
@ -113,12 +138,20 @@ pub struct EventLoop {
///
/// The future will yield the swap_id and the response channel which are used to send an acknowledgement to Alice.
pending_transfer_proof_acks: FuturesUnordered<BoxFuture<'static, (Uuid, ResponseChannel<()>)>>,
/// Queue for adding peer addresses to the swarm
add_peer_address_requests:
bmrng::unbounded::UnboundedRequestReceiverStream<(PeerId, libp2p::Multiaddr), ()>,
cached_quotes_sender: tokio::sync::watch::Sender<Vec<QuoteWithAddress>>,
tauri_handle: Option<TauriHandle>,
}
impl EventLoop {
pub fn new(
swarm: Swarm<Behaviour>,
db: Arc<dyn Database + Send + Sync>,
tauri_handle: Option<TauriHandle>,
) -> Result<(Self, EventLoopHandle)> {
// We still use a timeout here because we trust our own implementation of the swap setup protocol less than the libp2p library
let (execution_setup_sender, execution_setup_receiver) =
@ -132,6 +165,11 @@ impl EventLoop {
bmrng::unbounded::channel();
let (queued_transfer_proof_sender, queued_transfer_proof_receiver) =
bmrng::unbounded::channel();
let (add_peer_address_sender, add_peer_address_receiver) = bmrng::unbounded::channel();
// TODO: We should probably differentiate between empty and none
let (cached_quotes_sender, cached_quotes_receiver) =
tokio::sync::watch::channel(Vec::new());
let event_loop = EventLoop {
swarm,
@ -147,6 +185,9 @@ impl EventLoop {
inflight_encrypted_signature_requests: HashMap::default(),
inflight_cooperative_xmr_redeem_requests: HashMap::default(),
pending_transfer_proof_acks: FuturesUnordered::new(),
add_peer_address_requests: add_peer_address_receiver.into(),
cached_quotes_sender,
tauri_handle,
};
let handle = EventLoopHandle {
@ -155,6 +196,8 @@ impl EventLoop {
cooperative_xmr_redeem_sender,
quote_sender,
queued_transfer_proof_sender,
add_peer_address_sender,
cached_quotes_receiver,
};
Ok((event_loop, handle))
@ -167,22 +210,26 @@ impl EventLoop {
swarm_event = self.swarm.select_next_some() => {
match swarm_event {
SwarmEvent::Behaviour(OutEvent::QuoteReceived { id, response }) => {
tracing::trace!(
%id,
"Received quote"
);
if let Some((responder, span)) = self.inflight_quote_requests.remove(&id) {
let _span_guard = span.enter();
tracing::trace!(
%id,
"Received quote"
);
if let Some(responder) = self.inflight_quote_requests.remove(&id) {
let _ = responder.respond(Ok(response));
}
}
SwarmEvent::Behaviour(OutEvent::SwapSetupCompleted { peer, swap_id, result }) => {
tracing::trace!(
%peer,
"Processing swap setup completion"
);
if let Some((responder, span)) = self.inflight_swap_setup.remove(&(peer, swap_id)) {
let _span_guard = span.enter();
tracing::trace!(
%peer,
"Processing swap setup completion"
);
if let Some(responder) = self.inflight_swap_setup.remove(&(peer, swap_id)) {
let _ = responder.respond(*result);
}
}
@ -196,7 +243,7 @@ impl EventLoop {
let swap_id = msg.swap_id;
// Check if we have a registered handler for this swap
if let Some((expected_peer_id, sender)) = self.registered_swap_handlers.get(&swap_id) {
if let Some((expected_peer_id, sender, _)) = self.registered_swap_handlers.get(&swap_id) {
// Ensure the transfer proof is coming from the expected peer
if peer != *expected_peer_id {
tracing::warn!(
@ -267,38 +314,53 @@ impl EventLoop {
}
}
SwarmEvent::Behaviour(OutEvent::EncryptedSignatureAcknowledged { id }) => {
if let Some(responder) = self.inflight_encrypted_signature_requests.remove(&id) {
if let Some((responder, span)) = self.inflight_encrypted_signature_requests.remove(&id) {
let _span_guard = span.enter();
let _ = responder.respond(Ok(()));
}
}
SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemFulfilled { id, swap_id, s_a, lock_transfer_proof }) => {
if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
if let Some((responder, span)) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
let _span_guard = span.enter();
let _ = responder.respond(Ok(Response::Fullfilled { s_a, swap_id, lock_transfer_proof }));
}
}
SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemRejected { id, swap_id, reason }) => {
if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
if let Some((responder, span)) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
let _span_guard = span.enter();
let _ = responder.respond(Ok(Response::Rejected { reason, swap_id }));
}
}
SwarmEvent::Behaviour(OutEvent::Failure { peer, error }) => {
let span = self.get_peer_span(peer);
let _span_guard = span.enter();
tracing::warn!(%peer, err = ?error, "Communication error");
return;
}
SwarmEvent::ConnectionEstablished { peer_id: _, endpoint, .. } => {
tracing::info!(peer_id = %endpoint.get_remote_address(), "Connected to peer");
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => {
let span = self.get_peer_span(peer_id);
let _span_guard = span.enter();
tracing::info!(%peer_id, peer_addr = %endpoint.get_remote_address(), "Connected to peer");
}
SwarmEvent::Dialing { peer_id: Some(peer_id), connection_id } => {
let span = self.get_peer_span(peer_id);
let _span_guard = span.enter();
tracing::debug!(%peer_id, %connection_id, "Dialing peer");
}
SwarmEvent::ConnectionClosed { peer_id: _, endpoint, num_established, cause: Some(error), connection_id } if num_established == 0 => {
tracing::warn!(peer_id = %endpoint.get_remote_address(), cause = ?error, %connection_id, "Lost connection to peer");
SwarmEvent::ConnectionClosed { peer_id, num_established, cause: Some(error), connection_id, endpoint } if num_established == 0 => {
let span = self.get_peer_span(peer_id);
let _span_guard = span.enter();
tracing::warn!(%peer_id, peer_addr = %endpoint.get_remote_address(), cause = ?error, %connection_id, "Lost connection to peer");
}
SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if num_established == 0 => {
// no error means the disconnection was requested
let span = self.get_peer_span(peer_id);
let _span_guard = span.enter();
tracing::info!(%peer_id, "Successfully closed connection to peer");
}
SwarmEvent::OutgoingConnectionError { peer_id: Some(peer_id), error, connection_id } => {
let span = self.get_peer_span(peer_id);
let _span_guard = span.enter();
tracing::warn!(%peer_id, %connection_id, ?error, "Outgoing connection error to peer");
}
SwarmEvent::Behaviour(OutEvent::OutboundRequestResponseFailure {peer, error, request_id, protocol}) => {
@ -313,19 +375,22 @@ impl EventLoop {
// 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) {
if let Some((responder, span)) = self.inflight_encrypted_signature_requests.remove(&request_id) {
let _span_guard = span.enter();
let _ = responder.respond(Err(error));
continue;
}
// Check for quote requests
if let Some(responder) = self.inflight_quote_requests.remove(&request_id) {
if let Some((responder, span)) = self.inflight_quote_requests.remove(&request_id) {
let _span_guard = span.enter();
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) {
if let Some((responder, span)) = self.inflight_cooperative_xmr_redeem_requests.remove(&request_id) {
let _span_guard = span.enter();
let _ = responder.respond(Err(error));
continue;
}
@ -345,14 +410,40 @@ impl EventLoop {
"Scheduled redial for peer"
);
}
SwarmEvent::Behaviour(OutEvent::CachedQuotes { quotes }) => {
tracing::trace!(
?quotes,
"Received cached quotes"
);
let quotes_with_addresses: Vec<QuoteWithAddress> = quotes
.into_iter()
.map(|(peer_id, multiaddr, quote, version)| QuoteWithAddress {
peer_id,
multiaddr,
quote,
version,
})
.collect();
let _ = self.cached_quotes_sender.send(quotes_with_addresses);
}
SwarmEvent::Behaviour(OutEvent::CachedQuotesProgress { peers }) => {
self.tauri_handle.emit_quotes_progress(peers.clone());
}
SwarmEvent::Behaviour(OutEvent::Observe(event)) => {
self.tauri_handle.emit_peer_connection_change(event.peer_id, event.update);
}
_ => {}
}
},
// Handle to-be-sent outgoing requests for all our network protocols.
Some((peer_id, responder)) = self.quote_requests.next().fuse() => {
let outbound_request_id = self.swarm.behaviour_mut().quote.send_request(&peer_id, ());
self.inflight_quote_requests.insert(outbound_request_id, responder);
Some(((peer_id, span), responder)) = self.quote_requests.next().fuse() => {
let _span_guard = span.enter();
let outbound_request_id = self.swarm.behaviour_mut().direct_quote.send_request(&peer_id, ());
self.inflight_quote_requests.insert(outbound_request_id, (responder, span.clone()));
tracing::trace!(
%peer_id,
@ -360,14 +451,16 @@ impl EventLoop {
"Dispatching outgoing quote request"
);
},
Some(((peer_id, swap_id, tx_redeem_encsig), responder)) = self.encrypted_signatures_requests.next().fuse() => {
Some(((peer_id, swap_id, tx_redeem_encsig, span), responder)) = self.encrypted_signatures_requests.next().fuse() => {
let _span_guard = span.enter();
let request = encrypted_signature::Request {
swap_id,
tx_redeem_encsig
};
let outbound_request_id = self.swarm.behaviour_mut().encrypted_signature.send_request(&peer_id, request);
self.inflight_encrypted_signature_requests.insert(outbound_request_id, responder);
self.inflight_encrypted_signature_requests.insert(outbound_request_id, (responder, span.clone()));
tracing::trace!(
%peer_id,
@ -376,11 +469,13 @@ impl EventLoop {
"Dispatching outgoing encrypted signature"
);
},
Some(((peer_id, swap_id), responder)) = self.cooperative_xmr_redeem_requests.next().fuse() => {
Some(((peer_id, swap_id, span), responder)) = self.cooperative_xmr_redeem_requests.next().fuse() => {
let _span_guard = span.enter();
let outbound_request_id = self.swarm.behaviour_mut().cooperative_xmr_redeem.send_request(&peer_id, Request {
swap_id
});
self.inflight_cooperative_xmr_redeem_requests.insert(outbound_request_id, responder);
self.inflight_cooperative_xmr_redeem_requests.insert(outbound_request_id, (responder, span.clone()));
tracing::trace!(
%peer_id,
@ -392,11 +487,26 @@ impl EventLoop {
// Instruct the swap setup behaviour to do a swap setup request
// The behaviour will instruct the swarm to dial Alice, so we don't need to check if we are connected
Some(((alice_peer_id, swap), responder)) = self.execution_setup_requests.next().fuse() => {
Some(((alice_peer_id, swap, span), responder)) = self.execution_setup_requests.next().fuse() => {
let _span_guard = span.enter();
let swap_id = swap.swap_id.clone();
self.swarm.behaviour_mut().swap_setup.start(alice_peer_id, swap).await;
self.inflight_swap_setup.insert((alice_peer_id, swap_id), responder);
// Check if we have an inflight swap setup request for this swap already
if self.inflight_swap_setup.contains_key(&(alice_peer_id, swap_id)) {
tracing::warn!(
%alice_peer_id,
%swap_id,
"We already have an inflight swap setup request for this swap. We will not instruct the Behaviour to start a new one. Responding with an error to the caller immediately.",
);
// Immediately respond with an error to the caller
let _ = responder.respond(Err(anyhow::anyhow!("We already have an inflight swap setup request for this swap")));
continue;
}
self.swarm.behaviour_mut().swap_setup.queue_new_swap(alice_peer_id, swap);
self.inflight_swap_setup.insert((alice_peer_id, swap_id), (responder, span.clone()));
tracing::trace!(
%alice_peer_id,
@ -419,11 +529,12 @@ impl EventLoop {
}
},
Some(((swap_id, peer_id, sender), responder)) = self.queued_swap_handlers.next().fuse() => {
Some(((swap_id, peer_id, sender, span), responder)) = self.queued_swap_handlers.next().fuse() => {
let _guard = span.enter();
tracing::trace!(%swap_id, %peer_id, "Registering swap handle for a swap internally inside the event loop");
// This registers the swap_id -> peer_id and swap_id -> transfer_proof_sender
self.registered_swap_handlers.insert(swap_id, (peer_id, sender));
self.registered_swap_handlers.insert(swap_id, (peer_id, sender, span.clone()));
// Instruct the swarm to contineously redial the peer
// TODO: We must remove it again once the swap is complete, otherwise we will redial indefinitely
@ -432,9 +543,26 @@ impl EventLoop {
// Acknowledge the registration
let _ = responder.respond(());
},
Some(((peer_id, addr), responder)) = self.add_peer_address_requests.next().fuse() => {
tracing::trace!(%peer_id, %addr, "Adding peer address to swarm");
self.swarm.add_peer_address(peer_id, addr);
let _ = responder.respond(());
},
}
}
}
fn get_peer_span(&self, peer_id: PeerId) -> tracing::Span {
let span = tracing::debug_span!("peer_context", %peer_id);
for (peer, _, s) in self.registered_swap_handlers.values() {
if *peer == peer_id {
span.follows_from(s);
}
}
span
}
}
#[derive(Debug, Clone)]
@ -444,14 +572,14 @@ pub struct EventLoopHandle {
/// 2. Return the resulting State2 if successful
/// 3. Return an anyhow error if the request fails
execution_setup_sender:
bmrng::unbounded::UnboundedRequestSender<(PeerId, NewSwap), Result<State2>>,
bmrng::unbounded::UnboundedRequestSender<(PeerId, NewSwap, tracing::Span), Result<State2>>,
/// When a (PeerId, Uuid, EncryptedSignature) tuple is sent into this channel, the EventLoop will:
/// 1. Send the encrypted signature to the specified peer over the network
/// 2. Return Ok(()) if the peer acknowledges receipt, or
/// 3. Return an OutboundFailure error if the request fails
encrypted_signature_sender: bmrng::unbounded::UnboundedRequestSender<
(PeerId, Uuid, EncryptedSignature),
(PeerId, Uuid, EncryptedSignature, tracing::Span),
Result<(), OutboundFailure>,
>,
@ -459,8 +587,10 @@ pub struct EventLoopHandle {
/// 1. Request a price quote from the specified peer
/// 2. Return the quote if successful
/// 3. Return an OutboundFailure error if the request fails
quote_sender:
bmrng::unbounded::UnboundedRequestSender<PeerId, Result<BidQuote, OutboundFailure>>,
quote_sender: bmrng::unbounded::UnboundedRequestSender<
(PeerId, tracing::Span),
Result<BidQuote, OutboundFailure>,
>,
/// When a (PeerId, Uuid) tuple is sent into this channel, the EventLoop will:
/// 1. Request the specified peer's cooperation in redeeming the Monero for the given swap
@ -468,7 +598,7 @@ pub struct EventLoopHandle {
/// 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::unbounded::UnboundedRequestSender<
(PeerId, Uuid),
(PeerId, Uuid, tracing::Span),
Result<cooperative_xmr_redeem_after_punish::Response, OutboundFailure>,
>,
@ -477,12 +607,37 @@ pub struct EventLoopHandle {
Uuid,
PeerId,
bmrng::unbounded::UnboundedRequestSender<monero::TransferProof, ()>,
tracing::Span,
),
(),
>,
/// Channel for adding peer addresses to the swarm
add_peer_address_sender:
bmrng::unbounded::UnboundedRequestSender<(PeerId, libp2p::Multiaddr), ()>,
// TODO: Extract the Vec<_> into its own struct (QuotesBatch?)
cached_quotes_receiver: tokio::sync::watch::Receiver<Vec<QuoteWithAddress>>,
}
impl EventLoopHandle {
pub fn cached_quotes(&self) -> tokio::sync::watch::Receiver<Vec<QuoteWithAddress>> {
self.cached_quotes_receiver.clone()
}
/// Adds a peer address to the swarm
pub async fn queue_peer_address(
&mut self,
peer_id: PeerId,
addr: libp2p::Multiaddr,
) -> Result<()> {
self.add_peer_address_sender
.send((peer_id, addr))
.context("Failed to queue peer address into event loop")?;
Ok(())
}
/// Creates a SwapEventLoopHandle for a specific swap
/// This registers the swap's transfer proof receiver with the event loop
pub async fn swap_handle(
@ -494,14 +649,15 @@ impl EventLoopHandle {
//
// The sender is stored in the `EventLoop`. The receiver is stored in the `SwapEventLoopHandle`.
let (transfer_proof_sender, transfer_proof_receiver) = bmrng::unbounded_channel();
let span = tracing::Span::current();
// Register this sender in the `EventLoop`
// It is put into the queue and then later moved into `registered_transfer_proof_senders`
//
// We use `send(...) instead of send_receive(...)` because the event loop needs to be running for this to respond
self.queued_transfer_proof_sender
.send((swap_id, peer_id, transfer_proof_sender))
.context("Failed to register transfer proof sender with event loop")?;
.send((swap_id, peer_id, transfer_proof_sender, span))
.context("Failed to queue transfer proof sender into event loop")?;
Ok(SwapEventLoopHandle {
handle: self.clone(),
@ -515,13 +671,14 @@ impl EventLoopHandle {
///
/// This will retry until the maximum elapsed time is reached. It is therefore fallible.
pub async fn setup_swap(&mut self, peer_id: PeerId, swap: NewSwap) -> Result<State2> {
let span = tracing::Span::current();
tracing::debug!(swap = ?swap, %peer_id, "Sending swap setup request");
let backoff =
retry::give_up_eventually(RETRY_MAX_INTERVAL, EXECUTION_SETUP_MAX_ELAPSED_TIME);
backoff::future::retry_notify(backoff, || async {
match self.execution_setup_sender.send_receive((peer_id, swap.clone())).await {
match self.execution_setup_sender.send_receive((peer_id, swap.clone(), span.clone())).await {
Ok(Ok(state2)) => {
Ok(state2)
}
@ -562,8 +719,9 @@ impl EventLoopHandle {
REQUEST_RESPONSE_PROTOCOL_RETRY_MAX_ELASPED_TIME,
);
let span = tracing::Span::current();
backoff::future::retry_notify(backoff, || async {
match self.quote_sender.send_receive(peer_id).await {
match self.quote_sender.send_receive((peer_id, span.clone())).await {
Ok(Ok(quote)) => Ok(quote),
Ok(Err(err)) => {
Err(backoff::Error::transient(anyhow!(err).context("A network error occurred while requesting a quote")))
@ -591,6 +749,7 @@ impl EventLoopHandle {
peer_id: PeerId,
swap_id: Uuid,
) -> Result<Response> {
let span = tracing::Span::current();
tracing::debug!(%peer_id, %swap_id, "Requesting cooperative XMR redeem");
// We want to give up eventually here
@ -600,7 +759,7 @@ impl EventLoopHandle {
);
backoff::future::retry_notify(backoff, || async {
match self.cooperative_xmr_redeem_sender.send_receive((peer_id, swap_id)).await {
match self.cooperative_xmr_redeem_sender.send_receive((peer_id, swap_id, span.clone())).await {
Ok(Ok(response)) => Ok(response),
Ok(Err(err)) => {
Err(backoff::Error::transient(anyhow!(err).context("A network error occurred while requesting cooperative XMR redeem")))
@ -629,13 +788,14 @@ impl EventLoopHandle {
swap_id: Uuid,
tx_redeem_encsig: EncryptedSignature,
) -> () {
let span = tracing::Span::current();
tracing::debug!(%peer_id, %swap_id, "Sending encrypted signature");
// We will retry indefinitely until we succeed
let backoff = retry::never_give_up(RETRY_MAX_INTERVAL);
backoff::future::retry_notify(backoff, || async {
match self.encrypted_signature_sender.send_receive((peer_id, swap_id, tx_redeem_encsig.clone())).await {
match self.encrypted_signature_sender.send_receive((peer_id, swap_id, tx_redeem_encsig.clone(), span.clone())).await {
Ok(Ok(_)) => Ok(()),
Ok(Err(err)) => {
Err(backoff::Error::transient(anyhow!(err).context("A network error occurred while sending the encrypted signature")))

File diff suppressed because it is too large Load diff

View file

@ -217,6 +217,7 @@ mod crates {
"libp2p_yamux",
"libp2p_tor",
"libp2p_tcp",
// TODO: Maybe add "swap_p2p" here too?
];
pub const OUR_CRATES: &[&str] = &[

View file

@ -472,6 +472,24 @@ impl Database for SqliteDatabase {
Ok(Some(proof))
}
async fn has_swap(&self, swap_id: Uuid) -> Result<bool> {
let swap_id = swap_id.to_string();
let row = sqlx::query!(
r#"
SELECT 1 as found
FROM swap_states
WHERE swap_id = ?
LIMIT 1
"#,
swap_id
)
.fetch_optional(&self.pool)
.await?;
Ok(row.is_some())
}
}
#[cfg(test)]

View file

@ -21,7 +21,6 @@ pub mod bitcoin;
pub mod cli;
pub mod common;
pub mod database;
pub mod libp2p_ext;
pub mod monero;
pub mod network;
pub mod protocol;

View file

@ -1,24 +0,0 @@
use libp2p::multiaddr::Protocol;
use libp2p::{Multiaddr, PeerId};
pub trait MultiAddrExt {
fn extract_peer_id(&self) -> Option<PeerId>;
fn split_peer_id(&self) -> Option<(PeerId, Multiaddr)>;
}
impl MultiAddrExt for Multiaddr {
fn extract_peer_id(&self) -> Option<PeerId> {
match self.iter().last()? {
Protocol::P2p(peer_id) => Some(peer_id),
_ => None,
}
}
// Takes a peer id like /ip4/192.168.178.64/tcp/9939/p2p/12D3KooWQsqsCyJ9ae1YEAJZAfoVdVFZdDdUq3yvZ92btq7hSv9f
// and returns the peer id and the original address *with* the peer id
fn split_peer_id(&self) -> Option<(PeerId, Multiaddr)> {
let peer_id = self.extract_peer_id()?;
let address = self.clone();
Some((peer_id, address))
}
}

View file

@ -1,6 +1,8 @@
pub use swap_p2p::protocols::cooperative_xmr_redeem_after_punish;
pub use swap_p2p::protocols::encrypted_signature;
pub use swap_p2p::protocols::quote;
pub use swap_p2p::protocols::quotes;
pub use swap_p2p::protocols::quotes_cached;
pub use swap_p2p::protocols::redial;
pub use swap_p2p::protocols::rendezvous;
pub use swap_p2p::protocols::swap_setup;
@ -9,4 +11,5 @@ pub use swap_p2p::protocols::transfer_proof;
pub mod swarm;
pub mod transport;
#[cfg(test)]
pub use swap_p2p::test;

View file

@ -1,21 +1,22 @@
use crate::asb::{register, LatestRate};
use crate::libp2p_ext::MultiAddrExt;
use crate::asb::LatestRate;
use crate::network::rendezvous::XmrBtcNamespace;
use crate::seed::Seed;
use crate::{asb, cli};
use anyhow::Result;
use arti_client::TorClient;
use libp2p::swarm::NetworkBehaviour;
use libp2p::SwarmBuilder;
use libp2p::{identity, Multiaddr, Swarm};
use libp2p::{PeerId, SwarmBuilder};
use std::fmt::Debug;
use std::sync::Arc;
use std::time::Duration;
use swap_core::bitcoin;
use swap_env::env;
use swap_p2p::libp2p_ext::MultiAddrExt;
use tor_rtcompat::tokio::TokioRustlsRuntime;
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2); // 2 hours
// We keep connections open for 15 minutes
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 15);
#[allow(clippy::too_many_arguments)]
pub fn asb<LR>(
@ -36,14 +37,11 @@ where
{
let identity = seed.derive_libp2p_identity();
let rendezvous_nodes = rendezvous_addrs
let rendezvous_nodes: Vec<PeerId> = rendezvous_addrs
.iter()
.map(|addr| {
let peer_id = addr
.extract_peer_id()
.expect("Rendezvous node address must contain peer ID");
register::RendezvousNode::new(addr, peer_id, namespace, None)
addr.extract_peer_id()
.expect("Rendezvous node address must contain peer ID")
})
.collect();
@ -64,13 +62,20 @@ where
num_intro_points,
)?;
let swarm = SwarmBuilder::with_existing_identity(identity)
let mut swarm = SwarmBuilder::with_existing_identity(identity)
.with_tokio()
.with_other_transport(|_| transport)?
.with_behaviour(|_| behaviour)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT))
.build();
for addr in rendezvous_addrs {
let peer_id = addr
.extract_peer_id()
.expect("Rendezvous node address must contain peer ID");
swarm.add_peer_address(peer_id, addr.clone());
}
Ok((swarm, onion_addresses))
}

View file

@ -103,7 +103,6 @@ pub async fn setup_test<T, F, C>(
.await
.main_address()
.await
.unwrap()
.into();
let developer_tip_monero_wallet_subaddress = developer_tip_monero_wallet
@ -385,7 +384,7 @@ async fn init_test_wallets(
let xmr_wallet = wallets.main_wallet().await;
tracing::info!(
address = %xmr_wallet.main_address().await.unwrap(),
address = %xmr_wallet.main_address().await,
"Initialized monero wallet"
);
@ -535,12 +534,7 @@ impl BobParams {
pub async fn get_change_receive_addresses(&self) -> (bitcoin::Address, monero::Address) {
(
self.bitcoin_wallet.new_address().await.unwrap(),
self.monero_wallet
.main_wallet()
.await
.main_address()
.await
.unwrap(),
self.monero_wallet.main_wallet().await.main_address().await,
)
}
@ -569,7 +563,6 @@ impl BobParams {
.await
.main_address()
.await
.unwrap()
.into(),
)
.await
@ -610,7 +603,6 @@ impl BobParams {
.await
.main_address()
.await
.unwrap()
.into(),
self.bitcoin_wallet.new_address().await?,
btc_amount,
@ -629,7 +621,9 @@ impl BobParams {
let behaviour = cli::Behaviour::new(
self.env_config,
self.bitcoin_wallet.clone(),
(identity.clone(), XmrBtcNamespace::Testnet),
identity.clone(),
XmrBtcNamespace::Testnet,
Vec::new(),
);
let mut swarm = swarm::cli(identity.clone(), None, behaviour).await?;
swarm.add_peer_address(self.alice_peer_id, self.alice_address.clone());