Merge pull request #12 from comit-network/on-chain-protocol

pull/20/head
Tobin C. Harding 4 years ago committed by GitHub
commit 71e09413aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,2 +1,2 @@
[workspace]
members = ["monero-harness", "xmr-btc"]
members = ["monero-harness", "xmr-btc", "swap"]

@ -48,18 +48,17 @@ const WAIT_WALLET_SYNC_MILLIS: u64 = 1000;
/// Wallet sub-account indices.
const ACCOUNT_INDEX_PRIMARY: u32 = 0;
#[derive(Debug)]
pub struct Monero<'c> {
pub docker: Container<'c, Cli, image::Monero>,
pub monerod_rpc_port: u16,
pub miner_wallet_rpc_port: u16,
pub alice_wallet_rpc_port: u16,
pub bob_wallet_rpc_port: u16,
#[derive(Copy, Clone, Debug)]
pub struct Monero {
monerod_rpc_port: u16,
miner_wallet_rpc_port: u16,
alice_wallet_rpc_port: u16,
bob_wallet_rpc_port: u16,
}
impl<'c> Monero<'c> {
impl<'c> Monero {
/// Starts a new regtest monero container.
pub fn new(cli: &'c Cli) -> Self {
pub fn new(cli: &'c Cli) -> (Self, Container<'c, Cli, image::Monero>) {
let mut rng = rand::thread_rng();
let monerod_rpc_port: u16 = rng.gen_range(1024, u16::MAX);
let miner_wallet_rpc_port: u16 = rng.gen_range(1024, u16::MAX);
@ -91,13 +90,15 @@ impl<'c> Monero<'c> {
let docker = cli.run(image);
println!("image ran");
Self {
(
Self {
monerod_rpc_port,
miner_wallet_rpc_port,
alice_wallet_rpc_port,
bob_wallet_rpc_port,
},
docker,
monerod_rpc_port,
miner_wallet_rpc_port,
alice_wallet_rpc_port,
bob_wallet_rpc_port,
}
)
}
pub fn miner_wallet_rpc_client(&self) -> wallet::Client {
@ -156,21 +157,6 @@ impl<'c> Monero<'c> {
Ok(())
}
/// Just create a wallet and start mining (you probably want `init()`).
pub async fn init_just_miner(&self, blocks: u32) -> Result<()> {
let wallet = self.miner_wallet_rpc_client();
let monerod = self.monerod_rpc_client();
wallet.create_wallet("miner_wallet").await?;
let miner = self.get_address_miner().await?.address;
let _ = monerod.generate_blocks(blocks, &miner).await?;
let _ = tokio::spawn(mine(monerod.clone(), miner));
Ok(())
}
async fn fund_account(&self, address: &str, miner: &str, funding: u64) -> Result<()> {
let monerod = self.monerod_rpc_client();
@ -208,64 +194,42 @@ impl<'c> Monero<'c> {
}
/// Get addresses for the primary account.
pub async fn get_address_miner(&self) -> Result<GetAddress> {
async fn get_address_miner(&self) -> Result<GetAddress> {
let wallet = self.miner_wallet_rpc_client();
wallet.get_address(ACCOUNT_INDEX_PRIMARY).await
}
/// Get addresses for the Alice's account.
pub async fn get_address_alice(&self) -> Result<GetAddress> {
async fn get_address_alice(&self) -> Result<GetAddress> {
let wallet = self.alice_wallet_rpc_client();
wallet.get_address(ACCOUNT_INDEX_PRIMARY).await
}
/// Get addresses for the Bob's account.
pub async fn get_address_bob(&self) -> Result<GetAddress> {
async fn get_address_bob(&self) -> Result<GetAddress> {
let wallet = self.bob_wallet_rpc_client();
wallet.get_address(ACCOUNT_INDEX_PRIMARY).await
}
/// Gets the balance of the wallet primary account.
pub async fn get_balance_primary(&self) -> Result<u64> {
let wallet = self.miner_wallet_rpc_client();
wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await
}
/// Gets the balance of Alice's account.
pub async fn get_balance_alice(&self) -> Result<u64> {
async fn get_balance_alice(&self) -> Result<u64> {
let wallet = self.alice_wallet_rpc_client();
wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await
}
/// Gets the balance of Bob's account.
pub async fn get_balance_bob(&self) -> Result<u64> {
async fn get_balance_bob(&self) -> Result<u64> {
let wallet = self.bob_wallet_rpc_client();
wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await
}
/// Transfers moneroj from the primary account.
pub async fn transfer_from_primary(&self, amount: u64, address: &str) -> Result<Transfer> {
async fn transfer_from_primary(&self, amount: u64, address: &str) -> Result<Transfer> {
let wallet = self.miner_wallet_rpc_client();
wallet
.transfer(ACCOUNT_INDEX_PRIMARY, amount, address)
.await
}
/// Transfers moneroj from Alice's account.
pub async fn transfer_from_alice(&self, amount: u64, address: &str) -> Result<Transfer> {
let wallet = self.alice_wallet_rpc_client();
wallet
.transfer(ACCOUNT_INDEX_PRIMARY, amount, address)
.await
}
/// Transfers moneroj from Bob's account.
pub async fn transfer_from_bob(&self, amount: u64, address: &str) -> Result<Transfer> {
let wallet = self.bob_wallet_rpc_client();
wallet
.transfer(ACCOUNT_INDEX_PRIMARY, amount, address)
.await
}
}
/// Mine a block ever BLOCK_TIME_SECS seconds.

@ -5,29 +5,24 @@ use testcontainers::clients::Cli;
const ALICE_FUND_AMOUNT: u64 = 1_000_000_000_000;
const BOB_FUND_AMOUNT: u64 = 0;
fn init_cli() -> Cli {
Cli::default()
}
async fn init_monero(tc: &'_ Cli) -> Monero<'_> {
let monero = Monero::new(tc);
let _ = monero.init(ALICE_FUND_AMOUNT, BOB_FUND_AMOUNT).await;
monero
}
#[tokio::test]
async fn init_accounts_for_alice_and_bob() {
let cli = init_cli();
let monero = init_monero(&cli).await;
let tc = Cli::default();
let (monero, _container) = Monero::new(&tc);
monero
.init(ALICE_FUND_AMOUNT, BOB_FUND_AMOUNT)
.await
.unwrap();
let got_balance_alice = monero
.get_balance_alice()
.alice_wallet_rpc_client()
.get_balance(0)
.await
.expect("failed to get alice's balance");
let got_balance_bob = monero
.get_balance_bob()
.bob_wallet_rpc_client()
.get_balance(0)
.await
.expect("failed to get bob's balance");

@ -1,8 +1,6 @@
use monero_harness::{rpc::monerod::Client, Monero};
use monero_harness::Monero;
use spectral::prelude::*;
use std::time::Duration;
use testcontainers::clients::Cli;
use tokio::time;
fn init_cli() -> Cli {
Cli::default()
@ -11,8 +9,8 @@ fn init_cli() -> Cli {
#[tokio::test]
async fn connect_to_monerod() {
let tc = init_cli();
let monero = Monero::new(&tc);
let cli = Client::localhost(monero.monerod_rpc_port);
let (monero, _container) = Monero::new(&tc);
let cli = monero.monerod_rpc_client();
let header = cli
.get_block_header_by_height(0)
@ -21,27 +19,3 @@ async fn connect_to_monerod() {
assert_that!(header.height).is_equal_to(0);
}
#[tokio::test]
async fn miner_is_running_and_producing_blocks() {
let tc = init_cli();
let monero = Monero::new(&tc);
let cli = Client::localhost(monero.monerod_rpc_port);
monero
.init_just_miner(2)
.await
.expect("Failed to initialize");
// Only need 3 seconds since we mine a block every second but
// give it 5 just for good measure.
time::delay_for(Duration::from_secs(5)).await;
// We should have at least 5 blocks by now.
let header = cli
.get_block_header_by_height(5)
.await
.expect("failed to get block");
assert_that!(header.height).is_equal_to(5);
}

@ -1,24 +1,21 @@
use monero_harness::{rpc::wallet::Client, Monero};
use monero_harness::Monero;
use spectral::prelude::*;
use testcontainers::clients::Cli;
#[tokio::test]
async fn wallet_and_accounts() {
let tc = Cli::default();
let monero = Monero::new(&tc);
let miner_wallet = Client::localhost(monero.miner_wallet_rpc_port);
let (monero, _container) = Monero::new(&tc);
let cli = monero.miner_wallet_rpc_client();
println!("creating wallet ...");
let _ = miner_wallet
let _ = cli
.create_wallet("wallet")
.await
.expect("failed to create wallet");
let got = miner_wallet
.get_balance(0)
.await
.expect("failed to get balance");
let got = cli.get_balance(0).await.expect("failed to get balance");
let want = 0;
assert_that!(got).is_equal_to(want);
@ -27,8 +24,8 @@ async fn wallet_and_accounts() {
#[tokio::test]
async fn create_account_and_retrieve_it() {
let tc = Cli::default();
let monero = Monero::new(&tc);
let cli = Client::localhost(monero.miner_wallet_rpc_port);
let (monero, _container) = Monero::new(&tc);
let cli = monero.miner_wallet_rpc_client();
let label = "Iron Man"; // This is intentionally _not_ Alice or Bob.
@ -61,18 +58,20 @@ async fn transfer_and_check_tx_key() {
let fund_bob = 0;
let tc = Cli::default();
let monero = Monero::new(&tc);
let (monero, _container) = Monero::new(&tc);
let _ = monero.init(fund_alice, fund_bob).await;
let address_bob = monero
.get_address_bob()
.bob_wallet_rpc_client()
.get_address(0)
.await
.expect("failed to get Bob's address")
.address;
let transfer_amount = 100;
let transfer = monero
.transfer_from_alice(transfer_amount, &address_bob)
.alice_wallet_rpc_client()
.transfer(0, transfer_amount, &address_bob)
.await
.expect("transfer failed");

@ -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(())
}

@ -4,17 +4,20 @@ version = "0.1.0"
authors = ["CoBloX Team <team@coblox.tech>"]
edition = "2018"
# TODO: Check for stale dependencies, this looks like its a bit of a mess.
[dependencies]
anyhow = "1"
async-trait = "0.1"
bitcoin = { version = "0.23", features = ["rand"] }
cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "49171f5e08473d46f951fb1fc338fe437d974d3c" }
bitcoin = { version = "0.23", features = ["rand", "serde"] }
cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "1931c0436f259e1a1f53a4ec8acbbaaf614bd1e4", features = ["serde"] }
curve25519-dalek = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat"] }
ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3
lazy_static = "1.4"
miniscript = "1"
monero = "0.9"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] }
ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3
futures = "0.3"
genawaiter = "0.99.1"
miniscript = { version = "1", features = ["serde"] }
monero = { version = "0.9", features = ["serde_support"] }
rand = "0.7"
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
serde = { version = "1", features = ["derive"] }
@ -26,12 +29,16 @@ torut = { version = "0.1", optional = true }
tracing = "0.1"
[dev-dependencies]
backoff = { version = "0.2", features = ["tokio"] }
base64 = "0.12"
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" }
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "7ff30a559ab57cc3aa71189e71433ef6b2a6c3a2" }
futures = "0.3"
hyper = "0.13"
monero-harness = { path = "../monero-harness" }
port_check = "0.1"
reqwest = { version = "0.10", default-features = false }
serde_cbor = "0.11"
sled = "0.34"
spectral = "0.6"
tempfile = "3"
testcontainers = "0.10"

@ -3,6 +3,7 @@ use crate::{
bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction},
bob, monero,
monero::{CreateWalletForOutput, Transfer},
serde::{bitcoin_amount, cross_curve_dleq_scalar, ecdsa_fun_signature},
transport::{ReceiveMessage, SendMessage},
};
use anyhow::{anyhow, Result};
@ -11,6 +12,7 @@ use ecdsa_fun::{
nonce::Deterministic,
};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::convert::{TryFrom, TryInto};
@ -129,11 +131,13 @@ impl State {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State0 {
a: bitcoin::SecretKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -215,14 +219,16 @@ impl State0 {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State1 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -253,14 +259,16 @@ impl State1 {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State2 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -328,24 +336,28 @@ impl State2 {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State3 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
pub a: bitcoin::SecretKey,
pub B: bitcoin::PublicKey,
#[serde(with = "cross_curve_dleq_scalar")]
pub s_a: cross_curve_dleq::Scalar,
pub S_b_monero: monero::PublicKey,
pub S_b_bitcoin: bitcoin::PublicKey,
pub v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_punish_sig_bob: bitcoin::Signature,
tx_cancel_sig_bob: bitcoin::Signature,
pub xmr: monero::Amount,
pub refund_timelock: u32,
pub punish_timelock: u32,
pub refund_address: bitcoin::Address,
pub redeem_address: bitcoin::Address,
pub punish_address: bitcoin::Address,
pub tx_lock: bitcoin::TxLock,
#[serde(with = "ecdsa_fun_signature")]
pub tx_punish_sig_bob: bitcoin::Signature,
#[serde(with = "ecdsa_fun_signature")]
pub tx_cancel_sig_bob: bitcoin::Signature,
}
impl State3 {
@ -356,7 +368,7 @@ impl State3 {
tracing::info!("watching for lock btc with txid: {}", self.tx_lock.txid());
let tx = bitcoin_wallet
.watch_for_raw_transaction(self.tx_lock.txid())
.await?;
.await;
tracing::info!("tx lock seen with txid: {}", tx.txid());
@ -381,14 +393,16 @@ impl State3 {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State4 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -397,7 +411,9 @@ pub struct State4 {
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
#[serde(with = "ecdsa_fun_signature")]
tx_punish_sig_bob: bitcoin::Signature,
#[serde(with = "ecdsa_fun_signature")]
tx_cancel_sig_bob: bitcoin::Signature,
}
@ -484,14 +500,16 @@ impl State4 {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State5 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -501,7 +519,9 @@ pub struct State5 {
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_lock_proof: monero::TransferProof,
#[serde(with = "ecdsa_fun_signature")]
tx_punish_sig_bob: bitcoin::Signature,
#[serde(with = "ecdsa_fun_signature")]
tx_cancel_sig_bob: bitcoin::Signature,
lock_xmr_fee: monero::Amount,
}
@ -554,7 +574,7 @@ impl State5 {
let tx_refund_candidate = bitcoin_wallet
.watch_for_raw_transaction(tx_refund.txid())
.await?;
.await;
let tx_refund_sig =
tx_refund.extract_signature_by_key(tx_refund_candidate, self.a.public())?;
@ -575,14 +595,16 @@ impl State5 {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State6 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -591,6 +613,7 @@ pub struct State6 {
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
#[serde(with = "ecdsa_fun_signature")]
tx_punish_sig_bob: bitcoin::Signature,
tx_redeem_encsig: EncryptedSignature,
lock_xmr_fee: monero::Amount,

@ -1,5 +1,6 @@
use anyhow::Result;
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use crate::{bitcoin, monero};
@ -11,7 +12,7 @@ pub enum Message {
Message2(Message2),
}
#[derive(Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message0 {
pub(crate) A: bitcoin::PublicKey,
pub(crate) S_a_monero: monero::PublicKey,
@ -22,13 +23,13 @@ pub struct Message0 {
pub(crate) punish_address: bitcoin::Address,
}
#[derive(Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message1 {
pub(crate) tx_cancel_sig: Signature,
pub(crate) tx_refund_encsig: EncryptedSignature,
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Message2 {
pub(crate) tx_lock_proof: monero::TransferProof,
}

@ -6,29 +6,27 @@ use bitcoin::{
hashes::{hex::ToHex, Hash},
secp256k1,
util::psbt::PartiallySignedTransaction,
SigHash, Transaction,
SigHash,
};
pub use bitcoin::{Address, Amount, OutPoint, Txid};
use ecdsa_fun::{
adaptor::Adaptor,
fun::{
marker::{Jacobian, Mark},
Point, Scalar,
},
fun::{Point, Scalar},
nonce::Deterministic,
ECDSA,
};
pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
use miniscript::{Descriptor, Segwitv0};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::str::FromStr;
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
pub use bitcoin::{Address, Amount, OutPoint, Transaction, Txid};
pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
pub const TX_FEE: u64 = 10_000;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct SecretKey {
inner: Scalar,
public: Point,
@ -83,12 +81,12 @@ impl SecretKey {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PublicKey(Point);
impl From<PublicKey> for Point<Jacobian> {
impl From<PublicKey> for Point {
fn from(from: PublicKey) -> Self {
from.0.mark::<Jacobian>()
from.0
}
}
@ -189,7 +187,7 @@ pub trait BroadcastSignedTransaction {
#[async_trait]
pub trait WatchForRawTransaction {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
}
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {

@ -8,9 +8,10 @@ use bitcoin::{
};
use ecdsa_fun::Signature;
use miniscript::Descriptor;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TxLock {
inner: Transaction,
output_descriptor: Descriptor<::bitcoin::PublicKey>,
@ -260,6 +261,10 @@ impl TxCancel {
}
}
pub fn txid(&self) -> Txid {
self.inner.txid()
}
pub fn digest(&self) -> SigHash {
self.digest
}
@ -459,6 +464,10 @@ impl TxPunish {
}
}
pub fn txid(&self) -> Txid {
self.inner.txid()
}
pub fn digest(&self) -> SigHash {
self.digest
}

@ -5,7 +5,8 @@ use crate::{
WatchForRawTransaction,
},
monero,
monero::{CheckTransfer, CreateWalletForOutput},
monero::{CreateWalletForOutput, WatchForTransfer},
serde::{bitcoin_amount, cross_curve_dleq_scalar, monero_private_key},
transport::{ReceiveMessage, SendMessage},
};
use anyhow::{anyhow, Result};
@ -15,6 +16,7 @@ use ecdsa_fun::{
Signature,
};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::convert::{TryFrom, TryInto};
@ -27,7 +29,7 @@ pub use message::{Message, Message0, Message1, Message2, Message3};
pub async fn next_state<
R: RngCore + CryptoRng,
B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction,
M: CreateWalletForOutput + CheckTransfer,
M: CreateWalletForOutput + WatchForTransfer,
T: SendMessage<Message> + ReceiveMessage<alice::Message>,
>(
bitcoin_wallet: &B,
@ -50,13 +52,15 @@ pub async fn next_state<
let message1 = transport.receive_message().await?.try_into()?;
let state2 = state1.receive(message1)?;
let message2 = state2.next_message();
transport.send_message(message2.into()).await?;
Ok(state2.into())
}
State::State2(state2) => {
let message2 = state2.next_message();
let state3 = state2.lock_btc(bitcoin_wallet).await?;
tracing::info!("bob has locked btc");
transport.send_message(message2.into()).await?;
Ok(state3.into())
}
State::State3(state3) => {
@ -102,11 +106,13 @@ impl_from_child_enum!(State3, State);
impl_from_child_enum!(State4, State);
impl_from_child_enum!(State5, State);
#[derive(Debug)]
#[derive(Debug, Deserialize, Serialize)]
pub struct State0 {
b: bitcoin::SecretKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_b: cross_curve_dleq::Scalar,
v_b: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -190,14 +196,16 @@ impl State0 {
}
}
#[derive(Debug)]
#[derive(Debug, Deserialize, Serialize)]
pub struct State1 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -253,24 +261,26 @@ impl State1 {
}
}
#[derive(Debug)]
#[derive(Debug, Deserialize, Serialize)]
pub struct State2 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
pub A: bitcoin::PublicKey,
pub b: bitcoin::SecretKey,
#[serde(with = "cross_curve_dleq_scalar")]
pub s_b: cross_curve_dleq::Scalar,
pub S_a_monero: monero::PublicKey,
pub S_a_bitcoin: bitcoin::PublicKey,
pub v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
pub xmr: monero::Amount,
pub refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
pub refund_address: bitcoin::Address,
pub redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
pub tx_lock: bitcoin::TxLock,
pub tx_cancel_sig_a: Signature,
pub tx_refund_encsig: EncryptedSignature,
}
impl State2 {
@ -324,14 +334,16 @@ impl State2 {
}
}
#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize)]
pub struct State3 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -347,7 +359,7 @@ pub struct State3 {
impl State3 {
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State4>
where
W: monero::CheckTransfer,
W: monero::WatchForTransfer,
{
let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(
self.s_b.into_ed25519(),
@ -355,7 +367,13 @@ impl State3 {
let S = self.S_a_monero + S_b_monero;
xmr_wallet
.check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr)
.watch_for_transfer(
S,
self.v.public(),
msg.tx_lock_proof,
self.xmr,
monero::MIN_CONFIRMATIONS,
)
.await?;
Ok(State4 {
@ -429,14 +447,16 @@ impl State3 {
}
}
#[derive(Debug)]
#[derive(Debug, Deserialize, Serialize)]
pub struct State4 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -466,7 +486,7 @@ impl State4 {
let tx_redeem_candidate = bitcoin_wallet
.watch_for_raw_transaction(tx_redeem.txid())
.await?;
.await;
let tx_redeem_sig =
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
@ -496,15 +516,18 @@ impl State4 {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct State5 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
#[serde(with = "monero_private_key")]
s_a: monero::PrivateKey,
#[serde(with = "cross_curve_dleq_scalar")]
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "bitcoin_amount")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,

@ -1,9 +1,10 @@
use crate::{bitcoin, monero};
use anyhow::Result;
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum Message {
Message0(Message0),
Message1(Message1),
@ -11,7 +12,7 @@ pub enum Message {
Message3(Message3),
}
#[derive(Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message0 {
pub(crate) B: bitcoin::PublicKey,
pub(crate) S_b_monero: monero::PublicKey,
@ -21,18 +22,18 @@ pub struct Message0 {
pub(crate) refund_address: bitcoin::Address,
}
#[derive(Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message1 {
pub(crate) tx_lock: bitcoin::TxLock,
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Message2 {
pub(crate) tx_punish_sig: Signature,
pub(crate) tx_cancel_sig: Signature,
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Message3 {
pub(crate) tx_redeem_encsig: EncryptedSignature,
}

@ -49,6 +49,564 @@ pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod monero;
pub mod serde;
#[cfg(feature = "tor")]
pub mod tor;
pub mod transport;
use async_trait::async_trait;
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
use futures::{
future::{select, Either},
Future, FutureExt,
};
use genawaiter::sync::{Gen, GenBoxed};
use sha2::Sha256;
use std::{sync::Arc, time::Duration};
use tokio::time::timeout;
use tracing::error;
// TODO: Replace this with something configurable, such as an function argument.
/// Time that Bob has to publish the Bitcoin lock transaction before both
/// parties will abort, in seconds.
const SECS_TO_ACT_BOB: u64 = 60;
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum BobAction {
LockBitcoin(bitcoin::TxLock),
SendBitcoinRedeemEncsig(bitcoin::EncryptedSignature),
CreateMoneroWalletForOutput {
spend_key: monero::PrivateKey,
view_key: monero::PrivateViewKey,
},
CancelBitcoin(bitcoin::Transaction),
RefundBitcoin(bitcoin::Transaction),
}
// TODO: This could be moved to the monero module
#[async_trait]
pub trait ReceiveTransferProof {
async fn receive_transfer_proof(&mut self) -> monero::TransferProof;
}
#[async_trait]
pub trait BlockHeight {
async fn block_height(&self) -> u32;
}
#[async_trait]
pub trait TransactionBlockHeight {
async fn transaction_block_height(&self, txid: bitcoin::Txid) -> u32;
}
/// Perform the on-chain protocol to swap monero and bitcoin as Bob.
///
/// This is called post handshake, after all the keys, addresses and most of the
/// signatures have been exchanged.
pub fn action_generator_bob<N, M, B>(
mut network: N,
monero_client: Arc<M>,
bitcoin_client: Arc<B>,
// TODO: Replace this with a new, slimmer struct?
bob::State2 {
A,
b,
s_b,
S_a_monero,
S_a_bitcoin,
v,
xmr,
refund_timelock,
redeem_address,
refund_address,
tx_lock,
tx_cancel_sig_a,
tx_refund_encsig,
..
}: bob::State2,
) -> GenBoxed<BobAction, (), ()>
where
N: ReceiveTransferProof + Send + Sync + 'static,
M: monero::WatchForTransfer + Send + Sync + 'static,
B: BlockHeight
+ TransactionBlockHeight
+ bitcoin::WatchForRawTransaction
+ Send
+ Sync
+ 'static,
{
#[derive(Debug)]
enum SwapFailed {
BeforeBtcLock,
AfterBtcLock(Reason),
AfterBtcRedeem(Reason),
}
/// Reason why the swap has failed.
#[derive(Debug)]
enum Reason {
/// The refund timelock has been reached.
BtcExpired,
/// Alice did not lock up enough monero in the shared output.
InsufficientXmr(monero::InsufficientFunds),
/// Could not find Bob's signature on the redeem transaction witness
/// stack.
BtcRedeemSignature,
/// Could not recover secret `s_a` from Bob's redeem transaction
/// signature.
SecretRecovery,
}
async fn poll_until(condition_future: impl Future<Output = bool> + Clone) {
loop {
if condition_future.clone().await {
return;
}
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
}
}
async fn bitcoin_block_height_is_gte<B>(bitcoin_client: &B, n_blocks: u32) -> bool
where
B: BlockHeight,
{
bitcoin_client.block_height().await >= n_blocks
}
Gen::new_boxed(|co| async move {
let swap_result: Result<(), SwapFailed> = async {
co.yield_(BobAction::LockBitcoin(tx_lock.clone())).await;
timeout(
Duration::from_secs(SECS_TO_ACT_BOB),
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
)
.await
.map(|tx| tx.txid())
.map_err(|_| SwapFailed::BeforeBtcLock)?;
let tx_lock_height = bitcoin_client
.transaction_block_height(tx_lock.txid())
.await;
let btc_has_expired = bitcoin_block_height_is_gte(
bitcoin_client.as_ref(),
tx_lock_height + refund_timelock,
)
.shared();
let poll_until_btc_has_expired = poll_until(btc_has_expired).shared();
futures::pin_mut!(poll_until_btc_has_expired);
let transfer_proof = match select(
network.receive_transfer_proof(),
poll_until_btc_has_expired.clone(),
)
.await
{
Either::Left((proof, _)) => proof,
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
};
let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(
s_b.into_ed25519(),
));
let S = S_a_monero + S_b_monero;
match select(
monero_client.watch_for_transfer(
S,
v.public(),
transfer_proof,
xmr,
monero::MIN_CONFIRMATIONS,
),
poll_until_btc_has_expired.clone(),
)
.await
{
Either::Left((Err(e), _)) => {
return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e)))
}
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
_ => {}
}
let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address);
let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest());
co.yield_(BobAction::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone()))
.await;
let tx_redeem_published = match select(
bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()),
poll_until_btc_has_expired,
)
.await
{
Either::Left((tx, _)) => tx,
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
};
let tx_redeem_sig = tx_redeem
.extract_signature_by_key(tx_redeem_published, b.public())
.map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?;
let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)
.map_err(|_| SwapFailed::AfterBtcRedeem(Reason::SecretRecovery))?;
let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(
s_a.to_bytes(),
));
let s_b = monero::PrivateKey {
scalar: s_b.into_ed25519(),
};
co.yield_(BobAction::CreateMoneroWalletForOutput {
spend_key: s_a + s_b,
view_key: v,
})
.await;
Ok(())
}
.await;
if let Err(err @ SwapFailed::AfterBtcLock(_)) = swap_result {
error!("Swap failed, reason: {:?}", err);
let tx_cancel =
bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public());
let tx_cancel_txid = tx_cancel.txid();
let signed_tx_cancel = {
let sig_a = tx_cancel_sig_a.clone();
let sig_b = b.sign(tx_cancel.digest());
tx_cancel
.clone()
.add_signatures(&tx_lock, (A.clone(), sig_a), (b.public(), sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel")
};
co.yield_(BobAction::CancelBitcoin(signed_tx_cancel)).await;
let _ = bitcoin_client
.watch_for_raw_transaction(tx_cancel_txid)
.await;
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
let tx_refund_txid = tx_refund.txid();
let signed_tx_refund = {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let sig_a =
adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone());
let sig_b = b.sign(tx_refund.digest());
tx_refund
.add_signatures(&tx_cancel, (A.clone(), sig_a), (b.public(), sig_b))
.expect("sig_{a,b} to be valid signatures for tx_refund")
};
co.yield_(BobAction::RefundBitcoin(signed_tx_refund)).await;
let _ = bitcoin_client
.watch_for_raw_transaction(tx_refund_txid)
.await;
}
})
}
#[derive(Debug)]
pub enum AliceAction {
// This action also includes proving to Bob that this has happened, given that our current
// protocol requires a transfer proof to verify that the coins have been locked on Monero
LockXmr {
amount: monero::Amount,
public_spend_key: monero::PublicKey,
public_view_key: monero::PublicViewKey,
},
RedeemBtc(bitcoin::Transaction),
CreateMoneroWalletForOutput {
spend_key: monero::PrivateKey,
view_key: monero::PrivateViewKey,
},
CancelBtc(bitcoin::Transaction),
PunishBtc(bitcoin::Transaction),
}
// TODO: This could be moved to the bitcoin module
#[async_trait]
pub trait ReceiveBitcoinRedeemEncsig {
async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature;
}
/// Perform the on-chain protocol to swap monero and bitcoin as Alice.
///
/// This is called post handshake, after all the keys, addresses and most of the
/// signatures have been exchanged.
pub fn action_generator_alice<N, B>(
mut network: N,
bitcoin_client: Arc<B>,
// TODO: Replace this with a new, slimmer struct?
alice::State3 {
a,
B,
s_a,
S_b_monero,
S_b_bitcoin,
v,
xmr,
refund_timelock,
punish_timelock,
refund_address,
redeem_address,
punish_address,
tx_lock,
tx_punish_sig_bob,
tx_cancel_sig_bob,
..
}: alice::State3,
) -> GenBoxed<AliceAction, (), ()>
where
N: ReceiveBitcoinRedeemEncsig + Send + Sync + 'static,
B: BlockHeight
+ TransactionBlockHeight
+ bitcoin::WatchForRawTransaction
+ Send
+ Sync
+ 'static,
{
#[derive(Debug)]
enum SwapFailed {
BeforeBtcLock,
AfterXmrLock(Reason),
}
/// Reason why the swap has failed.
#[derive(Debug)]
enum Reason {
/// The refund timelock has been reached.
BtcExpired,
}
enum RefundFailed {
BtcPunishable {
tx_cancel_was_published: bool,
},
/// Could not find Alice's signature on the refund transaction witness
/// stack.
BtcRefundSignature,
/// Could not recover secret `s_b` from Alice's refund transaction
/// signature.
SecretRecovery,
}
async fn poll_until(condition_future: impl Future<Output = bool> + Clone) {
loop {
if condition_future.clone().await {
return;
}
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
}
}
async fn bitcoin_block_height_is_gte<B>(bitcoin_client: &B, n_blocks: u32) -> bool
where
B: BlockHeight,
{
bitcoin_client.block_height().await >= n_blocks
}
Gen::new_boxed(|co| async move {
let swap_result: Result<(), SwapFailed> = async {
timeout(
Duration::from_secs(SECS_TO_ACT_BOB),
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
)
.await
.map_err(|_| SwapFailed::BeforeBtcLock)?;
let tx_lock_height = bitcoin_client
.transaction_block_height(tx_lock.txid())
.await;
let btc_has_expired = bitcoin_block_height_is_gte(
bitcoin_client.as_ref(),
tx_lock_height + refund_timelock,
)
.shared();
let poll_until_btc_has_expired = poll_until(btc_has_expired).shared();
futures::pin_mut!(poll_until_btc_has_expired);
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: s_a.into_ed25519(),
});
co.yield_(AliceAction::LockXmr {
amount: xmr,
public_spend_key: S_a + S_b_monero,
public_view_key: v.public(),
})
.await;
// TODO: Watch for LockXmr using watch-only wallet. Doing so will prevent Alice
// from cancelling/refunding unnecessarily.
let tx_redeem_encsig = match select(
network.receive_bitcoin_redeem_encsig(),
poll_until_btc_has_expired.clone(),
)
.await
{
Either::Left((encsig, _)) => encsig,
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
};
let (signed_tx_redeem, tx_redeem_txid) = {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address);
let sig_a = a.sign(tx_redeem.digest());
let sig_b =
adaptor.decrypt_signature(&s_a.into_secp256k1(), tx_redeem_encsig.clone());
let tx = tx_redeem
.add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b))
.expect("sig_{a,b} to be valid signatures for tx_redeem");
let txid = tx.txid();
(tx, txid)
};
co.yield_(AliceAction::RedeemBtc(signed_tx_redeem)).await;
match select(
bitcoin_client.watch_for_raw_transaction(tx_redeem_txid),
poll_until_btc_has_expired,
)
.await
{
Either::Left(_) => {}
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
};
Ok(())
}
.await;
if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result {
let refund_result: Result<(), RefundFailed> = async {
let bob_can_be_punished =
bitcoin_block_height_is_gte(bitcoin_client.as_ref(), punish_timelock).shared();
let poll_until_bob_can_be_punished = poll_until(bob_can_be_punished).shared();
futures::pin_mut!(poll_until_bob_can_be_punished);
let tx_cancel =
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
match select(
bitcoin_client.watch_for_raw_transaction(tx_cancel.txid()),
poll_until_bob_can_be_punished.clone(),
)
.await
{
Either::Left(_) => {}
Either::Right(_) => {
return Err(RefundFailed::BtcPunishable {
tx_cancel_was_published: false,
})
}
};
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
let tx_refund_published = match select(
bitcoin_client.watch_for_raw_transaction(tx_refund.txid()),
poll_until_bob_can_be_punished,
)
.await
{
Either::Left((tx, _)) => tx,
Either::Right(_) => {
return Err(RefundFailed::BtcPunishable {
tx_cancel_was_published: true,
})
}
};
let s_a = monero::PrivateKey {
scalar: s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(tx_refund_published, B.clone())
.map_err(|_| RefundFailed::BtcRefundSignature)?;
let tx_refund_encsig = a.encsign(S_b_bitcoin.clone(), tx_refund.digest());
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
.map_err(|_| RefundFailed::SecretRecovery)?;
let s_b = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(
s_b.to_bytes(),
));
co.yield_(AliceAction::CreateMoneroWalletForOutput {
spend_key: s_a + s_b,
view_key: v,
})
.await;
Ok(())
}
.await;
// LIMITATION: When approaching the punish scenario, Bob could theoretically
// wake up in between Alice's publication of tx cancel and beat Alice's punish
// transaction with his refund transaction. Alice would then need to carry on
// with the refund on Monero. Doing so may be too verbose with the current,
// linear approach. A different design may be required
if let Err(RefundFailed::BtcPunishable {
tx_cancel_was_published,
}) = refund_result
{
let tx_cancel =
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
if !tx_cancel_was_published {
let tx_cancel_txid = tx_cancel.txid();
let signed_tx_cancel = {
let sig_a = a.sign(tx_cancel.digest());
let sig_b = tx_cancel_sig_bob;
tx_cancel
.clone()
.add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel")
};
co.yield_(AliceAction::CancelBtc(signed_tx_cancel)).await;
let _ = bitcoin_client
.watch_for_raw_transaction(tx_cancel_txid)
.await;
}
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
let tx_punish_txid = tx_punish.txid();
let signed_tx_punish = {
let sig_a = a.sign(tx_punish.digest());
let sig_b = tx_punish_sig_bob;
tx_punish
.add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel")
};
co.yield_(AliceAction::PunishBtc(signed_tx_punish)).await;
let _ = bitcoin_client
.watch_for_raw_transaction(tx_punish_txid)
.await;
}
}
})
}

@ -1,9 +1,13 @@
use crate::serde::monero_private_key;
use anyhow::Result;
use async_trait::async_trait;
pub use curve25519_dalek::scalar::Scalar;
pub use monero::{Address, PrivateKey, PublicKey};
use rand::{CryptoRng, RngCore};
use std::ops::Add;
use serde::{Deserialize, Serialize};
use std::ops::{Add, Sub};
pub const MIN_CONFIRMATIONS: u32 = 10;
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
let scalar = Scalar::random(rng);
@ -11,8 +15,8 @@ pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
PrivateKey::from_scalar(scalar)
}
#[derive(Clone, Copy, Debug)]
pub struct PrivateViewKey(PrivateKey);
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
impl PrivateViewKey {
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
@ -50,7 +54,7 @@ impl From<PublicViewKey> for PublicKey {
#[derive(Clone, Copy, Debug)]
pub struct PublicViewKey(PublicKey);
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
pub struct Amount(u64);
impl Amount {
@ -66,15 +70,32 @@ impl Amount {
}
}
impl Add for Amount {
type Output = Amount;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for Amount {
type Output = Amount;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl From<Amount> for u64 {
fn from(from: Amount) -> u64 {
from.0
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TransferProof {
tx_hash: TxHash,
#[serde(with = "monero_private_key")]
tx_key: PrivateKey,
}
@ -91,7 +112,7 @@ impl TransferProof {
}
// TODO: add constructor/ change String to fixed length byte array
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TxHash(pub String);
impl From<TxHash> for String {
@ -107,18 +128,26 @@ pub trait Transfer {
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> Result<(TransferProof, Amount)>;
) -> anyhow::Result<(TransferProof, Amount)>;
}
#[async_trait]
pub trait CheckTransfer {
async fn check_transfer(
pub trait WatchForTransfer {
async fn watch_for_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
amount: Amount,
) -> Result<()>;
expected_confirmations: u32,
) -> Result<(), InsufficientFunds>;
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
pub struct InsufficientFunds {
pub expected: Amount,
pub actual: Amount,
}
#[async_trait]
@ -127,5 +156,5 @@ pub trait CreateWalletForOutput {
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> Result<()>;
) -> anyhow::Result<()>;
}

@ -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);
}
}

@ -1,145 +1,4 @@
use crate::harness::wallet;
use bitcoin_harness::Bitcoind;
use harness::{
node::{AliceNode, BobNode},
transport::Transport,
};
use monero_harness::Monero;
use rand::rngs::OsRng;
use testcontainers::clients::Cli;
use tokio::sync::{
mpsc,
mpsc::{Receiver, Sender},
};
use xmr_btc::{alice, bitcoin, bob, monero};
mod harness;
const TEN_XMR: u64 = 10_000_000_000_000;
const RELATIVE_REFUND_TIMELOCK: u32 = 1;
const RELATIVE_PUNISH_TIMELOCK: u32 = 1;
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
let _ = bitcoind.init(5).await;
bitcoind
}
pub struct InitialBalances {
alice_xmr: u64,
alice_btc: bitcoin::Amount,
bob_xmr: u64,
bob_btc: bitcoin::Amount,
}
pub struct SwapAmounts {
xmr: monero::Amount,
btc: bitcoin::Amount,
}
pub fn init_alice_and_bob_transports() -> (
Transport<alice::Message, bob::Message>,
Transport<bob::Message, alice::Message>,
) {
let (a_sender, b_receiver): (Sender<alice::Message>, Receiver<alice::Message>) =
mpsc::channel(5);
let (b_sender, a_receiver): (Sender<bob::Message>, Receiver<bob::Message>) = mpsc::channel(5);
let a_transport = Transport {
sender: a_sender,
receiver: a_receiver,
};
let b_transport = Transport {
sender: b_sender,
receiver: b_receiver,
};
(a_transport, b_transport)
}
pub async fn init_test<'a>(
monero: &'a Monero<'a>,
bitcoind: &Bitcoind<'_>,
) -> (
alice::State0,
bob::State0,
AliceNode<'a>,
BobNode<'a>,
InitialBalances,
SwapAmounts,
) {
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let swap_amounts = SwapAmounts {
xmr: xmr_amount,
btc: btc_amount,
};
let fund_alice = TEN_XMR;
let fund_bob = 0;
monero.init(fund_alice, fund_bob).await.unwrap();
let alice_monero_wallet = wallet::monero::AliceWallet(&monero);
let bob_monero_wallet = wallet::monero::BobWallet(&monero);
let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let (alice_transport, bob_transport) = init_alice_and_bob_transports();
let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet);
let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet);
let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap();
let alice_initial_xmr_balance = alice.monero_wallet.0.get_balance_alice().await.unwrap();
let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance_bob().await.unwrap();
let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
RELATIVE_REFUND_TIMELOCK,
RELATIVE_PUNISH_TIMELOCK,
redeem_address.clone(),
punish_address.clone(),
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
RELATIVE_REFUND_TIMELOCK,
RELATIVE_PUNISH_TIMELOCK,
refund_address,
);
let initial_balances = InitialBalances {
alice_xmr: alice_initial_xmr_balance,
alice_btc: alice_initial_btc_balance,
bob_xmr: bob_initial_xmr_balance,
bob_btc: bob_initial_btc_balance,
};
(
alice_state0,
bob_state0,
alice,
bob,
initial_balances,
swap_amounts,
)
}
pub mod harness;
mod tests {
// NOTE: For some reason running these tests overflows the stack. In order to
@ -149,13 +8,17 @@ mod tests {
use crate::{
harness,
harness::node::{run_alice_until, run_bob_until},
init_bitcoind, init_test,
harness::{
init_bitcoind, init_test,
node::{run_alice_until, run_bob_until},
ALICE_TEST_DB_FOLDER, BOB_TEST_DB_FOLDER,
},
};
use futures::future;
use monero_harness::Monero;
use rand::rngs::OsRng;
use std::convert::TryInto;
use std::{convert::TryInto, path::Path};
use testcontainers::clients::Cli;
use tracing_subscriber::util::SubscriberInitExt;
use xmr_btc::{
@ -171,7 +34,7 @@ mod tests {
.set_default();
let cli = Cli::default();
let monero = Monero::new(&cli);
let (monero, _container) = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
let (
@ -181,7 +44,7 @@ mod tests {
mut bob_node,
initial_balances,
swap_amounts,
) = init_test(&monero, &bitcoind).await;
) = init_test(&monero, &bitcoind, None, None).await;
let (alice_state, bob_state) = future::try_join(
run_alice_until(
@ -212,21 +75,11 @@ mod tests {
.await
.unwrap();
let alice_final_xmr_balance = alice_node
.monero_wallet
.0
.get_balance_alice()
.await
.unwrap();
let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap();
bob_node
.monero_wallet
.0
.wait_for_bob_wallet_block_height()
.await
.unwrap();
monero.wait_for_bob_wallet_block_height().await.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap();
assert_eq!(
alice_final_btc_balance,
@ -240,13 +93,11 @@ mod tests {
assert_eq!(
alice_final_xmr_balance,
initial_balances.alice_xmr
- u64::from(swap_amounts.xmr)
- u64::from(alice_state6.lock_xmr_fee())
initial_balances.alice_xmr - swap_amounts.xmr - alice_state6.lock_xmr_fee()
);
assert_eq!(
bob_final_xmr_balance,
initial_balances.bob_xmr + u64::from(swap_amounts.xmr)
initial_balances.bob_xmr + swap_amounts.xmr
);
}
@ -257,7 +108,7 @@ mod tests {
.set_default();
let cli = Cli::default();
let monero = Monero::new(&cli);
let (monero, _container) = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
let (
@ -267,7 +118,7 @@ mod tests {
mut bob_node,
initial_balances,
swap_amounts,
) = init_test(&monero, &bitcoind).await;
) = init_test(&monero, &bitcoind, None, None).await;
let (alice_state, bob_state) = future::try_join(
run_alice_until(
@ -309,19 +160,9 @@ mod tests {
.await
.unwrap();
alice_node
.monero_wallet
.0
.wait_for_alice_wallet_block_height()
.await
.unwrap();
let alice_final_xmr_balance = alice_node
.monero_wallet
.0
.get_balance_alice()
.await
.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap();
monero.wait_for_alice_wallet_block_height().await.unwrap();
let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap();
assert_eq!(alice_final_btc_balance, initial_balances.alice_btc);
assert_eq!(
@ -332,7 +173,7 @@ mod tests {
// Because we create a new wallet when claiming Monero, we can only assert on
// this new wallet owning all of `xmr_amount` after refund
assert_eq!(alice_final_xmr_balance, u64::from(swap_amounts.xmr));
assert_eq!(alice_final_xmr_balance, swap_amounts.xmr);
assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr);
}
@ -343,7 +184,7 @@ mod tests {
.set_default();
let cli = Cli::default();
let monero = Monero::new(&cli);
let (monero, _container) = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
let (
@ -353,7 +194,7 @@ mod tests {
mut bob_node,
initial_balances,
swap_amounts,
) = init_test(&monero, &bitcoind).await;
) = init_test(&monero, &bitcoind, None, None).await;
let (alice_state, bob_state) = future::try_join(
run_alice_until(
@ -400,4 +241,116 @@ mod tests {
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
);
}
#[tokio::test]
async fn recover_protocol_state_from_db() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let (monero, _container) = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
let alice_db = harness::storage::Database::open(Path::new(ALICE_TEST_DB_FOLDER)).unwrap();
let bob_db = harness::storage::Database::open(Path::new(BOB_TEST_DB_FOLDER)).unwrap();
let (
alice_state0,
bob_state0,
mut alice_node,
mut bob_node,
initial_balances,
swap_amounts,
) = init_test(&monero, &bitcoind, None, None).await;
{
let (alice_state, bob_state) = future::try_join(
run_alice_until(
&mut alice_node,
alice_state0.into(),
harness::alice::is_state5,
&mut OsRng,
),
run_bob_until(
&mut bob_node,
bob_state0.into(),
harness::bob::is_state3,
&mut OsRng,
),
)
.await
.unwrap();
let alice_state5: alice::State5 = alice_state.try_into().unwrap();
let bob_state3: bob::State3 = bob_state.try_into().unwrap();
// save state to db
alice_db.insert_latest_state(&alice_state5).await.unwrap();
bob_db.insert_latest_state(&bob_state3).await.unwrap();
};
let (alice_state6, bob_state5) = {
// recover state from db
let alice_state5: alice::State5 = alice_db.get_latest_state().unwrap();
let bob_state3: bob::State3 = bob_db.get_latest_state().unwrap();
let (alice_state, bob_state) = future::try_join(
run_alice_until(
&mut alice_node,
alice_state5.into(),
harness::alice::is_state6,
&mut OsRng,
),
run_bob_until(
&mut bob_node,
bob_state3.into(),
harness::bob::is_state5,
&mut OsRng,
),
)
.await
.unwrap();
let alice_state: alice::State6 = alice_state.try_into().unwrap();
let bob_state: bob::State5 = bob_state.try_into().unwrap();
(alice_state, bob_state)
};
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap();
let lock_tx_bitcoin_fee = bob_node
.bitcoin_wallet
.transaction_fee(bob_state5.tx_lock_id())
.await
.unwrap();
let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap();
monero.wait_for_bob_wallet_block_height().await.unwrap();
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap();
assert_eq!(
alice_final_btc_balance,
initial_balances.alice_btc + swap_amounts.btc
- bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
);
assert_eq!(
alice_final_xmr_balance,
initial_balances.alice_xmr.as_piconero()
- swap_amounts.xmr.as_piconero()
- alice_state6.lock_xmr_fee().as_piconero()
);
assert_eq!(
bob_final_xmr_balance,
initial_balances.bob_xmr.as_piconero() + swap_amounts.xmr.as_piconero()
);
}
}

@ -1,10 +1,15 @@
pub mod node;
pub mod storage;
pub mod transport;
pub mod wallet;
pub mod bob {
use xmr_btc::bob::State;
pub fn is_state2(state: &State) -> bool {
matches!(state, State::State2 { .. })
}
// TODO: use macro or generics
pub fn is_state5(state: &State) -> bool {
matches!(state, State::State5 { .. })
@ -19,6 +24,10 @@ pub mod bob {
pub mod alice {
use xmr_btc::alice::State;
pub fn is_state3(state: &State) -> bool {
matches!(state, State::State3 { .. })
}
// TODO: use macro or generics
pub fn is_state4(state: &State) -> bool {
matches!(state, State::State4 { .. })
@ -34,3 +43,150 @@ pub mod alice {
matches!(state, State::State6 { .. })
}
}
use bitcoin_harness::Bitcoind;
use monero_harness::Monero;
use node::{AliceNode, BobNode};
use rand::rngs::OsRng;
use testcontainers::clients::Cli;
use tokio::sync::{
mpsc,
mpsc::{Receiver, Sender},
};
use transport::Transport;
use xmr_btc::{bitcoin, monero};
const TEN_XMR: u64 = 10_000_000_000_000;
const RELATIVE_REFUND_TIMELOCK: u32 = 1;
const RELATIVE_PUNISH_TIMELOCK: u32 = 1;
pub const ALICE_TEST_DB_FOLDER: &str = "../target/e2e-test-alice-recover";
pub const BOB_TEST_DB_FOLDER: &str = "../target/e2e-test-bob-recover";
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
let _ = bitcoind.init(5).await;
bitcoind
}
pub struct InitialBalances {
pub alice_xmr: monero::Amount,
pub alice_btc: bitcoin::Amount,
pub bob_xmr: monero::Amount,
pub bob_btc: bitcoin::Amount,
}
pub struct SwapAmounts {
pub xmr: monero::Amount,
pub btc: bitcoin::Amount,
}
pub fn init_alice_and_bob_transports() -> (
Transport<xmr_btc::alice::Message, xmr_btc::bob::Message>,
Transport<xmr_btc::bob::Message, xmr_btc::alice::Message>,
) {
let (a_sender, b_receiver): (
Sender<xmr_btc::alice::Message>,
Receiver<xmr_btc::alice::Message>,
) = mpsc::channel(5);
let (b_sender, a_receiver): (
Sender<xmr_btc::bob::Message>,
Receiver<xmr_btc::bob::Message>,
) = mpsc::channel(5);
let a_transport = Transport {
sender: a_sender,
receiver: a_receiver,
};
let b_transport = Transport {
sender: b_sender,
receiver: b_receiver,
};
(a_transport, b_transport)
}
pub async fn init_test(
monero: &Monero,
bitcoind: &Bitcoind<'_>,
refund_timelock: Option<u32>,
punish_timelock: Option<u32>,
) -> (
xmr_btc::alice::State0,
xmr_btc::bob::State0,
AliceNode,
BobNode,
InitialBalances,
SwapAmounts,
) {
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let swap_amounts = SwapAmounts {
xmr: xmr_amount,
btc: btc_amount,
};
let fund_alice = TEN_XMR;
let fund_bob = 0;
monero.init(fund_alice, fund_bob).await.unwrap();
let alice_monero_wallet = wallet::monero::Wallet(monero.alice_wallet_rpc_client());
let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client());
let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let (alice_transport, bob_transport) = init_alice_and_bob_transports();
let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet);
let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet);
let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap();
let alice_initial_xmr_balance = alice.monero_wallet.get_balance().await.unwrap();
let bob_initial_xmr_balance = bob.monero_wallet.get_balance().await.unwrap();
let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
let alice_state0 = xmr_btc::alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
redeem_address.clone(),
punish_address.clone(),
);
let bob_state0 = xmr_btc::bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
refund_address,
);
let initial_balances = InitialBalances {
alice_xmr: alice_initial_xmr_balance,
alice_btc: alice_initial_btc_balance,
bob_xmr: bob_initial_xmr_balance,
bob_btc: bob_initial_btc_balance,
};
(
alice_state0,
bob_state0,
alice,
bob,
initial_balances,
swap_amounts,
)
}

@ -5,18 +5,18 @@ use xmr_btc::{alice, bob};
// TODO: merge this with bob node
// This struct is responsible for I/O
pub struct AliceNode<'a> {
pub struct AliceNode {
transport: Transport<alice::Message, bob::Message>,
pub bitcoin_wallet: wallet::bitcoin::Wallet,
pub monero_wallet: wallet::monero::AliceWallet<'a>,
pub monero_wallet: wallet::monero::Wallet,
}
impl<'a> AliceNode<'a> {
impl AliceNode {
pub fn new(
transport: Transport<alice::Message, bob::Message>,
bitcoin_wallet: wallet::bitcoin::Wallet,
monero_wallet: wallet::monero::AliceWallet<'a>,
) -> AliceNode<'a> {
monero_wallet: wallet::monero::Wallet,
) -> AliceNode {
Self {
transport,
bitcoin_wallet,
@ -25,8 +25,8 @@ impl<'a> AliceNode<'a> {
}
}
pub async fn run_alice_until<'a, R: RngCore + CryptoRng>(
alice: &mut AliceNode<'a>,
pub async fn run_alice_until<R: RngCore + CryptoRng>(
alice: &mut AliceNode,
initial_state: alice::State,
is_state: fn(&alice::State) -> bool,
rng: &mut R,
@ -49,18 +49,18 @@ pub async fn run_alice_until<'a, R: RngCore + CryptoRng>(
// TODO: merge this with alice node
// This struct is responsible for I/O
pub struct BobNode<'a> {
pub struct BobNode {
transport: Transport<bob::Message, alice::Message>,
pub bitcoin_wallet: wallet::bitcoin::Wallet,
pub monero_wallet: wallet::monero::BobWallet<'a>,
pub monero_wallet: wallet::monero::Wallet,
}
impl<'a> BobNode<'a> {
impl BobNode {
pub fn new(
transport: Transport<bob::Message, alice::Message>,
bitcoin_wallet: wallet::bitcoin::Wallet,
monero_wallet: wallet::monero::BobWallet<'a>,
) -> BobNode<'a> {
monero_wallet: wallet::monero::Wallet,
) -> BobNode {
Self {
transport,
bitcoin_wallet,
@ -69,8 +69,8 @@ impl<'a> BobNode<'a> {
}
}
pub async fn run_bob_until<'a, R: RngCore + CryptoRng>(
bob: &mut BobNode<'a>,
pub async fn run_bob_until<R: RngCore + CryptoRng>(
bob: &mut BobNode,
initial_state: bob::State,
is_state: fn(&bob::State) -> bool,
rng: &mut R,

@ -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);
}
}

@ -1,12 +1,16 @@
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind};
use reqwest::Url;
use std::time::Duration;
use tokio::time;
use xmr_btc::bitcoin::{
BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction,
use xmr_btc::{
bitcoin::{
BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction,
},
BlockHeight, TransactionBlockHeight,
};
#[derive(Debug)]
@ -108,12 +112,47 @@ impl BroadcastSignedTransaction for Wallet {
#[async_trait]
impl WatchForRawTransaction for Wallet {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
loop {
if let Ok(tx) = self.0.get_raw_transaction(txid).await {
return Ok(tx);
}
time::delay_for(Duration::from_millis(200)).await;
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
(|| async { Ok(self.0.get_raw_transaction(txid).await?) })
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried")
}
}
#[async_trait]
impl BlockHeight for Wallet {
async fn block_height(&self) -> u32 {
(|| async { Ok(self.0.block_height().await?) })
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried")
}
}
#[async_trait]
impl TransactionBlockHeight for Wallet {
async fn transaction_block_height(&self, txid: Txid) -> u32 {
#[derive(Debug)]
enum Error {
Io,
NotYetMined,
}
(|| async {
let block_height = self
.0
.transaction_block_height(txid)
.await
.map_err(|_| backoff::Error::Transient(Error::Io))?;
let block_height =
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
Result::<_, backoff::Error<Error>>::Ok(block_height)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried")
}
}

@ -1,18 +1,27 @@
use anyhow::{bail, Result};
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use monero::{Address, Network, PrivateKey};
use monero_harness::Monero;
use std::str::FromStr;
use monero_harness::rpc::wallet;
use std::{str::FromStr, time::Duration};
use xmr_btc::monero::{
Amount, CheckTransfer, CreateWalletForOutput, PrivateViewKey, PublicKey, PublicViewKey,
Transfer, TransferProof, TxHash,
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
Transfer, TransferProof, TxHash, WatchForTransfer,
};
#[derive(Debug)]
pub struct AliceWallet<'c>(pub &'c Monero<'c>);
pub struct Wallet(pub wallet::Client);
impl Wallet {
/// Get the balance of the primary account.
pub async fn get_balance(&self) -> Result<Amount> {
let amount = self.0.get_balance(0).await?;
Ok(Amount::from_piconero(amount))
}
}
#[async_trait]
impl Transfer for AliceWallet<'_> {
impl Transfer for Wallet {
async fn transfer(
&self,
public_spend_key: PublicKey,
@ -24,7 +33,7 @@ impl Transfer for AliceWallet<'_> {
let res = self
.0
.transfer_from_alice(amount.as_piconero(), &destination_address.to_string())
.transfer(0, amount.as_piconero(), &destination_address.to_string())
.await?;
let tx_hash = TxHash(res.tx_hash);
@ -37,7 +46,7 @@ impl Transfer for AliceWallet<'_> {
}
#[async_trait]
impl CreateWalletForOutput for AliceWallet<'_> {
impl CreateWalletForOutput for Wallet {
async fn create_and_load_wallet_for_output(
&self,
private_spend_key: PrivateKey,
@ -50,7 +59,6 @@ impl CreateWalletForOutput for AliceWallet<'_> {
let _ = self
.0
.alice_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
@ -62,63 +70,57 @@ impl CreateWalletForOutput for AliceWallet<'_> {
}
}
#[derive(Debug)]
pub struct BobWallet<'c>(pub &'c Monero<'c>);
#[async_trait]
impl CheckTransfer for BobWallet<'_> {
async fn check_transfer(
impl WatchForTransfer for Wallet {
async fn watch_for_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
amount: Amount,
) -> Result<()> {
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
let cli = self.0.bob_wallet_rpc_client();
let res = cli
.check_tx_key(
&String::from(transfer_proof.tx_hash()),
&transfer_proof.tx_key().to_string(),
&address.to_string(),
)
.await?;
if res.received != u64::from(amount) {
bail!(
"tx_lock doesn't pay enough: expected {:?}, got {:?}",
res.received,
amount
)
expected_amount: Amount,
expected_confirmations: u32,
) -> Result<(), InsufficientFunds> {
enum Error {
TxNotFound,
InsufficientConfirmations,
InsufficientFunds { expected: Amount, actual: Amount },
}
Ok(())
}
}
#[async_trait]
impl CreateWalletForOutput for BobWallet<'_> {
async fn create_and_load_wallet_for_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> Result<()> {
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key);
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
let _ = self
.0
.bob_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
let res = (|| async {
// NOTE: Currently, this is conflating IO errors with the transaction not being
// in the blockchain yet, or not having enough confirmations on it. All these
// errors warrant a retry, but the strategy should probably differ per case
let proof = self
.0
.check_tx_key(
&String::from(transfer_proof.tx_hash()),
&transfer_proof.tx_key().to_string(),
&address.to_string(),
)
.await
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
if proof.received != expected_amount.as_piconero() {
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
expected: expected_amount,
actual: Amount::from_piconero(proof.received),
}));
}
if proof.confirmations < expected_confirmations {
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
}
Ok(proof)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await;
if let Err(Error::InsufficientFunds { expected, actual }) = res {
return Err(InsufficientFunds { expected, actual });
};
Ok(())
}

@ -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…
Cancel
Save