mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Merge pull request #36 from comit-network/recovery
Recover from a failed swap
This commit is contained in:
commit
e7682a42a4
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs
|
# Created by https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust,clion+all,emacs
|
# Edit at https://www.toptal.com/developers/gitignore?templates=rust,clion+all,emacs
|
||||||
|
|
||||||
@ -154,4 +153,7 @@ Cargo.lock
|
|||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# sled DB directory generated during local development
|
||||||
|
.swap-db/
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs
|
# End of https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs
|
||||||
|
@ -14,19 +14,22 @@ base64 = "0.12"
|
|||||||
bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version.
|
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 = "3be644cd9512c157d3337a189298b8257ed54d04" }
|
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" }
|
||||||
derivative = "2"
|
derivative = "2"
|
||||||
|
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] }
|
||||||
futures = { version = "0.3", default-features = false }
|
futures = { version = "0.3", default-features = false }
|
||||||
genawaiter = "0.99.1"
|
genawaiter = "0.99.1"
|
||||||
libp2p = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] }
|
libp2p = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] }
|
||||||
libp2p-tokio-socks5 = "0.4"
|
libp2p-tokio-socks5 = "0.4"
|
||||||
log = { version = "0.4", features = ["serde"] }
|
log = { version = "0.4", features = ["serde"] }
|
||||||
monero = "0.9"
|
monero = { version = "0.9", features = ["serde_support"] }
|
||||||
monero-harness = { path = "../monero-harness" }
|
monero-harness = { path = "../monero-harness" }
|
||||||
|
prettytable-rs = "0.8"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
|
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_cbor = "0.11"
|
serde_cbor = "0.11"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
sha2 = "0.9"
|
||||||
sled = "0.34"
|
sled = "0.34"
|
||||||
structopt = "0.3"
|
structopt = "0.3"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
@ -39,6 +42,7 @@ tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] }
|
|||||||
tracing-log = "0.1"
|
tracing-log = "0.1"
|
||||||
tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] }
|
tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] }
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||||
void = "1"
|
void = "1"
|
||||||
xmr-btc = { path = "../xmr-btc" }
|
xmr-btc = { path = "../xmr-btc" }
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ use rand::rngs::OsRng;
|
|||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
mod amounts;
|
mod amounts;
|
||||||
mod message0;
|
mod message0;
|
||||||
@ -31,6 +32,8 @@ use crate::{
|
|||||||
transport::SwapTransport,
|
transport::SwapTransport,
|
||||||
TokioExecutor,
|
TokioExecutor,
|
||||||
},
|
},
|
||||||
|
state,
|
||||||
|
storage::Database,
|
||||||
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||||
};
|
};
|
||||||
use xmr_btc::{
|
use xmr_btc::{
|
||||||
@ -43,6 +46,7 @@ use xmr_btc::{
|
|||||||
pub async fn swap(
|
pub async fn swap(
|
||||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<monero::Wallet>,
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
|
db: Database,
|
||||||
listen: Multiaddr,
|
listen: Multiaddr,
|
||||||
transport: SwapTransport,
|
transport: SwapTransport,
|
||||||
behaviour: Alice,
|
behaviour: Alice,
|
||||||
@ -71,7 +75,7 @@ pub async fn swap(
|
|||||||
// to `ConstantBackoff`.
|
// to `ConstantBackoff`.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ReceiveBitcoinRedeemEncsig for Network {
|
impl ReceiveBitcoinRedeemEncsig for Network {
|
||||||
async fn receive_bitcoin_redeem_encsig(&mut self) -> xmr_btc::bitcoin::EncryptedSignature {
|
async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct UnexpectedMessage;
|
struct UnexpectedMessage;
|
||||||
|
|
||||||
@ -173,6 +177,10 @@ pub async fn swap(
|
|||||||
other => panic!("Unexpected event: {:?}", other),
|
other => panic!("Unexpected event: {:?}", other),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let swap_id = Uuid::new_v4();
|
||||||
|
db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
info!("Handshake complete, we now have State3 for Alice.");
|
info!("Handshake complete, we now have State3 for Alice.");
|
||||||
|
|
||||||
let network = Arc::new(Mutex::new(Network {
|
let network = Arc::new(Mutex::new(Network {
|
||||||
@ -183,7 +191,7 @@ pub async fn swap(
|
|||||||
let mut action_generator = action_generator(
|
let mut action_generator = action_generator(
|
||||||
network.clone(),
|
network.clone(),
|
||||||
bitcoin_wallet.clone(),
|
bitcoin_wallet.clone(),
|
||||||
state3,
|
state3.clone(),
|
||||||
TX_LOCK_MINE_TIMEOUT,
|
TX_LOCK_MINE_TIMEOUT,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -198,33 +206,68 @@ pub async fn swap(
|
|||||||
public_spend_key,
|
public_spend_key,
|
||||||
public_view_key,
|
public_view_key,
|
||||||
}) => {
|
}) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let (transfer_proof, _) = monero_wallet
|
let (transfer_proof, _) = monero_wallet
|
||||||
.transfer(public_spend_key, public_view_key, amount)
|
.transfer(public_spend_key, public_view_key, amount)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut guard = network.as_ref().lock().await;
|
let mut guard = network.as_ref().lock().await;
|
||||||
guard.send_message2(transfer_proof).await;
|
guard.send_message2(transfer_proof).await;
|
||||||
info!("Sent transfer proof");
|
info!("Sent transfer proof");
|
||||||
}
|
}
|
||||||
|
|
||||||
GeneratorState::Yielded(Action::RedeemBtc(tx)) => {
|
GeneratorState::Yielded(Action::RedeemBtc(tx)) => {
|
||||||
|
db.insert_latest_state(
|
||||||
|
swap_id,
|
||||||
|
state::Alice::BtcRedeemable {
|
||||||
|
state: state3.clone(),
|
||||||
|
redeem_tx: tx.clone(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Yielded(Action::CancelBtc(tx)) => {
|
GeneratorState::Yielded(Action::CancelBtc(tx)) => {
|
||||||
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Yielded(Action::PunishBtc(tx)) => {
|
GeneratorState::Yielded(Action::PunishBtc(tx)) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Alice::BtcPunishable(state3.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Yielded(Action::CreateMoneroWalletForOutput {
|
GeneratorState::Yielded(Action::CreateMoneroWalletForOutput {
|
||||||
spend_key,
|
spend_key,
|
||||||
view_key,
|
view_key,
|
||||||
}) => {
|
}) => {
|
||||||
|
db.insert_latest_state(
|
||||||
|
swap_id,
|
||||||
|
state::Alice::BtcRefunded {
|
||||||
|
state: state3.clone(),
|
||||||
|
spend_key,
|
||||||
|
view_key,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
monero_wallet
|
monero_wallet
|
||||||
.create_and_load_wallet_for_output(spend_key, view_key)
|
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Complete(()) => return Ok(()),
|
GeneratorState::Complete(()) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Alice::SwapComplete.into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,13 @@ use bitcoin_harness::bitcoind_rpc::PsbtBase64;
|
|||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use xmr_btc::bitcoin::{
|
use xmr_btc::bitcoin::{
|
||||||
Amount, BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock,
|
BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight,
|
||||||
TransactionBlockHeight, TxLock, Txid, WatchForRawTransaction,
|
WatchForRawTransaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
pub use xmr_btc::bitcoin::*;
|
||||||
|
|
||||||
// This is cut'n'paste from xmr_btc/tests/harness/wallet/bitcoin.rs
|
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Wallet(pub bitcoin_harness::Wallet);
|
pub struct Wallet(pub bitcoin_harness::Wallet);
|
||||||
|
@ -13,6 +13,7 @@ use rand::rngs::OsRng;
|
|||||||
use std::{process, sync::Arc, time::Duration};
|
use std::{process, sync::Arc, time::Duration};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
mod amounts;
|
mod amounts;
|
||||||
mod message0;
|
mod message0;
|
||||||
@ -22,14 +23,15 @@ mod message3;
|
|||||||
|
|
||||||
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
|
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
|
||||||
use crate::{
|
use crate::{
|
||||||
bitcoin,
|
bitcoin::{self, TX_LOCK_MINE_TIMEOUT},
|
||||||
bitcoin::TX_LOCK_MINE_TIMEOUT,
|
|
||||||
monero,
|
monero,
|
||||||
network::{
|
network::{
|
||||||
peer_tracker::{self, PeerTracker},
|
peer_tracker::{self, PeerTracker},
|
||||||
transport::SwapTransport,
|
transport::SwapTransport,
|
||||||
TokioExecutor,
|
TokioExecutor,
|
||||||
},
|
},
|
||||||
|
state,
|
||||||
|
storage::Database,
|
||||||
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||||
};
|
};
|
||||||
use xmr_btc::{
|
use xmr_btc::{
|
||||||
@ -43,6 +45,7 @@ use xmr_btc::{
|
|||||||
pub async fn swap(
|
pub async fn swap(
|
||||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<monero::Wallet>,
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
|
db: Database,
|
||||||
btc: u64,
|
btc: u64,
|
||||||
addr: Multiaddr,
|
addr: Multiaddr,
|
||||||
mut cmd_tx: Sender<Cmd>,
|
mut cmd_tx: Sender<Cmd>,
|
||||||
@ -141,6 +144,10 @@ pub async fn swap(
|
|||||||
other => panic!("unexpected event: {:?}", other),
|
other => panic!("unexpected event: {:?}", other),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let swap_id = Uuid::new_v4();
|
||||||
|
db.insert_latest_state(swap_id, state::Bob::Handshaken(state2.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
swarm.send_message2(alice.clone(), state2.next_message());
|
swarm.send_message2(alice.clone(), state2.next_message());
|
||||||
|
|
||||||
info!("Handshake complete");
|
info!("Handshake complete");
|
||||||
@ -151,7 +158,7 @@ pub async fn swap(
|
|||||||
network.clone(),
|
network.clone(),
|
||||||
monero_wallet.clone(),
|
monero_wallet.clone(),
|
||||||
bitcoin_wallet.clone(),
|
bitcoin_wallet.clone(),
|
||||||
state2,
|
state2.clone(),
|
||||||
TX_LOCK_MINE_TIMEOUT,
|
TX_LOCK_MINE_TIMEOUT,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -160,20 +167,29 @@ pub async fn swap(
|
|||||||
|
|
||||||
info!("Resumed execution of generator, got: {:?}", state);
|
info!("Resumed execution of generator, got: {:?}", state);
|
||||||
|
|
||||||
|
// TODO: Protect against transient errors
|
||||||
|
// TODO: Ignore transaction-already-in-block-chain errors
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
GeneratorState::Yielded(bob::Action::LockBtc(tx_lock)) => {
|
GeneratorState::Yielded(bob::Action::LockBtc(tx_lock)) => {
|
||||||
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
|
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
|
||||||
let _ = bitcoin_wallet
|
let _ = bitcoin_wallet
|
||||||
.broadcast_signed_transaction(signed_tx_lock)
|
.broadcast_signed_transaction(signed_tx_lock)
|
||||||
.await?;
|
.await?;
|
||||||
|
db.insert_latest_state(swap_id, state::Bob::BtcLocked(state2.clone()).into())
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Yielded(bob::Action::SendBtcRedeemEncsig(tx_redeem_encsig)) => {
|
GeneratorState::Yielded(bob::Action::SendBtcRedeemEncsig(tx_redeem_encsig)) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Bob::XmrLocked(state2.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut guard = network.as_ref().lock().await;
|
let mut guard = network.as_ref().lock().await;
|
||||||
guard.0.send_message3(alice.clone(), tx_redeem_encsig);
|
guard.0.send_message3(alice.clone(), tx_redeem_encsig);
|
||||||
info!("Sent Bitcoin redeem encsig");
|
info!("Sent Bitcoin redeem encsig");
|
||||||
|
|
||||||
// TODO: Does Bob need to wait for Alice to send an empty response, or can we
|
// FIXME: Having to wait for Alice's response here is a big problem, because
|
||||||
// just continue?
|
// we're stuck if she doesn't send her response back. I believe this is
|
||||||
|
// currently necessary, so we may have to rework this and/or how we use libp2p
|
||||||
match guard.0.next().shared().await {
|
match guard.0.next().shared().await {
|
||||||
OutEvent::Message3 => {
|
OutEvent::Message3 => {
|
||||||
debug!("Got Message3 empty response");
|
debug!("Got Message3 empty response");
|
||||||
@ -185,21 +201,35 @@ pub async fn swap(
|
|||||||
spend_key,
|
spend_key,
|
||||||
view_key,
|
view_key,
|
||||||
}) => {
|
}) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Bob::BtcRedeemed(state2.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
monero_wallet
|
monero_wallet
|
||||||
.create_and_load_wallet_for_output(spend_key, view_key)
|
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Yielded(bob::Action::CancelBtc(tx_cancel)) => {
|
GeneratorState::Yielded(bob::Action::CancelBtc(tx_cancel)) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let _ = bitcoin_wallet
|
let _ = bitcoin_wallet
|
||||||
.broadcast_signed_transaction(tx_cancel)
|
.broadcast_signed_transaction(tx_cancel)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Yielded(bob::Action::RefundBtc(tx_refund)) => {
|
GeneratorState::Yielded(bob::Action::RefundBtc(tx_refund)) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let _ = bitcoin_wallet
|
let _ = bitcoin_wallet
|
||||||
.broadcast_signed_transaction(tx_refund)
|
.broadcast_signed_transaction(tx_refund)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
GeneratorState::Complete(()) => return Ok(()),
|
GeneratorState::Complete(()) => {
|
||||||
|
db.insert_latest_state(swap_id, state::Bob::SwapComplete.into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use libp2p::core::Multiaddr;
|
use libp2p::core::Multiaddr;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
#[structopt(name = "xmr-btc-swap", about = "Trustless XMR BTC swaps")]
|
#[structopt(name = "xmr-btc-swap", about = "Trustless XMR BTC swaps")]
|
||||||
@ -8,7 +9,7 @@ pub enum Options {
|
|||||||
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
|
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
|
||||||
bitcoind_url: Url,
|
bitcoind_url: Url,
|
||||||
|
|
||||||
#[structopt(default_value = "http://127.0.0.1:18083", long = "monerod")]
|
#[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")]
|
||||||
monerod_url: Url,
|
monerod_url: Url,
|
||||||
|
|
||||||
#[structopt(default_value = "/ip4/127.0.0.1/tcp/9876", long = "listen-addr")]
|
#[structopt(default_value = "/ip4/127.0.0.1/tcp/9876", long = "listen-addr")]
|
||||||
@ -27,10 +28,21 @@ pub enum Options {
|
|||||||
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
|
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
|
||||||
bitcoind_url: Url,
|
bitcoind_url: Url,
|
||||||
|
|
||||||
#[structopt(default_value = "http://127.0.0.1:18083", long = "monerod")]
|
#[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")]
|
||||||
monerod_url: Url,
|
monerod_url: Url,
|
||||||
|
|
||||||
#[structopt(long = "tor")]
|
#[structopt(long = "tor")]
|
||||||
tor: bool,
|
tor: bool,
|
||||||
},
|
},
|
||||||
|
History,
|
||||||
|
Recover {
|
||||||
|
#[structopt(required = true)]
|
||||||
|
swap_id: Uuid,
|
||||||
|
|
||||||
|
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
|
||||||
|
bitcoind_url: Url,
|
||||||
|
|
||||||
|
#[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")]
|
||||||
|
monerod_url: Url,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ pub mod bitcoin;
|
|||||||
pub mod bob;
|
pub mod bob;
|
||||||
pub mod monero;
|
pub mod monero;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod recover;
|
||||||
|
pub mod state;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod tor;
|
pub mod tor;
|
||||||
|
|
||||||
@ -32,10 +34,10 @@ pub enum Rsp {
|
|||||||
pub struct SwapAmounts {
|
pub struct SwapAmounts {
|
||||||
/// Amount of BTC to swap.
|
/// Amount of BTC to swap.
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
pub btc: ::bitcoin::Amount,
|
pub btc: bitcoin::Amount,
|
||||||
/// Amount of XMR to swap.
|
/// Amount of XMR to swap.
|
||||||
#[serde(with = "xmr_btc::serde::monero_amount")]
|
#[serde(with = "xmr_btc::serde::monero_amount")]
|
||||||
pub xmr: xmr_btc::monero::Amount,
|
pub xmr: monero::Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Display in XMR and BTC (not picos and sats).
|
// TODO: Display in XMR and BTC (not picos and sats).
|
||||||
|
@ -16,23 +16,28 @@ use anyhow::Result;
|
|||||||
use futures::{channel::mpsc, StreamExt};
|
use futures::{channel::mpsc, StreamExt};
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
use prettytable::{row, Table};
|
||||||
use std::{io, io::Write, process, sync::Arc};
|
use std::{io, io::Write, process, sync::Arc};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use swap::{
|
use swap::{
|
||||||
alice,
|
alice::{self, Alice},
|
||||||
alice::Alice,
|
bitcoin,
|
||||||
bitcoin, bob,
|
bob::{self, Bob},
|
||||||
bob::Bob,
|
|
||||||
monero,
|
monero,
|
||||||
network::transport::{build, build_tor, SwapTransport},
|
network::transport::{build, build_tor, SwapTransport},
|
||||||
|
recover::recover,
|
||||||
Cmd, Rsp, SwapAmounts,
|
Cmd, Rsp, SwapAmounts,
|
||||||
};
|
};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate prettytable;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod trace;
|
mod trace;
|
||||||
|
|
||||||
use cli::Options;
|
use cli::Options;
|
||||||
|
use swap::storage::Database;
|
||||||
|
|
||||||
// TODO: Add root seed file instead of generating new seed each run.
|
// TODO: Add root seed file instead of generating new seed each run.
|
||||||
|
|
||||||
@ -42,6 +47,9 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
trace::init_tracing(LevelFilter::Debug)?;
|
trace::init_tracing(LevelFilter::Debug)?;
|
||||||
|
|
||||||
|
// This currently creates the directory if it's not there in the first place
|
||||||
|
let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap();
|
||||||
|
|
||||||
match opt {
|
match opt {
|
||||||
Options::Alice {
|
Options::Alice {
|
||||||
bitcoind_url,
|
bitcoind_url,
|
||||||
@ -83,6 +91,7 @@ async fn main() -> Result<()> {
|
|||||||
swap_as_alice(
|
swap_as_alice(
|
||||||
bitcoin_wallet,
|
bitcoin_wallet,
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
|
db,
|
||||||
listen_addr,
|
listen_addr,
|
||||||
transport,
|
transport,
|
||||||
behaviour,
|
behaviour,
|
||||||
@ -116,6 +125,7 @@ async fn main() -> Result<()> {
|
|||||||
swap_as_bob(
|
swap_as_bob(
|
||||||
bitcoin_wallet,
|
bitcoin_wallet,
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
|
db,
|
||||||
satoshis,
|
satoshis,
|
||||||
alice_addr,
|
alice_addr,
|
||||||
transport,
|
transport,
|
||||||
@ -123,6 +133,31 @@ async fn main() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
Options::History => {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table.add_row(row!["SWAP ID", "STATE"]);
|
||||||
|
|
||||||
|
for (swap_id, state) in db.all()? {
|
||||||
|
table.add_row(row![swap_id, state]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the table to stdout
|
||||||
|
table.printstd();
|
||||||
|
}
|
||||||
|
Options::Recover {
|
||||||
|
swap_id,
|
||||||
|
bitcoind_url,
|
||||||
|
monerod_url,
|
||||||
|
} => {
|
||||||
|
let state = db.get_state(swap_id)?;
|
||||||
|
let bitcoin_wallet = bitcoin::Wallet::new("bob", bitcoind_url)
|
||||||
|
.await
|
||||||
|
.expect("failed to create bitcoin wallet");
|
||||||
|
let monero_wallet = monero::Wallet::new(monerod_url);
|
||||||
|
|
||||||
|
recover(bitcoin_wallet, monero_wallet, state).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -149,16 +184,26 @@ async fn create_tor_service(
|
|||||||
async fn swap_as_alice(
|
async fn swap_as_alice(
|
||||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<swap::monero::Wallet>,
|
monero_wallet: Arc<swap::monero::Wallet>,
|
||||||
|
db: Database,
|
||||||
addr: Multiaddr,
|
addr: Multiaddr,
|
||||||
transport: SwapTransport,
|
transport: SwapTransport,
|
||||||
behaviour: Alice,
|
behaviour: Alice,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
alice::swap(bitcoin_wallet, monero_wallet, addr, transport, behaviour).await
|
alice::swap(
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
db,
|
||||||
|
addr,
|
||||||
|
transport,
|
||||||
|
behaviour,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn swap_as_bob(
|
async fn swap_as_bob(
|
||||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<swap::monero::Wallet>,
|
monero_wallet: Arc<swap::monero::Wallet>,
|
||||||
|
db: Database,
|
||||||
sats: u64,
|
sats: u64,
|
||||||
alice: Multiaddr,
|
alice: Multiaddr,
|
||||||
transport: SwapTransport,
|
transport: SwapTransport,
|
||||||
@ -169,6 +214,7 @@ async fn swap_as_bob(
|
|||||||
tokio::spawn(bob::swap(
|
tokio::spawn(bob::swap(
|
||||||
bitcoin_wallet,
|
bitcoin_wallet,
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
|
db,
|
||||||
sats,
|
sats,
|
||||||
alice,
|
alice,
|
||||||
cmd_tx,
|
cmd_tx,
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||||
use monero::{Address, Network, PrivateKey};
|
|
||||||
use monero_harness::rpc::wallet;
|
use monero_harness::rpc::wallet;
|
||||||
use std::{str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
pub use xmr_btc::monero::{
|
|
||||||
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
|
pub use xmr_btc::monero::*;
|
||||||
Transfer, TransferProof, TxHash, WatchForTransfer, *,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Wallet(pub wallet::Client);
|
pub struct Wallet(pub wallet::Client);
|
||||||
|
|
||||||
|
481
swap/src/recover.rs
Normal file
481
swap/src/recover.rs
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
//! This module is used to attempt to recover an unfinished swap.
|
||||||
|
//!
|
||||||
|
//! Recovery is only supported for certain states and the strategy followed is
|
||||||
|
//! to perform the simplest steps that require no further action from the
|
||||||
|
//! counterparty.
|
||||||
|
//!
|
||||||
|
//! The quality of this module is bad because there is a lot of code
|
||||||
|
//! duplication, both within the module and with respect to
|
||||||
|
//! `xmr_btc/src/{alice,bob}.rs`. In my opinion, a better approach to support
|
||||||
|
//! swap recovery would be through the `action_generator`s themselves, but this
|
||||||
|
//! was deemed too complicated for the time being.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bitcoin, monero,
|
||||||
|
monero::CreateWalletForOutput,
|
||||||
|
state::{Alice, Bob, Swap},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
||||||
|
use futures::{
|
||||||
|
future::{select, Either},
|
||||||
|
pin_mut,
|
||||||
|
};
|
||||||
|
use sha2::Sha256;
|
||||||
|
use tracing::info;
|
||||||
|
use xmr_btc::bitcoin::{
|
||||||
|
poll_until_block_height_is_gte, BroadcastSignedTransaction, TransactionBlockHeight,
|
||||||
|
WatchForRawTransaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn recover(
|
||||||
|
bitcoin_wallet: bitcoin::Wallet,
|
||||||
|
monero_wallet: monero::Wallet,
|
||||||
|
state: Swap,
|
||||||
|
) -> Result<()> {
|
||||||
|
match state {
|
||||||
|
Swap::Alice(state) => alice_recover(bitcoin_wallet, monero_wallet, state).await,
|
||||||
|
Swap::Bob(state) => bob_recover(bitcoin_wallet, monero_wallet, state).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn alice_recover(
|
||||||
|
bitcoin_wallet: bitcoin::Wallet,
|
||||||
|
monero_wallet: monero::Wallet,
|
||||||
|
state: Alice,
|
||||||
|
) -> Result<()> {
|
||||||
|
match state {
|
||||||
|
Alice::Handshaken(_) | Alice::BtcLocked(_) | Alice::SwapComplete => {
|
||||||
|
info!("Nothing to do");
|
||||||
|
}
|
||||||
|
Alice::XmrLocked(state) => {
|
||||||
|
info!("Monero still locked up");
|
||||||
|
|
||||||
|
let tx_cancel = bitcoin::TxCancel::new(
|
||||||
|
&state.tx_lock,
|
||||||
|
state.refund_timelock,
|
||||||
|
state.a.public(),
|
||||||
|
state.B.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("Checking if the Bitcoin cancel transaction has been published");
|
||||||
|
if bitcoin_wallet
|
||||||
|
.0
|
||||||
|
.get_raw_transaction(tx_cancel.txid())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
info!("Bitcoin cancel transaction not yet published");
|
||||||
|
|
||||||
|
let tx_lock_height = bitcoin_wallet
|
||||||
|
.transaction_block_height(state.tx_lock.txid())
|
||||||
|
.await;
|
||||||
|
poll_until_block_height_is_gte(
|
||||||
|
&bitcoin_wallet,
|
||||||
|
tx_lock_height + state.refund_timelock,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let sig_a = state.a.sign(tx_cancel.digest());
|
||||||
|
let sig_b = state.tx_cancel_sig_bob.clone();
|
||||||
|
|
||||||
|
let tx_cancel = tx_cancel
|
||||||
|
.clone()
|
||||||
|
.add_signatures(
|
||||||
|
&state.tx_lock,
|
||||||
|
(state.a.public(), sig_a),
|
||||||
|
(state.B.clone(), sig_b),
|
||||||
|
)
|
||||||
|
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||||
|
|
||||||
|
// TODO: We should not fail if the transaction is already on the blockchain
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(tx_cancel)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
|
||||||
|
|
||||||
|
let tx_cancel_height = bitcoin_wallet
|
||||||
|
.transaction_block_height(tx_cancel.txid())
|
||||||
|
.await;
|
||||||
|
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
|
||||||
|
&bitcoin_wallet,
|
||||||
|
tx_cancel_height + state.punish_timelock,
|
||||||
|
);
|
||||||
|
pin_mut!(poll_until_bob_can_be_punished);
|
||||||
|
|
||||||
|
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||||
|
|
||||||
|
info!("Waiting for either Bitcoin refund or punish timelock");
|
||||||
|
match select(
|
||||||
|
bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()),
|
||||||
|
poll_until_bob_can_be_punished,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Either::Left((tx_refund_published, ..)) => {
|
||||||
|
info!("Found Bitcoin refund transaction");
|
||||||
|
|
||||||
|
let s_a = monero::PrivateKey {
|
||||||
|
scalar: state.s_a.into_ed25519(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tx_refund_sig = tx_refund
|
||||||
|
.extract_signature_by_key(tx_refund_published, state.a.public())?;
|
||||||
|
let tx_refund_encsig = state
|
||||||
|
.a
|
||||||
|
.encsign(state.S_b_bitcoin.clone(), tx_refund.digest());
|
||||||
|
|
||||||
|
let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
|
||||||
|
let s_b = monero::PrivateKey::from_scalar(
|
||||||
|
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
|
||||||
|
);
|
||||||
|
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully refunded monero");
|
||||||
|
}
|
||||||
|
Either::Right(_) => {
|
||||||
|
info!("Punish timelock reached, attempting to punish Bob");
|
||||||
|
|
||||||
|
let tx_punish = bitcoin::TxPunish::new(
|
||||||
|
&tx_cancel,
|
||||||
|
&state.punish_address,
|
||||||
|
state.punish_timelock,
|
||||||
|
);
|
||||||
|
|
||||||
|
let sig_a = state.a.sign(tx_punish.digest());
|
||||||
|
let sig_b = state.tx_punish_sig_bob.clone();
|
||||||
|
|
||||||
|
let sig_tx_punish = tx_punish.add_signatures(
|
||||||
|
&tx_cancel,
|
||||||
|
(state.a.public(), sig_a),
|
||||||
|
(state.B.clone(), sig_b),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(sig_tx_punish)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully punished Bob's inactivity by taking bitcoin");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Alice::BtcRedeemable { redeem_tx, state } => {
|
||||||
|
info!("Have the means to redeem the Bitcoin");
|
||||||
|
|
||||||
|
let tx_lock_height = bitcoin_wallet
|
||||||
|
.transaction_block_height(state.tx_lock.txid())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let block_height = bitcoin_wallet.0.block_height().await?;
|
||||||
|
let refund_absolute_expiry = tx_lock_height + state.refund_timelock;
|
||||||
|
|
||||||
|
info!("Checking refund timelock");
|
||||||
|
if block_height < refund_absolute_expiry {
|
||||||
|
info!("Safe to redeem");
|
||||||
|
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(redeem_tx)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully redeemed bitcoin");
|
||||||
|
} else {
|
||||||
|
info!("Refund timelock reached");
|
||||||
|
|
||||||
|
let tx_cancel = bitcoin::TxCancel::new(
|
||||||
|
&state.tx_lock,
|
||||||
|
state.refund_timelock,
|
||||||
|
state.a.public(),
|
||||||
|
state.B.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("Checking if the Bitcoin cancel transaction has been published");
|
||||||
|
if bitcoin_wallet
|
||||||
|
.0
|
||||||
|
.get_raw_transaction(tx_cancel.txid())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
let sig_a = state.a.sign(tx_cancel.digest());
|
||||||
|
let sig_b = state.tx_cancel_sig_bob.clone();
|
||||||
|
|
||||||
|
let tx_cancel = tx_cancel
|
||||||
|
.clone()
|
||||||
|
.add_signatures(
|
||||||
|
&state.tx_lock,
|
||||||
|
(state.a.public(), sig_a),
|
||||||
|
(state.B.clone(), sig_b),
|
||||||
|
)
|
||||||
|
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||||
|
|
||||||
|
// TODO: We should not fail if the transaction is already on the blockchain
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(tx_cancel)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
|
||||||
|
|
||||||
|
let tx_cancel_height = bitcoin_wallet
|
||||||
|
.transaction_block_height(tx_cancel.txid())
|
||||||
|
.await;
|
||||||
|
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
|
||||||
|
&bitcoin_wallet,
|
||||||
|
tx_cancel_height + state.punish_timelock,
|
||||||
|
);
|
||||||
|
pin_mut!(poll_until_bob_can_be_punished);
|
||||||
|
|
||||||
|
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||||
|
|
||||||
|
info!("Waiting for either Bitcoin refund or punish timelock");
|
||||||
|
match select(
|
||||||
|
bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()),
|
||||||
|
poll_until_bob_can_be_punished,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Either::Left((tx_refund_published, ..)) => {
|
||||||
|
info!("Found Bitcoin refund transaction");
|
||||||
|
|
||||||
|
let s_a = monero::PrivateKey {
|
||||||
|
scalar: state.s_a.into_ed25519(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tx_refund_sig = tx_refund
|
||||||
|
.extract_signature_by_key(tx_refund_published, state.a.public())?;
|
||||||
|
let tx_refund_encsig = state
|
||||||
|
.a
|
||||||
|
.encsign(state.S_b_bitcoin.clone(), tx_refund.digest());
|
||||||
|
|
||||||
|
let s_b =
|
||||||
|
bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
|
||||||
|
let s_b = monero::PrivateKey::from_scalar(
|
||||||
|
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
|
||||||
|
);
|
||||||
|
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully refunded monero");
|
||||||
|
}
|
||||||
|
Either::Right(_) => {
|
||||||
|
info!("Punish timelock reached, attempting to punish Bob");
|
||||||
|
|
||||||
|
let tx_punish = bitcoin::TxPunish::new(
|
||||||
|
&tx_cancel,
|
||||||
|
&state.punish_address,
|
||||||
|
state.punish_timelock,
|
||||||
|
);
|
||||||
|
|
||||||
|
let sig_a = state.a.sign(tx_punish.digest());
|
||||||
|
let sig_b = state.tx_punish_sig_bob.clone();
|
||||||
|
|
||||||
|
let sig_tx_punish = tx_punish.add_signatures(
|
||||||
|
&tx_cancel,
|
||||||
|
(state.a.public(), sig_a),
|
||||||
|
(state.B.clone(), sig_b),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(sig_tx_punish)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully punished Bob's inactivity by taking bitcoin");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Alice::BtcPunishable(state) => {
|
||||||
|
info!("Punish timelock reached, attempting to punish Bob");
|
||||||
|
|
||||||
|
let tx_cancel = bitcoin::TxCancel::new(
|
||||||
|
&state.tx_lock,
|
||||||
|
state.refund_timelock,
|
||||||
|
state.a.public(),
|
||||||
|
state.B.clone(),
|
||||||
|
);
|
||||||
|
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||||
|
|
||||||
|
info!("Checking if Bitcoin has already been refunded");
|
||||||
|
|
||||||
|
// TODO: Protect against transient errors so that we can correctly decide if the
|
||||||
|
// bitcoin has been refunded
|
||||||
|
match bitcoin_wallet.0.get_raw_transaction(tx_refund.txid()).await {
|
||||||
|
Ok(tx_refund_published) => {
|
||||||
|
info!("Bitcoin already refunded");
|
||||||
|
|
||||||
|
let s_a = monero::PrivateKey {
|
||||||
|
scalar: state.s_a.into_ed25519(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tx_refund_sig = tx_refund
|
||||||
|
.extract_signature_by_key(tx_refund_published, state.a.public())?;
|
||||||
|
let tx_refund_encsig = state
|
||||||
|
.a
|
||||||
|
.encsign(state.S_b_bitcoin.clone(), tx_refund.digest());
|
||||||
|
|
||||||
|
let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
|
||||||
|
let s_b = monero::PrivateKey::from_scalar(
|
||||||
|
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
|
||||||
|
);
|
||||||
|
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully refunded monero");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!("Bitcoin not yet refunded");
|
||||||
|
|
||||||
|
let tx_punish = bitcoin::TxPunish::new(
|
||||||
|
&tx_cancel,
|
||||||
|
&state.punish_address,
|
||||||
|
state.punish_timelock,
|
||||||
|
);
|
||||||
|
|
||||||
|
let sig_a = state.a.sign(tx_punish.digest());
|
||||||
|
let sig_b = state.tx_punish_sig_bob.clone();
|
||||||
|
|
||||||
|
let sig_tx_punish = tx_punish.add_signatures(
|
||||||
|
&tx_cancel,
|
||||||
|
(state.a.public(), sig_a),
|
||||||
|
(state.B.clone(), sig_b),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(sig_tx_punish)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully punished Bob's inactivity by taking bitcoin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Alice::BtcRefunded {
|
||||||
|
view_key,
|
||||||
|
spend_key,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
info!("Bitcoin was refunded, attempting to refund monero");
|
||||||
|
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully refunded monero");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bob_recover(
|
||||||
|
bitcoin_wallet: crate::bitcoin::Wallet,
|
||||||
|
monero_wallet: crate::monero::Wallet,
|
||||||
|
state: Bob,
|
||||||
|
) -> Result<()> {
|
||||||
|
match state {
|
||||||
|
Bob::Handshaken(_) | Bob::SwapComplete => {
|
||||||
|
info!("Nothing to do");
|
||||||
|
}
|
||||||
|
Bob::BtcLocked(state) | Bob::XmrLocked(state) | Bob::BtcRefundable(state) => {
|
||||||
|
info!("Bitcoin may still be locked up, attempting to refund");
|
||||||
|
|
||||||
|
let tx_cancel = bitcoin::TxCancel::new(
|
||||||
|
&state.tx_lock,
|
||||||
|
state.refund_timelock,
|
||||||
|
state.A.clone(),
|
||||||
|
state.b.public(),
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("Checking if the Bitcoin cancel transaction has been published");
|
||||||
|
if bitcoin_wallet
|
||||||
|
.0
|
||||||
|
.get_raw_transaction(tx_cancel.txid())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
info!("Bitcoin cancel transaction not yet published");
|
||||||
|
|
||||||
|
let tx_lock_height = bitcoin_wallet
|
||||||
|
.transaction_block_height(state.tx_lock.txid())
|
||||||
|
.await;
|
||||||
|
poll_until_block_height_is_gte(
|
||||||
|
&bitcoin_wallet,
|
||||||
|
tx_lock_height + state.refund_timelock,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let sig_a = state.tx_cancel_sig_a.clone();
|
||||||
|
let sig_b = state.b.sign(tx_cancel.digest());
|
||||||
|
|
||||||
|
let tx_cancel = tx_cancel
|
||||||
|
.clone()
|
||||||
|
.add_signatures(
|
||||||
|
&state.tx_lock,
|
||||||
|
(state.A.clone(), sig_a),
|
||||||
|
(state.b.public(), sig_b),
|
||||||
|
)
|
||||||
|
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||||
|
|
||||||
|
// TODO: We should not fail if the transaction is already on the blockchain
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(tx_cancel)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
|
||||||
|
|
||||||
|
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
|
||||||
|
let signed_tx_refund = {
|
||||||
|
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||||
|
let sig_a = adaptor
|
||||||
|
.decrypt_signature(&state.s_b.into_secp256k1(), state.tx_refund_encsig.clone());
|
||||||
|
let sig_b = state.b.sign(tx_refund.digest());
|
||||||
|
|
||||||
|
tx_refund
|
||||||
|
.add_signatures(
|
||||||
|
&tx_cancel,
|
||||||
|
(state.A.clone(), sig_a),
|
||||||
|
(state.b.public(), sig_b),
|
||||||
|
)
|
||||||
|
.expect("sig_{a,b} to be valid signatures for tx_refund")
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Check if Bitcoin has already been punished and provide a useful error
|
||||||
|
// message/log to the user if so
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(signed_tx_refund)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully refunded bitcoin");
|
||||||
|
}
|
||||||
|
Bob::BtcRedeemed(state) => {
|
||||||
|
info!("Bitcoin was redeemed, attempting to redeem monero");
|
||||||
|
|
||||||
|
let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address);
|
||||||
|
let tx_redeem_published = bitcoin_wallet
|
||||||
|
.0
|
||||||
|
.get_raw_transaction(tx_redeem.txid())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let tx_redeem_encsig = state
|
||||||
|
.b
|
||||||
|
.encsign(state.S_a_bitcoin.clone(), tx_redeem.digest());
|
||||||
|
let tx_redeem_sig =
|
||||||
|
tx_redeem.extract_signature_by_key(tx_redeem_published, state.b.public())?;
|
||||||
|
|
||||||
|
let s_a = bitcoin::recover(state.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
|
||||||
|
let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(
|
||||||
|
s_a.to_bytes(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let s_b = monero::PrivateKey {
|
||||||
|
scalar: state.s_b.into_ed25519(),
|
||||||
|
};
|
||||||
|
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(s_a + s_b, state.v)
|
||||||
|
.await?;
|
||||||
|
info!("Successfully redeemed monero")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
88
swap/src/state.rs
Normal file
88
swap/src/state.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Display;
|
||||||
|
use xmr_btc::{alice, bob, monero, serde::monero_private_key};
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum Swap {
|
||||||
|
Alice(Alice),
|
||||||
|
Bob(Bob),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum Alice {
|
||||||
|
Handshaken(alice::State3),
|
||||||
|
BtcLocked(alice::State3),
|
||||||
|
XmrLocked(alice::State3),
|
||||||
|
BtcRedeemable {
|
||||||
|
state: alice::State3,
|
||||||
|
redeem_tx: bitcoin::Transaction,
|
||||||
|
},
|
||||||
|
BtcPunishable(alice::State3),
|
||||||
|
BtcRefunded {
|
||||||
|
state: alice::State3,
|
||||||
|
#[serde(with = "monero_private_key")]
|
||||||
|
spend_key: monero::PrivateKey,
|
||||||
|
view_key: monero::PrivateViewKey,
|
||||||
|
},
|
||||||
|
SwapComplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum Bob {
|
||||||
|
Handshaken(bob::State2),
|
||||||
|
BtcLocked(bob::State2),
|
||||||
|
XmrLocked(bob::State2),
|
||||||
|
BtcRedeemed(bob::State2),
|
||||||
|
BtcRefundable(bob::State2),
|
||||||
|
SwapComplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Alice> for Swap {
|
||||||
|
fn from(from: Alice) -> Self {
|
||||||
|
Swap::Alice(from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Bob> for Swap {
|
||||||
|
fn from(from: Bob) -> Self {
|
||||||
|
Swap::Bob(from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Swap {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Swap::Alice(alice) => Display::fmt(alice, f),
|
||||||
|
Swap::Bob(bob) => Display::fmt(bob, f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Alice {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Alice::Handshaken(_) => f.write_str("Handshake complete"),
|
||||||
|
Alice::BtcLocked(_) => f.write_str("Bitcoin locked"),
|
||||||
|
Alice::XmrLocked(_) => f.write_str("Monero locked"),
|
||||||
|
Alice::BtcRedeemable { .. } => f.write_str("Bitcoin redeemable"),
|
||||||
|
Alice::BtcPunishable(_) => f.write_str("Bitcoin punishable"),
|
||||||
|
Alice::BtcRefunded { .. } => f.write_str("Monero refundable"),
|
||||||
|
Alice::SwapComplete => f.write_str("Swap complete"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Bob {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Bob::Handshaken(_) => f.write_str("Handshake complete"),
|
||||||
|
Bob::BtcLocked(_) | Bob::XmrLocked(_) | Bob::BtcRefundable(_) => {
|
||||||
|
f.write_str("Bitcoin refundable")
|
||||||
|
}
|
||||||
|
Bob::BtcRedeemed(_) => f.write_str("Monero redeemable"),
|
||||||
|
Bob::SwapComplete => f.write_str("Swap complete"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,62 +1,68 @@
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use crate::state::Swap;
|
||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct Database<T>
|
pub struct Database(sled::Db);
|
||||||
where
|
|
||||||
T: Serialize + DeserializeOwned,
|
|
||||||
{
|
|
||||||
db: sled::Db,
|
|
||||||
_marker: std::marker::PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Database<T>
|
|
||||||
where
|
|
||||||
T: Serialize + DeserializeOwned,
|
|
||||||
{
|
|
||||||
// TODO: serialize using lazy/one-time initlisation
|
|
||||||
const LAST_STATE_KEY: &'static str = "latest_state";
|
|
||||||
|
|
||||||
|
impl Database {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
let db =
|
let db =
|
||||||
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
|
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
|
||||||
|
|
||||||
Ok(Database {
|
Ok(Database(db))
|
||||||
db,
|
|
||||||
_marker: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_latest_state(&self, state: &T) -> Result<()> {
|
pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> {
|
||||||
let key = serialize(&Self::LAST_STATE_KEY)?;
|
let key = serialize(&swap_id)?;
|
||||||
let new_value = serialize(&state).context("Could not serialize new state value")?;
|
let new_value = serialize(&state).context("Could not serialize new state value")?;
|
||||||
|
|
||||||
let old_value = self.db.get(&key)?;
|
let old_value = self.0.get(&key)?;
|
||||||
|
|
||||||
self.db
|
self.0
|
||||||
.compare_and_swap(key, old_value, Some(new_value))
|
.compare_and_swap(key, old_value, Some(new_value))
|
||||||
.context("Could not write in the DB")?
|
.context("Could not write in the DB")?
|
||||||
.context("Stored swap somehow changed, aborting saving")?;
|
.context("Stored swap somehow changed, aborting saving")?;
|
||||||
|
|
||||||
// TODO: see if this can be done through sled config
|
// TODO: see if this can be done through sled config
|
||||||
self.db
|
self.0
|
||||||
.flush_async()
|
.flush_async()
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.context("Could not flush db")
|
.context("Could not flush db")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_latest_state(&self) -> anyhow::Result<T> {
|
pub fn get_state(&self, swap_id: Uuid) -> anyhow::Result<Swap> {
|
||||||
let key = serialize(&Self::LAST_STATE_KEY)?;
|
let key = serialize(&swap_id)?;
|
||||||
|
|
||||||
let encoded = self
|
let encoded = self
|
||||||
.db
|
.0
|
||||||
.get(&key)?
|
.get(&key)?
|
||||||
.ok_or_else(|| anyhow!("State does not exist {:?}", key))?;
|
.ok_or_else(|| anyhow!("State does not exist {:?}", key))?;
|
||||||
|
|
||||||
let state = deserialize(&encoded).context("Could not deserialize state")?;
|
let state = deserialize(&encoded).context("Could not deserialize state")?;
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn all(&self) -> Result<Vec<(Uuid, Swap)>> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.map(|item| match item {
|
||||||
|
Ok((key, value)) => {
|
||||||
|
let swap_id = deserialize::<Uuid>(&key);
|
||||||
|
let swap = deserialize::<Swap>(&value).context("failed to deserialize swap");
|
||||||
|
|
||||||
|
match (swap_id, swap) {
|
||||||
|
(Ok(swap_id), Ok(swap)) => Ok((swap_id, swap)),
|
||||||
|
(Ok(_), Err(err)) => Err(err),
|
||||||
|
_ => bail!("failed to deserialize swap"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(err).context("failed to retrieve swap from DB"),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize<T>(t: &T) -> anyhow::Result<Vec<u8>>
|
pub fn serialize<T>(t: &T) -> anyhow::Result<Vec<u8>>
|
||||||
@ -75,86 +81,87 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#![allow(non_snake_case)]
|
use crate::state::{Alice, Bob};
|
||||||
use super::*;
|
|
||||||
use bitcoin::SigHash;
|
|
||||||
use rand::rngs::OsRng;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::str::FromStr;
|
|
||||||
use xmr_btc::{cross_curve_dleq, monero, serde::monero_private_key};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
use super::*;
|
||||||
pub struct TestState {
|
|
||||||
A: xmr_btc::bitcoin::PublicKey,
|
|
||||||
a: xmr_btc::bitcoin::SecretKey,
|
|
||||||
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::util::amount::serde::as_sat")]
|
|
||||||
btc: ::bitcoin::Amount,
|
|
||||||
xmr: xmr_btc::monero::Amount,
|
|
||||||
refund_timelock: u32,
|
|
||||||
refund_address: ::bitcoin::Address,
|
|
||||||
transaction: ::bitcoin::Transaction,
|
|
||||||
tx_punish_sig: xmr_btc::bitcoin::Signature,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn recover_state_from_db() {
|
async fn can_write_and_read_to_multiple_keys() {
|
||||||
let db_dir = tempfile::tempdir().unwrap();
|
let db_dir = tempfile::tempdir().unwrap();
|
||||||
let db = Database::open(db_dir.path()).unwrap();
|
let db = Database::open(db_dir.path()).unwrap();
|
||||||
|
|
||||||
let a = xmr_btc::bitcoin::SecretKey::new_random(&mut OsRng);
|
let state_1 = Swap::Alice(Alice::SwapComplete);
|
||||||
let s_a = cross_curve_dleq::Scalar::random(&mut OsRng);
|
let swap_id_1 = Uuid::new_v4();
|
||||||
let s_b = monero::PrivateKey::from_scalar(monero::Scalar::random(&mut OsRng));
|
db.insert_latest_state(swap_id_1, state_1.clone())
|
||||||
let v_a = xmr_btc::monero::PrivateViewKey::new_random(&mut OsRng);
|
.await
|
||||||
let S_a_monero = monero::PublicKey::from_private_key(&monero::PrivateKey {
|
.expect("Failed to save second state");
|
||||||
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 {
|
let state_2 = Swap::Bob(Bob::SwapComplete);
|
||||||
A: a.public(),
|
let swap_id_2 = Uuid::new_v4();
|
||||||
a,
|
db.insert_latest_state(swap_id_2, state_2.clone())
|
||||||
s_b,
|
.await
|
||||||
s_a,
|
.expect("Failed to save first state");
|
||||||
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)
|
let recovered_1 = db
|
||||||
|
.get_state(swap_id_1)
|
||||||
|
.expect("Failed to recover first state");
|
||||||
|
|
||||||
|
let recovered_2 = db
|
||||||
|
.get_state(swap_id_2)
|
||||||
|
.expect("Failed to recover second state");
|
||||||
|
|
||||||
|
assert_eq!(recovered_1, state_1);
|
||||||
|
assert_eq!(recovered_2, state_2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_write_twice_to_one_key() {
|
||||||
|
let db_dir = tempfile::tempdir().unwrap();
|
||||||
|
let db = Database::open(db_dir.path()).unwrap();
|
||||||
|
|
||||||
|
let state = Swap::Alice(Alice::SwapComplete);
|
||||||
|
|
||||||
|
let swap_id = Uuid::new_v4();
|
||||||
|
db.insert_latest_state(swap_id, state.clone())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to save state the first time");
|
.expect("Failed to save state the first time");
|
||||||
let recovered: TestState = db
|
let recovered = db
|
||||||
.get_latest_state()
|
.get_state(swap_id)
|
||||||
.expect("Failed to recover state the first time");
|
.expect("Failed to recover state the first time");
|
||||||
|
|
||||||
// We insert and recover twice to ensure database implementation allows the
|
// We insert and recover twice to ensure database implementation allows the
|
||||||
// caller to write to an existing key
|
// caller to write to an existing key
|
||||||
db.insert_latest_state(&recovered)
|
db.insert_latest_state(swap_id, recovered)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to save state the second time");
|
.expect("Failed to save state the second time");
|
||||||
let recovered: TestState = db
|
let recovered = db
|
||||||
.get_latest_state()
|
.get_state(swap_id)
|
||||||
.expect("Failed to recover state the second time");
|
.expect("Failed to recover state the second time");
|
||||||
|
|
||||||
assert_eq!(state, recovered);
|
assert_eq!(recovered, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_fetch_all_keys() {
|
||||||
|
let db_dir = tempfile::tempdir().unwrap();
|
||||||
|
let db = Database::open(db_dir.path()).unwrap();
|
||||||
|
|
||||||
|
let state_1 = Swap::Alice(Alice::SwapComplete);
|
||||||
|
let swap_id_1 = Uuid::new_v4();
|
||||||
|
db.insert_latest_state(swap_id_1, state_1.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to save second state");
|
||||||
|
|
||||||
|
let state_2 = Swap::Bob(Bob::SwapComplete);
|
||||||
|
let swap_id_2 = Uuid::new_v4();
|
||||||
|
db.insert_latest_state(swap_id_2, state_2.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to save first state");
|
||||||
|
|
||||||
|
let swaps = db.all().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(swaps.len(), 2);
|
||||||
|
assert!(swaps.contains(&(swap_id_1, state_1)));
|
||||||
|
assert!(swaps.contains(&(swap_id_2, state_2)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@ pub fn init_tracing(level: log::LevelFilter) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let is_terminal = atty::is(Stream::Stdout);
|
let is_terminal = atty::is(Stream::Stdout);
|
||||||
let subscriber = FmtSubscriber::builder()
|
let subscriber = FmtSubscriber::builder()
|
||||||
.with_env_filter(format!("swap={}", level))
|
.with_env_filter(format!(
|
||||||
|
"swap={},xmr_btc={},monero_harness={}",
|
||||||
|
level, level, level
|
||||||
|
))
|
||||||
.with_ansi(is_terminal)
|
.with_ansi(is_terminal)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
#[cfg(not(feature = "tor"))]
|
|
||||||
mod e2e_test {
|
|
||||||
use bitcoin_harness::Bitcoind;
|
use bitcoin_harness::Bitcoind;
|
||||||
use futures::{channel::mpsc, future::try_join};
|
use futures::{channel::mpsc, future::try_join};
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use monero_harness::Monero;
|
use monero_harness::Monero;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use swap::{alice, bob, network::transport::build};
|
use swap::{alice, bob, network::transport::build, storage::Database};
|
||||||
|
use tempfile::tempdir;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use xmr_btc::bitcoin;
|
||||||
|
|
||||||
|
// 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]
|
#[tokio::test]
|
||||||
async fn swap() {
|
async fn swap() {
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||||
let _guard = tracing_subscriber::fmt()
|
let _guard = tracing_subscriber::fmt()
|
||||||
.with_env_filter(
|
.with_env_filter("swap=info,xmr_btc=info")
|
||||||
"swap=debug,xmr_btc=debug,hyper=off,reqwest=off,monero_harness=info,testcontainers=info,libp2p=debug",
|
|
||||||
)
|
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.set_default();
|
.set_default();
|
||||||
|
|
||||||
@ -24,6 +27,7 @@ mod e2e_test {
|
|||||||
|
|
||||||
let cli = Cli::default();
|
let cli = Cli::default();
|
||||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||||
|
dbg!(&bitcoind.node_url);
|
||||||
let _ = bitcoind.init(5).await;
|
let _ = bitcoind.init(5).await;
|
||||||
|
|
||||||
let btc = bitcoin::Amount::from_sat(1_000_000);
|
let btc = bitcoin::Amount::from_sat(1_000_000);
|
||||||
@ -51,10 +55,8 @@ mod e2e_test {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (monero, _container) = Monero::new(&cli, Some("swap_".to_string()), vec![
|
let (monero, _container) =
|
||||||
"alice".to_string(),
|
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
|
||||||
"bob".to_string(),
|
|
||||||
])
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
monero
|
monero
|
||||||
@ -69,14 +71,19 @@ mod e2e_test {
|
|||||||
|
|
||||||
let alice_behaviour = alice::Alice::default();
|
let alice_behaviour = alice::Alice::default();
|
||||||
let alice_transport = build(alice_behaviour.identity()).unwrap();
|
let alice_transport = build(alice_behaviour.identity()).unwrap();
|
||||||
|
|
||||||
|
let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap();
|
||||||
let alice_swap = alice::swap(
|
let alice_swap = alice::swap(
|
||||||
alice_btc_wallet.clone(),
|
alice_btc_wallet.clone(),
|
||||||
alice_xmr_wallet.clone(),
|
alice_xmr_wallet.clone(),
|
||||||
|
db,
|
||||||
alice_multiaddr.clone(),
|
alice_multiaddr.clone(),
|
||||||
alice_transport,
|
alice_transport,
|
||||||
alice_behaviour,
|
alice_behaviour,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let db_dir = tempdir().unwrap();
|
||||||
|
let db = Database::open(db_dir.path()).unwrap();
|
||||||
let (cmd_tx, mut _cmd_rx) = mpsc::channel(1);
|
let (cmd_tx, mut _cmd_rx) = mpsc::channel(1);
|
||||||
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
|
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
|
||||||
let bob_behaviour = bob::Bob::default();
|
let bob_behaviour = bob::Bob::default();
|
||||||
@ -84,6 +91,7 @@ mod e2e_test {
|
|||||||
let bob_swap = bob::swap(
|
let bob_swap = bob::swap(
|
||||||
bob_btc_wallet.clone(),
|
bob_btc_wallet.clone(),
|
||||||
bob_xmr_wallet.clone(),
|
bob_xmr_wallet.clone(),
|
||||||
|
db,
|
||||||
btc.as_sat(),
|
btc.as_sat(),
|
||||||
alice_multiaddr,
|
alice_multiaddr,
|
||||||
cmd_tx,
|
cmd_tx,
|
||||||
@ -107,11 +115,10 @@ mod e2e_test {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
btc_alice_final,
|
btc_alice_final,
|
||||||
btc_alice + btc - bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
|
btc_alice + btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||||
);
|
);
|
||||||
assert!(btc_bob_final <= btc_bob - btc);
|
assert!(btc_bob_final <= btc_bob - btc);
|
||||||
|
|
||||||
assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr);
|
assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr);
|
||||||
assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr);
|
assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -13,7 +13,6 @@ mod tor_test {
|
|||||||
onion::TorSecretKeyV3,
|
onion::TorSecretKeyV3,
|
||||||
utils::{run_tor, AutoKillChild},
|
utils::{run_tor, AutoKillChild},
|
||||||
};
|
};
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
|
||||||
|
|
||||||
async fn hello_world(
|
async fn hello_world(
|
||||||
_req: hyper::Request<hyper::Body>,
|
_req: hyper::Request<hyper::Body>,
|
||||||
@ -76,10 +75,6 @@ mod tor_test {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_tor_control_port() -> Result<()> {
|
async fn test_tor_control_port() -> Result<()> {
|
||||||
let _guard = tracing_subscriber::fmt()
|
|
||||||
.with_env_filter("info")
|
|
||||||
.set_default();
|
|
||||||
|
|
||||||
// start tmp tor
|
// start tmp tor
|
||||||
let (_child, control_port, proxy_port, _tmp_torrc) = run_tmp_tor()?;
|
let (_child, control_port, proxy_port, _tmp_torrc) = run_tmp_tor()?;
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ where
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum SwapFailed {
|
enum SwapFailed {
|
||||||
BeforeBtcLock(Reason),
|
BeforeBtcLock(Reason),
|
||||||
AfterXmrLock { tx_lock_height: u32, reason: Reason },
|
AfterXmrLock(Reason),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reason why the swap has failed.
|
/// Reason why the swap has failed.
|
||||||
@ -114,9 +114,7 @@ where
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum RefundFailed {
|
enum RefundFailed {
|
||||||
BtcPunishable {
|
BtcPunishable,
|
||||||
tx_cancel_was_published: bool,
|
|
||||||
},
|
|
||||||
/// Could not find Alice's signature on the refund transaction witness
|
/// Could not find Alice's signature on the refund transaction witness
|
||||||
/// stack.
|
/// stack.
|
||||||
BtcRefundSignature,
|
BtcRefundSignature,
|
||||||
@ -167,12 +165,7 @@ where
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Either::Left((encsig, _)) => encsig,
|
Either::Left((encsig, _)) => encsig,
|
||||||
Either::Right(_) => {
|
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
|
||||||
return Err(SwapFailed::AfterXmrLock {
|
|
||||||
reason: Reason::BtcExpired,
|
|
||||||
tx_lock_height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::debug!("select returned redeem encsig from message");
|
tracing::debug!("select returned redeem encsig from message");
|
||||||
@ -191,10 +184,7 @@ where
|
|||||||
&tx_redeem.digest(),
|
&tx_redeem.digest(),
|
||||||
&tx_redeem_encsig,
|
&tx_redeem_encsig,
|
||||||
)
|
)
|
||||||
.map_err(|_| SwapFailed::AfterXmrLock {
|
.map_err(|_| SwapFailed::AfterXmrLock(Reason::InvalidEncryptedSignature))?;
|
||||||
reason: Reason::InvalidEncryptedSignature,
|
|
||||||
tx_lock_height,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let sig_a = a.sign(tx_redeem.digest());
|
let sig_a = a.sign(tx_redeem.digest());
|
||||||
let sig_b =
|
let sig_b =
|
||||||
@ -217,12 +207,7 @@ where
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Either::Left(_) => {}
|
Either::Left(_) => {}
|
||||||
Either::Right(_) => {
|
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
|
||||||
return Err(SwapFailed::AfterXmrLock {
|
|
||||||
reason: Reason::BtcExpired,
|
|
||||||
tx_lock_height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -233,19 +218,8 @@ where
|
|||||||
error!("swap failed: {:?}", err);
|
error!("swap failed: {:?}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(SwapFailed::AfterXmrLock {
|
if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result {
|
||||||
reason: Reason::BtcExpired,
|
|
||||||
tx_lock_height,
|
|
||||||
}) = swap_result
|
|
||||||
{
|
|
||||||
let refund_result: Result<(), RefundFailed> = async {
|
let refund_result: Result<(), RefundFailed> = async {
|
||||||
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
|
|
||||||
bitcoin_client.as_ref(),
|
|
||||||
tx_lock_height + refund_timelock + punish_timelock,
|
|
||||||
)
|
|
||||||
.shared();
|
|
||||||
pin_mut!(poll_until_bob_can_be_punished);
|
|
||||||
|
|
||||||
let tx_cancel =
|
let tx_cancel =
|
||||||
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
|
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
|
||||||
let signed_tx_cancel = {
|
let signed_tx_cancel = {
|
||||||
@ -260,19 +234,19 @@ where
|
|||||||
|
|
||||||
co.yield_(Action::CancelBtc(signed_tx_cancel)).await;
|
co.yield_(Action::CancelBtc(signed_tx_cancel)).await;
|
||||||
|
|
||||||
match select(
|
bitcoin_client
|
||||||
bitcoin_client.watch_for_raw_transaction(tx_cancel.txid()),
|
.watch_for_raw_transaction(tx_cancel.txid())
|
||||||
poll_until_bob_can_be_punished.clone(),
|
.await;
|
||||||
|
|
||||||
|
let tx_cancel_height = bitcoin_client
|
||||||
|
.transaction_block_height(tx_cancel.txid())
|
||||||
|
.await;
|
||||||
|
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
|
||||||
|
bitcoin_client.as_ref(),
|
||||||
|
tx_cancel_height + punish_timelock,
|
||||||
)
|
)
|
||||||
.await
|
.shared();
|
||||||
{
|
pin_mut!(poll_until_bob_can_be_punished);
|
||||||
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 = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
|
||||||
let tx_refund_published = match select(
|
let tx_refund_published = match select(
|
||||||
@ -282,11 +256,7 @@ where
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Either::Left((tx, _)) => tx,
|
Either::Left((tx, _)) => tx,
|
||||||
Either::Right(_) => {
|
Either::Right(_) => return Err(RefundFailed::BtcPunishable),
|
||||||
return Err(RefundFailed::BtcPunishable {
|
|
||||||
tx_cancel_was_published: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let s_a = monero::PrivateKey {
|
let s_a = monero::PrivateKey {
|
||||||
@ -321,32 +291,9 @@ where
|
|||||||
// transaction with his refund transaction. Alice would then need to carry on
|
// 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,
|
// with the refund on Monero. Doing so may be too verbose with the current,
|
||||||
// linear approach. A different design may be required
|
// linear approach. A different design may be required
|
||||||
if let Err(RefundFailed::BtcPunishable {
|
if let Err(RefundFailed::BtcPunishable) = refund_result {
|
||||||
tx_cancel_was_published,
|
|
||||||
}) = refund_result
|
|
||||||
{
|
|
||||||
let tx_cancel =
|
let tx_cancel =
|
||||||
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
|
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_(Action::CancelBtc(signed_tx_cancel)).await;
|
|
||||||
|
|
||||||
let _ = bitcoin_client
|
|
||||||
.watch_for_raw_transaction(tx_cancel_txid)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_punish =
|
let tx_punish =
|
||||||
bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
|
bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
|
||||||
let tx_punish_txid = tx_punish.txid();
|
let tx_punish_txid = tx_punish.txid();
|
||||||
@ -684,7 +631,7 @@ impl State2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct State3 {
|
pub struct State3 {
|
||||||
pub a: bitcoin::SecretKey,
|
pub a: bitcoin::SecretKey,
|
||||||
pub B: bitcoin::PublicKey,
|
pub B: bitcoin::PublicKey,
|
||||||
|
@ -2,12 +2,7 @@ pub mod transactions;
|
|||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bitcoin::{
|
use bitcoin::hashes::{hex::ToHex, Hash};
|
||||||
hashes::{hex::ToHex, Hash},
|
|
||||||
secp256k1,
|
|
||||||
util::psbt::PartiallySignedTransaction,
|
|
||||||
SigHash,
|
|
||||||
};
|
|
||||||
use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA};
|
use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA};
|
||||||
use miniscript::{Descriptor, Segwitv0};
|
use miniscript::{Descriptor, Segwitv0};
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
@ -15,9 +10,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
pub use bitcoin::{util::psbt::PartiallySignedTransaction, *};
|
||||||
pub use bitcoin::{Address, Amount, OutPoint, Transaction, Txid};
|
|
||||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
||||||
|
pub use transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
||||||
|
|
||||||
pub const TX_FEE: u64 = 10_000;
|
pub const TX_FEE: u64 = 10_000;
|
||||||
|
|
||||||
|
@ -495,7 +495,7 @@ impl State1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct State2 {
|
pub struct State2 {
|
||||||
pub A: bitcoin::PublicKey,
|
pub A: bitcoin::PublicKey,
|
||||||
pub b: bitcoin::SecretKey,
|
pub b: bitcoin::SecretKey,
|
||||||
@ -507,10 +507,10 @@ pub struct State2 {
|
|||||||
btc: bitcoin::Amount,
|
btc: bitcoin::Amount,
|
||||||
pub xmr: monero::Amount,
|
pub xmr: monero::Amount,
|
||||||
pub refund_timelock: u32,
|
pub refund_timelock: u32,
|
||||||
punish_timelock: u32,
|
pub punish_timelock: u32,
|
||||||
pub refund_address: bitcoin::Address,
|
pub refund_address: bitcoin::Address,
|
||||||
pub redeem_address: bitcoin::Address,
|
pub redeem_address: bitcoin::Address,
|
||||||
punish_address: bitcoin::Address,
|
pub punish_address: bitcoin::Address,
|
||||||
pub tx_lock: bitcoin::TxLock,
|
pub tx_lock: bitcoin::TxLock,
|
||||||
pub tx_cancel_sig_a: Signature,
|
pub tx_cancel_sig_a: Signature,
|
||||||
pub tx_refund_encsig: EncryptedSignature,
|
pub tx_refund_encsig: EncryptedSignature,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use crate::serde::monero_private_key;
|
use crate::serde::monero_private_key;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
pub use curve25519_dalek::scalar::Scalar;
|
|
||||||
pub use monero::{Address, PrivateKey, PublicKey};
|
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ops::{Add, Sub};
|
use std::ops::{Add, Sub};
|
||||||
|
|
||||||
|
pub use curve25519_dalek::scalar::Scalar;
|
||||||
|
pub use monero::*;
|
||||||
|
|
||||||
pub const MIN_CONFIRMATIONS: u32 = 10;
|
pub const MIN_CONFIRMATIONS: u32 = 10;
|
||||||
|
|
||||||
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
||||||
|
@ -13,7 +13,6 @@ mod tests {
|
|||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
|
||||||
use xmr_btc::{
|
use xmr_btc::{
|
||||||
alice, bitcoin,
|
alice, bitcoin,
|
||||||
bitcoin::{Amount, TX_FEE},
|
bitcoin::{Amount, TX_FEE},
|
||||||
@ -22,10 +21,6 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn happy_path() {
|
async fn happy_path() {
|
||||||
let _guard = tracing_subscriber::fmt()
|
|
||||||
.with_env_filter("info")
|
|
||||||
.set_default();
|
|
||||||
|
|
||||||
let cli = Cli::default();
|
let cli = Cli::default();
|
||||||
let (monero, _container) = Monero::new(&cli, Some("hp".to_string()), vec![
|
let (monero, _container) = Monero::new(&cli, Some("hp".to_string()), vec![
|
||||||
"alice".to_string(),
|
"alice".to_string(),
|
||||||
@ -101,10 +96,6 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn both_refund() {
|
async fn both_refund() {
|
||||||
let _guard = tracing_subscriber::fmt()
|
|
||||||
.with_env_filter("info")
|
|
||||||
.set_default();
|
|
||||||
|
|
||||||
let cli = Cli::default();
|
let cli = Cli::default();
|
||||||
let (monero, _container) = Monero::new(&cli, Some("br".to_string()), vec![
|
let (monero, _container) = Monero::new(&cli, Some("br".to_string()), vec![
|
||||||
"alice".to_string(),
|
"alice".to_string(),
|
||||||
@ -182,10 +173,6 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn alice_punishes() {
|
async fn alice_punishes() {
|
||||||
let _guard = tracing_subscriber::fmt()
|
|
||||||
.with_env_filter("info")
|
|
||||||
.set_default();
|
|
||||||
|
|
||||||
let cli = Cli::default();
|
let cli = Cli::default();
|
||||||
let (monero, _containers) = Monero::new(&cli, Some("ap".to_string()), vec![
|
let (monero, _containers) = Monero::new(&cli, Some("ap".to_string()), vec![
|
||||||
"alice".to_string(),
|
"alice".to_string(),
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||||
use monero::{Address, Network, PrivateKey};
|
|
||||||
use monero_harness::rpc::wallet;
|
use monero_harness::rpc::wallet;
|
||||||
use std::{str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
use xmr_btc::monero::{
|
use xmr_btc::monero::{
|
||||||
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
|
Address, Amount, CreateWalletForOutput, InsufficientFunds, Network, PrivateKey, PrivateViewKey,
|
||||||
Transfer, TransferProof, TxHash, WatchForTransfer,
|
PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Wallet(pub wallet::Client);
|
pub struct Wallet(pub wallet::Client);
|
||||||
|
@ -20,7 +20,7 @@ use tokio::sync::Mutex;
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
use xmr_btc::{
|
use xmr_btc::{
|
||||||
alice::{self, ReceiveBitcoinRedeemEncsig},
|
alice::{self, ReceiveBitcoinRedeemEncsig},
|
||||||
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
bitcoin::{self, BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
||||||
bob::{self, ReceiveTransferProof},
|
bob::{self, ReceiveTransferProof},
|
||||||
monero::{CreateWalletForOutput, Transfer, TransferProof},
|
monero::{CreateWalletForOutput, Transfer, TransferProof},
|
||||||
};
|
};
|
||||||
@ -309,8 +309,7 @@ async fn on_chain_happy_path() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
alice_final_btc_balance,
|
alice_final_btc_balance,
|
||||||
initial_balances.alice_btc + swap_amounts.btc
|
initial_balances.alice_btc + swap_amounts.btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||||
- bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bob_final_btc_balance,
|
bob_final_btc_balance,
|
||||||
@ -411,7 +410,7 @@ async fn on_chain_both_refund_if_alice_never_redeems() {
|
|||||||
bob_final_btc_balance,
|
bob_final_btc_balance,
|
||||||
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
|
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
|
||||||
initial_balances.bob_btc
|
initial_balances.bob_btc
|
||||||
- bitcoin::Amount::from_sat(2 * xmr_btc::bitcoin::TX_FEE)
|
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
|
||||||
- lock_tx_bitcoin_fee
|
- lock_tx_bitcoin_fee
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -508,7 +507,7 @@ async fn on_chain_alice_punishes_if_bob_never_acts_after_fund() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
alice_final_btc_balance,
|
alice_final_btc_balance,
|
||||||
initial_balances.alice_btc + swap_amounts.btc
|
initial_balances.alice_btc + swap_amounts.btc
|
||||||
- bitcoin::Amount::from_sat(2 * xmr_btc::bitcoin::TX_FEE)
|
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bob_final_btc_balance,
|
bob_final_btc_balance,
|
||||||
|
Loading…
Reference in New Issue
Block a user