mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-25 15:39:25 -05: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
|
||||
# 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
|
||||
**/*.rs.bk
|
||||
|
||||
# sled DB directory generated during local development
|
||||
.swap-db/
|
||||
|
||||
# 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-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" }
|
||||
derivative = "2"
|
||||
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] }
|
||||
futures = { version = "0.3", default-features = false }
|
||||
genawaiter = "0.99.1"
|
||||
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"
|
||||
monero = { version = "0.9", features = ["serde_support"] }
|
||||
monero-harness = { path = "../monero-harness" }
|
||||
prettytable-rs = "0.8"
|
||||
rand = "0.7"
|
||||
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_cbor = "0.11"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1"
|
||||
sha2 = "0.9"
|
||||
sled = "0.34"
|
||||
structopt = "0.3"
|
||||
tempfile = "3"
|
||||
@ -39,6 +42,7 @@ 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"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
void = "1"
|
||||
xmr-btc = { path = "../xmr-btc" }
|
||||
|
||||
|
@ -13,6 +13,7 @@ use rand::rngs::OsRng;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod amounts;
|
||||
mod message0;
|
||||
@ -31,6 +32,8 @@ use crate::{
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
state,
|
||||
storage::Database,
|
||||
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||
};
|
||||
use xmr_btc::{
|
||||
@ -43,6 +46,7 @@ use xmr_btc::{
|
||||
pub async fn swap(
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
db: Database,
|
||||
listen: Multiaddr,
|
||||
transport: SwapTransport,
|
||||
behaviour: Alice,
|
||||
@ -71,7 +75,7 @@ pub async fn swap(
|
||||
// to `ConstantBackoff`.
|
||||
#[async_trait]
|
||||
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)]
|
||||
struct UnexpectedMessage;
|
||||
|
||||
@ -173,6 +177,10 @@ pub async fn swap(
|
||||
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.");
|
||||
|
||||
let network = Arc::new(Mutex::new(Network {
|
||||
@ -183,7 +191,7 @@ pub async fn swap(
|
||||
let mut action_generator = action_generator(
|
||||
network.clone(),
|
||||
bitcoin_wallet.clone(),
|
||||
state3,
|
||||
state3.clone(),
|
||||
TX_LOCK_MINE_TIMEOUT,
|
||||
);
|
||||
|
||||
@ -198,33 +206,68 @@ pub async fn swap(
|
||||
public_spend_key,
|
||||
public_view_key,
|
||||
}) => {
|
||||
db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into())
|
||||
.await?;
|
||||
|
||||
let (transfer_proof, _) = monero_wallet
|
||||
.transfer(public_spend_key, public_view_key, amount)
|
||||
.await?;
|
||||
|
||||
db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into())
|
||||
.await?;
|
||||
|
||||
let mut guard = network.as_ref().lock().await;
|
||||
guard.send_message2(transfer_proof).await;
|
||||
info!("Sent transfer proof");
|
||||
}
|
||||
|
||||
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?;
|
||||
}
|
||||
GeneratorState::Yielded(Action::CancelBtc(tx)) => {
|
||||
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
|
||||
}
|
||||
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?;
|
||||
}
|
||||
GeneratorState::Yielded(Action::CreateMoneroWalletForOutput {
|
||||
spend_key,
|
||||
view_key,
|
||||
}) => {
|
||||
db.insert_latest_state(
|
||||
swap_id,
|
||||
state::Alice::BtcRefunded {
|
||||
state: state3.clone(),
|
||||
spend_key,
|
||||
view_key,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||
.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 tokio::time;
|
||||
use xmr_btc::bitcoin::{
|
||||
Amount, BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock,
|
||||
TransactionBlockHeight, TxLock, Txid, WatchForRawTransaction,
|
||||
BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight,
|
||||
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)]
|
||||
pub struct Wallet(pub bitcoin_harness::Wallet);
|
||||
|
@ -13,6 +13,7 @@ use rand::rngs::OsRng;
|
||||
use std::{process, sync::Arc, time::Duration};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod amounts;
|
||||
mod message0;
|
||||
@ -22,14 +23,15 @@ mod message3;
|
||||
|
||||
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
|
||||
use crate::{
|
||||
bitcoin,
|
||||
bitcoin::TX_LOCK_MINE_TIMEOUT,
|
||||
bitcoin::{self, TX_LOCK_MINE_TIMEOUT},
|
||||
monero,
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
state,
|
||||
storage::Database,
|
||||
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||
};
|
||||
use xmr_btc::{
|
||||
@ -43,6 +45,7 @@ use xmr_btc::{
|
||||
pub async fn swap(
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
db: Database,
|
||||
btc: u64,
|
||||
addr: Multiaddr,
|
||||
mut cmd_tx: Sender<Cmd>,
|
||||
@ -141,6 +144,10 @@ pub async fn swap(
|
||||
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());
|
||||
|
||||
info!("Handshake complete");
|
||||
@ -151,7 +158,7 @@ pub async fn swap(
|
||||
network.clone(),
|
||||
monero_wallet.clone(),
|
||||
bitcoin_wallet.clone(),
|
||||
state2,
|
||||
state2.clone(),
|
||||
TX_LOCK_MINE_TIMEOUT,
|
||||
);
|
||||
|
||||
@ -160,20 +167,29 @@ pub async fn swap(
|
||||
|
||||
info!("Resumed execution of generator, got: {:?}", state);
|
||||
|
||||
// TODO: Protect against transient errors
|
||||
// TODO: Ignore transaction-already-in-block-chain errors
|
||||
|
||||
match state {
|
||||
GeneratorState::Yielded(bob::Action::LockBtc(tx_lock)) => {
|
||||
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_lock)
|
||||
.await?;
|
||||
db.insert_latest_state(swap_id, state::Bob::BtcLocked(state2.clone()).into())
|
||||
.await?;
|
||||
}
|
||||
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;
|
||||
guard.0.send_message3(alice.clone(), tx_redeem_encsig);
|
||||
info!("Sent Bitcoin redeem encsig");
|
||||
|
||||
// TODO: Does Bob need to wait for Alice to send an empty response, or can we
|
||||
// just continue?
|
||||
// FIXME: Having to wait for Alice's response here is a big problem, because
|
||||
// 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 {
|
||||
OutEvent::Message3 => {
|
||||
debug!("Got Message3 empty response");
|
||||
@ -185,21 +201,35 @@ pub async fn swap(
|
||||
spend_key,
|
||||
view_key,
|
||||
}) => {
|
||||
db.insert_latest_state(swap_id, state::Bob::BtcRedeemed(state2.clone()).into())
|
||||
.await?;
|
||||
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||
.await?;
|
||||
}
|
||||
GeneratorState::Yielded(bob::Action::CancelBtc(tx_cancel)) => {
|
||||
db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into())
|
||||
.await?;
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
GeneratorState::Yielded(bob::Action::RefundBtc(tx_refund)) => {
|
||||
db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into())
|
||||
.await?;
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_refund)
|
||||
.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 url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(structopt::StructOpt, Debug)]
|
||||
#[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")]
|
||||
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,
|
||||
|
||||
#[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")]
|
||||
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,
|
||||
|
||||
#[structopt(long = "tor")]
|
||||
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 monero;
|
||||
pub mod network;
|
||||
pub mod recover;
|
||||
pub mod state;
|
||||
pub mod storage;
|
||||
pub mod tor;
|
||||
|
||||
@ -32,10 +34,10 @@ pub enum Rsp {
|
||||
pub struct SwapAmounts {
|
||||
/// Amount of BTC to swap.
|
||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||
pub btc: ::bitcoin::Amount,
|
||||
pub btc: bitcoin::Amount,
|
||||
/// Amount of XMR to swap.
|
||||
#[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).
|
||||
|
@ -16,23 +16,28 @@ use anyhow::Result;
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use libp2p::Multiaddr;
|
||||
use log::LevelFilter;
|
||||
use prettytable::{row, Table};
|
||||
use std::{io, io::Write, process, sync::Arc};
|
||||
use structopt::StructOpt;
|
||||
use swap::{
|
||||
alice,
|
||||
alice::Alice,
|
||||
bitcoin, bob,
|
||||
bob::Bob,
|
||||
alice::{self, Alice},
|
||||
bitcoin,
|
||||
bob::{self, Bob},
|
||||
monero,
|
||||
network::transport::{build, build_tor, SwapTransport},
|
||||
recover::recover,
|
||||
Cmd, Rsp, SwapAmounts,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
|
||||
mod cli;
|
||||
mod trace;
|
||||
|
||||
use cli::Options;
|
||||
use swap::storage::Database;
|
||||
|
||||
// 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)?;
|
||||
|
||||
// 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 {
|
||||
Options::Alice {
|
||||
bitcoind_url,
|
||||
@ -83,6 +91,7 @@ async fn main() -> Result<()> {
|
||||
swap_as_alice(
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
db,
|
||||
listen_addr,
|
||||
transport,
|
||||
behaviour,
|
||||
@ -116,6 +125,7 @@ async fn main() -> Result<()> {
|
||||
swap_as_bob(
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
db,
|
||||
satoshis,
|
||||
alice_addr,
|
||||
transport,
|
||||
@ -123,6 +133,31 @@ async fn main() -> Result<()> {
|
||||
)
|
||||
.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(())
|
||||
@ -149,16 +184,26 @@ async fn create_tor_service(
|
||||
async fn swap_as_alice(
|
||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<swap::monero::Wallet>,
|
||||
db: Database,
|
||||
addr: Multiaddr,
|
||||
transport: SwapTransport,
|
||||
behaviour: Alice,
|
||||
) -> 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(
|
||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<swap::monero::Wallet>,
|
||||
db: Database,
|
||||
sats: u64,
|
||||
alice: Multiaddr,
|
||||
transport: SwapTransport,
|
||||
@ -169,6 +214,7 @@ async fn swap_as_bob(
|
||||
tokio::spawn(bob::swap(
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
db,
|
||||
sats,
|
||||
alice,
|
||||
cmd_tx,
|
||||
|
@ -1,15 +1,11 @@
|
||||
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::rpc::wallet;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use url::Url;
|
||||
pub use xmr_btc::monero::{
|
||||
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
|
||||
Transfer, TransferProof, TxHash, WatchForTransfer, *,
|
||||
};
|
||||
|
||||
pub use xmr_btc::monero::*;
|
||||
|
||||
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 std::path::Path;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Database<T>
|
||||
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";
|
||||
pub struct Database(sled::Db);
|
||||
|
||||
impl Database {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let db =
|
||||
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
|
||||
|
||||
Ok(Database {
|
||||
db,
|
||||
_marker: Default::default(),
|
||||
})
|
||||
Ok(Database(db))
|
||||
}
|
||||
|
||||
pub async fn insert_latest_state(&self, state: &T) -> Result<()> {
|
||||
let key = serialize(&Self::LAST_STATE_KEY)?;
|
||||
pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> {
|
||||
let key = serialize(&swap_id)?;
|
||||
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))
|
||||
.context("Could not write in the DB")?
|
||||
.context("Stored swap somehow changed, aborting saving")?;
|
||||
|
||||
// TODO: see if this can be done through sled config
|
||||
self.db
|
||||
self.0
|
||||
.flush_async()
|
||||
.await
|
||||
.map(|_| ())
|
||||
.context("Could not flush db")
|
||||
}
|
||||
|
||||
pub fn get_latest_state(&self) -> anyhow::Result<T> {
|
||||
let key = serialize(&Self::LAST_STATE_KEY)?;
|
||||
pub fn get_state(&self, swap_id: Uuid) -> anyhow::Result<Swap> {
|
||||
let key = serialize(&swap_id)?;
|
||||
|
||||
let encoded = self
|
||||
.db
|
||||
.0
|
||||
.get(&key)?
|
||||
.ok_or_else(|| anyhow!("State does not exist {:?}", key))?;
|
||||
|
||||
let state = deserialize(&encoded).context("Could not deserialize 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>>
|
||||
@ -75,86 +81,87 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(non_snake_case)]
|
||||
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};
|
||||
use crate::state::{Alice, Bob};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
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,
|
||||
}
|
||||
use super::*;
|
||||
|
||||
#[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 = Database::open(db_dir.path()).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(monero::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_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 = 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,
|
||||
};
|
||||
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");
|
||||
|
||||
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
|
||||
.expect("Failed to save state the first time");
|
||||
let recovered: TestState = db
|
||||
.get_latest_state()
|
||||
let recovered = db
|
||||
.get_state(swap_id)
|
||||
.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)
|
||||
db.insert_latest_state(swap_id, recovered)
|
||||
.await
|
||||
.expect("Failed to save state the second time");
|
||||
let recovered: TestState = db
|
||||
.get_latest_state()
|
||||
let recovered = db
|
||||
.get_state(swap_id)
|
||||
.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 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)
|
||||
.finish();
|
||||
|
||||
|
@ -1,117 +1,124 @@
|
||||
#[cfg(not(feature = "tor"))]
|
||||
mod e2e_test {
|
||||
use bitcoin_harness::Bitcoind;
|
||||
use futures::{channel::mpsc, future::try_join};
|
||||
use libp2p::Multiaddr;
|
||||
use monero_harness::Monero;
|
||||
use std::sync::Arc;
|
||||
use swap::{alice, bob, network::transport::build};
|
||||
use testcontainers::clients::Cli;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use bitcoin_harness::Bitcoind;
|
||||
use futures::{channel::mpsc, future::try_join};
|
||||
use libp2p::Multiaddr;
|
||||
use monero_harness::Monero;
|
||||
use std::sync::Arc;
|
||||
use swap::{alice, bob, network::transport::build, storage::Database};
|
||||
use tempfile::tempdir;
|
||||
use testcontainers::clients::Cli;
|
||||
use xmr_btc::bitcoin;
|
||||
|
||||
#[tokio::test]
|
||||
async fn swap() {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
"swap=debug,xmr_btc=debug,hyper=off,reqwest=off,monero_harness=info,testcontainers=info,libp2p=debug",
|
||||
)
|
||||
// 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 swap() {
|
||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("swap=info,xmr_btc=info")
|
||||
.with_ansi(false)
|
||||
.set_default();
|
||||
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
|
||||
.parse()
|
||||
.expect("failed to parse Alice's address");
|
||||
|
||||
let cli = Cli::default();
|
||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||
let _ = bitcoind.init(5).await;
|
||||
let cli = Cli::default();
|
||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||
dbg!(&bitcoind.node_url);
|
||||
let _ = bitcoind.init(5).await;
|
||||
|
||||
let btc = bitcoin::Amount::from_sat(1_000_000);
|
||||
let btc_alice = bitcoin::Amount::ZERO;
|
||||
let btc_bob = btc * 10;
|
||||
let btc = bitcoin::Amount::from_sat(1_000_000);
|
||||
let btc_alice = bitcoin::Amount::ZERO;
|
||||
let btc_bob = btc * 10;
|
||||
|
||||
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
|
||||
// 10_000 * 100
|
||||
let xmr = 1_000_000_000_000;
|
||||
let xmr_alice = xmr * 10;
|
||||
let xmr_bob = 0;
|
||||
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
|
||||
// 10_000 * 100
|
||||
let xmr = 1_000_000_000_000;
|
||||
let xmr_alice = xmr * 10;
|
||||
let xmr_bob = 0;
|
||||
|
||||
let alice_btc_wallet = Arc::new(
|
||||
swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
let bob_btc_wallet = Arc::new(
|
||||
swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
bitcoind
|
||||
.mint(bob_btc_wallet.0.new_address().await.unwrap(), btc_bob)
|
||||
let alice_btc_wallet = Arc::new(
|
||||
swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (monero, _container) = Monero::new(&cli, Some("swap_".to_string()), vec![
|
||||
"alice".to_string(),
|
||||
"bob".to_string(),
|
||||
])
|
||||
.unwrap(),
|
||||
);
|
||||
let bob_btc_wallet = Arc::new(
|
||||
swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
bitcoind
|
||||
.mint(bob_btc_wallet.0.new_address().await.unwrap(), btc_bob)
|
||||
.await
|
||||
.unwrap();
|
||||
monero
|
||||
.init(vec![("alice", xmr_alice), ("bob", xmr_bob)])
|
||||
|
||||
let (monero, _container) =
|
||||
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
monero
|
||||
.init(vec![("alice", xmr_alice), ("bob", xmr_bob)])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_xmr_wallet = Arc::new(swap::monero::Wallet(
|
||||
monero.wallet("alice").unwrap().client(),
|
||||
));
|
||||
let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client()));
|
||||
let alice_xmr_wallet = Arc::new(swap::monero::Wallet(
|
||||
monero.wallet("alice").unwrap().client(),
|
||||
));
|
||||
let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client()));
|
||||
|
||||
let alice_behaviour = alice::Alice::default();
|
||||
let alice_transport = build(alice_behaviour.identity()).unwrap();
|
||||
let alice_swap = alice::swap(
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
alice_multiaddr.clone(),
|
||||
alice_transport,
|
||||
alice_behaviour,
|
||||
);
|
||||
let alice_behaviour = alice::Alice::default();
|
||||
let alice_transport = build(alice_behaviour.identity()).unwrap();
|
||||
|
||||
let (cmd_tx, mut _cmd_rx) = mpsc::channel(1);
|
||||
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
|
||||
let bob_behaviour = bob::Bob::default();
|
||||
let bob_transport = build(bob_behaviour.identity()).unwrap();
|
||||
let bob_swap = bob::swap(
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
btc.as_sat(),
|
||||
alice_multiaddr,
|
||||
cmd_tx,
|
||||
rsp_rx,
|
||||
bob_transport,
|
||||
bob_behaviour,
|
||||
);
|
||||
let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap();
|
||||
let alice_swap = alice::swap(
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
db,
|
||||
alice_multiaddr.clone(),
|
||||
alice_transport,
|
||||
alice_behaviour,
|
||||
);
|
||||
|
||||
// automate the verification step by accepting any amounts sent over by Alice
|
||||
rsp_tx.try_send(swap::Rsp::VerifiedAmounts).unwrap();
|
||||
let db_dir = tempdir().unwrap();
|
||||
let db = Database::open(db_dir.path()).unwrap();
|
||||
let (cmd_tx, mut _cmd_rx) = mpsc::channel(1);
|
||||
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
|
||||
let bob_behaviour = bob::Bob::default();
|
||||
let bob_transport = build(bob_behaviour.identity()).unwrap();
|
||||
let bob_swap = bob::swap(
|
||||
bob_btc_wallet.clone(),
|
||||
bob_xmr_wallet.clone(),
|
||||
db,
|
||||
btc.as_sat(),
|
||||
alice_multiaddr,
|
||||
cmd_tx,
|
||||
rsp_rx,
|
||||
bob_transport,
|
||||
bob_behaviour,
|
||||
);
|
||||
|
||||
try_join(alice_swap, bob_swap).await.unwrap();
|
||||
// automate the verification step by accepting any amounts sent over by Alice
|
||||
rsp_tx.try_send(swap::Rsp::VerifiedAmounts).unwrap();
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
try_join(alice_swap, bob_swap).await.unwrap();
|
||||
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
btc_alice + btc - bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
|
||||
);
|
||||
assert!(btc_bob_final <= btc_bob - btc);
|
||||
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
|
||||
assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr);
|
||||
assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr);
|
||||
}
|
||||
assert_eq!(
|
||||
btc_alice_final,
|
||||
btc_alice + btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert!(btc_bob_final <= btc_bob - btc);
|
||||
|
||||
assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr);
|
||||
assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr);
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ mod tor_test {
|
||||
onion::TorSecretKeyV3,
|
||||
utils::{run_tor, AutoKillChild},
|
||||
};
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
async fn hello_world(
|
||||
_req: hyper::Request<hyper::Body>,
|
||||
@ -76,10 +75,6 @@ mod tor_test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tor_control_port() -> Result<()> {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("info")
|
||||
.set_default();
|
||||
|
||||
// start tmp tor
|
||||
let (_child, control_port, proxy_port, _tmp_torrc) = run_tmp_tor()?;
|
||||
|
||||
|
@ -97,7 +97,7 @@ where
|
||||
#[derive(Debug)]
|
||||
enum SwapFailed {
|
||||
BeforeBtcLock(Reason),
|
||||
AfterXmrLock { tx_lock_height: u32, reason: Reason },
|
||||
AfterXmrLock(Reason),
|
||||
}
|
||||
|
||||
/// Reason why the swap has failed.
|
||||
@ -114,9 +114,7 @@ where
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RefundFailed {
|
||||
BtcPunishable {
|
||||
tx_cancel_was_published: bool,
|
||||
},
|
||||
BtcPunishable,
|
||||
/// Could not find Alice's signature on the refund transaction witness
|
||||
/// stack.
|
||||
BtcRefundSignature,
|
||||
@ -167,12 +165,7 @@ where
|
||||
.await
|
||||
{
|
||||
Either::Left((encsig, _)) => encsig,
|
||||
Either::Right(_) => {
|
||||
return Err(SwapFailed::AfterXmrLock {
|
||||
reason: Reason::BtcExpired,
|
||||
tx_lock_height,
|
||||
})
|
||||
}
|
||||
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
|
||||
};
|
||||
|
||||
tracing::debug!("select returned redeem encsig from message");
|
||||
@ -191,10 +184,7 @@ where
|
||||
&tx_redeem.digest(),
|
||||
&tx_redeem_encsig,
|
||||
)
|
||||
.map_err(|_| SwapFailed::AfterXmrLock {
|
||||
reason: Reason::InvalidEncryptedSignature,
|
||||
tx_lock_height,
|
||||
})?;
|
||||
.map_err(|_| SwapFailed::AfterXmrLock(Reason::InvalidEncryptedSignature))?;
|
||||
|
||||
let sig_a = a.sign(tx_redeem.digest());
|
||||
let sig_b =
|
||||
@ -217,12 +207,7 @@ where
|
||||
.await
|
||||
{
|
||||
Either::Left(_) => {}
|
||||
Either::Right(_) => {
|
||||
return Err(SwapFailed::AfterXmrLock {
|
||||
reason: Reason::BtcExpired,
|
||||
tx_lock_height,
|
||||
})
|
||||
}
|
||||
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
@ -233,19 +218,8 @@ where
|
||||
error!("swap failed: {:?}", err);
|
||||
}
|
||||
|
||||
if let Err(SwapFailed::AfterXmrLock {
|
||||
reason: Reason::BtcExpired,
|
||||
tx_lock_height,
|
||||
}) = swap_result
|
||||
{
|
||||
if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result {
|
||||
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 =
|
||||
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
|
||||
let signed_tx_cancel = {
|
||||
@ -260,19 +234,19 @@ where
|
||||
|
||||
co.yield_(Action::CancelBtc(signed_tx_cancel)).await;
|
||||
|
||||
match select(
|
||||
bitcoin_client.watch_for_raw_transaction(tx_cancel.txid()),
|
||||
poll_until_bob_can_be_punished.clone(),
|
||||
bitcoin_client
|
||||
.watch_for_raw_transaction(tx_cancel.txid())
|
||||
.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
|
||||
{
|
||||
Either::Left(_) => {}
|
||||
Either::Right(_) => {
|
||||
return Err(RefundFailed::BtcPunishable {
|
||||
tx_cancel_was_published: false,
|
||||
})
|
||||
}
|
||||
};
|
||||
.shared();
|
||||
pin_mut!(poll_until_bob_can_be_punished);
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
|
||||
let tx_refund_published = match select(
|
||||
@ -282,11 +256,7 @@ where
|
||||
.await
|
||||
{
|
||||
Either::Left((tx, _)) => tx,
|
||||
Either::Right(_) => {
|
||||
return Err(RefundFailed::BtcPunishable {
|
||||
tx_cancel_was_published: true,
|
||||
});
|
||||
}
|
||||
Either::Right(_) => return Err(RefundFailed::BtcPunishable),
|
||||
};
|
||||
|
||||
let s_a = monero::PrivateKey {
|
||||
@ -321,32 +291,9 @@ where
|
||||
// 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
|
||||
{
|
||||
if let Err(RefundFailed::BtcPunishable) = 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_(Action::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();
|
||||
@ -684,7 +631,7 @@ impl State2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State3 {
|
||||
pub a: bitcoin::SecretKey,
|
||||
pub B: bitcoin::PublicKey,
|
||||
|
@ -2,12 +2,7 @@ pub mod transactions;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use bitcoin::{
|
||||
hashes::{hex::ToHex, Hash},
|
||||
secp256k1,
|
||||
util::psbt::PartiallySignedTransaction,
|
||||
SigHash,
|
||||
};
|
||||
use bitcoin::hashes::{hex::ToHex, Hash};
|
||||
use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA};
|
||||
use miniscript::{Descriptor, Segwitv0};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
@ -15,9 +10,9 @@ 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 bitcoin::{util::psbt::PartiallySignedTransaction, *};
|
||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
||||
pub use transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
||||
|
||||
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 A: bitcoin::PublicKey,
|
||||
pub b: bitcoin::SecretKey,
|
||||
@ -507,10 +507,10 @@ pub struct State2 {
|
||||
btc: bitcoin::Amount,
|
||||
pub xmr: monero::Amount,
|
||||
pub refund_timelock: u32,
|
||||
punish_timelock: u32,
|
||||
pub punish_timelock: u32,
|
||||
pub refund_address: bitcoin::Address,
|
||||
pub redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
pub punish_address: bitcoin::Address,
|
||||
pub tx_lock: bitcoin::TxLock,
|
||||
pub tx_cancel_sig_a: Signature,
|
||||
pub tx_refund_encsig: EncryptedSignature,
|
||||
|
@ -1,12 +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 serde::{Deserialize, Serialize};
|
||||
use std::ops::{Add, Sub};
|
||||
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use monero::*;
|
||||
|
||||
pub const MIN_CONFIRMATIONS: u32 = 10;
|
||||
|
||||
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
||||
|
@ -13,7 +13,6 @@ mod tests {
|
||||
use rand::rngs::OsRng;
|
||||
use std::convert::TryInto;
|
||||
use testcontainers::clients::Cli;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use xmr_btc::{
|
||||
alice, bitcoin,
|
||||
bitcoin::{Amount, TX_FEE},
|
||||
@ -22,10 +21,6 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn happy_path() {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("info")
|
||||
.set_default();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (monero, _container) = Monero::new(&cli, Some("hp".to_string()), vec![
|
||||
"alice".to_string(),
|
||||
@ -101,10 +96,6 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn both_refund() {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("info")
|
||||
.set_default();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (monero, _container) = Monero::new(&cli, Some("br".to_string()), vec![
|
||||
"alice".to_string(),
|
||||
@ -182,10 +173,6 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn alice_punishes() {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("info")
|
||||
.set_default();
|
||||
|
||||
let cli = Cli::default();
|
||||
let (monero, _containers) = Monero::new(&cli, Some("ap".to_string()), vec![
|
||||
"alice".to_string(),
|
||||
|
@ -1,12 +1,11 @@
|
||||
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::rpc::wallet;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use xmr_btc::monero::{
|
||||
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
|
||||
Transfer, TransferProof, TxHash, WatchForTransfer,
|
||||
Address, Amount, CreateWalletForOutput, InsufficientFunds, Network, PrivateKey, PrivateViewKey,
|
||||
PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer,
|
||||
};
|
||||
|
||||
pub struct Wallet(pub wallet::Client);
|
||||
|
@ -20,7 +20,7 @@ use tokio::sync::Mutex;
|
||||
use tracing::info;
|
||||
use xmr_btc::{
|
||||
alice::{self, ReceiveBitcoinRedeemEncsig},
|
||||
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
||||
bitcoin::{self, BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
||||
bob::{self, ReceiveTransferProof},
|
||||
monero::{CreateWalletForOutput, Transfer, TransferProof},
|
||||
};
|
||||
@ -309,8 +309,7 @@ async fn on_chain_happy_path() {
|
||||
|
||||
assert_eq!(
|
||||
alice_final_btc_balance,
|
||||
initial_balances.alice_btc + swap_amounts.btc
|
||||
- bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
|
||||
initial_balances.alice_btc + swap_amounts.btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
@ -411,7 +410,7 @@ async fn on_chain_both_refund_if_alice_never_redeems() {
|
||||
bob_final_btc_balance,
|
||||
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
|
||||
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
|
||||
);
|
||||
|
||||
@ -508,7 +507,7 @@ async fn on_chain_alice_punishes_if_bob_never_acts_after_fund() {
|
||||
assert_eq!(
|
||||
alice_final_btc_balance,
|
||||
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!(
|
||||
bob_final_btc_balance,
|
||||
|
Loading…
Reference in New Issue
Block a user