mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-17 09:34:16 -05:00
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:
parent
a54b7618d5
commit
801ce8fd9d
86 changed files with 4470 additions and 3800 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -21,3 +21,6 @@ swap-orchestrator/config.toml
|
|||
release-build.sh
|
||||
cn_macos
|
||||
libp2p-rendezvous-node/rendezvous-data
|
||||
rust-electrum-client/
|
||||
rust-libp2p/
|
||||
bdk/
|
||||
|
|
|
|||
18
.sqlx/query-60462ce4f45f174eb4603a2d94e67cf98eb7d6176515e6a28c4e8ce9fda6ef15.json
generated
Normal file
18
.sqlx/query-60462ce4f45f174eb4603a2d94e67cf98eb7d6176515e6a28c4e8ce9fda6ef15.json
generated
Normal 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"
|
||||
}
|
||||
|
|
@ -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
1
CLAUDE.md
Normal 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
7
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
||||
|
|
|
|||
2
justfile
2
justfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
42
src-gui/src/renderer/components/other/ClickToCopy.tsx
Normal file
42
src-gui/src/renderer/components/other/ClickToCopy.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
39
src-gui/src/renderer/components/other/Jdenticon.tsx
Normal file
39
src-gui/src/renderer/components/other/Jdenticon.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
54
src-gui/src/store/features/p2pSlice.ts
Normal file
54
src-gui/src/store/features/p2pSlice.ts
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.*"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
157
swap-p2p/examples/fetch_quotes.rs
Normal file
157
swap-p2p/examples/fetch_quotes.rs
Normal 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, .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
379
swap-p2p/src/behaviour_util.rs
Normal file
379
swap-p2p/src/behaviour_util.rs
Normal 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
34
swap-p2p/src/defaults.rs
Normal 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);
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
80
swap-p2p/src/libp2p_ext.rs
Normal file
80
swap-p2p/src/libp2p_ext.rs
Normal 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
163
swap-p2p/src/observe.rs
Normal 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![])
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
1
swap-p2p/src/patches.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod identify;
|
||||
136
swap-p2p/src/patches/identify.rs
Normal file
136
swap-p2p/src/patches/identify.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
162
swap-p2p/src/protocols/notice.rs
Normal file
162
swap-p2p/src/protocols/notice.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
502
swap-p2p/src/protocols/quotes.rs
Normal file
502
swap-p2p/src/protocols/quotes.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
263
swap-p2p/src/protocols/quotes_cached.rs
Normal file
263
swap-p2p/src/protocols/quotes_cached.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
289
swap-p2p/src/protocols/rendezvous/discovery.rs
Normal file
289
swap-p2p/src/protocols/rendezvous/discovery.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
445
swap-p2p/src/protocols/rendezvous/register.rs
Normal file
445
swap-p2p/src/protocols/rendezvous/register.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
273
swap/src/cli.rs
273
swap/src/cli.rs
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -217,6 +217,7 @@ mod crates {
|
|||
"libp2p_yamux",
|
||||
"libp2p_tor",
|
||||
"libp2p_tcp",
|
||||
// TODO: Maybe add "swap_p2p" here too?
|
||||
];
|
||||
|
||||
pub const OUR_CRATES: &[&str] = &[
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue