mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-01-11 07:29:39 -05:00
Add swap/
Add a binary crate `swap` that implements two nodes (Alice and Bob). With this applied we can start up a node for each role and do: - Bob: Requests current amounts using BTC is input - Alice: Responds with amounts - Bob: (mock) get user input to Ok the amounts ... continue with swap (TODO)
This commit is contained in:
parent
4723626fc0
commit
05766d3146
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = ["monero-harness", "xmr-btc"]
|
||||
members = ["monero-harness", "xmr-btc", "swap"]
|
||||
|
32
swap/Cargo.toml
Normal file
32
swap/Cargo.toml
Normal file
@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "swap"
|
||||
version = "0.1.0"
|
||||
authors = ["CoBloX developers <team@coblox.tech>"]
|
||||
edition = "2018"
|
||||
description = "XMR/BTC trustless atomic swaps."
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
atty = "0.2"
|
||||
bitcoin = "0.25" # TODO: Upgrade other crates in this repo to use this version.
|
||||
derivative = "2"
|
||||
futures = { version = "0.3", default-features = false }
|
||||
libp2p = { version = "0.28", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] }
|
||||
libp2p-tokio-socks5 = "0.3"
|
||||
log = { version = "0.4", features = ["serde"] }
|
||||
monero = "0.9"
|
||||
rand = "0.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1"
|
||||
structopt = "0.3"
|
||||
time = "0.2"
|
||||
tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] }
|
||||
tracing = { version = "0.1", features = ["attributes"] }
|
||||
tracing-core = "0.1"
|
||||
tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] }
|
||||
tracing-log = "0.1"
|
||||
tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] }
|
||||
void = "1"
|
||||
xmr-btc = { path = "../xmr-btc" }
|
141
swap/src/alice.rs
Normal file
141
swap/src/alice.rs
Normal file
@ -0,0 +1,141 @@
|
||||
//! Run an XMR/BTC swap in the role of Alice.
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
use anyhow::Result;
|
||||
use libp2p::{
|
||||
core::{identity::Keypair, Multiaddr},
|
||||
request_response::ResponseChannel,
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use std::{thread, time::Duration};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
mod messenger;
|
||||
|
||||
use self::messenger::*;
|
||||
use crate::{
|
||||
monero,
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
request_response::{AliceToBob, TIMEOUT},
|
||||
transport, TokioExecutor,
|
||||
},
|
||||
Never, SwapParams,
|
||||
};
|
||||
|
||||
pub type Swarm = libp2p::Swarm<Alice>;
|
||||
|
||||
pub async fn swap(listen: Multiaddr) -> Result<()> {
|
||||
let mut swarm = new_swarm(listen)?;
|
||||
|
||||
match swarm.next().await {
|
||||
BehaviourOutEvent::Request(messenger::BehaviourOutEvent::Btc { btc, channel }) => {
|
||||
debug!("Got request from Bob");
|
||||
let params = SwapParams {
|
||||
btc,
|
||||
// TODO: Do a real calculation.
|
||||
xmr: monero::Amount::from_piconero(10),
|
||||
};
|
||||
|
||||
let msg = AliceToBob::Amounts(params);
|
||||
swarm.send(channel, msg);
|
||||
}
|
||||
other => panic!("unexpected event: {:?}", other),
|
||||
}
|
||||
|
||||
warn!("parking thread ...");
|
||||
thread::park();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_swarm(listen: Multiaddr) -> Result<Swarm> {
|
||||
use anyhow::Context as _;
|
||||
|
||||
let behaviour = Alice::default();
|
||||
|
||||
let local_key_pair = behaviour.identity();
|
||||
let local_peer_id = behaviour.peer_id();
|
||||
|
||||
let transport = transport::build(local_key_pair)?;
|
||||
|
||||
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone())
|
||||
.executor(Box::new(TokioExecutor {
|
||||
handle: tokio::runtime::Handle::current(),
|
||||
}))
|
||||
.build();
|
||||
|
||||
Swarm::listen_on(&mut swarm, listen.clone())
|
||||
.with_context(|| format!("Address is not supported: {:#}", listen))?;
|
||||
|
||||
tracing::info!("Initialized swarm: {}", local_peer_id);
|
||||
|
||||
Ok(swarm)
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum BehaviourOutEvent {
|
||||
Request(messenger::BehaviourOutEvent),
|
||||
ConnectionEstablished(PeerId),
|
||||
Never, // FIXME: Why do we need this?
|
||||
}
|
||||
|
||||
impl From<Never> for BehaviourOutEvent {
|
||||
fn from(_: Never) -> Self {
|
||||
BehaviourOutEvent::Never
|
||||
}
|
||||
}
|
||||
|
||||
impl From<messenger::BehaviourOutEvent> for BehaviourOutEvent {
|
||||
fn from(event: messenger::BehaviourOutEvent) -> Self {
|
||||
BehaviourOutEvent::Request(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<peer_tracker::BehaviourOutEvent> for BehaviourOutEvent {
|
||||
fn from(event: peer_tracker::BehaviourOutEvent) -> Self {
|
||||
match event {
|
||||
peer_tracker::BehaviourOutEvent::ConnectionEstablished(id) => {
|
||||
BehaviourOutEvent::ConnectionEstablished(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "BehaviourOutEvent", event_process = false)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Alice {
|
||||
net: Messenger,
|
||||
pt: PeerTracker,
|
||||
#[behaviour(ignore)]
|
||||
identity: Keypair,
|
||||
}
|
||||
|
||||
impl Alice {
|
||||
pub fn identity(&self) -> Keypair {
|
||||
self.identity.clone()
|
||||
}
|
||||
|
||||
pub fn peer_id(&self) -> PeerId {
|
||||
PeerId::from(self.identity.public())
|
||||
}
|
||||
|
||||
/// Alice always sends her messages as a response to a request from Bob.
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: AliceToBob) {
|
||||
self.net.send(channel, msg);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Alice {
|
||||
fn default() -> Self {
|
||||
let identity = Keypair::generate_ed25519();
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
|
||||
Self {
|
||||
net: Messenger::new(timeout),
|
||||
pt: PeerTracker::default(),
|
||||
identity,
|
||||
}
|
||||
}
|
||||
}
|
135
swap/src/alice/messenger.rs
Normal file
135
swap/src/alice/messenger.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use anyhow::Result;
|
||||
use libp2p::{
|
||||
request_response::{
|
||||
handler::RequestProtocol, ProtocolSupport, RequestId, RequestResponse,
|
||||
RequestResponseConfig, RequestResponseEvent, RequestResponseMessage, ResponseChannel,
|
||||
},
|
||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
bitcoin,
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Protocol},
|
||||
Never,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BehaviourOutEvent {
|
||||
Btc {
|
||||
btc: bitcoin::Amount,
|
||||
channel: ResponseChannel<AliceToBob>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "BehaviourOutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Messenger {
|
||||
rr: RequestResponse<Codec>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<BehaviourOutEvent>,
|
||||
}
|
||||
|
||||
impl Messenger {
|
||||
pub fn new(timeout: Duration) -> Self {
|
||||
let mut config = RequestResponseConfig::default();
|
||||
config.set_request_timeout(timeout);
|
||||
|
||||
Self {
|
||||
rr: RequestResponse::new(
|
||||
Codec::default(),
|
||||
vec![(Protocol, ProtocolSupport::Full)],
|
||||
config,
|
||||
),
|
||||
events: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Alice always sends her messages as a response to a request from Bob.
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: AliceToBob) {
|
||||
self.rr.send_response(channel, msg);
|
||||
}
|
||||
|
||||
pub async fn request_amounts(
|
||||
&mut self,
|
||||
alice: PeerId,
|
||||
btc: bitcoin::Amount,
|
||||
) -> Result<RequestId> {
|
||||
let msg = BobToAlice::AmountsFromBtc(btc);
|
||||
let id = self.rr.send_request(&alice, msg);
|
||||
debug!("Request sent to: {}", alice);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn poll(
|
||||
&mut self,
|
||||
_: &mut Context<'_>,
|
||||
_: &mut impl PollParameters,
|
||||
) -> Poll<NetworkBehaviourAction<RequestProtocol<Codec>, BehaviourOutEvent>> {
|
||||
if let Some(event) = self.events.pop_front() {
|
||||
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Messenger {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
||||
peer: _,
|
||||
message:
|
||||
RequestResponseMessage::Request {
|
||||
request,
|
||||
request_id: _,
|
||||
channel,
|
||||
},
|
||||
} => match request {
|
||||
BobToAlice::AmountsFromBtc(btc) => self
|
||||
.events
|
||||
.push_back(BehaviourOutEvent::Btc { btc, channel }),
|
||||
_ => panic!("unexpected request"),
|
||||
},
|
||||
RequestResponseEvent::Message {
|
||||
peer: _,
|
||||
message:
|
||||
RequestResponseMessage::Response {
|
||||
response: _,
|
||||
request_id: _,
|
||||
},
|
||||
} => panic!("unexpected response"),
|
||||
RequestResponseEvent::InboundFailure {
|
||||
peer: _,
|
||||
request_id: _,
|
||||
error,
|
||||
} => {
|
||||
error!("Inbound failure: {:?}", error);
|
||||
}
|
||||
RequestResponseEvent::OutboundFailure {
|
||||
peer: _,
|
||||
request_id: _,
|
||||
error,
|
||||
} => {
|
||||
error!("Outbound failure: {:?}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl libp2p::swarm::NetworkBehaviourEventProcess<()> for Messenger {
|
||||
fn inject_event(&mut self, _event: ()) {}
|
||||
}
|
||||
|
||||
impl libp2p::swarm::NetworkBehaviourEventProcess<Never> for Messenger {
|
||||
fn inject_event(&mut self, _: Never) {}
|
||||
}
|
157
swap/src/bob.rs
Normal file
157
swap/src/bob.rs
Normal file
@ -0,0 +1,157 @@
|
||||
//! Run an XMR/BTC swap in the role of Bob.
|
||||
//! Bob holds BTC and wishes receive XMR.
|
||||
use anyhow::Result;
|
||||
use futures::{
|
||||
channel::mpsc::{Receiver, Sender},
|
||||
StreamExt,
|
||||
};
|
||||
use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId};
|
||||
use std::{process, thread, time::Duration};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
mod messenger;
|
||||
|
||||
use self::messenger::*;
|
||||
use crate::{
|
||||
bitcoin,
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
request_response::TIMEOUT,
|
||||
transport, TokioExecutor,
|
||||
},
|
||||
Cmd, Never, Rsp,
|
||||
};
|
||||
|
||||
pub async fn swap(
|
||||
btc: u64,
|
||||
addr: Multiaddr,
|
||||
mut cmd_tx: Sender<Cmd>,
|
||||
mut rsp_rx: Receiver<Rsp>,
|
||||
) -> Result<()> {
|
||||
let mut swarm = new_swarm()?;
|
||||
|
||||
libp2p::Swarm::dial_addr(&mut swarm, addr)?;
|
||||
let id = match swarm.next().await {
|
||||
BehaviourOutEvent::ConnectionEstablished(id) => id,
|
||||
other => panic!("unexpected event: {:?}", other),
|
||||
};
|
||||
info!("Connection established.");
|
||||
|
||||
swarm.request_amounts(id, btc).await;
|
||||
|
||||
match swarm.next().await {
|
||||
BehaviourOutEvent::Response(messenger::BehaviourOutEvent::Amounts(p)) => {
|
||||
debug!("Got response from Alice: {:?}", p);
|
||||
let cmd = Cmd::VerifyAmounts(p);
|
||||
cmd_tx.try_send(cmd)?;
|
||||
let response = rsp_rx.next().await;
|
||||
if response == Some(Rsp::Abort) {
|
||||
info!("Amounts no good, aborting ...");
|
||||
process::exit(0);
|
||||
}
|
||||
info!("User verified amounts, continuing with swap ...");
|
||||
}
|
||||
other => panic!("unexpected event: {:?}", other),
|
||||
}
|
||||
|
||||
warn!("parking thread ...");
|
||||
thread::park();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub type Swarm = libp2p::Swarm<Bob>;
|
||||
|
||||
fn new_swarm() -> Result<Swarm> {
|
||||
let behaviour = Bob::default();
|
||||
|
||||
let local_key_pair = behaviour.identity();
|
||||
let local_peer_id = behaviour.peer_id();
|
||||
|
||||
let transport = transport::build(local_key_pair)?;
|
||||
|
||||
let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone())
|
||||
.executor(Box::new(TokioExecutor {
|
||||
handle: tokio::runtime::Handle::current(),
|
||||
}))
|
||||
.build();
|
||||
|
||||
info!("Initialized swarm with identity {}", local_peer_id);
|
||||
|
||||
Ok(swarm)
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum BehaviourOutEvent {
|
||||
Response(messenger::BehaviourOutEvent),
|
||||
ConnectionEstablished(PeerId),
|
||||
Never, // FIXME: Why do we need this?
|
||||
}
|
||||
|
||||
impl From<Never> for BehaviourOutEvent {
|
||||
fn from(_: Never) -> Self {
|
||||
BehaviourOutEvent::Never
|
||||
}
|
||||
}
|
||||
|
||||
impl From<messenger::BehaviourOutEvent> for BehaviourOutEvent {
|
||||
fn from(event: messenger::BehaviourOutEvent) -> Self {
|
||||
BehaviourOutEvent::Response(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<peer_tracker::BehaviourOutEvent> for BehaviourOutEvent {
|
||||
fn from(event: peer_tracker::BehaviourOutEvent) -> Self {
|
||||
match event {
|
||||
peer_tracker::BehaviourOutEvent::ConnectionEstablished(id) => {
|
||||
BehaviourOutEvent::ConnectionEstablished(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "BehaviourOutEvent", event_process = false)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Bob {
|
||||
net: Messenger,
|
||||
pt: PeerTracker,
|
||||
#[behaviour(ignore)]
|
||||
identity: Keypair,
|
||||
}
|
||||
|
||||
impl Bob {
|
||||
pub fn identity(&self) -> Keypair {
|
||||
self.identity.clone()
|
||||
}
|
||||
|
||||
pub fn peer_id(&self) -> PeerId {
|
||||
PeerId::from(self.identity.public())
|
||||
}
|
||||
|
||||
/// Sends a message to Alice to get current amounts based on `btc`.
|
||||
pub async fn request_amounts(&mut self, alice: PeerId, btc: u64) {
|
||||
let btc = bitcoin::Amount::from_sat(btc);
|
||||
let _id = self.net.request_amounts(alice.clone(), btc).await;
|
||||
debug!("Requesting amounts from: {}", alice);
|
||||
}
|
||||
|
||||
/// Returns Alice's peer id if we are connected.
|
||||
pub fn peer_id_of_alice(&self) -> Option<PeerId> {
|
||||
self.pt.counterparty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Bob {
|
||||
fn default() -> Bob {
|
||||
let identity = Keypair::generate_ed25519();
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
|
||||
Self {
|
||||
net: Messenger::new(timeout),
|
||||
pt: PeerTracker::default(),
|
||||
identity,
|
||||
}
|
||||
}
|
||||
}
|
117
swap/src/bob/messenger.rs
Normal file
117
swap/src/bob/messenger.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use anyhow::Result;
|
||||
use libp2p::{
|
||||
request_response::{
|
||||
handler::RequestProtocol, ProtocolSupport, RequestId, RequestResponse,
|
||||
RequestResponseConfig, RequestResponseEvent, RequestResponseMessage,
|
||||
},
|
||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
bitcoin,
|
||||
network::request_response::{AliceToBob, BobToAlice, Codec, Protocol},
|
||||
Never, SwapParams,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BehaviourOutEvent {
|
||||
Amounts(SwapParams),
|
||||
}
|
||||
|
||||
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "BehaviourOutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Messenger {
|
||||
rr: RequestResponse<Codec>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<BehaviourOutEvent>,
|
||||
}
|
||||
|
||||
impl Messenger {
|
||||
pub fn new(timeout: Duration) -> Self {
|
||||
let mut config = RequestResponseConfig::default();
|
||||
config.set_request_timeout(timeout);
|
||||
|
||||
Self {
|
||||
rr: RequestResponse::new(
|
||||
Codec::default(),
|
||||
vec![(Protocol, ProtocolSupport::Full)],
|
||||
config,
|
||||
),
|
||||
events: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_amounts(
|
||||
&mut self,
|
||||
alice: PeerId,
|
||||
btc: bitcoin::Amount,
|
||||
) -> Result<RequestId> {
|
||||
debug!("Sending request ...");
|
||||
let msg = BobToAlice::AmountsFromBtc(btc);
|
||||
let id = self.rr.send_request(&alice, msg);
|
||||
debug!("Sent.");
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn poll(
|
||||
&mut self,
|
||||
_: &mut Context<'_>,
|
||||
_: &mut impl PollParameters,
|
||||
) -> Poll<NetworkBehaviourAction<RequestProtocol<Codec>, BehaviourOutEvent>> {
|
||||
if let Some(event) = self.events.pop_front() {
|
||||
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Messenger {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
||||
peer: _,
|
||||
message: RequestResponseMessage::Request { .. },
|
||||
} => panic!("Bob should never get a request from Alice"),
|
||||
RequestResponseEvent::Message {
|
||||
peer: _,
|
||||
message:
|
||||
RequestResponseMessage::Response {
|
||||
response,
|
||||
request_id: _,
|
||||
},
|
||||
} => match response {
|
||||
AliceToBob::Amounts(p) => self.events.push_back(BehaviourOutEvent::Amounts(p)),
|
||||
},
|
||||
|
||||
RequestResponseEvent::InboundFailure { .. } => {
|
||||
panic!("Bob should never get a request from Alice, so should never get an InboundFailure");
|
||||
}
|
||||
RequestResponseEvent::OutboundFailure {
|
||||
peer: _,
|
||||
request_id: _,
|
||||
error,
|
||||
} => {
|
||||
error!("Outbound failure: {:?}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl libp2p::swarm::NetworkBehaviourEventProcess<()> for Messenger {
|
||||
fn inject_event(&mut self, _event: ()) {}
|
||||
}
|
||||
|
||||
impl libp2p::swarm::NetworkBehaviourEventProcess<Never> for Messenger {
|
||||
fn inject_event(&mut self, _: Never) {}
|
||||
}
|
14
swap/src/cli.rs
Normal file
14
swap/src/cli.rs
Normal file
@ -0,0 +1,14 @@
|
||||
#[derive(structopt::StructOpt, Debug)]
|
||||
pub struct Options {
|
||||
/// Run the swap as Alice.
|
||||
#[structopt(long = "as-alice")]
|
||||
pub as_alice: bool,
|
||||
|
||||
/// Run the swap as Bob and try to swap this many XMR (in piconero).
|
||||
#[structopt(long = "picos")]
|
||||
pub piconeros: Option<u64>,
|
||||
|
||||
/// Run the swap as Bob and try to swap this many BTC (in satoshi).
|
||||
#[structopt(long = "sats")]
|
||||
pub satoshis: Option<u64>,
|
||||
}
|
95
swap/src/lib.rs
Normal file
95
swap/src/lib.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod alice;
|
||||
pub mod bob;
|
||||
pub mod network;
|
||||
|
||||
pub const ONE_BTC: u64 = 100_000_000;
|
||||
|
||||
pub type Never = std::convert::Infallible;
|
||||
|
||||
/// Commands sent from Bob to the main task.
|
||||
#[derive(Debug)]
|
||||
pub enum Cmd {
|
||||
VerifyAmounts(SwapParams),
|
||||
}
|
||||
|
||||
/// Responses send from the main task back to Bob.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Rsp {
|
||||
Verified,
|
||||
Abort,
|
||||
}
|
||||
|
||||
/// XMR/BTC swap parameters.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SwapParams {
|
||||
/// Amount of BTC to swap.
|
||||
pub btc: bitcoin::Amount,
|
||||
/// Amount of XMR to swap.
|
||||
pub xmr: monero::Amount,
|
||||
}
|
||||
|
||||
// FIXME: Amount modules are a quick hack so we can derive serde.
|
||||
|
||||
pub mod monero {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Amount(u64);
|
||||
|
||||
impl Amount {
|
||||
/// Create an [Amount] with piconero precision and the given number of
|
||||
/// piconeros.
|
||||
///
|
||||
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
|
||||
pub fn from_piconero(amount: u64) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
pub fn as_piconero(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for u64 {
|
||||
fn from(from: Amount) -> u64 {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Amount {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{} piconeros", self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bitcoin {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Amount(u64);
|
||||
|
||||
impl Amount {
|
||||
/// The zero amount.
|
||||
pub const ZERO: Amount = Amount(0);
|
||||
/// Exactly one satoshi.
|
||||
pub const ONE_SAT: Amount = Amount(1);
|
||||
/// Exactly one bitcoin.
|
||||
pub const ONE_BTC: Amount = Amount(100_000_000);
|
||||
|
||||
/// Create an [Amount] with satoshi precision and the given number of
|
||||
/// satoshis.
|
||||
pub fn from_sat(satoshi: u64) -> Amount {
|
||||
Amount(satoshi)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Amount {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{} satoshis", self.0)
|
||||
}
|
||||
}
|
||||
}
|
96
swap/src/main.rs
Normal file
96
swap/src/main.rs
Normal file
@ -0,0 +1,96 @@
|
||||
#![warn(
|
||||
unused_extern_crates,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations,
|
||||
rust_2018_idioms,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::fallible_impl_from,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use libp2p::Multiaddr;
|
||||
use log::LevelFilter;
|
||||
use structopt::StructOpt;
|
||||
use tracing::info;
|
||||
|
||||
mod cli;
|
||||
mod trace;
|
||||
|
||||
use cli::Options;
|
||||
use swap::{alice, bob, Cmd, Rsp, SwapParams};
|
||||
|
||||
// TODO: Add root seed file instead of generating new seed each run.
|
||||
|
||||
// Alice's address and port until we have a config file.
|
||||
pub const PORT: u16 = 9876; // Arbitrarily chosen.
|
||||
pub const ADDR: &str = "127.0.0.1";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let opt = Options::from_args();
|
||||
|
||||
trace::init_tracing(LevelFilter::Debug)?;
|
||||
|
||||
let addr = format!("/ip4/{}/tcp/{}", ADDR, PORT);
|
||||
let alice_addr: Multiaddr = addr.parse().expect("failed to parse Alice's address");
|
||||
|
||||
if opt.as_alice {
|
||||
info!("running swap node as Alice ...");
|
||||
|
||||
if opt.piconeros.is_some() || opt.satoshis.is_some() {
|
||||
bail!("Alice cannot set the amount to swap via the cli");
|
||||
}
|
||||
|
||||
swap_as_alice(alice_addr).await?;
|
||||
} else {
|
||||
info!("running swap node as Bob ...");
|
||||
|
||||
match (opt.piconeros, opt.satoshis) {
|
||||
(Some(_), Some(_)) => bail!("Please supply only a single amount to swap"),
|
||||
(None, None) => bail!("Please supply an amount to swap"),
|
||||
(Some(_picos), _) => todo!("support starting with picos"),
|
||||
(None, Some(sats)) => {
|
||||
swap_as_bob(sats, alice_addr).await?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn swap_as_alice(addr: Multiaddr) -> Result<()> {
|
||||
alice::swap(addr).await
|
||||
}
|
||||
|
||||
async fn swap_as_bob(sats: u64, addr: Multiaddr) -> Result<()> {
|
||||
let (cmd_tx, mut cmd_rx) = mpsc::channel(1);
|
||||
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
|
||||
tokio::spawn(bob::swap(sats, addr, cmd_tx, rsp_rx));
|
||||
loop {
|
||||
let read = cmd_rx.next().await;
|
||||
match read {
|
||||
Some(cmd) => match cmd {
|
||||
Cmd::VerifyAmounts(p) => {
|
||||
if verified(p) {
|
||||
rsp_tx.try_send(Rsp::Verified)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
info!("Channel closed from other end");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verified(_p: SwapParams) -> bool {
|
||||
// TODO: Read input from the shell.
|
||||
true
|
||||
}
|
18
swap/src/network.rs
Normal file
18
swap/src/network.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use futures::prelude::*;
|
||||
use libp2p::core::Executor;
|
||||
use std::pin::Pin;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
pub mod peer_tracker;
|
||||
pub mod request_response;
|
||||
pub mod transport;
|
||||
|
||||
pub struct TokioExecutor {
|
||||
pub handle: Handle,
|
||||
}
|
||||
|
||||
impl Executor for TokioExecutor {
|
||||
fn exec(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
|
||||
let _ = self.handle.spawn(future);
|
||||
}
|
||||
}
|
148
swap/src/network/peer_tracker.rs
Normal file
148
swap/src/network/peer_tracker.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use futures::task::Context;
|
||||
use libp2p::{
|
||||
core::{connection::ConnectionId, ConnectedPoint},
|
||||
swarm::{
|
||||
protocols_handler::DummyProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction,
|
||||
PollParameters,
|
||||
},
|
||||
Multiaddr, PeerId,
|
||||
};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap, VecDeque},
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BehaviourOutEvent {
|
||||
ConnectionEstablished(PeerId),
|
||||
}
|
||||
|
||||
/// A NetworkBehaviour that tracks connections to other peers.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct PeerTracker {
|
||||
connected_peers: HashMap<PeerId, Vec<Multiaddr>>,
|
||||
address_hints: HashMap<PeerId, VecDeque<Multiaddr>>,
|
||||
events: VecDeque<BehaviourOutEvent>,
|
||||
}
|
||||
|
||||
impl PeerTracker {
|
||||
/// Returns an arbitrary connected counterparty.
|
||||
/// This is useful if we are connected to a single other node.
|
||||
pub fn counterparty(&self) -> Option<PeerId> {
|
||||
// TODO: Refactor to use combinators.
|
||||
if let Some((id, _)) = self.connected_peers().next() {
|
||||
return Some(id);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn connected_peers(&self) -> impl Iterator<Item = (PeerId, Vec<Multiaddr>)> {
|
||||
self.connected_peers.clone().into_iter()
|
||||
}
|
||||
|
||||
/// Adds an address hint for the given peer id. The added address is
|
||||
/// considered most recent and hence is added at the start of the list
|
||||
/// because libp2p tries to connect with the first address first.
|
||||
pub fn add_recent_address_hint(&mut self, id: PeerId, addr: Multiaddr) {
|
||||
let old_addresses = self.address_hints.get_mut(&id);
|
||||
|
||||
match old_addresses {
|
||||
None => {
|
||||
let mut hints = VecDeque::new();
|
||||
hints.push_back(addr);
|
||||
self.address_hints.insert(id, hints);
|
||||
}
|
||||
Some(hints) => {
|
||||
hints.push_front(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviour for PeerTracker {
|
||||
type ProtocolsHandler = DummyProtocolsHandler;
|
||||
type OutEvent = BehaviourOutEvent;
|
||||
|
||||
fn new_handler(&mut self) -> Self::ProtocolsHandler {
|
||||
DummyProtocolsHandler::default()
|
||||
}
|
||||
|
||||
/// Note (from libp2p doc):
|
||||
/// The addresses will be tried in the order returned by this function,
|
||||
/// which means that they should be ordered by decreasing likelihood of
|
||||
/// reachability. In other words, the first address should be the most
|
||||
/// likely to be reachable.
|
||||
fn addresses_of_peer(&mut self, peer: &PeerId) -> Vec<Multiaddr> {
|
||||
let mut addresses: Vec<Multiaddr> = vec![];
|
||||
|
||||
// If we are connected then this address is most likely to be valid
|
||||
if let Some(connected) = self.connected_peers.get(peer) {
|
||||
for addr in connected.iter() {
|
||||
addresses.push(addr.clone())
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hints) = self.address_hints.get(peer) {
|
||||
for hint in hints {
|
||||
addresses.push(hint.clone());
|
||||
}
|
||||
}
|
||||
|
||||
addresses
|
||||
}
|
||||
|
||||
fn inject_connected(&mut self, _: &PeerId) {}
|
||||
|
||||
fn inject_disconnected(&mut self, _: &PeerId) {}
|
||||
|
||||
fn inject_connection_established(
|
||||
&mut self,
|
||||
peer: &PeerId,
|
||||
_: &ConnectionId,
|
||||
point: &ConnectedPoint,
|
||||
) {
|
||||
if let ConnectedPoint::Dialer { address } = point {
|
||||
self.connected_peers
|
||||
.entry(peer.clone())
|
||||
.or_default()
|
||||
.push(address.clone());
|
||||
|
||||
self.events
|
||||
.push_back(BehaviourOutEvent::ConnectionEstablished(peer.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_connection_closed(
|
||||
&mut self,
|
||||
peer: &PeerId,
|
||||
_: &ConnectionId,
|
||||
point: &ConnectedPoint,
|
||||
) {
|
||||
if let ConnectedPoint::Dialer { address } = point {
|
||||
match self.connected_peers.entry(peer.clone()) {
|
||||
Entry::Vacant(_) => {}
|
||||
Entry::Occupied(mut entry) => {
|
||||
let addresses = entry.get_mut();
|
||||
|
||||
if let Some(pos) = addresses.iter().position(|a| a == address) {
|
||||
addresses.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_event(&mut self, _: PeerId, _: ConnectionId, _: void::Void) {}
|
||||
|
||||
fn poll(
|
||||
&mut self,
|
||||
_: &mut Context<'_>,
|
||||
_: &mut impl PollParameters,
|
||||
) -> Poll<NetworkBehaviourAction<void::Void, Self::OutEvent>> {
|
||||
if let Some(event) = self.events.pop_front() {
|
||||
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
109
swap/src/network/request_response.rs
Normal file
109
swap/src/network/request_response.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use async_trait::async_trait;
|
||||
use futures::prelude::*;
|
||||
use libp2p::{
|
||||
core::upgrade,
|
||||
request_response::{ProtocolName, RequestResponseCodec},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Debug, io};
|
||||
|
||||
use crate::{bitcoin, monero, SwapParams};
|
||||
|
||||
/// Time to wait for a response back once we send a request.
|
||||
pub const TIMEOUT: u64 = 3600; // One hour.
|
||||
|
||||
/// Messages Bob sends to Alice.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BobToAlice {
|
||||
AmountsFromBtc(bitcoin::Amount),
|
||||
AmountsFromXmr(monero::Amount),
|
||||
/* TODO: How are we going to do this when the messages are not Clone?
|
||||
* Msg(bob::Message), */
|
||||
}
|
||||
|
||||
/// Messages Alice sends to Bob.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum AliceToBob {
|
||||
Amounts(SwapParams),
|
||||
/* TODO: How are we going to do this when the messages are not Clone?
|
||||
* Msg(alice::Message) */
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Protocol;
|
||||
|
||||
impl ProtocolName for Protocol {
|
||||
fn protocol_name(&self) -> &[u8] {
|
||||
b"/xmr/btc/1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Codec;
|
||||
|
||||
#[async_trait]
|
||||
impl RequestResponseCodec for Codec {
|
||||
type Protocol = Protocol;
|
||||
type Request = BobToAlice;
|
||||
type Response = AliceToBob;
|
||||
|
||||
async fn read_request<T>(&mut self, _: &Self::Protocol, io: &mut T) -> io::Result<Self::Request>
|
||||
where
|
||||
T: AsyncRead + Unpin + Send,
|
||||
{
|
||||
let message = upgrade::read_one(io, 1024)
|
||||
.await
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
let mut de = serde_json::Deserializer::from_slice(&message);
|
||||
let msg = BobToAlice::deserialize(&mut de)?;
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
async fn read_response<T>(
|
||||
&mut self,
|
||||
_: &Self::Protocol,
|
||||
io: &mut T,
|
||||
) -> io::Result<Self::Response>
|
||||
where
|
||||
T: AsyncRead + Unpin + Send,
|
||||
{
|
||||
let message = upgrade::read_one(io, 1024)
|
||||
.await
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
let mut de = serde_json::Deserializer::from_slice(&message);
|
||||
let msg = AliceToBob::deserialize(&mut de)?;
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
async fn write_request<T>(
|
||||
&mut self,
|
||||
_: &Self::Protocol,
|
||||
io: &mut T,
|
||||
req: Self::Request,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
T: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let bytes = serde_json::to_vec(&req)?;
|
||||
upgrade::write_one(io, &bytes).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_response<T>(
|
||||
&mut self,
|
||||
_: &Self::Protocol,
|
||||
io: &mut T,
|
||||
res: Self::Response,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
T: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let bytes = serde_json::to_vec(&res)?;
|
||||
upgrade::write_one(io, &bytes).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
53
swap/src/network/transport.rs
Normal file
53
swap/src/network/transport.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use anyhow::Result;
|
||||
use libp2p::{
|
||||
core::{
|
||||
either::EitherError,
|
||||
identity,
|
||||
muxing::StreamMuxerBox,
|
||||
transport::{boxed::Boxed, timeout::TransportTimeoutError},
|
||||
upgrade::{SelectUpgrade, Version},
|
||||
Transport, UpgradeError,
|
||||
},
|
||||
dns::{DnsConfig, DnsErr},
|
||||
mplex::MplexConfig,
|
||||
noise::{self, NoiseConfig, NoiseError, X25519Spec},
|
||||
tcp::TokioTcpConfig,
|
||||
yamux, PeerId,
|
||||
};
|
||||
use std::{io, time::Duration};
|
||||
|
||||
/// Builds a libp2p transport with the following features:
|
||||
/// - TcpConnection
|
||||
/// - DNS name resolution
|
||||
/// - authentication via noise
|
||||
/// - multiplexing via yamux or mplex
|
||||
pub fn build(id_keys: identity::Keypair) -> Result<SwapTransport> {
|
||||
let dh_keys = noise::Keypair::<X25519Spec>::new().into_authentic(&id_keys)?;
|
||||
let noise = NoiseConfig::xx(dh_keys).into_authenticated();
|
||||
|
||||
let tcp = TokioTcpConfig::new().nodelay(true);
|
||||
let dns = DnsConfig::new(tcp)?;
|
||||
|
||||
let transport = dns
|
||||
.upgrade(Version::V1)
|
||||
.authenticate(noise)
|
||||
.multiplex(SelectUpgrade::new(
|
||||
yamux::Config::default(),
|
||||
MplexConfig::new(),
|
||||
))
|
||||
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
|
||||
.timeout(Duration::from_secs(20))
|
||||
.boxed();
|
||||
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
pub type SwapTransport = Boxed<
|
||||
(PeerId, StreamMuxerBox),
|
||||
TransportTimeoutError<
|
||||
EitherError<
|
||||
EitherError<DnsErr<io::Error>, UpgradeError<NoiseError>>,
|
||||
UpgradeError<EitherError<io::Error, io::Error>>,
|
||||
>,
|
||||
>,
|
||||
>;
|
25
swap/src/trace.rs
Normal file
25
swap/src/trace.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use atty::{self, Stream};
|
||||
use log::LevelFilter;
|
||||
use tracing::{info, subscriber};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
pub fn init_tracing(level: log::LevelFilter) -> anyhow::Result<()> {
|
||||
if level == LevelFilter::Off {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Upstream log filter.
|
||||
LogTracer::init_with_filter(LevelFilter::Debug)?;
|
||||
|
||||
let is_terminal = atty::is(Stream::Stdout);
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_env_filter(format!("swap={}", level))
|
||||
.with_ansi(is_terminal)
|
||||
.finish();
|
||||
|
||||
subscriber::set_global_default(subscriber)?;
|
||||
info!("Initialized tracing with level: {}", level);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user