Merge pull request #12 from comit-network/on-chain-protocol
commit
71e09413aa
@ -1,2 +1,2 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["monero-harness", "xmr-btc"]
|
members = ["monero-harness", "xmr-btc", "swap"]
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
[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"
|
||||||
|
backoff = { version = "0.2", features = ["tokio"] }
|
||||||
|
base64 = "0.12"
|
||||||
|
bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version.
|
||||||
|
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" }
|
||||||
|
derivative = "2"
|
||||||
|
futures = { version = "0.3", default-features = false }
|
||||||
|
libp2p = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] }
|
||||||
|
libp2p-tokio-socks5 = "0.4"
|
||||||
|
log = { version = "0.4", features = ["serde"] }
|
||||||
|
monero = "0.9"
|
||||||
|
rand = "0.7"
|
||||||
|
reqwest = { version = "0.10", default-features = false }
|
||||||
|
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"] }
|
||||||
|
url = "2.1"
|
||||||
|
void = "1"
|
||||||
|
xmr-btc = { path = "../xmr-btc" }
|
@ -0,0 +1,259 @@
|
|||||||
|
//! 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 rand::rngs::OsRng;
|
||||||
|
use std::thread;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
mod amounts;
|
||||||
|
mod message0;
|
||||||
|
mod message1;
|
||||||
|
|
||||||
|
use self::{amounts::*, message0::*, message1::*};
|
||||||
|
use crate::{
|
||||||
|
network::{
|
||||||
|
peer_tracker::{self, PeerTracker},
|
||||||
|
request_response::AliceToBob,
|
||||||
|
transport, TokioExecutor,
|
||||||
|
},
|
||||||
|
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||||
|
};
|
||||||
|
use xmr_btc::{alice::State0, bob, monero};
|
||||||
|
|
||||||
|
pub type Swarm = libp2p::Swarm<Alice>;
|
||||||
|
|
||||||
|
// FIXME: This whole function is horrible, needs total re-write.
|
||||||
|
#[allow(unused_assignments)] // Due to the mutable message0?
|
||||||
|
pub async fn swap(
|
||||||
|
listen: Multiaddr,
|
||||||
|
redeem_address: ::bitcoin::Address,
|
||||||
|
punish_address: ::bitcoin::Address,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut message0: Option<bob::Message0> = None;
|
||||||
|
let mut last_amounts: Option<SwapAmounts> = None;
|
||||||
|
|
||||||
|
let mut swarm = new_swarm(listen)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match swarm.next().await {
|
||||||
|
OutEvent::ConnectionEstablished(id) => {
|
||||||
|
tracing::info!("Connection established with: {}", id);
|
||||||
|
}
|
||||||
|
OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => {
|
||||||
|
debug!("Got request from Bob to swap {}", btc);
|
||||||
|
let p = calculate_amounts(btc);
|
||||||
|
last_amounts = Some(p);
|
||||||
|
swarm.send_amounts(channel, p);
|
||||||
|
}
|
||||||
|
OutEvent::Message0(msg) => {
|
||||||
|
debug!("Got message0 from Bob");
|
||||||
|
// TODO: Do this in a more Rusty/functional way.
|
||||||
|
message0 = Some(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
other => panic!("Unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let (xmr, btc) = match last_amounts {
|
||||||
|
Some(p) => (p.xmr, p.btc),
|
||||||
|
None => unreachable!("should have amounts by here"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let xmr = monero::Amount::from_piconero(xmr.as_piconero());
|
||||||
|
// TODO: This should be the Amount exported by xmr_btc.
|
||||||
|
let btc = ::bitcoin::Amount::from_sat(btc.as_sat());
|
||||||
|
|
||||||
|
// TODO: Pass this in using <R: RngCore + CryptoRng>
|
||||||
|
let rng = &mut OsRng;
|
||||||
|
let state0 = State0::new(
|
||||||
|
rng,
|
||||||
|
btc,
|
||||||
|
xmr,
|
||||||
|
REFUND_TIMELOCK,
|
||||||
|
PUNISH_TIMELOCK,
|
||||||
|
redeem_address,
|
||||||
|
punish_address,
|
||||||
|
);
|
||||||
|
swarm.set_state0(state0.clone());
|
||||||
|
|
||||||
|
let state1 = match message0 {
|
||||||
|
Some(msg) => state0.receive(msg).expect("failed to receive msg 0"),
|
||||||
|
None => panic!("should have the message by here"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (state2, channel) = match swarm.next().await {
|
||||||
|
OutEvent::Message1 { msg, channel } => {
|
||||||
|
let state2 = state1.receive(msg);
|
||||||
|
(state2, channel)
|
||||||
|
}
|
||||||
|
other => panic!("Unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = state2.next_message();
|
||||||
|
swarm.send_message1(channel, msg);
|
||||||
|
|
||||||
|
tracing::info!("handshake complete, we now have State2 for Alice.");
|
||||||
|
|
||||||
|
tracing::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 OutEvent {
|
||||||
|
ConnectionEstablished(PeerId),
|
||||||
|
Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event.
|
||||||
|
Message0(bob::Message0),
|
||||||
|
Message1 {
|
||||||
|
msg: bob::Message1,
|
||||||
|
channel: ResponseChannel<AliceToBob>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<peer_tracker::OutEvent> for OutEvent {
|
||||||
|
fn from(event: peer_tracker::OutEvent) -> Self {
|
||||||
|
match event {
|
||||||
|
peer_tracker::OutEvent::ConnectionEstablished(id) => {
|
||||||
|
OutEvent::ConnectionEstablished(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<amounts::OutEvent> for OutEvent {
|
||||||
|
fn from(event: amounts::OutEvent) -> Self {
|
||||||
|
OutEvent::Request(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<message0::OutEvent> for OutEvent {
|
||||||
|
fn from(event: message0::OutEvent) -> Self {
|
||||||
|
match event {
|
||||||
|
message0::OutEvent::Msg(msg) => OutEvent::Message0(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<message1::OutEvent> for OutEvent {
|
||||||
|
fn from(event: message1::OutEvent) -> Self {
|
||||||
|
match event {
|
||||||
|
message1::OutEvent::Msg { msg, channel } => OutEvent::Message1 { msg, channel },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", event_process = false)]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Alice {
|
||||||
|
pt: PeerTracker,
|
||||||
|
amounts: Amounts,
|
||||||
|
message0: Message0,
|
||||||
|
message1: Message1,
|
||||||
|
#[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_amounts(&mut self, channel: ResponseChannel<AliceToBob>, amounts: SwapAmounts) {
|
||||||
|
let msg = AliceToBob::Amounts(amounts);
|
||||||
|
self.amounts.send(channel, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Message0 gets sent within the network layer using this state0.
|
||||||
|
pub fn set_state0(&mut self, state: State0) {
|
||||||
|
let _ = self.message0.set_state(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send Message1 to Bob in response to receiving his Message1.
|
||||||
|
pub fn send_message1(
|
||||||
|
&mut self,
|
||||||
|
channel: ResponseChannel<AliceToBob>,
|
||||||
|
msg: xmr_btc::alice::Message1,
|
||||||
|
) {
|
||||||
|
self.message1.send(channel, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Alice {
|
||||||
|
fn default() -> Self {
|
||||||
|
let identity = Keypair::generate_ed25519();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pt: PeerTracker::default(),
|
||||||
|
amounts: Amounts::default(),
|
||||||
|
message0: Message0::default(),
|
||||||
|
message1: Message1::default(),
|
||||||
|
identity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts {
|
||||||
|
const XMR_PER_BTC: u64 = 100; // TODO: Get this from an exchange.
|
||||||
|
|
||||||
|
// TODO: Check that this is correct.
|
||||||
|
// XMR uses 12 zerose BTC uses 8.
|
||||||
|
let picos = (btc.as_sat() * 10000) * XMR_PER_BTC;
|
||||||
|
let xmr = monero::Amount::from_piconero(picos);
|
||||||
|
|
||||||
|
SwapAmounts { btc, xmr }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const ONE_BTC: u64 = 100_000_000;
|
||||||
|
const HUNDRED_XMR: u64 = 100_000_000_000_000;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_bitcoin_equals_a_hundred_moneroj() {
|
||||||
|
let btc = ::bitcoin::Amount::from_sat(ONE_BTC);
|
||||||
|
let want = monero::Amount::from_piconero(HUNDRED_XMR);
|
||||||
|
|
||||||
|
let SwapAmounts { xmr: got, .. } = calculate_amounts(btc);
|
||||||
|
assert_eq!(got, want);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
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::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
Btc {
|
||||||
|
btc: ::bitcoin::Amount,
|
||||||
|
channel: ResponseChannel<AliceToBob>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Amounts {
|
||||||
|
rr: RequestResponse<Codec>,
|
||||||
|
#[behaviour(ignore)]
|
||||||
|
events: VecDeque<OutEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Amounts {
|
||||||
|
/// 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>, OutEvent>> {
|
||||||
|
if let Some(event) = self.events.pop_front() {
|
||||||
|
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Amounts {
|
||||||
|
fn default() -> Self {
|
||||||
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
|
|
||||||
|
let mut config = RequestResponseConfig::default();
|
||||||
|
config.set_request_timeout(timeout);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rr: RequestResponse::new(
|
||||||
|
Codec::default(),
|
||||||
|
vec![(Protocol, ProtocolSupport::Full)],
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
events: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Amounts {
|
||||||
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
|
match event {
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message:
|
||||||
|
RequestResponseMessage::Request {
|
||||||
|
request, channel, ..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => match request {
|
||||||
|
BobToAlice::AmountsFromBtc(btc) => {
|
||||||
|
self.events.push_back(OutEvent::Btc { btc, channel })
|
||||||
|
}
|
||||||
|
other => debug!("got request: {:?}", other),
|
||||||
|
},
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Response { .. },
|
||||||
|
..
|
||||||
|
} => panic!("Alice should not get a Response"),
|
||||||
|
RequestResponseEvent::InboundFailure { error, .. } => {
|
||||||
|
error!("Inbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
RequestResponseEvent::OutboundFailure { error, .. } => {
|
||||||
|
error!("Outbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
use anyhow::{bail, Result};
|
||||||
|
use libp2p::{
|
||||||
|
request_response::{
|
||||||
|
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||||
|
RequestResponseEvent, RequestResponseMessage,
|
||||||
|
},
|
||||||
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
|
NetworkBehaviour,
|
||||||
|
};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT};
|
||||||
|
use xmr_btc::{alice::State0, bob};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
Msg(bob::Message0),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents send/recv of message 0.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Message0 {
|
||||||
|
rr: RequestResponse<Codec>,
|
||||||
|
#[behaviour(ignore)]
|
||||||
|
events: VecDeque<OutEvent>,
|
||||||
|
#[behaviour(ignore)]
|
||||||
|
state: Option<State0>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message0 {
|
||||||
|
pub fn set_state(&mut self, state: State0) -> Result<()> {
|
||||||
|
if self.state.is_some() {
|
||||||
|
bail!("Trying to set state a second time");
|
||||||
|
}
|
||||||
|
self.state = Some(state);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
&mut self,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
_: &mut impl PollParameters,
|
||||||
|
) -> Poll<NetworkBehaviourAction<RequestProtocol<Codec>, OutEvent>> {
|
||||||
|
if let Some(event) = self.events.pop_front() {
|
||||||
|
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Message0 {
|
||||||
|
fn default() -> Self {
|
||||||
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
|
let mut config = RequestResponseConfig::default();
|
||||||
|
config.set_request_timeout(timeout);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rr: RequestResponse::new(
|
||||||
|
Codec::default(),
|
||||||
|
vec![(Protocol, ProtocolSupport::Full)],
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
events: Default::default(),
|
||||||
|
state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
||||||
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
|
match event {
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message:
|
||||||
|
RequestResponseMessage::Request {
|
||||||
|
request, channel, ..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => match request {
|
||||||
|
BobToAlice::Message0(msg) => {
|
||||||
|
let response = match &self.state {
|
||||||
|
None => panic!("No state, did you forget to set it?"),
|
||||||
|
Some(state) => {
|
||||||
|
// TODO: Get OsRng from somewhere?
|
||||||
|
AliceToBob::Message0(state.next_message(&mut OsRng))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.rr.send_response(channel, response);
|
||||||
|
self.events.push_back(OutEvent::Msg(msg));
|
||||||
|
}
|
||||||
|
other => debug!("got request: {:?}", other),
|
||||||
|
},
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Response { .. },
|
||||||
|
..
|
||||||
|
} => panic!("Alice should not get a Response"),
|
||||||
|
RequestResponseEvent::InboundFailure { error, .. } => {
|
||||||
|
error!("Inbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
RequestResponseEvent::OutboundFailure { error, .. } => {
|
||||||
|
error!("Outbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
use libp2p::{
|
||||||
|
request_response::{
|
||||||
|
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||||
|
RequestResponseEvent, RequestResponseMessage, ResponseChannel,
|
||||||
|
},
|
||||||
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
|
NetworkBehaviour,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT};
|
||||||
|
use xmr_btc::bob;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
Msg {
|
||||||
|
/// Received message from Bob.
|
||||||
|
msg: bob::Message1,
|
||||||
|
/// Channel to send back Alice's message 1.
|
||||||
|
channel: ResponseChannel<AliceToBob>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents send/recv of message 1.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Message1 {
|
||||||
|
rr: RequestResponse<Codec>,
|
||||||
|
#[behaviour(ignore)]
|
||||||
|
events: VecDeque<OutEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message1 {
|
||||||
|
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message1) {
|
||||||
|
let msg = AliceToBob::Message1(msg);
|
||||||
|
self.rr.send_response(channel, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
&mut self,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
_: &mut impl PollParameters,
|
||||||
|
) -> Poll<NetworkBehaviourAction<RequestProtocol<Codec>, OutEvent>> {
|
||||||
|
if let Some(event) = self.events.pop_front() {
|
||||||
|
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Message1 {
|
||||||
|
fn default() -> Self {
|
||||||
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
|
let mut config = RequestResponseConfig::default();
|
||||||
|
config.set_request_timeout(timeout);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rr: RequestResponse::new(
|
||||||
|
Codec::default(),
|
||||||
|
vec![(Protocol, ProtocolSupport::Full)],
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
events: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
||||||
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
|
match event {
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message:
|
||||||
|
RequestResponseMessage::Request {
|
||||||
|
request, channel, ..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => match request {
|
||||||
|
BobToAlice::Message1(msg) => {
|
||||||
|
self.events.push_back(OutEvent::Msg { msg, channel });
|
||||||
|
}
|
||||||
|
other => debug!("got request: {:?}", other),
|
||||||
|
},
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Response { .. },
|
||||||
|
..
|
||||||
|
} => panic!("Alice should not get a Response"),
|
||||||
|
RequestResponseEvent::InboundFailure { error, .. } => {
|
||||||
|
error!("Inbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
RequestResponseEvent::OutboundFailure { error, .. } => {
|
||||||
|
error!("Outbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use backoff::{future::FutureOperation as _, ExponentialBackoff};
|
||||||
|
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Transaction};
|
||||||
|
use bitcoin_harness::bitcoind_rpc::PsbtBase64;
|
||||||
|
use reqwest::Url;
|
||||||
|
use xmr_btc::bitcoin::{
|
||||||
|
Amount, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, Txid,
|
||||||
|
WatchForRawTransaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is cut'n'paste from xmr_btc/tests/harness/wallet/bitcoin.rs
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Wallet(pub bitcoin_harness::Wallet);
|
||||||
|
|
||||||
|
impl Wallet {
|
||||||
|
pub async fn new(name: &str, url: &Url) -> Result<Self> {
|
||||||
|
let wallet = bitcoin_harness::Wallet::new(name, url.clone()).await?;
|
||||||
|
|
||||||
|
Ok(Self(wallet))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn balance(&self) -> Result<Amount> {
|
||||||
|
let balance = self.0.balance().await?;
|
||||||
|
Ok(balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_address(&self) -> Result<Address> {
|
||||||
|
self.0.new_address().await.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||||
|
let fee = self
|
||||||
|
.0
|
||||||
|
.get_wallet_transaction(txid)
|
||||||
|
.await
|
||||||
|
.map(|res| bitcoin::Amount::from_btc(-res.fee))??;
|
||||||
|
|
||||||
|
Ok(fee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl BuildTxLockPsbt for Wallet {
|
||||||
|
async fn build_tx_lock_psbt(
|
||||||
|
&self,
|
||||||
|
output_address: Address,
|
||||||
|
output_amount: Amount,
|
||||||
|
) -> Result<PartiallySignedTransaction> {
|
||||||
|
let psbt = self.0.fund_psbt(output_address, output_amount).await?;
|
||||||
|
let as_hex = base64::decode(psbt)?;
|
||||||
|
|
||||||
|
let psbt = bitcoin::consensus::deserialize(&as_hex)?;
|
||||||
|
|
||||||
|
Ok(psbt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SignTxLock for Wallet {
|
||||||
|
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
|
||||||
|
let psbt = PartiallySignedTransaction::from(tx_lock);
|
||||||
|
|
||||||
|
let psbt = bitcoin::consensus::serialize(&psbt);
|
||||||
|
let as_base64 = base64::encode(psbt);
|
||||||
|
|
||||||
|
let psbt = self.0.wallet_process_psbt(PsbtBase64(as_base64)).await?;
|
||||||
|
let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt);
|
||||||
|
|
||||||
|
let as_hex = base64::decode(signed_psbt)?;
|
||||||
|
let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?;
|
||||||
|
|
||||||
|
let tx = psbt.extract_tx();
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl BroadcastSignedTransaction for Wallet {
|
||||||
|
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||||
|
let txid = self.0.send_raw_transaction(transaction).await?;
|
||||||
|
Ok(txid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WatchForRawTransaction for Wallet {
|
||||||
|
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
|
||||||
|
(|| async { Ok(self.0.get_raw_transaction(txid).await?) })
|
||||||
|
.retry(ExponentialBackoff {
|
||||||
|
max_elapsed_time: None,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("transient errors to be retried")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,230 @@
|
|||||||
|
//! 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 rand::rngs::OsRng;
|
||||||
|
use std::{process, thread};
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
mod amounts;
|
||||||
|
mod message0;
|
||||||
|
mod message1;
|
||||||
|
|
||||||
|
use self::{amounts::*, message0::*, message1::*};
|
||||||
|
use crate::{
|
||||||
|
network::{
|
||||||
|
peer_tracker::{self, PeerTracker},
|
||||||
|
transport, TokioExecutor,
|
||||||
|
},
|
||||||
|
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||||
|
};
|
||||||
|
use xmr_btc::{
|
||||||
|
alice,
|
||||||
|
bitcoin::BuildTxLockPsbt,
|
||||||
|
bob::{self, State0},
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME: This whole function is horrible, needs total re-write.
|
||||||
|
pub async fn swap<W>(
|
||||||
|
btc: u64,
|
||||||
|
addr: Multiaddr,
|
||||||
|
mut cmd_tx: Sender<Cmd>,
|
||||||
|
mut rsp_rx: Receiver<Rsp>,
|
||||||
|
refund_address: ::bitcoin::Address,
|
||||||
|
wallet: W,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
W: BuildTxLockPsbt + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let mut swarm = new_swarm()?;
|
||||||
|
|
||||||
|
libp2p::Swarm::dial_addr(&mut swarm, addr)?;
|
||||||
|
let alice = match swarm.next().await {
|
||||||
|
OutEvent::ConnectionEstablished(alice) => alice,
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
info!("Connection established.");
|
||||||
|
|
||||||
|
swarm.request_amounts(alice.clone(), btc);
|
||||||
|
|
||||||
|
let (btc, xmr) = match swarm.next().await {
|
||||||
|
OutEvent::Amounts(amounts) => {
|
||||||
|
debug!("Got amounts from Alice: {:?}", amounts);
|
||||||
|
let cmd = Cmd::VerifyAmounts(amounts);
|
||||||
|
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 ...");
|
||||||
|
(amounts.btc, amounts.xmr)
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME: Too many `bitcoin` crates/modules.
|
||||||
|
let xmr = xmr_btc::monero::Amount::from_piconero(xmr.as_piconero());
|
||||||
|
let btc = ::bitcoin::Amount::from_sat(btc.as_sat());
|
||||||
|
|
||||||
|
// TODO: Pass this in using <R: RngCore + CryptoRng>
|
||||||
|
let rng = &mut OsRng;
|
||||||
|
let state0 = State0::new(
|
||||||
|
rng,
|
||||||
|
btc,
|
||||||
|
xmr,
|
||||||
|
REFUND_TIMELOCK,
|
||||||
|
PUNISH_TIMELOCK,
|
||||||
|
refund_address,
|
||||||
|
);
|
||||||
|
|
||||||
|
swarm.send_message0(alice.clone(), state0.next_message(rng));
|
||||||
|
let state1 = match swarm.next().await {
|
||||||
|
OutEvent::Message0(msg) => {
|
||||||
|
state0.receive(&wallet, msg).await? // TODO: More graceful error
|
||||||
|
// handling.
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
swarm.send_message1(alice.clone(), state1.next_message());
|
||||||
|
let _state2 = match swarm.next().await {
|
||||||
|
OutEvent::Message1(msg) => {
|
||||||
|
state1.receive(msg) // TODO: More graceful error handling.
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("handshake complete, we now have State2 for Bob.");
|
||||||
|
|
||||||
|
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 OutEvent {
|
||||||
|
ConnectionEstablished(PeerId),
|
||||||
|
Amounts(SwapAmounts),
|
||||||
|
Message0(alice::Message0),
|
||||||
|
Message1(alice::Message1),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<peer_tracker::OutEvent> for OutEvent {
|
||||||
|
fn from(event: peer_tracker::OutEvent) -> Self {
|
||||||
|
match event {
|
||||||
|
peer_tracker::OutEvent::ConnectionEstablished(id) => {
|
||||||
|
OutEvent::ConnectionEstablished(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<amounts::OutEvent> for OutEvent {
|
||||||
|
fn from(event: amounts::OutEvent) -> Self {
|
||||||
|
match event {
|
||||||
|
amounts::OutEvent::Amounts(amounts) => OutEvent::Amounts(amounts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<message0::OutEvent> for OutEvent {
|
||||||
|
fn from(event: message0::OutEvent) -> Self {
|
||||||
|
match event {
|
||||||
|
message0::OutEvent::Msg(msg) => OutEvent::Message0(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<message1::OutEvent> for OutEvent {
|
||||||
|
fn from(event: message1::OutEvent) -> Self {
|
||||||
|
match event {
|
||||||
|
message1::OutEvent::Msg(msg) => OutEvent::Message1(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", event_process = false)]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Bob {
|
||||||
|
pt: PeerTracker,
|
||||||
|
amounts: Amounts,
|
||||||
|
message0: Message0,
|
||||||
|
message1: Message1,
|
||||||
|
#[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 fn request_amounts(&mut self, alice: PeerId, btc: u64) {
|
||||||
|
let btc = ::bitcoin::Amount::from_sat(btc);
|
||||||
|
let _id = self.amounts.request_amounts(alice.clone(), btc);
|
||||||
|
debug!("Requesting amounts from: {}", alice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends Bob's first message to Alice.
|
||||||
|
pub fn send_message0(&mut self, alice: PeerId, msg: bob::Message0) {
|
||||||
|
self.message0.send(alice, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends Bob's second message to Alice.
|
||||||
|
pub fn send_message1(&mut self, alice: PeerId, msg: bob::Message1) {
|
||||||
|
self.message1.send(alice, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns Alice's peer id if we are connected.
|
||||||
|
pub fn peer_id_of_alice(&self) -> Option<PeerId> {
|
||||||
|
self.pt.counterparty_peer_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Bob {
|
||||||
|
fn default() -> Bob {
|
||||||
|
let identity = Keypair::generate_ed25519();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pt: PeerTracker::default(),
|
||||||
|
amounts: Amounts::default(),
|
||||||
|
message0: Message0::default(),
|
||||||
|
message1: Message1::default(),
|
||||||
|
identity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
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::{
|
||||||
|
network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT},
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
Amounts(SwapAmounts),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Amounts {
|
||||||
|
rr: RequestResponse<Codec>,
|
||||||
|
#[behaviour(ignore)]
|
||||||
|
events: VecDeque<OutEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Amounts {
|
||||||
|
pub 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);
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
&mut self,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
_: &mut impl PollParameters,
|
||||||
|
) -> Poll<NetworkBehaviourAction<RequestProtocol<Codec>, OutEvent>> {
|
||||||
|
if let Some(event) = self.events.pop_front() {
|
||||||
|
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Amounts {
|
||||||
|
fn default() -> Self {
|
||||||
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
|
|
||||||
|
let mut config = RequestResponseConfig::default();
|
||||||
|
config.set_request_timeout(timeout);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rr: RequestResponse::new(
|
||||||
|
Codec::default(),
|
||||||
|
vec![(Protocol, ProtocolSupport::Full)],
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
events: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Amounts {
|
||||||
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
|
match event {
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Request { .. },
|
||||||
|
..
|
||||||
|
} => panic!("Bob should never get a request from Alice"),
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Response { response, .. },
|
||||||
|
..
|
||||||
|
} => match response {
|
||||||
|
AliceToBob::Amounts(p) => self.events.push_back(OutEvent::Amounts(p)),
|
||||||
|
other => debug!("got response: {:?}", other),
|
||||||
|
},
|
||||||
|
RequestResponseEvent::InboundFailure { error, .. } => {
|
||||||
|
error!("Inbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
RequestResponseEvent::OutboundFailure { error, .. } => {
|
||||||
|
error!("Outbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
use libp2p::{
|
||||||
|
request_response::{
|
||||||
|
handler::RequestProtocol, ProtocolSupport, 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::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT};
|
||||||
|
use xmr_btc::{alice, bob};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
Msg(alice::Message0),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents send/recv of message 0.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Message0 {
|
||||||
|
rr: RequestResponse<Codec>,
|
||||||
|
#[behaviour(ignore)]
|
||||||
|
events: VecDeque<OutEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message0 {
|
||||||
|
pub fn send(&mut self, alice: PeerId, msg: bob::Message0) {
|
||||||
|
let msg = BobToAlice::Message0(msg);
|
||||||
|
let _id = self.rr.send_request(&alice, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
&mut self,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
_: &mut impl PollParameters,
|
||||||
|
) -> Poll<NetworkBehaviourAction<RequestProtocol<Codec>, OutEvent>> {
|
||||||
|
if let Some(event) = self.events.pop_front() {
|
||||||
|
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Message0 {
|
||||||
|
fn default() -> Self {
|
||||||
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
|
let mut config = RequestResponseConfig::default();
|
||||||
|
config.set_request_timeout(timeout);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rr: RequestResponse::new(
|
||||||
|
Codec::default(),
|
||||||
|
vec![(Protocol, ProtocolSupport::Full)],
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
events: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
||||||
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
|
match event {
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Request { .. },
|
||||||
|
..
|
||||||
|
} => panic!("Bob should never get a request from Alice"),
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Response { response, .. },
|
||||||
|
..
|
||||||
|
} => match response {
|
||||||
|
AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)),
|
||||||
|
other => debug!("got response: {:?}", other),
|
||||||
|
},
|
||||||
|
RequestResponseEvent::InboundFailure { error, .. } => {
|
||||||
|
error!("Inbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
RequestResponseEvent::OutboundFailure { error, .. } => {
|
||||||
|
error!("Outbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
use libp2p::{
|
||||||
|
request_response::{
|
||||||
|
handler::RequestProtocol, ProtocolSupport, 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::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT};
|
||||||
|
use xmr_btc::{alice, bob};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
Msg(alice::Message1),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `NetworkBehaviour` that represents send/recv of message 1.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Message1 {
|
||||||
|
rr: RequestResponse<Codec>,
|
||||||
|
#[behaviour(ignore)]
|
||||||
|
events: VecDeque<OutEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message1 {
|
||||||
|
pub fn send(&mut self, alice: PeerId, msg: bob::Message1) {
|
||||||
|
let msg = BobToAlice::Message1(msg);
|
||||||
|
let _id = self.rr.send_request(&alice, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
&mut self,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
_: &mut impl PollParameters,
|
||||||
|
) -> Poll<NetworkBehaviourAction<RequestProtocol<Codec>, OutEvent>> {
|
||||||
|
if let Some(event) = self.events.pop_front() {
|
||||||
|
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Message1 {
|
||||||
|
fn default() -> Self {
|
||||||
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
|
let mut config = RequestResponseConfig::default();
|
||||||
|
config.set_request_timeout(timeout);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rr: RequestResponse::new(
|
||||||
|
Codec::default(),
|
||||||
|
vec![(Protocol, ProtocolSupport::Full)],
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
events: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
||||||
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
|
match event {
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Request { .. },
|
||||||
|
..
|
||||||
|
} => panic!("Bob should never get a request from Alice"),
|
||||||
|
RequestResponseEvent::Message {
|
||||||
|
message: RequestResponseMessage::Response { response, .. },
|
||||||
|
..
|
||||||
|
} => match response {
|
||||||
|
AliceToBob::Message1(msg) => self.events.push_back(OutEvent::Msg(msg)),
|
||||||
|
other => debug!("got response: {:?}", other),
|
||||||
|
},
|
||||||
|
RequestResponseEvent::InboundFailure { error, .. } => {
|
||||||
|
error!("Inbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
RequestResponseEvent::OutboundFailure { error, .. } => {
|
||||||
|
error!("Outbound failure: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>,
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
pub mod alice;
|
||||||
|
pub mod bitcoin;
|
||||||
|
pub mod bob;
|
||||||
|
pub mod network;
|
||||||
|
|
||||||
|
pub const ONE_BTC: u64 = 100_000_000;
|
||||||
|
|
||||||
|
const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be?
|
||||||
|
const PUNISH_TIMELOCK: u32 = 20; // FIXME: What should this be?
|
||||||
|
|
||||||
|
pub type Never = std::convert::Infallible;
|
||||||
|
|
||||||
|
/// Commands sent from Bob to the main task.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Cmd {
|
||||||
|
VerifyAmounts(SwapAmounts),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responses sent from the main task back to Bob.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Rsp {
|
||||||
|
VerifiedAmounts,
|
||||||
|
Abort,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// XMR/BTC swap amounts.
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SwapAmounts {
|
||||||
|
/// Amount of BTC to swap.
|
||||||
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
|
pub btc: ::bitcoin::Amount,
|
||||||
|
/// Amount of XMR to swap.
|
||||||
|
#[serde(with = "xmr_btc::serde::monero_amount")]
|
||||||
|
pub xmr: xmr_btc::monero::Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Display in XMR and BTC (not picos and sats).
|
||||||
|
impl Display for SwapAmounts {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} sats for {} piconeros",
|
||||||
|
self.btc.as_sat(),
|
||||||
|
self.xmr.as_piconero()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
#![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 std::{io, io::Write, process};
|
||||||
|
use structopt::StructOpt;
|
||||||
|
use tracing::info;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod trace;
|
||||||
|
|
||||||
|
use cli::Options;
|
||||||
|
use swap::{alice, bitcoin::Wallet, bob, Cmd, Rsp, SwapAmounts};
|
||||||
|
use xmr_btc::bitcoin::BuildTxLockPsbt;
|
||||||
|
|
||||||
|
// TODO: Add root seed file instead of generating new seed each run.
|
||||||
|
// TODO: Remove all instances of the todo! macro
|
||||||
|
|
||||||
|
// TODO: Add a config file with these in it.
|
||||||
|
// 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";
|
||||||
|
pub const BITCOIND_JSON_RPC_URL: &str = "127.0.0.1:8332";
|
||||||
|
|
||||||
|
#[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: 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = Url::parse(BITCOIND_JSON_RPC_URL).expect("failed to parse url");
|
||||||
|
let bitcoin_wallet = Wallet::new("alice", &url)
|
||||||
|
.await
|
||||||
|
.expect("failed to create bitcoin wallet");
|
||||||
|
|
||||||
|
let redeem = bitcoin_wallet
|
||||||
|
.new_address()
|
||||||
|
.await
|
||||||
|
.expect("failed to get new redeem address");
|
||||||
|
let punish = bitcoin_wallet
|
||||||
|
.new_address()
|
||||||
|
.await
|
||||||
|
.expect("failed to get new punish address");
|
||||||
|
|
||||||
|
swap_as_alice(alice.clone(), redeem, punish).await?;
|
||||||
|
} else {
|
||||||
|
info!("running swap node as Bob ...");
|
||||||
|
|
||||||
|
let url = Url::parse(BITCOIND_JSON_RPC_URL).expect("failed to parse url");
|
||||||
|
let bitcoin_wallet = Wallet::new("bob", &url)
|
||||||
|
.await
|
||||||
|
.expect("failed to create bitcoin wallet");
|
||||||
|
|
||||||
|
let refund = bitcoin_wallet
|
||||||
|
.new_address()
|
||||||
|
.await
|
||||||
|
.expect("failed to get new address");
|
||||||
|
|
||||||
|
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, refund, bitcoin_wallet).await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swap_as_alice(
|
||||||
|
addr: Multiaddr,
|
||||||
|
redeem: bitcoin::Address,
|
||||||
|
punish: bitcoin::Address,
|
||||||
|
) -> Result<()> {
|
||||||
|
alice::swap(addr, redeem, punish).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swap_as_bob<W>(
|
||||||
|
sats: u64,
|
||||||
|
alice: Multiaddr,
|
||||||
|
refund: bitcoin::Address,
|
||||||
|
wallet: W,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
W: BuildTxLockPsbt + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let (cmd_tx, mut cmd_rx) = mpsc::channel(1);
|
||||||
|
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
|
||||||
|
tokio::spawn(bob::swap(sats, alice, cmd_tx, rsp_rx, refund, wallet));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let read = cmd_rx.next().await;
|
||||||
|
match read {
|
||||||
|
Some(cmd) => match cmd {
|
||||||
|
Cmd::VerifyAmounts(p) => {
|
||||||
|
let rsp = verify(p);
|
||||||
|
rsp_tx.try_send(rsp)?;
|
||||||
|
if rsp == Rsp::Abort {
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
info!("Channel closed from other end");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(amounts: SwapAmounts) -> Rsp {
|
||||||
|
let mut s = String::new();
|
||||||
|
println!("Got rate from Alice for XMR/BTC swap\n");
|
||||||
|
println!("{}", amounts);
|
||||||
|
print!("Would you like to continue with this swap [y/N]: ");
|
||||||
|
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut s)
|
||||||
|
.expect("Did not enter a correct string");
|
||||||
|
|
||||||
|
if let Some('\n') = s.chars().next_back() {
|
||||||
|
s.pop();
|
||||||
|
}
|
||||||
|
if let Some('\r') = s.chars().next_back() {
|
||||||
|
s.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_yes(&s) {
|
||||||
|
println!("No worries, try again later - Alice updates her rate regularly");
|
||||||
|
return Rsp::Abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rsp::VerifiedAmounts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_yes(s: &str) -> bool {
|
||||||
|
matches!(s, "y" | "Y" | "yes" | "YES" | "Yes")
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
use futures::task::Context;
|
||||||
|
use libp2p::{
|
||||||
|
core::{connection::ConnectionId, ConnectedPoint},
|
||||||
|
swarm::{
|
||||||
|
protocols_handler::DummyProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction,
|
||||||
|
PollParameters,
|
||||||
|
},
|
||||||
|
Multiaddr, PeerId,
|
||||||
|
};
|
||||||
|
use std::{collections::VecDeque, task::Poll};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
ConnectionEstablished(PeerId),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A NetworkBehaviour that tracks connections to the counterparty. Although the
|
||||||
|
/// libp2p `NetworkBehaviour` abstraction encompasses connections to multiple
|
||||||
|
/// peers we only ever connect to a single counterparty. Peer Tracker tracks
|
||||||
|
/// that connection.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct PeerTracker {
|
||||||
|
connected: Option<(PeerId, Multiaddr)>,
|
||||||
|
events: VecDeque<OutEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PeerTracker {
|
||||||
|
/// Returns the peer id of counterparty if we are connected.
|
||||||
|
pub fn counterparty_peer_id(&self) -> Option<PeerId> {
|
||||||
|
if let Some((id, _)) = &self.connected {
|
||||||
|
return Some(id.clone());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the multiaddr of counterparty if we are connected.
|
||||||
|
pub fn counterparty_addr(&self) -> Option<Multiaddr> {
|
||||||
|
if let Some((_, addr)) = &self.connected {
|
||||||
|
return Some(addr.clone());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkBehaviour for PeerTracker {
|
||||||
|
type ProtocolsHandler = DummyProtocolsHandler;
|
||||||
|
type OutEvent = OutEvent;
|
||||||
|
|
||||||
|
fn new_handler(&mut self) -> Self::ProtocolsHandler {
|
||||||
|
DummyProtocolsHandler::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addresses_of_peer(&mut self, _: &PeerId) -> Vec<Multiaddr> {
|
||||||
|
let mut addresses: Vec<Multiaddr> = vec![];
|
||||||
|
|
||||||
|
if let Some(addr) = self.counterparty_addr() {
|
||||||
|
addresses.push(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_connected(&mut self, _: &PeerId) {}
|
||||||
|
|
||||||
|
fn inject_disconnected(&mut self, _: &PeerId) {}
|
||||||
|
|
||||||
|
fn inject_connection_established(
|
||||||
|
&mut self,
|
||||||
|
peer: &PeerId,
|
||||||
|
_: &ConnectionId,
|
||||||
|
point: &ConnectedPoint,
|
||||||
|
) {
|
||||||
|
match point {
|
||||||
|
ConnectedPoint::Dialer { address } => {
|
||||||
|
self.connected = Some((peer.clone(), address.clone()));
|
||||||
|
}
|
||||||
|
ConnectedPoint::Listener {
|
||||||
|
local_addr: _,
|
||||||
|
send_back_addr,
|
||||||
|
} => {
|
||||||
|
self.connected = Some((peer.clone(), send_back_addr.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.events
|
||||||
|
.push_back(OutEvent::ConnectionEstablished(peer.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_connection_closed(&mut self, _: &PeerId, _: &ConnectionId, _: &ConnectedPoint) {
|
||||||
|
self.connected = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
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::SwapAmounts;
|
||||||
|
use xmr_btc::{alice, bob, monero};
|
||||||
|
|
||||||
|
/// Time to wait for a response back once we send a request.
|
||||||
|
pub const TIMEOUT: u64 = 3600; // One hour.
|
||||||
|
|
||||||
|
// TODO: Think about whether there is a better way to do this, e.g., separate
|
||||||
|
// Codec for each Message and a macro that implements them.
|
||||||
|
|
||||||
|
/// Messages Bob sends to Alice.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum BobToAlice {
|
||||||
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
|
AmountsFromBtc(::bitcoin::Amount),
|
||||||
|
AmountsFromXmr(monero::Amount),
|
||||||
|
Message0(bob::Message0),
|
||||||
|
Message1(bob::Message1),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Messages Alice sends to Bob.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum AliceToBob {
|
||||||
|
Amounts(SwapAmounts),
|
||||||
|
Message0(alice::Message0),
|
||||||
|
Message1(alice::Message1),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use libp2p::{
|
||||||
|
core::{
|
||||||
|
identity,
|
||||||
|
muxing::StreamMuxerBox,
|
||||||
|
transport::Boxed,
|
||||||
|
upgrade::{SelectUpgrade, Version},
|
||||||
|
Transport,
|
||||||
|
},
|
||||||
|
dns::DnsConfig,
|
||||||
|
mplex::MplexConfig,
|
||||||
|
noise::{self, NoiseConfig, X25519Spec},
|
||||||
|
tcp::TokioTcpConfig,
|
||||||
|
yamux, PeerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TOOD: Add the tor transport builder.
|
||||||
|
|
||||||
|
/// 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)))
|
||||||
|
.boxed();
|
||||||
|
|
||||||
|
Ok(transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SwapTransport = Boxed<(PeerId, StreamMuxerBox)>;
|
@ -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(())
|
||||||
|
}
|
@ -0,0 +1,231 @@
|
|||||||
|
pub mod ecdsa_fun_signature {
|
||||||
|
use serde::{de, de::Visitor, Deserializer, Serializer};
|
||||||
|
use std::{convert::TryFrom, fmt};
|
||||||
|
|
||||||
|
struct Bytes64Visitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for Bytes64Visitor {
|
||||||
|
type Value = ecdsa_fun::Signature;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(formatter, "a string containing 64 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
if let Ok(value) = <[u8; 64]>::try_from(s) {
|
||||||
|
let sig = ecdsa_fun::Signature::from_bytes(value)
|
||||||
|
.expect("bytes represent an integer greater than or equal to the curve order");
|
||||||
|
Ok(sig)
|
||||||
|
} else {
|
||||||
|
Err(de::Error::invalid_length(s.len(), &self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S>(x: &ecdsa_fun::Signature, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
s.serialize_bytes(&x.to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<ecdsa_fun::Signature, <D as Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let sig = deserializer.deserialize_bytes(Bytes64Visitor)?;
|
||||||
|
Ok(sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod cross_curve_dleq_scalar {
|
||||||
|
use serde::{de, de::Visitor, Deserializer, Serializer};
|
||||||
|
use std::{convert::TryFrom, fmt};
|
||||||
|
|
||||||
|
struct Bytes32Visitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for Bytes32Visitor {
|
||||||
|
type Value = cross_curve_dleq::Scalar;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(formatter, "a string containing 32 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
if let Ok(value) = <[u8; 32]>::try_from(s) {
|
||||||
|
Ok(cross_curve_dleq::Scalar::from(value))
|
||||||
|
} else {
|
||||||
|
Err(de::Error::invalid_length(s.len(), &self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S>(x: &cross_curve_dleq::Scalar, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
// Serialise as ed25519 because the inner bytes are private
|
||||||
|
// TODO: Open PR in cross_curve_dleq to allow accessing the inner bytes
|
||||||
|
s.serialize_bytes(&x.into_ed25519().to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<cross_curve_dleq::Scalar, <D as Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let dleq = deserializer.deserialize_bytes(Bytes32Visitor)?;
|
||||||
|
Ok(dleq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod monero_private_key {
|
||||||
|
use serde::{de, de::Visitor, Deserializer, Serializer};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
struct BytesVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for BytesVisitor {
|
||||||
|
type Value = monero::PrivateKey;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(formatter, "a string containing 32 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
if let Ok(key) = monero::PrivateKey::from_slice(s) {
|
||||||
|
Ok(key)
|
||||||
|
} else {
|
||||||
|
Err(de::Error::invalid_length(s.len(), &self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S>(x: &monero::PrivateKey, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
s.serialize_bytes(x.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<monero::PrivateKey, <D as Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let key = deserializer.deserialize_bytes(BytesVisitor)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod bitcoin_amount {
|
||||||
|
use bitcoin::Amount;
|
||||||
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
s.serialize_u64(x.as_sat())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let sats = u64::deserialize(deserializer)?;
|
||||||
|
let amount = Amount::from_sat(sats);
|
||||||
|
|
||||||
|
Ok(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod monero_amount {
|
||||||
|
use crate::monero::Amount;
|
||||||
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
s.serialize_u64(x.as_piconero())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let picos = u64::deserialize(deserializer)?;
|
||||||
|
let amount = Amount::from_piconero(picos);
|
||||||
|
|
||||||
|
Ok(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use ::bitcoin::SigHash;
|
||||||
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct CrossCurveDleqScalar(
|
||||||
|
#[serde(with = "cross_curve_dleq_scalar")] cross_curve_dleq::Scalar,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ECDSAFunSignature(#[serde(with = "ecdsa_fun_signature")] ecdsa_fun::Signature);
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct BitcoinAmount(#[serde(with = "bitcoin_amount")] ::bitcoin::Amount);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serde_cross_curv_dleq_scalar() {
|
||||||
|
let scalar = CrossCurveDleqScalar(cross_curve_dleq::Scalar::random(&mut OsRng));
|
||||||
|
let encoded = serde_cbor::to_vec(&scalar).unwrap();
|
||||||
|
let decoded: CrossCurveDleqScalar = serde_cbor::from_slice(&encoded).unwrap();
|
||||||
|
assert_eq!(scalar, decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serde_ecdsa_fun_sig() {
|
||||||
|
let secret_key = crate::bitcoin::SecretKey::new_random(&mut OsRng);
|
||||||
|
let sig = ECDSAFunSignature(secret_key.sign(SigHash::default()));
|
||||||
|
let encoded = serde_cbor::to_vec(&sig).unwrap();
|
||||||
|
let decoded: ECDSAFunSignature = serde_cbor::from_slice(&encoded).unwrap();
|
||||||
|
assert_eq!(sig, decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serde_monero_private_key() {
|
||||||
|
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng)));
|
||||||
|
let encoded = serde_cbor::to_vec(&key).unwrap();
|
||||||
|
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
||||||
|
assert_eq!(key, decoded);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn serde_bitcoin_amount() {
|
||||||
|
let amount = BitcoinAmount(::bitcoin::Amount::from_sat(100));
|
||||||
|
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
||||||
|
let decoded: BitcoinAmount = serde_cbor::from_slice(&encoded).unwrap();
|
||||||
|
assert_eq!(amount, decoded);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct Database {
|
||||||
|
db: sled::Db,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
const LAST_STATE_KEY: &'static str = "latest_state";
|
||||||
|
|
||||||
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
|
let path = path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("The path is not utf-8 valid: {:?}", path))?;
|
||||||
|
let db = sled::open(path).with_context(|| format!("Could not open the DB at {}", path))?;
|
||||||
|
|
||||||
|
Ok(Database { db })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_latest_state<T>(&self, state: &T) -> Result<()>
|
||||||
|
where
|
||||||
|
T: Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
let key = serialize(&Self::LAST_STATE_KEY)?;
|
||||||
|
let new_value = serialize(&state).context("Could not serialize new state value")?;
|
||||||
|
|
||||||
|
let old_value = self.db.get(&key)?;
|
||||||
|
|
||||||
|
self.db
|
||||||
|
.compare_and_swap(key, old_value, Some(new_value))
|
||||||
|
.context("Could not write in the DB")?
|
||||||
|
.context("Stored swap somehow changed, aborting saving")?; // let _ =
|
||||||
|
|
||||||
|
self.db
|
||||||
|
.flush_async()
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.context("Could not flush db")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latest_state<T>(&self) -> anyhow::Result<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let key = serialize(&Self::LAST_STATE_KEY)?;
|
||||||
|
|
||||||
|
let encoded = self
|
||||||
|
.db
|
||||||
|
.get(&key)?
|
||||||
|
.ok_or_else(|| anyhow!("State does not exist {:?}", key))?;
|
||||||
|
|
||||||
|
let state = deserialize(&encoded).context("Could not deserialize state")?;
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<T>(t: &T) -> anyhow::Result<Vec<u8>>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
Ok(serde_cbor::to_vec(t)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<T>(v: &[u8]) -> anyhow::Result<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
Ok(serde_cbor::from_slice(&v)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
use super::*;
|
||||||
|
use bitcoin::SigHash;
|
||||||
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
use ecdsa_fun::fun::rand_core::OsRng;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use xmr_btc::serde::{
|
||||||
|
bitcoin_amount, cross_curve_dleq_scalar, ecdsa_fun_signature, monero_private_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct TestState {
|
||||||
|
A: xmr_btc::bitcoin::PublicKey,
|
||||||
|
a: xmr_btc::bitcoin::SecretKey,
|
||||||
|
#[serde(with = "cross_curve_dleq_scalar")]
|
||||||
|
s_a: ::cross_curve_dleq::Scalar,
|
||||||
|
#[serde(with = "monero_private_key")]
|
||||||
|
s_b: monero::PrivateKey,
|
||||||
|
S_a_monero: ::monero::PublicKey,
|
||||||
|
S_a_bitcoin: xmr_btc::bitcoin::PublicKey,
|
||||||
|
v: xmr_btc::monero::PrivateViewKey,
|
||||||
|
#[serde(with = "bitcoin_amount")]
|
||||||
|
btc: ::bitcoin::Amount,
|
||||||
|
xmr: xmr_btc::monero::Amount,
|
||||||
|
refund_timelock: u32,
|
||||||
|
refund_address: ::bitcoin::Address,
|
||||||
|
transaction: ::bitcoin::Transaction,
|
||||||
|
#[serde(with = "ecdsa_fun_signature")]
|
||||||
|
tx_punish_sig: xmr_btc::bitcoin::Signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn recover_state_from_db() {
|
||||||
|
let db = Database::open(Path::new("../target/test_recover.db")).unwrap();
|
||||||
|
|
||||||
|
let a = xmr_btc::bitcoin::SecretKey::new_random(&mut OsRng);
|
||||||
|
let s_a = cross_curve_dleq::Scalar::random(&mut OsRng);
|
||||||
|
let s_b = monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng));
|
||||||
|
let v_a = xmr_btc::monero::PrivateViewKey::new_random(&mut OsRng);
|
||||||
|
let S_a_monero = monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||||
|
scalar: s_a.into_ed25519(),
|
||||||
|
});
|
||||||
|
let S_a_bitcoin = s_a.into_secp256k1().into();
|
||||||
|
let tx_punish_sig = a.sign(SigHash::default());
|
||||||
|
|
||||||
|
let state = TestState {
|
||||||
|
A: a.public(),
|
||||||
|
a,
|
||||||
|
s_b,
|
||||||
|
s_a,
|
||||||
|
S_a_monero,
|
||||||
|
S_a_bitcoin,
|
||||||
|
v: v_a,
|
||||||
|
btc: ::bitcoin::Amount::from_sat(100),
|
||||||
|
xmr: xmr_btc::monero::Amount::from_piconero(1000),
|
||||||
|
refund_timelock: 0,
|
||||||
|
refund_address: ::bitcoin::Address::from_str("1L5wSMgerhHg8GZGcsNmAx5EXMRXSKR3He")
|
||||||
|
.unwrap(),
|
||||||
|
transaction: ::bitcoin::Transaction {
|
||||||
|
version: 0,
|
||||||
|
lock_time: 0,
|
||||||
|
input: vec![::bitcoin::TxIn::default()],
|
||||||
|
output: vec![::bitcoin::TxOut::default()],
|
||||||
|
},
|
||||||
|
tx_punish_sig,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.insert_latest_state(&state)
|
||||||
|
.await
|
||||||
|
.expect("Failed to save state the first time");
|
||||||
|
let recovered: TestState = db
|
||||||
|
.get_latest_state()
|
||||||
|
.expect("Failed to recover state the first time");
|
||||||
|
|
||||||
|
// We insert and recover twice to ensure database implementation allows the
|
||||||
|
// caller to write to an existing key
|
||||||
|
db.insert_latest_state(&recovered)
|
||||||
|
.await
|
||||||
|
.expect("Failed to save state the second time");
|
||||||
|
let recovered: TestState = db
|
||||||
|
.get_latest_state()
|
||||||
|
.expect("Failed to recover state the second time");
|
||||||
|
|
||||||
|
assert_eq!(state, recovered);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,251 @@
|
|||||||
|
pub mod harness;
|
||||||
|
|
||||||
|
use std::{convert::TryInto, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use futures::{
|
||||||
|
channel::mpsc::{channel, Receiver, Sender},
|
||||||
|
future::try_join,
|
||||||
|
SinkExt, StreamExt,
|
||||||
|
};
|
||||||
|
use genawaiter::GeneratorState;
|
||||||
|
use harness::{
|
||||||
|
init_bitcoind, init_test,
|
||||||
|
node::{run_alice_until, run_bob_until},
|
||||||
|
};
|
||||||
|
use monero_harness::Monero;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use testcontainers::clients::Cli;
|
||||||
|
use tracing::info;
|
||||||
|
use xmr_btc::{
|
||||||
|
action_generator_alice, action_generator_bob, alice,
|
||||||
|
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
||||||
|
bob,
|
||||||
|
monero::{CreateWalletForOutput, Transfer, TransferProof},
|
||||||
|
AliceAction, BobAction, ReceiveBitcoinRedeemEncsig, ReceiveTransferProof,
|
||||||
|
};
|
||||||
|
|
||||||
|
type AliceNetwork = Network<EncryptedSignature>;
|
||||||
|
type BobNetwork = Network<TransferProof>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Network<M> {
|
||||||
|
// TODO: It is weird to use mpsc's in a situation where only one message is expected, but the
|
||||||
|
// ownership rules of Rust are making this painful
|
||||||
|
pub receiver: Receiver<M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M> Network<M> {
|
||||||
|
pub fn new() -> (Network<M>, Sender<M>) {
|
||||||
|
let (sender, receiver) = channel(1);
|
||||||
|
|
||||||
|
(Self { receiver }, sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ReceiveTransferProof for BobNetwork {
|
||||||
|
async fn receive_transfer_proof(&mut self) -> TransferProof {
|
||||||
|
self.receiver.next().await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ReceiveBitcoinRedeemEncsig for AliceNetwork {
|
||||||
|
async fn receive_bitcoin_redeem_encsig(&mut self) -> EncryptedSignature {
|
||||||
|
self.receiver.next().await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swap_as_alice(
|
||||||
|
network: AliceNetwork,
|
||||||
|
// FIXME: It would be more intuitive to have a single network/transport struct instead of
|
||||||
|
// splitting into two, but Rust ownership rules make this tedious
|
||||||
|
mut sender: Sender<TransferProof>,
|
||||||
|
monero_wallet: &harness::wallet::monero::Wallet,
|
||||||
|
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
|
||||||
|
state: alice::State3,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut action_generator = action_generator_alice(network, bitcoin_wallet.clone(), state);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let state = action_generator.async_resume().await;
|
||||||
|
|
||||||
|
info!("resumed execution of generator, got: {:?}", state);
|
||||||
|
|
||||||
|
match state {
|
||||||
|
GeneratorState::Yielded(AliceAction::LockXmr {
|
||||||
|
amount,
|
||||||
|
public_spend_key,
|
||||||
|
public_view_key,
|
||||||
|
}) => {
|
||||||
|
let (transfer_proof, _) = monero_wallet
|
||||||
|
.transfer(public_spend_key, public_view_key, amount)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sender.send(transfer_proof).await.unwrap();
|
||||||
|
}
|
||||||
|
GeneratorState::Yielded(AliceAction::RedeemBtc(tx))
|
||||||
|
| GeneratorState::Yielded(AliceAction::CancelBtc(tx))
|
||||||
|
| GeneratorState::Yielded(AliceAction::PunishBtc(tx)) => {
|
||||||
|
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
||||||
|
}
|
||||||
|
GeneratorState::Yielded(AliceAction::CreateMoneroWalletForOutput {
|
||||||
|
spend_key,
|
||||||
|
view_key,
|
||||||
|
}) => {
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
GeneratorState::Complete(()) => return Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swap_as_bob(
|
||||||
|
network: BobNetwork,
|
||||||
|
mut sender: Sender<EncryptedSignature>,
|
||||||
|
monero_wallet: Arc<harness::wallet::monero::Wallet>,
|
||||||
|
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
|
||||||
|
state: bob::State2,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut action_generator = action_generator_bob(
|
||||||
|
network,
|
||||||
|
monero_wallet.clone(),
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let state = action_generator.async_resume().await;
|
||||||
|
|
||||||
|
info!("resumed execution of generator, got: {:?}", state);
|
||||||
|
|
||||||
|
match state {
|
||||||
|
GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => {
|
||||||
|
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
|
||||||
|
let _ = bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(signed_tx_lock)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
GeneratorState::Yielded(BobAction::SendBitcoinRedeemEncsig(tx_redeem_encsig)) => {
|
||||||
|
sender.send(tx_redeem_encsig).await.unwrap();
|
||||||
|
}
|
||||||
|
GeneratorState::Yielded(BobAction::CreateMoneroWalletForOutput {
|
||||||
|
spend_key,
|
||||||
|
view_key,
|
||||||
|
}) => {
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
GeneratorState::Yielded(BobAction::CancelBitcoin(tx_cancel)) => {
|
||||||
|
let _ = bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(tx_cancel)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
GeneratorState::Yielded(BobAction::RefundBitcoin(tx_refund)) => {
|
||||||
|
let _ = bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(tx_refund)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
GeneratorState::Complete(()) => return Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: For some reason running these tests overflows the stack. In order to
|
||||||
|
// mitigate this run them with:
|
||||||
|
//
|
||||||
|
// RUST_MIN_STACK=100000000 cargo test
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn on_chain_happy_path() {
|
||||||
|
let cli = Cli::default();
|
||||||
|
let (monero, _container) = Monero::new(&cli);
|
||||||
|
let bitcoind = init_bitcoind(&cli).await;
|
||||||
|
|
||||||
|
let (alice_state0, bob_state0, mut alice_node, mut bob_node, initial_balances, swap_amounts) =
|
||||||
|
init_test(&monero, &bitcoind, Some(100), Some(100)).await;
|
||||||
|
|
||||||
|
// run the handshake as part of the setup
|
||||||
|
let (alice_state, bob_state) = try_join(
|
||||||
|
run_alice_until(
|
||||||
|
&mut alice_node,
|
||||||
|
alice_state0.into(),
|
||||||
|
harness::alice::is_state3,
|
||||||
|
&mut OsRng,
|
||||||
|
),
|
||||||
|
run_bob_until(
|
||||||
|
&mut bob_node,
|
||||||
|
bob_state0.into(),
|
||||||
|
harness::bob::is_state2,
|
||||||
|
&mut OsRng,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let alice: alice::State3 = alice_state.try_into().unwrap();
|
||||||
|
let bob: bob::State2 = bob_state.try_into().unwrap();
|
||||||
|
let tx_lock_txid = bob.tx_lock.txid();
|
||||||
|
|
||||||
|
let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet);
|
||||||
|
let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet);
|
||||||
|
let alice_monero_wallet = Arc::new(alice_node.monero_wallet);
|
||||||
|
let bob_monero_wallet = Arc::new(bob_node.monero_wallet);
|
||||||
|
|
||||||
|
let (alice_network, bob_sender) = Network::<EncryptedSignature>::new();
|
||||||
|
let (bob_network, alice_sender) = Network::<TransferProof>::new();
|
||||||
|
|
||||||
|
try_join(
|
||||||
|
swap_as_alice(
|
||||||
|
alice_network,
|
||||||
|
alice_sender,
|
||||||
|
&alice_monero_wallet.clone(),
|
||||||
|
alice_bitcoin_wallet.clone(),
|
||||||
|
alice,
|
||||||
|
),
|
||||||
|
swap_as_bob(
|
||||||
|
bob_network,
|
||||||
|
bob_sender,
|
||||||
|
bob_monero_wallet.clone(),
|
||||||
|
bob_bitcoin_wallet.clone(),
|
||||||
|
bob,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap();
|
||||||
|
let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap();
|
||||||
|
|
||||||
|
let lock_tx_bitcoin_fee = bob_bitcoin_wallet
|
||||||
|
.transaction_fee(tx_lock_txid)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
|
monero.wait_for_bob_wallet_block_height().await.unwrap();
|
||||||
|
let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
alice_final_btc_balance,
|
||||||
|
initial_balances.alice_btc + swap_amounts.btc
|
||||||
|
- bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bob_final_btc_balance,
|
||||||
|
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
|
||||||
|
);
|
||||||
|
|
||||||
|
// Getting the Monero LockTx fee is tricky in a clean way, I think checking this
|
||||||
|
// condition is sufficient
|
||||||
|
assert!(alice_final_xmr_balance <= initial_balances.alice_xmr - swap_amounts.xmr,);
|
||||||
|
assert_eq!(
|
||||||
|
bob_final_xmr_balance,
|
||||||
|
initial_balances.bob_xmr + swap_amounts.xmr
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue