mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-01-11 15:39:37 -05:00
Merge pull request #12 from comit-network/on-chain-protocol
This commit is contained in:
commit
71e09413aa
@ -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 {
|
||||
docker,
|
||||
monerod_rpc_port,
|
||||
miner_wallet_rpc_port,
|
||||
alice_wallet_rpc_port,
|
||||
bob_wallet_rpc_port,
|
||||
}
|
||||
},
|
||||
docker,
|
||||
)
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
|
37
swap/Cargo.toml
Normal file
37
swap/Cargo.toml
Normal file
@ -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" }
|
259
swap/src/alice.rs
Normal file
259
swap/src/alice.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
113
swap/src/alice/amounts.rs
Normal file
113
swap/src/alice/amounts.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
swap/src/alice/message0.rs
Normal file
114
swap/src/alice/message0.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
swap/src/alice/message1.rs
Normal file
102
swap/src/alice/message1.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
99
swap/src/bitcoin.rs
Normal file
99
swap/src/bitcoin.rs
Normal file
@ -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")
|
||||
}
|
||||
}
|
230
swap/src/bob.rs
Normal file
230
swap/src/bob.rs
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
98
swap/src/bob/amounts.rs
Normal file
98
swap/src/bob/amounts.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
swap/src/bob/message0.rs
Normal file
92
swap/src/bob/message0.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
swap/src/bob/message1.rs
Normal file
92
swap/src/bob/message1.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
swap/src/cli.rs
Normal file
14
swap/src/cli.rs
Normal file
@ -0,0 +1,14 @@
|
||||
#[derive(structopt::StructOpt, Debug)]
|
||||
pub struct Options {
|
||||
/// Run the swap as Alice.
|
||||
#[structopt(long = "as-alice")]
|
||||
pub as_alice: bool,
|
||||
|
||||
/// Run the swap as Bob and try to swap this many XMR (in piconero).
|
||||
#[structopt(long = "picos")]
|
||||
pub piconeros: Option<u64>,
|
||||
|
||||
/// Run the swap as Bob and try to swap this many BTC (in satoshi).
|
||||
#[structopt(long = "sats")]
|
||||
pub satoshis: Option<u64>,
|
||||
}
|
50
swap/src/lib.rs
Normal file
50
swap/src/lib.rs
Normal file
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
166
swap/src/main.rs
Normal file
166
swap/src/main.rs
Normal file
@ -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")
|
||||
}
|
18
swap/src/network.rs
Normal file
18
swap/src/network.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use futures::prelude::*;
|
||||
use libp2p::core::Executor;
|
||||
use std::pin::Pin;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
pub mod peer_tracker;
|
||||
pub mod request_response;
|
||||
pub mod transport;
|
||||
|
||||
pub struct TokioExecutor {
|
||||
pub handle: Handle,
|
||||
}
|
||||
|
||||
impl Executor for TokioExecutor {
|
||||
fn exec(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
|
||||
let _ = self.handle.spawn(future);
|
||||
}
|
||||
}
|
106
swap/src/network/peer_tracker.rs
Normal file
106
swap/src/network/peer_tracker.rs
Normal file
@ -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
|
||||
}
|
||||
}
|
116
swap/src/network/request_response.rs
Normal file
116
swap/src/network/request_response.rs
Normal file
@ -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(())
|
||||
}
|
||||
}
|
44
swap/src/network/transport.rs
Normal file
44
swap/src/network/transport.rs
Normal file
@ -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)>;
|
25
swap/src/trace.rs
Normal file
25
swap/src/trace.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use atty::{self, Stream};
|
||||
use log::LevelFilter;
|
||||
use tracing::{info, subscriber};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
pub fn init_tracing(level: log::LevelFilter) -> anyhow::Result<()> {
|
||||
if level == LevelFilter::Off {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Upstream log filter.
|
||||
LogTracer::init_with_filter(LevelFilter::Debug)?;
|
||||
|
||||
let is_terminal = atty::is(Stream::Stdout);
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_env_filter(format!("swap={}", level))
|
||||
.with_ansi(is_terminal)
|
||||
.finish();
|
||||
|
||||
subscriber::set_global_default(subscriber)?;
|
||||
info!("Initialized tracing with level: {}", level);
|
||||
|
||||
Ok(())
|
||||
}
|
@ -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<()>;
|
||||
}
|
||||
|
231
xmr-btc/src/serde.rs
Normal file
231
xmr-btc/src/serde.rs
Normal file
@ -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},
|
||||
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,
|
||||
|
159
xmr-btc/tests/harness/storage.rs
Normal file
159
xmr-btc/tests/harness/storage.rs
Normal file
@ -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::{
|
||||
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<()> {
|
||||
expected_amount: Amount,
|
||||
expected_confirmations: u32,
|
||||
) -> Result<(), InsufficientFunds> {
|
||||
enum Error {
|
||||
TxNotFound,
|
||||
InsufficientConfirmations,
|
||||
InsufficientFunds { expected: Amount, actual: Amount },
|
||||
}
|
||||
|
||||
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
|
||||
|
||||
let cli = self.0.bob_wallet_rpc_client();
|
||||
|
||||
let res = cli
|
||||
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?;
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
|
||||
|
||||
if res.received != u64::from(amount) {
|
||||
bail!(
|
||||
"tx_lock doesn't pay enough: expected {:?}, got {:?}",
|
||||
res.received,
|
||||
amount
|
||||
)
|
||||
if proof.received != expected_amount.as_piconero() {
|
||||
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
|
||||
expected: expected_amount,
|
||||
actual: Amount::from_piconero(proof.received),
|
||||
}));
|
||||
}
|
||||
|
||||
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 _ = 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?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
251
xmr-btc/tests/on_chain.rs
Normal file
251
xmr-btc/tests/on_chain.rs
Normal file
@ -0,0 +1,251 @@
|
||||
pub mod harness;
|
||||
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::{
|
||||
channel::mpsc::{channel, Receiver, Sender},
|
||||
future::try_join,
|
||||
SinkExt, StreamExt,
|
||||
};
|
||||
use genawaiter::GeneratorState;
|
||||
use harness::{
|
||||
init_bitcoind, init_test,
|
||||
node::{run_alice_until, run_bob_until},
|
||||
};
|
||||
use monero_harness::Monero;
|
||||
use rand::rngs::OsRng;
|
||||
use testcontainers::clients::Cli;
|
||||
use tracing::info;
|
||||
use xmr_btc::{
|
||||
action_generator_alice, action_generator_bob, alice,
|
||||
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
||||
bob,
|
||||
monero::{CreateWalletForOutput, Transfer, TransferProof},
|
||||
AliceAction, BobAction, ReceiveBitcoinRedeemEncsig, ReceiveTransferProof,
|
||||
};
|
||||
|
||||
type AliceNetwork = Network<EncryptedSignature>;
|
||||
type BobNetwork = Network<TransferProof>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Network<M> {
|
||||
// TODO: It is weird to use mpsc's in a situation where only one message is expected, but the
|
||||
// ownership rules of Rust are making this painful
|
||||
pub receiver: Receiver<M>,
|
||||
}
|
||||
|
||||
impl<M> Network<M> {
|
||||
pub fn new() -> (Network<M>, Sender<M>) {
|
||||
let (sender, receiver) = channel(1);
|
||||
|
||||
(Self { receiver }, sender)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ReceiveTransferProof for BobNetwork {
|
||||
async fn receive_transfer_proof(&mut self) -> TransferProof {
|
||||
self.receiver.next().await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ReceiveBitcoinRedeemEncsig for AliceNetwork {
|
||||
async fn receive_bitcoin_redeem_encsig(&mut self) -> EncryptedSignature {
|
||||
self.receiver.next().await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
async fn swap_as_alice(
|
||||
network: AliceNetwork,
|
||||
// FIXME: It would be more intuitive to have a single network/transport struct instead of
|
||||
// splitting into two, but Rust ownership rules make this tedious
|
||||
mut sender: Sender<TransferProof>,
|
||||
monero_wallet: &harness::wallet::monero::Wallet,
|
||||
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
|
||||
state: alice::State3,
|
||||
) -> Result<()> {
|
||||
let mut action_generator = action_generator_alice(network, bitcoin_wallet.clone(), state);
|
||||
|
||||
loop {
|
||||
let state = action_generator.async_resume().await;
|
||||
|
||||
info!("resumed execution of generator, got: {:?}", state);
|
||||
|
||||
match state {
|
||||
GeneratorState::Yielded(AliceAction::LockXmr {
|
||||
amount,
|
||||
public_spend_key,
|
||||
public_view_key,
|
||||
}) => {
|
||||
let (transfer_proof, _) = monero_wallet
|
||||
.transfer(public_spend_key, public_view_key, amount)
|
||||
.await?;
|
||||
|
||||
sender.send(transfer_proof).await.unwrap();
|
||||
}
|
||||
GeneratorState::Yielded(AliceAction::RedeemBtc(tx))
|
||||
| GeneratorState::Yielded(AliceAction::CancelBtc(tx))
|
||||
| GeneratorState::Yielded(AliceAction::PunishBtc(tx)) => {
|
||||
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
||||
}
|
||||
GeneratorState::Yielded(AliceAction::CreateMoneroWalletForOutput {
|
||||
spend_key,
|
||||
view_key,
|
||||
}) => {
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||
.await?;
|
||||
}
|
||||
GeneratorState::Complete(()) => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn swap_as_bob(
|
||||
network: BobNetwork,
|
||||
mut sender: Sender<EncryptedSignature>,
|
||||
monero_wallet: Arc<harness::wallet::monero::Wallet>,
|
||||
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
|
||||
state: bob::State2,
|
||||
) -> Result<()> {
|
||||
let mut action_generator = action_generator_bob(
|
||||
network,
|
||||
monero_wallet.clone(),
|
||||
bitcoin_wallet.clone(),
|
||||
state,
|
||||
);
|
||||
|
||||
loop {
|
||||
let state = action_generator.async_resume().await;
|
||||
|
||||
info!("resumed execution of generator, got: {:?}", state);
|
||||
|
||||
match state {
|
||||
GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => {
|
||||
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_lock)
|
||||
.await?;
|
||||
}
|
||||
GeneratorState::Yielded(BobAction::SendBitcoinRedeemEncsig(tx_redeem_encsig)) => {
|
||||
sender.send(tx_redeem_encsig).await.unwrap();
|
||||
}
|
||||
GeneratorState::Yielded(BobAction::CreateMoneroWalletForOutput {
|
||||
spend_key,
|
||||
view_key,
|
||||
}) => {
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||
.await?;
|
||||
}
|
||||
GeneratorState::Yielded(BobAction::CancelBitcoin(tx_cancel)) => {
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
GeneratorState::Yielded(BobAction::RefundBitcoin(tx_refund)) => {
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_refund)
|
||||
.await?;
|
||||
}
|
||||
GeneratorState::Complete(()) => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: For some reason running these tests overflows the stack. In order to
|
||||
// mitigate this run them with:
|
||||
//
|
||||
// RUST_MIN_STACK=100000000 cargo test
|
||||
|
||||
#[tokio::test]
|
||||
async fn on_chain_happy_path() {
|
||||
let cli = Cli::default();
|
||||
let (monero, _container) = Monero::new(&cli);
|
||||
let bitcoind = init_bitcoind(&cli).await;
|
||||
|
||||
let (alice_state0, bob_state0, mut alice_node, mut bob_node, initial_balances, swap_amounts) =
|
||||
init_test(&monero, &bitcoind, Some(100), Some(100)).await;
|
||||
|
||||
// run the handshake as part of the setup
|
||||
let (alice_state, bob_state) = try_join(
|
||||
run_alice_until(
|
||||
&mut alice_node,
|
||||
alice_state0.into(),
|
||||
harness::alice::is_state3,
|
||||
&mut OsRng,
|
||||
),
|
||||
run_bob_until(
|
||||
&mut bob_node,
|
||||
bob_state0.into(),
|
||||
harness::bob::is_state2,
|
||||
&mut OsRng,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let alice: alice::State3 = alice_state.try_into().unwrap();
|
||||
let bob: bob::State2 = bob_state.try_into().unwrap();
|
||||
let tx_lock_txid = bob.tx_lock.txid();
|
||||
|
||||
let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet);
|
||||
let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet);
|
||||
let alice_monero_wallet = Arc::new(alice_node.monero_wallet);
|
||||
let bob_monero_wallet = Arc::new(bob_node.monero_wallet);
|
||||
|
||||
let (alice_network, bob_sender) = Network::<EncryptedSignature>::new();
|
||||
let (bob_network, alice_sender) = Network::<TransferProof>::new();
|
||||
|
||||
try_join(
|
||||
swap_as_alice(
|
||||
alice_network,
|
||||
alice_sender,
|
||||
&alice_monero_wallet.clone(),
|
||||
alice_bitcoin_wallet.clone(),
|
||||
alice,
|
||||
),
|
||||
swap_as_bob(
|
||||
bob_network,
|
||||
bob_sender,
|
||||
bob_monero_wallet.clone(),
|
||||
bob_bitcoin_wallet.clone(),
|
||||
bob,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap();
|
||||
let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap();
|
||||
|
||||
let lock_tx_bitcoin_fee = bob_bitcoin_wallet
|
||||
.transaction_fee(tx_lock_txid)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap();
|
||||
|
||||
monero.wait_for_bob_wallet_block_height().await.unwrap();
|
||||
let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
alice_final_btc_balance,
|
||||
initial_balances.alice_btc + swap_amounts.btc
|
||||
- bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
|
||||
);
|
||||
|
||||
// Getting the Monero LockTx fee is tricky in a clean way, I think checking this
|
||||
// condition is sufficient
|
||||
assert!(alice_final_xmr_balance <= initial_balances.alice_xmr - swap_amounts.xmr,);
|
||||
assert_eq!(
|
||||
bob_final_xmr_balance,
|
||||
initial_balances.bob_xmr + swap_amounts.xmr
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user