Include unreachable nodes in table output

We emit an `info!` log for every peer that we discover but only ever
emitted a `debug!` log if we fail to connect. This leads to a situation
where the user would run `swap list-sellers`, the logs would say
"Discovered XYZ at ABC" but then get a potentially empty table.

To not confuse the user, we include unreachable nodes in the table output.
For example:

```
Connected to rendezvous point, discovering nodes in 'xmr-btc-swap-testnet' namespace ...
Discovered peer 12D3KooWPZ69DRp4wbGB3wJsxxsg1XW1EVZ2evtVwcARCF3a1nrx at /dns4/ac4hgzmsmekwekjbdl77brufqqbylddugzze4tel6qsnlympgmr46iid.onion/tcp/8765
+-------+--------------+--------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------+
| PRICE | MIN_QUANTITY | MAX_QUANTITY | STATUS      | ADDRESS                                                                                                                                |
+============================================================================================================================================================================================+
| ???   | ???          | ???          | Unreachable | /dns4/ac4hgzmsmekwekjbdl77brufqqbylddugzze4tel6qsnlympgmr46iid.onion/tcp/8765/p2p/12D3KooWPZ69DRp4wbGB3wJsxxsg1XW1EVZ2evtVwcARCF3a1nrx |
+-------+--------------+--------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------+
```
This commit is contained in:
Thomas Eizinger 2021-07-07 13:32:44 +10:00 committed by Daniel Karzel
parent 09f395a26b
commit 987f8abb9d
No known key found for this signature in database
GPG Key ID: 30C3FC2E438ADB6E
4 changed files with 123 additions and 25 deletions

View File

@ -24,7 +24,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use swap::bitcoin::TxLock; use swap::bitcoin::TxLock;
use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command, ParseResult}; use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command, ParseResult};
use swap::cli::{list_sellers, EventLoop}; use swap::cli::{list_sellers, EventLoop, SellerStatus};
use swap::database::Database; use swap::database::Database;
use swap::env::Config; use swap::env::Config;
use swap::libp2p_ext::MultiAddrExt; use swap::libp2p_ext::MultiAddrExt;
@ -285,7 +285,7 @@ async fn main() -> Result<()> {
.context("Failed to read in seed file")?; .context("Failed to read in seed file")?;
let identity = seed.derive_libp2p_identity(); let identity = seed.derive_libp2p_identity();
let sellers = list_sellers( let mut sellers = list_sellers(
rendezvous_node_peer_id, rendezvous_node_peer_id,
rendezvous_node_addr, rendezvous_node_addr,
namespace, namespace,
@ -293,6 +293,7 @@ async fn main() -> Result<()> {
identity, identity,
) )
.await?; .await?;
sellers.sort();
if json { if json {
for seller in sellers { for seller in sellers {
@ -301,15 +302,37 @@ async fn main() -> Result<()> {
} else { } else {
let mut table = Table::new(); let mut table = Table::new();
table.set_header(vec!["PRICE", "MIN_QUANTITY", "MAX_QUANTITY", "ADDRESS"]); table.set_header(vec![
"PRICE",
"MIN_QUANTITY",
"MAX_QUANTITY",
"STATUS",
"ADDRESS",
]);
for seller in sellers { for seller in sellers {
table.add_row(vec![ let row = match seller.status {
seller.quote.price.to_string(), SellerStatus::Online(quote) => {
seller.quote.min_quantity.to_string(), vec![
seller.quote.max_quantity.to_string(), quote.price.to_string(),
seller.multiaddr.to_string(), quote.min_quantity.to_string(),
]); quote.max_quantity.to_string(),
"Online".to_owned(),
seller.multiaddr.to_string(),
]
}
SellerStatus::Unreachable => {
vec![
"???".to_owned(),
"???".to_owned(),
"???".to_owned(),
"Unreachable".to_owned(),
seller.multiaddr.to_string(),
]
}
};
table.add_row(row);
} }
println!("{}", table); println!("{}", table);

View File

@ -10,14 +10,14 @@ pub mod transport;
pub use behaviour::{Behaviour, OutEvent}; pub use behaviour::{Behaviour, OutEvent};
pub use cancel::cancel; pub use cancel::cancel;
pub use event_loop::{EventLoop, EventLoopHandle}; pub use event_loop::{EventLoop, EventLoopHandle};
pub use list_sellers::list_sellers; pub use list_sellers::{list_sellers, Seller, Status as SellerStatus};
pub use refund::refund; pub use refund::refund;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::asb; use crate::asb;
use crate::cli::list_sellers::Seller; use crate::cli::list_sellers::{Seller, Status};
use crate::network::quote; use crate::network::quote;
use crate::network::quote::BidQuote; use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace; use crate::network::rendezvous::XmrBtcNamespace;
@ -122,7 +122,7 @@ mod tests {
Seller { Seller {
multiaddr: asb_address.with(Protocol::P2p(asb_peer_id.into())), multiaddr: asb_address.with(Protocol::P2p(asb_peer_id.into())),
quote: static_quote, status: Status::Online(static_quote),
} }
} }

View File

@ -11,6 +11,7 @@ use libp2p::swarm::SwarmEvent;
use libp2p::{identity, rendezvous, Multiaddr, PeerId, Swarm}; use libp2p::{identity, rendezvous, Multiaddr, PeerId, Swarm};
use serde::Serialize; use serde::Serialize;
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr};
use std::collections::hash_map::Entry;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
@ -52,11 +53,17 @@ pub async fn list_sellers(
} }
#[serde_as] #[serde_as]
#[derive(Debug, Serialize, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Seller { pub struct Seller {
pub status: Status,
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
pub multiaddr: Multiaddr, pub multiaddr: Multiaddr,
pub quote: BidQuote, }
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)]
pub enum Status {
Online(BidQuote),
Unreachable,
} }
#[derive(Debug)] #[derive(Debug)]
@ -90,7 +97,7 @@ struct Behaviour {
#[derive(Debug)] #[derive(Debug)]
enum QuoteStatus { enum QuoteStatus {
Pending, Pending,
Received(BidQuote), Received(Status),
} }
#[derive(Debug)] #[derive(Debug)]
@ -104,7 +111,8 @@ struct EventLoop {
rendezvous_peer_id: PeerId, rendezvous_peer_id: PeerId,
rendezvous_addr: Multiaddr, rendezvous_addr: Multiaddr,
namespace: XmrBtcNamespace, namespace: XmrBtcNamespace,
asb_address: HashMap<PeerId, Multiaddr>, reachable_asb_address: HashMap<PeerId, Multiaddr>,
unreachable_asb_address: HashMap<PeerId, Multiaddr>,
asb_quote_status: HashMap<PeerId, QuoteStatus>, asb_quote_status: HashMap<PeerId, QuoteStatus>,
state: State, state: State,
} }
@ -121,7 +129,8 @@ impl EventLoop {
rendezvous_peer_id, rendezvous_peer_id,
rendezvous_addr, rendezvous_addr,
namespace, namespace,
asb_address: Default::default(), reachable_asb_address: Default::default(),
unreachable_asb_address: Default::default(),
asb_quote_status: Default::default(), asb_quote_status: Default::default(),
state: State::WaitForDiscovery, state: State::WaitForDiscovery,
} }
@ -147,7 +156,7 @@ impl EventLoop {
); );
} else { } else {
let address = endpoint.get_remote_address(); let address = endpoint.get_remote_address();
self.asb_address.insert(peer_id, address.clone()); self.reachable_asb_address.insert(peer_id, address.clone());
} }
} }
SwarmEvent::UnreachableAddr { peer_id, error, address, .. } => { SwarmEvent::UnreachableAddr { peer_id, error, address, .. } => {
@ -166,9 +175,16 @@ impl EventLoop {
address, address,
error error
); );
self.unreachable_asb_address.insert(peer_id, address.clone());
// if a different peer than the rendezvous node is unreachable (i.e. a seller) we remove that seller from the quote status state match self.asb_quote_status.entry(peer_id) {
self.asb_quote_status.remove(&peer_id); Entry::Occupied(mut entry) => {
entry.insert(QuoteStatus::Received(Status::Unreachable));
},
_ => {
tracing::debug!(%peer_id, %error, "Connection error with unexpected peer")
}
}
} }
} }
SwarmEvent::Behaviour(OutEvent::Rendezvous( SwarmEvent::Behaviour(OutEvent::Rendezvous(
@ -205,7 +221,7 @@ impl EventLoop {
RequestResponseEvent::Message { peer, message } => { RequestResponseEvent::Message { peer, message } => {
match message { match message {
RequestResponseMessage::Response { response, .. } => { RequestResponseMessage::Response { response, .. } => {
if self.asb_quote_status.insert(peer, QuoteStatus::Received(response)).is_none() { if self.asb_quote_status.insert(peer, QuoteStatus::Received(Status::Online(response))).is_none() {
tracing::error!(%peer, "Received bid quote from unexpected peer, this record will be removed!"); tracing::error!(%peer, "Received bid quote from unexpected peer, this record will be removed!");
self.asb_quote_status.remove(&peer); self.asb_quote_status.remove(&peer);
} }
@ -247,15 +263,26 @@ impl EventLoop {
.iter() .iter()
.map(|(peer_id, quote_status)| match quote_status { .map(|(peer_id, quote_status)| match quote_status {
QuoteStatus::Pending => Err(StillPending {}), QuoteStatus::Pending => Err(StillPending {}),
QuoteStatus::Received(quote) => { QuoteStatus::Received(Status::Online(quote)) => {
let address = self let address = self
.asb_address .reachable_asb_address
.get(&peer_id) .get(&peer_id)
.expect("if we got a quote we must have stored an address"); .expect("if we got a quote we must have stored an address");
Ok(Seller { Ok(Seller {
multiaddr: address.clone(), multiaddr: address.clone(),
quote: *quote, status: Status::Online(*quote),
})
}
QuoteStatus::Received(Status::Unreachable) => {
let address = self
.unreachable_asb_address
.get(&peer_id)
.expect("if we got a quote we must have stored an address");
Ok(Seller {
multiaddr: address.clone(),
status: Status::Unreachable,
}) })
} }
}) })
@ -279,3 +306,51 @@ impl From<PingEvent> for OutEvent {
OutEvent::Ping(event) OutEvent::Ping(event)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sellers_sort_with_unreachable_coming_last() {
let mut list = vec![
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
status: Status::Unreachable,
},
Seller {
multiaddr: Multiaddr::empty(),
status: Status::Unreachable,
},
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
status: Status::Online(BidQuote {
price: Default::default(),
min_quantity: Default::default(),
max_quantity: Default::default(),
}),
},
];
list.sort();
assert_eq!(list, vec![
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
status: Status::Online(BidQuote {
price: Default::default(),
min_quantity: Default::default(),
max_quantity: Default::default(),
})
},
Seller {
multiaddr: Multiaddr::empty(),
status: Status::Unreachable
},
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
status: Status::Unreachable
},
])
}
}

View File

@ -24,7 +24,7 @@ impl ProtocolName for BidQuoteProtocol {
} }
/// Represents a quote for buying XMR. /// Represents a quote for buying XMR.
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct BidQuote { pub struct BidQuote {
/// The price at which the maker is willing to buy at. /// The price at which the maker is willing to buy at.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")] #[serde(with = "::bitcoin::util::amount::serde::as_sat")]