Merge xmr_btc crate

Created network, storage and protocol modules. Organised
files into the modules where the belong.

xmr_btc crate moved into isolated modulein swap crate.

Remove the xmr_btc module and integrate into swap crate.

Consolidate message related code

Reorganise imports

Remove unused parent Message enum

Remove unused parent State enum

Remove unused dependencies from Cargo.toml
pull/125/head
rishflab 3 years ago
parent a0d859147a
commit c900d12593

@ -34,7 +34,6 @@ jobs:
- name: Check Cargo.toml formatting
run: |
cargo tomlfmt -d -p Cargo.toml
cargo tomlfmt -d -p xmr-btc/Cargo.toml
cargo tomlfmt -d -p monero-harness/Cargo.toml
cargo tomlfmt -d -p swap/Cargo.toml

105
Cargo.lock generated

@ -1089,36 +1089,6 @@ version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "genawaiter"
version = "0.99.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
dependencies = [
"genawaiter-macro",
"genawaiter-proc-macro",
"proc-macro-hack",
]
[[package]]
name = "genawaiter-macro"
version = "0.99.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
[[package]]
name = "genawaiter-proc-macro"
version = "0.99.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738"
dependencies = [
"proc-macro-error 0.4.12",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "generator"
version = "0.6.23"
@ -2379,42 +2349,16 @@ dependencies = [
"uint 0.8.5",
]
[[package]]
name = "proc-macro-error"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7"
dependencies = [
"proc-macro-error-attr 0.4.12",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr 1.0.4",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"syn-mid",
"version_check",
]
@ -3283,7 +3227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
dependencies = [
"heck",
"proc-macro-error 1.0.4",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
@ -3335,21 +3279,25 @@ dependencies = [
"bitcoin",
"bitcoin-harness",
"conquer-once",
"cross-curve-dleq",
"curve25519-dalek 2.1.0",
"derivative",
"ecdsa_fun",
"ed25519-dalek",
"futures",
"genawaiter",
"get-port",
"hyper",
"libp2p",
"libp2p-tokio-socks5",
"log",
"miniscript",
"monero",
"monero-harness",
"port_check",
"prettytable-rs",
"rand 0.7.3",
"reqwest",
"rust_decimal",
"serde",
"serde_cbor",
"serde_derive",
@ -3361,6 +3309,7 @@ dependencies = [
"strum",
"tempfile",
"testcontainers",
"thiserror",
"time",
"tokio",
"tracing",
@ -3371,7 +3320,6 @@ dependencies = [
"url",
"uuid",
"void",
"xmr-btc",
]
[[package]]
@ -3385,17 +3333,6 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "syn-mid"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42823f0ff906a3eb8109610e825221b07fb1456d45c7d01cf18cb581b23ecfb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "synstructure"
version = "0.12.4"
@ -4044,32 +3981,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "xmr-btc"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"bitcoin",
"conquer-once",
"cross-curve-dleq",
"curve25519-dalek 2.1.0",
"ecdsa_fun",
"ed25519-dalek",
"futures",
"genawaiter",
"miniscript",
"monero",
"rand 0.7.3",
"rust_decimal",
"serde",
"serde_cbor",
"sha2 0.9.2",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "yamux"
version = "0.8.0"

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

@ -15,18 +15,22 @@ base64 = "0.12"
bitcoin = { version = "0.25", features = ["rand", "use-serde"] }
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "864b55fcba2e770105f135781dd2e3002c503d12" }
conquer-once = "0.3"
cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "eddcdea1d1f16fa33ef581d1744014ece535c920", features = ["serde"] }
curve25519-dalek = "2"
derivative = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] }
ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3
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"] }
miniscript = { version = "4", features = ["serde"] }
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"] }
rust_decimal = "1.8"
serde = { version = "1", features = ["derive"] }
serde_cbor = "0.11"
serde_derive = "1.0"
@ -36,6 +40,7 @@ sled = "0.34"
structopt = "0.3"
strum = { version = "0.20", features = ["derive"] }
tempfile = "3"
thiserror = "1"
time = "0.2"
tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] }
tracing = { version = "0.1", features = ["attributes"] }
@ -46,12 +51,12 @@ tracing-subscriber = { version = "0.2", default-features = false, features = ["f
url = "2.1"
uuid = { version = "0.8", features = ["serde", "v4"] }
void = "1"
xmr-btc = { path = "../xmr-btc" }
[dev-dependencies]
get-port = "3"
hyper = "0.13"
port_check = "0.1"
serde_cbor = "0.11"
spectral = "0.6"
tempfile = "3"
testcontainers = "0.11"

@ -1,202 +1,288 @@
use anyhow::{Context, Result};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use bitcoin::util::psbt::PartiallySignedTransaction;
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi};
use reqwest::Url;
use std::time::Duration;
use tokio::time::interval;
use xmr_btc::{
bitcoin::{
BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, SignTxLock,
TransactionBlockHeight, WatchForRawTransaction,
},
config::Config,
};
pub use ::bitcoin::{Address, Transaction};
pub use xmr_btc::bitcoin::*;
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
#[derive(Debug)]
pub struct Wallet {
pub inner: bitcoin_harness::Wallet,
pub network: bitcoin::Network,
}
impl Wallet {
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
Ok(Self {
inner: wallet,
network,
})
use bitcoin::hashes::{hex::ToHex, Hash};
use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA};
use miniscript::{Descriptor, Segwitv0};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::str::FromStr;
use crate::{config::Config, ExpiredTimelocks};
use crate::bitcoin::timelocks::{BlockHeight, Timelock};
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
pub use ::bitcoin::{util::psbt::PartiallySignedTransaction, *};
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
pub use wallet::Wallet;
pub mod timelocks;
pub mod transactions;
pub mod wallet;
// TODO: Configurable tx-fee (note: parties have to agree prior to swapping)
// Current reasoning:
// tx with largest weight (as determined by get_weight() upon broadcast in e2e
// test) = 609 assuming segwit and 60 sat/vB:
// (609 / 4) * 60 (sat/vB) = 9135 sats
// Recommended: Overpay a bit to ensure we don't have to wait too long for test
// runs.
pub const TX_FEE: u64 = 15_000;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct SecretKey {
inner: Scalar,
public: Point,
}
impl SecretKey {
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let scalar = Scalar::random(rng);
let ecdsa = ECDSA::<()>::default();
let public = ecdsa.verification_key_for(&scalar);
Self {
inner: scalar,
public,
}
}
pub async fn balance(&self) -> Result<Amount> {
let balance = self.inner.balance().await?;
Ok(balance)
pub fn public(&self) -> PublicKey {
PublicKey(self.public)
}
pub async fn new_address(&self) -> Result<Address> {
self.inner.new_address().await.map_err(Into::into)
pub fn to_bytes(&self) -> [u8; 32] {
self.inner.to_bytes()
}
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
let fee = self
.inner
.get_wallet_transaction(txid)
.await
.map(|res| {
res.fee.map(|signed_amount| {
signed_amount
.abs()
.to_unsigned()
.expect("Absolute value is always positive")
})
})?
.context("Rpc response did not contain a fee")?;
Ok(fee)
pub fn sign(&self, digest: SigHash) -> Signature {
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
ecdsa.sign(&self.inner, &digest.into_inner())
}
}
#[async_trait]
impl BuildTxLockPsbt for Wallet {
async fn build_tx_lock_psbt(
&self,
output_address: Address,
output_amount: Amount,
) -> Result<PartiallySignedTransaction> {
let psbt = self.inner.fund_psbt(output_address, output_amount).await?;
let as_hex = base64::decode(psbt)?;
// TxRefund encsigning explanation:
//
// A and B, are the Bitcoin Public Keys which go on the joint output for
// TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the
// joint output for TxLock_Monero
// tx_refund: multisig(A, B), published by bob
// bob can produce sig on B for tx_refund using b
// alice sends over an encrypted signature on A for tx_refund using a encrypted
// with S_b we want to leak s_b
let psbt = bitcoin::consensus::deserialize(&as_hex)?;
// produced (by Alice) encsig - published (by Bob) sig = s_b (it's not really
// subtraction, it's recover)
Ok(psbt)
// self = a, Y = S_b, digest = tx_refund
pub fn encsign(&self, Y: PublicKey, digest: SigHash) -> EncryptedSignature {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
adaptor.encrypted_sign(&self.inner, &Y.0, &digest.into_inner())
}
}
#[async_trait]
impl SignTxLock for Wallet {
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
let psbt = PartiallySignedTransaction::from(tx_lock);
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct PublicKey(Point);
let psbt = bitcoin::consensus::serialize(&psbt);
let as_base64 = base64::encode(psbt);
impl From<PublicKey> for Point {
fn from(from: PublicKey) -> Self {
from.0
}
}
let psbt = self
.inner
.wallet_process_psbt(PsbtBase64(as_base64))
.await?;
let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt);
impl From<Scalar> for SecretKey {
fn from(scalar: Scalar) -> Self {
let ecdsa = ECDSA::<()>::default();
let public = ecdsa.verification_key_for(&scalar);
let as_hex = base64::decode(signed_psbt)?;
let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?;
Self {
inner: scalar,
public,
}
}
}
let tx = psbt.extract_tx();
impl From<SecretKey> for Scalar {
fn from(sk: SecretKey) -> Self {
sk.inner
}
}
Ok(tx)
impl From<Scalar> for PublicKey {
fn from(scalar: Scalar) -> Self {
let ecdsa = ECDSA::<()>::default();
PublicKey(ecdsa.verification_key_for(&scalar))
}
}
#[async_trait]
impl BroadcastSignedTransaction for Wallet {
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
let txid = self.inner.send_raw_transaction(transaction).await?;
tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid);
Ok(txid)
pub fn verify_sig(
verification_key: &PublicKey,
transaction_sighash: &SigHash,
sig: &Signature,
) -> Result<()> {
let ecdsa = ECDSA::verify_only();
if ecdsa.verify(&verification_key.0, &transaction_sighash.into_inner(), &sig) {
Ok(())
} else {
bail!(InvalidSignature)
}
}
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
// to `ConstantBackoff`.
#[async_trait]
impl WatchForRawTransaction for Wallet {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
(|| async { Ok(self.inner.get_raw_transaction(txid).await?) })
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried")
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("signature is invalid")]
pub struct InvalidSignature;
pub fn verify_encsig(
verification_key: PublicKey,
encryption_key: PublicKey,
digest: &SigHash,
encsig: &EncryptedSignature,
) -> Result<()> {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
if adaptor.verify_encrypted_signature(
&verification_key.0,
&encryption_key.0,
&digest.into_inner(),
&encsig,
) {
Ok(())
} else {
bail!(InvalidEncryptedSignature)
}
}
#[derive(Clone, Copy, Debug, thiserror::Error)]
#[error("encrypted signature is invalid")]
pub struct InvalidEncryptedSignature;
pub fn build_shared_output_descriptor(A: Point, B: Point) -> Descriptor<bitcoin::PublicKey> {
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
// NOTE: This shouldn't be a source of error, but maybe it is
let A = ToHex::to_hex(&secp256k1::PublicKey::from(A));
let B = ToHex::to_hex(&secp256k1::PublicKey::from(B));
let miniscript = MINISCRIPT_TEMPLATE.replace("A", &A).replace("B", &B);
let miniscript = miniscript::Miniscript::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
.expect("a valid miniscript");
Descriptor::Wsh(miniscript)
}
#[async_trait]
impl GetRawTransaction for Wallet {
// todo: potentially replace with option
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
Ok(self.inner.get_raw_transaction(txid).await?)
}
pub trait BuildTxLockPsbt {
async fn build_tx_lock_psbt(
&self,
output_address: Address,
output_amount: Amount,
) -> Result<PartiallySignedTransaction>;
}
#[async_trait]
impl GetBlockHeight for Wallet {
async fn get_block_height(&self) -> BlockHeight {
let height = (|| async { Ok(self.inner.client.getblockcount().await?) })
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried");
BlockHeight::new(height)
}
pub trait SignTxLock {
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
}
#[async_trait]
impl TransactionBlockHeight for Wallet {
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight {
#[derive(Debug)]
enum Error {
Io,
NotYetMined,
}
pub trait BroadcastSignedTransaction {
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
}
let height = (|| async {
let block_height = self
.inner
.transaction_block_height(txid)
.await
.map_err(|_| backoff::Error::Transient(Error::Io))?;
#[async_trait]
pub trait WatchForRawTransaction {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
}
let block_height =
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
#[async_trait]
pub trait WaitForTransactionFinality {
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
}
Result::<_, backoff::Error<Error>>::Ok(block_height)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried");
#[async_trait]
pub trait GetBlockHeight {
async fn get_block_height(&self) -> BlockHeight;
}
BlockHeight::new(height)
}
#[async_trait]
pub trait TransactionBlockHeight {
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
}
#[async_trait]
impl WaitForTransactionFinality for Wallet {
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> {
// TODO(Franck): This assumes that bitcoind runs with txindex=1
// Divide by 4 to not check too often yet still be aware of the new block early
// on.
let mut interval = interval(config.bitcoin_avg_block_time / 4);
loop {
let tx = self.inner.client.get_raw_transaction_verbose(txid).await?;
if let Some(confirmations) = tx.confirmations {
if confirmations >= config.bitcoin_finality_confirmations {
break;
}
}
interval.tick().await;
}
pub trait WaitForBlockHeight {
async fn wait_for_block_height(&self, height: BlockHeight);
}
Ok(())
#[async_trait]
pub trait GetRawTransaction {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
}
#[async_trait]
pub trait Network {
fn get_network(&self) -> bitcoin::Network;
}
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let s = adaptor
.recover_decryption_key(&S.0, &sig, &encsig)
.map(SecretKey::from)
.ok_or_else(|| anyhow!("secret recovery failure"))?;
Ok(s)
}
pub async fn poll_until_block_height_is_gte<B>(client: &B, target: BlockHeight)
where
B: GetBlockHeight,
{
while client.get_block_height().await < target {
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
}
}
impl Network for Wallet {
fn get_network(&self) -> bitcoin::Network {
self.network
pub async fn current_epoch<W>(
bitcoin_wallet: &W,
cancel_timelock: Timelock,
punish_timelock: Timelock,
lock_tx_id: ::bitcoin::Txid,
) -> anyhow::Result<ExpiredTimelocks>
where
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
{
let current_block_height = bitcoin_wallet.get_block_height().await;
let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await;
let cancel_timelock_height = lock_tx_height + cancel_timelock;
let punish_timelock_height = cancel_timelock_height + punish_timelock;
match (
current_block_height < cancel_timelock_height,
current_block_height < punish_timelock_height,
) {
(true, _) => Ok(ExpiredTimelocks::None),
(false, true) => Ok(ExpiredTimelocks::Cancel),
(false, false) => Ok(ExpiredTimelocks::Punish),
}
}
pub async fn wait_for_cancel_timelock_to_expire<W>(
bitcoin_wallet: &W,
cancel_timelock: Timelock,
lock_tx_id: ::bitcoin::Txid,
) -> Result<()>
where
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
{
let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await;
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await;
Ok(())
}

@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
use std::ops::Add;
/// Represent a timelock, expressed in relative block height as defined in
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
/// E.g. The timelock expires 10 blocks after the reference transaction is
/// mined.
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(transparent)]
pub struct Timelock(u32);
impl Timelock {
pub const fn new(number_of_blocks: u32) -> Self {
Self(number_of_blocks)
}
}
impl From<Timelock> for u32 {
fn from(timelock: Timelock) -> Self {
timelock.0
}
}
/// Represent a block height, or block number, expressed in absolute block
/// count. E.g. The transaction was included in block #655123, 655123 block
/// after the genesis block.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BlockHeight(u32);
impl From<BlockHeight> for u32 {
fn from(height: BlockHeight) -> Self {
height.0
}
}
impl BlockHeight {
pub const fn new(block_height: u32) -> Self {
Self(block_height)
}
}
impl Add<Timelock> for BlockHeight {
type Output = BlockHeight;
fn add(self, rhs: Timelock) -> Self::Output {
BlockHeight(self.0 + rhs.0)
}
}

@ -1,7 +1,3 @@
use crate::bitcoin::{
build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, Network, OutPoint, PublicKey,
Timelock, Txid, TX_FEE,
};
use anyhow::{bail, Context, Result};
use bitcoin::{
util::{bip143::SigHashCache, psbt::PartiallySignedTransaction},
@ -12,6 +8,11 @@ use miniscript::{Descriptor, NullCtx};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::bitcoin::{
build_shared_output_descriptor, timelocks::Timelock, verify_sig, BuildTxLockPsbt, Network,
OutPoint, PublicKey, Txid, TX_FEE,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TxLock {
inner: Transaction,

@ -0,0 +1,201 @@
use ::bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
use anyhow::{Context, Result};
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi};
use reqwest::Url;
use std::time::Duration;
use tokio::time::interval;
use crate::{
bitcoin::{
timelocks::BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight,
GetRawTransaction, Network, SignTxLock, TransactionBlockHeight, TxLock,
WaitForTransactionFinality, WatchForRawTransaction,
},
config::Config,
};
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
#[derive(Debug)]
pub struct Wallet {
pub inner: bitcoin_harness::Wallet,
pub network: bitcoin::Network,
}
impl Wallet {
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
Ok(Self {
inner: wallet,
network,
})
}
pub async fn balance(&self) -> Result<Amount> {
let balance = self.inner.balance().await?;
Ok(balance)
}
pub async fn new_address(&self) -> Result<Address> {
self.inner.new_address().await.map_err(Into::into)
}
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
let fee = self
.inner
.get_wallet_transaction(txid)
.await
.map(|res| {
res.fee.map(|signed_amount| {
signed_amount
.abs()
.to_unsigned()
.expect("Absolute value is always positive")
})
})?
.context("Rpc response did not contain a fee")?;
Ok(fee)
}
}
#[async_trait]
impl BuildTxLockPsbt for Wallet {
async fn build_tx_lock_psbt(
&self,
output_address: Address,
output_amount: Amount,
) -> Result<PartiallySignedTransaction> {
let psbt = self.inner.fund_psbt(output_address, output_amount).await?;
let as_hex = base64::decode(psbt)?;
let psbt = bitcoin::consensus::deserialize(&as_hex)?;
Ok(psbt)
}
}
#[async_trait]
impl SignTxLock for Wallet {
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
let psbt = PartiallySignedTransaction::from(tx_lock);
let psbt = bitcoin::consensus::serialize(&psbt);
let as_base64 = base64::encode(psbt);
let psbt = self
.inner
.wallet_process_psbt(PsbtBase64(as_base64))
.await?;
let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt);
let as_hex = base64::decode(signed_psbt)?;
let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?;
let tx = psbt.extract_tx();
Ok(tx)
}
}
#[async_trait]
impl BroadcastSignedTransaction for Wallet {
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
let txid = self.inner.send_raw_transaction(transaction).await?;
tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid);
Ok(txid)
}
}
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
// to `ConstantBackoff`.
#[async_trait]
impl WatchForRawTransaction for Wallet {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
(|| async { Ok(self.inner.get_raw_transaction(txid).await?) })
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried")
}
}
#[async_trait]
impl GetRawTransaction for Wallet {
// todo: potentially replace with option
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
Ok(self.inner.get_raw_transaction(txid).await?)
}
}
#[async_trait]
impl GetBlockHeight for Wallet {
async fn get_block_height(&self) -> BlockHeight {
let height = (|| async { Ok(self.inner.client.getblockcount().await?) })
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried");
BlockHeight::new(height)
}
}
#[async_trait]
impl TransactionBlockHeight for Wallet {
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight {
#[derive(Debug)]
enum Error {
Io,
NotYetMined,
}
let height = (|| async {
let block_height = self
.inner
.transaction_block_height(txid)
.await
.map_err(|_| backoff::Error::Transient(Error::Io))?;
let block_height =
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
Result::<_, backoff::Error<Error>>::Ok(block_height)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried");
BlockHeight::new(height)
}
}
#[async_trait]
impl WaitForTransactionFinality for Wallet {
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> {
// TODO(Franck): This assumes that bitcoind runs with txindex=1
// Divide by 4 to not check too often yet still be aware of the new block early
// on.
let mut interval = interval(config.bitcoin_avg_block_time / 4);
loop {
let tx = self.inner.client.get_raw_transaction_verbose(txid).await?;
if let Some(confirmations) = tx.confirmations {
if confirmations >= config.bitcoin_finality_confirmations {
break;
}
}
interval.tick().await;
}
Ok(())
}
}
impl Network for Wallet {
fn get_network(&self) -> bitcoin::Network {
self.network
}
}

@ -2,6 +2,8 @@ use libp2p::{core::Multiaddr, PeerId};
use url::Url;
use uuid::Uuid;
use crate::monero;
#[derive(structopt::StructOpt, Debug)]
pub struct Options {
// TODO: Default value should points to proper configuration folder in home folder
@ -13,7 +15,7 @@ pub struct Options {
}
#[derive(structopt::StructOpt, Debug)]
#[structopt(name = "xmr-btc-swap", about = "XMR BTC atomic swap")]
#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")]
pub enum Command {
SellXmr {
#[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")]
@ -32,10 +34,10 @@ pub enum Command {
listen_addr: Multiaddr,
#[structopt(long = "send-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))]
send_monero: xmr_btc::monero::Amount,
send_monero: monero::Amount,
#[structopt(long = "receive-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))]
receive_bitcoin: bitcoin::Amount,
receive_bitcoin: ::bitcoin::Amount,
},
BuyXmr {
#[structopt(long = "connect-peer-id")]
@ -57,10 +59,10 @@ pub enum Command {
monero_wallet_rpc_url: Url,
#[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))]
send_bitcoin: bitcoin::Amount,
send_bitcoin: ::bitcoin::Amount,
#[structopt(long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))]
receive_monero: xmr_btc::monero::Amount,
receive_monero: monero::Amount,
},
History,
Resume(Resume),
@ -116,7 +118,7 @@ fn parse_btc(str: &str) -> anyhow::Result<bitcoin::Amount> {
Ok(amount)
}
fn parse_xmr(str: &str) -> anyhow::Result<xmr_btc::monero::Amount> {
let amount = xmr_btc::monero::Amount::parse_monero(str)?;
fn parse_xmr(str: &str) -> anyhow::Result<monero::Amount> {
let amount = monero::Amount::parse_monero(str)?;
Ok(amount)
}

@ -1,4 +1,4 @@
use crate::bitcoin::Timelock;
use crate::bitcoin::timelocks::Timelock;
use conquer_once::Lazy;
use std::time::Duration;

@ -3,13 +3,39 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{fmt::Display, path::Path};
use uuid::Uuid;
mod alice;
mod bob;
pub mod alice;
pub mod bob;
pub use alice::*;
pub use bob::*;
pub use alice::Alice;
pub use bob::Bob;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Swap {
Alice(Alice),
Bob(Bob),
}
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),
}
}
}
#[derive(Debug)]
pub struct Database(sled::Db);
impl Database {
@ -85,37 +111,13 @@ where
Ok(serde_cbor::from_slice(&v)?)
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Swap {
Alice(Alice),
Bob(Bob),
}
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),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::{Alice, AliceEndState, Bob, BobEndState};
use crate::database::{
alice::{Alice, AliceEndState},
bob::{Bob, BobEndState},
};
#[tokio::test]
async fn can_write_and_read_to_multiple_keys() {

@ -1,14 +1,15 @@
use crate::{alice::swap::AliceState, SwapAmounts};
use bitcoin::hashes::core::fmt::Display;
use serde::{Deserialize, Serialize};
use xmr_btc::{
alice,
use crate::{
bitcoin::{EncryptedSignature, TxCancel, TxRefund},
monero,
protocol::{alice, alice::swap::AliceState},
serde::monero_private_key,
SwapAmounts,
};
// Large enum variant is fine because this is only used for storage
// Large enum variant is fine because this is only used for database
// and is dropped once written in DB.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]

@ -1,7 +1,10 @@
use crate::{bob::swap::BobState, SwapAmounts};
use bitcoin::hashes::core::fmt::Display;
use serde::{Deserialize, Serialize};
use xmr_btc::bob;
use crate::{
protocol::{bob, bob::swap::BobState},
SwapAmounts,
};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Bob {

@ -1,7 +1,5 @@
#![warn(
unused_extern_crates,
missing_debug_implementations,
missing_copy_implementations,
rust_2018_idioms,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
@ -10,19 +8,25 @@
clippy::cast_possible_wrap,
clippy::dbg_macro
)]
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
#![forbid(unsafe_code)]
#![allow(non_snake_case)]
#![allow(
non_snake_case,
missing_debug_implementations,
missing_copy_implementations
)]
use ::serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod cli;
pub mod config;
pub mod database;
pub mod monero;
pub mod network;
pub mod protocol;
pub mod serde;
pub mod trace;
pub type Never = std::convert::Infallible;
@ -48,7 +52,7 @@ pub struct SwapAmounts {
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub btc: bitcoin::Amount,
/// Amount of XMR to swap.
#[serde(with = "xmr_btc::serde::monero_amount")]
#[serde(with = "serde::monero_amount")]
pub xmr: monero::Amount,
}
@ -63,3 +67,10 @@ impl Display for SwapAmounts {
)
}
}
#[derive(Debug, Clone, Copy)]
pub enum ExpiredTimelocks {
None,
Cancel,
Punish,
}

@ -20,20 +20,18 @@ use rand::rngs::OsRng;
use std::sync::Arc;
use structopt::StructOpt;
use swap::{
alice,
alice::swap::AliceState,
bitcoin, bob,
bob::swap::BobState,
bitcoin,
cli::{Command, Options, Resume},
config::Config,
database::{Database, Swap},
monero,
network::transport::build,
protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState},
trace::init_tracing,
SwapAmounts,
};
use tracing::{info, log::LevelFilter};
use uuid::Uuid;
use xmr_btc::{alice::State0, config::Config, cross_curve_dleq};
#[macro_use]
extern crate prettytable;
@ -76,10 +74,10 @@ async fn main() -> Result<()> {
let rng = &mut OsRng;
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
let redeem_address = bitcoin_wallet.as_ref().new_address().await?;
let punish_address = redeem_address.clone();
let state0 = State0::new(
let state0 = alice::state::State0::new(
a,
s_a,
v_a,
@ -129,7 +127,7 @@ async fn main() -> Result<()> {
.await?;
let refund_address = bitcoin_wallet.new_address().await?;
let state0 = xmr_btc::bob::State0::new(
let state0 = bob::state::State0::new(
&mut OsRng,
send_bitcoin,
receive_monero,
@ -248,9 +246,10 @@ async fn setup_wallets(
bitcoin_wallet_name: &str,
monero_wallet_rpc_url: url::Url,
config: Config,
) -> Result<(Arc<bitcoin::Wallet>, Arc<monero::Wallet>)> {
) -> Result<(Arc<swap::bitcoin::Wallet>, Arc<swap::monero::Wallet>)> {
let bitcoin_wallet =
bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network).await?;
swap::bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network)
.await?;
let bitcoin_balance = bitcoin_wallet.balance().await?;
info!(
"Connection to Bitcoin wallet succeeded, balance: {}",
@ -273,8 +272,8 @@ async fn alice_swap(
swap_id: Uuid,
state: AliceState,
listen_addr: Multiaddr,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
monero_wallet: Arc<swap::monero::Wallet>,
config: Config,
db: Database,
) -> Result<AliceState> {
@ -306,8 +305,8 @@ async fn alice_swap(
async fn bob_swap(
swap_id: Uuid,
state: BobState,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
monero_wallet: Arc<swap::monero::Wallet>,
db: Database,
alice_peer_id: PeerId,
alice_addr: Multiaddr,

@ -1,143 +1,281 @@
pub mod wallet;
use ::bitcoin::hashes::core::fmt::Formatter;
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use monero_harness::rpc::wallet;
use std::{str::FromStr, time::Duration};
use url::Url;
use rand::{CryptoRng, RngCore};
use rust_decimal::{
prelude::{FromPrimitive, ToPrimitive},
Decimal,
};
use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
ops::{Add, Mul, Sub},
str::FromStr,
};
use crate::{bitcoin, serde::monero_private_key};
pub use curve25519_dalek::scalar::Scalar;
pub use monero::*;
pub use wallet::Wallet;
pub use xmr_btc::monero::*;
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
#[derive(Debug)]
pub struct Wallet {
pub inner: wallet::Client,
pub network: Network,
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
let scalar = Scalar::random(rng);
PrivateKey::from_scalar(scalar)
}
impl Wallet {
pub fn new(url: Url, network: Network) -> Self {
Self {
inner: wallet::Client::new(url),
network,
}
pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey {
let mut bytes = scalar.to_bytes();
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
// ed25519 scalar is little endian
bytes.reverse();
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
impl PrivateViewKey {
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let scalar = Scalar::random(rng);
let private_key = PrivateKey::from_scalar(scalar);
Self(private_key)
}
pub fn public(&self) -> PublicViewKey {
PublicViewKey(PublicKey::from_private_key(&self.0))
}
}
/// Get the balance of the primary account.
pub async fn get_balance(&self) -> Result<Amount> {
let amount = self.inner.get_balance(0).await?;
impl Add for PrivateViewKey {
type Output = Self;
Ok(Amount::from_piconero(amount))
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
#[async_trait]
impl Transfer for Wallet {
async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> Result<(TransferProof, Amount)> {
let destination_address =
Address::standard(self.network, public_spend_key, public_view_key.into());
impl From<PrivateViewKey> for PrivateKey {
fn from(from: PrivateViewKey) -> Self {
from.0
}
}
impl From<PublicViewKey> for PublicKey {
fn from(from: PublicViewKey) -> Self {
from.0
}
}
let res = self
.inner
.transfer(0, amount.as_piconero(), &destination_address.to_string())
.await?;
#[derive(Clone, Copy, Debug)]
pub struct PublicViewKey(PublicKey);
let tx_hash = TxHash(res.tx_hash);
tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash);
let tx_key = PrivateKey::from_str(&res.tx_key)?;
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
pub struct Amount(u64);
let fee = Amount::from_piconero(res.fee);
impl Amount {
pub const ZERO: Self = Self(0);
/// Create an [Amount] with piconero precision and the given number of
/// piconeros.
///
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
pub fn from_piconero(amount: u64) -> Self {
Amount(amount)
}
let transfer_proof = TransferProof::new(tx_hash, tx_key);
tracing::debug!(" Transfer proof: {:?}", transfer_proof);
pub fn as_piconero(&self) -> u64 {
self.0
}
Ok((transfer_proof, fee))
pub fn parse_monero(amount: &str) -> Result<Self> {
let decimal = Decimal::from_str(amount)?;
let piconeros_dec =
decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
let piconeros = piconeros_dec
.to_u64()
.ok_or_else(|| OverflowError(amount.to_owned()))?;
Ok(Amount(piconeros))
}
}
#[async_trait]
impl CreateWalletForOutput for Wallet {
async fn create_and_load_wallet_for_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> Result<()> {
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
impl Add for Amount {
type Output = Amount;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for Amount {
type Output = Amount;
let address = Address::standard(self.network, public_spend_key, public_view_key);
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
let _ = self
.inner
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
impl Mul<u64> for Amount {
type Output = Amount;
Ok(())
fn mul(self, rhs: u64) -> Self::Output {
Self(self.0 * rhs)
}
}
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
// to `ConstantBackoff`.
impl From<Amount> for u64 {
fn from(from: Amount) -> u64 {
from.0
}
}
impl Display for Amount {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut decimal = Decimal::from(self.0);
decimal
.set_scale(12)
.expect("12 is smaller than max precision of 28");
write!(f, "{} XMR", decimal)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TransferProof {
tx_hash: TxHash,
#[serde(with = "monero_private_key")]
tx_key: PrivateKey,
}
impl TransferProof {
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
Self { tx_hash, tx_key }
}
pub fn tx_hash(&self) -> TxHash {
self.tx_hash.clone()
}
pub fn tx_key(&self) -> PrivateKey {
self.tx_key
}
}
// TODO: add constructor/ change String to fixed length byte array
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TxHash(pub String);
impl From<TxHash> for String {
fn from(from: TxHash) -> Self {
from.0
}
}
#[async_trait]
pub trait Transfer {
async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> anyhow::Result<(TransferProof, Amount)>;
}
#[async_trait]
impl WatchForTransfer for Wallet {
pub trait WatchForTransfer {
async fn watch_for_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
expected_amount: Amount,
amount: Amount,
expected_confirmations: u32,
) -> Result<(), InsufficientFunds> {
enum Error {
TxNotFound,
InsufficientConfirmations,
InsufficientFunds { expected: Amount, actual: Amount },
}
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
let res = (|| async {
// NOTE: Currently, this is conflating IO errors with the transaction not being
// in the blockchain yet, or not having enough confirmations on it. All these
// errors warrant a retry, but the strategy should probably differ per case
let proof = self
.inner
.check_tx_key(
&String::from(transfer_proof.tx_hash()),
&transfer_proof.tx_key().to_string(),
&address.to_string(),
)
.await
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
if proof.received != expected_amount.as_piconero() {
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
expected: expected_amount,
actual: Amount::from_piconero(proof.received),
}));
}
if proof.confirmations < expected_confirmations {
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
}
Ok(proof)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await;
if let Err(Error::InsufficientFunds { expected, actual }) = res {
return Err(InsufficientFunds { expected, actual });
};
Ok(())
) -> Result<(), InsufficientFunds>;
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
pub struct InsufficientFunds {
pub expected: Amount,
pub actual: Amount,
}
#[async_trait]
pub trait CreateWalletForOutput {
async fn create_and_load_wallet_for_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> anyhow::Result<()>;
}
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
#[error("Overflow, cannot convert {0} to u64")]
pub struct OverflowError(pub String);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_monero_min() {
let min_pics = 1;
let amount = Amount::from_piconero(min_pics);
let monero = amount.to_string();
assert_eq!("0.000000000001 XMR", monero);
}
#[test]
fn display_monero_one() {
let min_pics = 1000000000000;
let amount = Amount::from_piconero(min_pics);
let monero = amount.to_string();
assert_eq!("1.000000000000 XMR", monero);
}
#[test]
fn display_monero_max() {
let max_pics = 18_446_744_073_709_551_615;
let amount = Amount::from_piconero(max_pics);
let monero = amount.to_string();
assert_eq!("18446744.073709551615 XMR", monero);
}
#[test]
fn parse_monero_min() {
let monero_min = "0.000000000001";
let amount = Amount::parse_monero(monero_min).unwrap();
let pics = amount.0;
assert_eq!(1, pics);
}
#[test]
fn parse_monero() {
let monero = "123";
let amount = Amount::parse_monero(monero).unwrap();
let pics = amount.0;
assert_eq!(123000000000000, pics);
}
#[test]
fn parse_monero_max() {
let monero = "18446744.073709551615";
let amount = Amount::parse_monero(monero).unwrap();
let pics = amount.0;
assert_eq!(18446744073709551615, pics);
}
#[test]
fn parse_monero_overflows() {
let overflow_pics = "18446744.073709551616";
let error = Amount::parse_monero(overflow_pics).unwrap_err();
assert_eq!(
error.downcast_ref::<OverflowError>().unwrap(),
&OverflowError(overflow_pics.to_owned())
);
}
}

@ -0,0 +1,147 @@
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use monero::{Address, Network, PrivateKey, PublicKey};
use monero_harness::rpc::wallet;
use std::{str::FromStr, time::Duration};
use url::Url;
use crate::monero::{
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicViewKey, Transfer,
TransferProof, TxHash, WatchForTransfer,
};
#[derive(Debug)]
pub struct Wallet {
pub inner: wallet::Client,
pub network: Network,
}
impl Wallet {
pub fn new(url: Url, network: Network) -> Self {
Self {
inner: wallet::Client::new(url),
network,
}
}
/// Get the balance of the primary account.
pub async fn get_balance(&self) -> Result<Amount> {
let amount = self.inner.get_balance(0).await?;
Ok(Amount::from_piconero(amount))
}
}
#[async_trait]
impl Transfer for Wallet {
async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> Result<(TransferProof, Amount)> {
let destination_address =
Address::standard(self.network, public_spend_key, public_view_key.into());
let res = self
.inner
.transfer(0, amount.as_piconero(), &destination_address.to_string())
.await?;
let tx_hash = TxHash(res.tx_hash);
tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash);
let tx_key = PrivateKey::from_str(&res.tx_key)?;
let fee = Amount::from_piconero(res.fee);
let transfer_proof = TransferProof::new(tx_hash, tx_key);
tracing::debug!(" Transfer proof: {:?}", transfer_proof);
Ok((transfer_proof, fee))
}
}
#[async_trait]
impl CreateWalletForOutput for Wallet {
async fn create_and_load_wallet_for_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> Result<()> {
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
let address = Address::standard(self.network, public_spend_key, public_view_key);
let _ = self
.inner
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
Ok(())
}
}
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
// to `ConstantBackoff`.
#[async_trait]
impl WatchForTransfer for Wallet {
async fn watch_for_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
expected_amount: Amount,
expected_confirmations: u32,
) -> Result<(), InsufficientFunds> {
enum Error {
TxNotFound,
InsufficientConfirmations,
InsufficientFunds { expected: Amount, actual: Amount },
}
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
let res = (|| async {
// NOTE: Currently, this is conflating IO errors with the transaction not being
// in the blockchain yet, or not having enough confirmations on it. All these
// errors warrant a retry, but the strategy should probably differ per case
let proof = self
.inner
.check_tx_key(
&String::from(transfer_proof.tx_hash()),
&transfer_proof.tx_key().to_string(),
&address.to_string(),
)
.await
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
if proof.received != expected_amount.as_piconero() {
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
expected: expected_amount,
actual: Amount::from_piconero(proof.received),
}));
}
if proof.confirmations < expected_confirmations {
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
}
Ok(proof)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await;
if let Err(Error::InsufficientFunds { expected, actual }) = res {
return Err(InsufficientFunds { expected, actual });
};
Ok(())
}
}

@ -1,3 +1,4 @@
use crate::monero;
use async_trait::async_trait;
use futures::prelude::*;
use libp2p::{
@ -8,8 +9,10 @@ use serde::{Deserialize, Serialize};
use std::{fmt::Debug, io, marker::PhantomData};
use tracing::debug;
use crate::SwapAmounts;
use xmr_btc::{alice, bob, monero};
use crate::{
protocol::{alice, bob},
SwapAmounts,
};
/// Time to wait for a response back once we send a request.
pub const TIMEOUT: u64 = 3600; // One hour.

@ -0,0 +1,2 @@
pub mod alice;
pub mod bob;

@ -1,6 +1,14 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
pub use self::{amounts::*, message0::*, message1::*, message2::*, message3::*, state::*};
use anyhow::Result;
use libp2p::{
core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel,
NetworkBehaviour, PeerId,
};
use tracing::{debug, info};
use crate::{
network::{
peer_tracker::{self, PeerTracker},
@ -8,16 +16,9 @@ use crate::{
transport::SwapTransport,
TokioExecutor,
},
protocol::bob,
SwapAmounts,
};
use anyhow::Result;
use libp2p::{
core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel,
NetworkBehaviour, PeerId,
};
use tracing::{debug, info};
use xmr_btc::bob;
mod amounts;
pub mod event_loop;
@ -25,6 +26,7 @@ mod message0;
mod message1;
mod message2;
mod message3;
pub mod state;
mod steps;
pub mod swap;
@ -133,10 +135,10 @@ impl From<message3::OutEvent> for OutEvent {
pub struct Behaviour {
pt: PeerTracker,
amounts: Amounts,
message0: Message0,
message1: Message1,
message2: Message2,
message3: Message3,
message0: Message0Behaviour,
message1: Message1Behaviour,
message2: Message2Behaviour,
message3: Message3Behaviour,
#[behaviour(ignore)]
identity: Keypair,
}
@ -158,31 +160,19 @@ impl Behaviour {
}
/// Send Message0 to Bob in response to receiving his Message0.
pub fn send_message0(
&mut self,
channel: ResponseChannel<AliceToBob>,
msg: xmr_btc::alice::Message0,
) {
pub fn send_message0(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message0) {
self.message0.send(channel, msg);
debug!("Sent Message0");
}
/// Send Message1 to Bob in response to receiving his Message1.
pub fn send_message1(
&mut self,
channel: ResponseChannel<AliceToBob>,
msg: xmr_btc::alice::Message1,
) {
pub fn send_message1(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message1) {
self.message1.send(channel, msg);
debug!("Sent Message1");
}
/// Send Message2 to Bob in response to receiving his Message2.
pub fn send_message2(
&mut self,
channel: ResponseChannel<AliceToBob>,
msg: xmr_btc::alice::Message2,
) {
pub fn send_message2(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message2) {
self.message2.send(channel, msg);
debug!("Sent Message2");
}
@ -195,10 +185,10 @@ impl Default for Behaviour {
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: Message0::default(),
message1: Message1::default(),
message2: Message2::default(),
message3: Message3::default(),
message0: Message0Behaviour::default(),
message1: Message1Behaviour::default(),
message2: Message2Behaviour::default(),
message3: Message3Behaviour::default(),
identity,
}
}

@ -14,8 +14,8 @@ use std::{
use tracing::{debug, error};
use crate::{
alice::amounts,
network::request_response::{AliceToBob, AmountsProtocol, BobToAlice, Codec, TIMEOUT},
protocol::alice::amounts,
};
#[derive(Debug)]

@ -1,15 +1,19 @@
use crate::{
alice::{Behaviour, OutEvent},
network::{request_response::AliceToBob, transport::SwapTransport, TokioExecutor},
SwapAmounts,
};
use anyhow::{anyhow, Context, Result};
use futures::FutureExt;
use libp2p::{
core::Multiaddr, futures::StreamExt, request_response::ResponseChannel, PeerId, Swarm,
};
use tokio::sync::mpsc::{Receiver, Sender};
use xmr_btc::{alice, bob};
use crate::{
network::{request_response::AliceToBob, transport::SwapTransport, TokioExecutor},
protocol::{
alice,
alice::{Behaviour, OutEvent},
bob,
},
SwapAmounts,
};
#[allow(missing_debug_implementations)]
pub struct Channels<T> {
@ -36,7 +40,7 @@ pub struct EventLoopHandle {
msg1: Receiver<(bob::Message1, ResponseChannel<AliceToBob>)>,
msg2: Receiver<(bob::Message2, ResponseChannel<AliceToBob>)>,
msg3: Receiver<bob::Message3>,
request: Receiver<crate::alice::amounts::OutEvent>,
request: Receiver<crate::protocol::alice::amounts::OutEvent>,
conn_established: Receiver<PeerId>,
send_amounts: Sender<(ResponseChannel<AliceToBob>, SwapAmounts)>,
send_msg0: Sender<(ResponseChannel<AliceToBob>, alice::Message0)>,
@ -80,7 +84,7 @@ impl EventLoopHandle {
.ok_or_else(|| anyhow!("Failed to receive Bitcoin encrypted signature from Bob"))
}
pub async fn recv_request(&mut self) -> Result<crate::alice::amounts::OutEvent> {
pub async fn recv_request(&mut self) -> Result<crate::protocol::alice::amounts::OutEvent> {
self.request
.recv()
.await
@ -131,7 +135,7 @@ pub struct EventLoop {
msg1: Sender<(bob::Message1, ResponseChannel<AliceToBob>)>,
msg2: Sender<(bob::Message2, ResponseChannel<AliceToBob>)>,
msg3: Sender<bob::Message3>,
request: Sender<crate::alice::amounts::OutEvent>,
request: Sender<crate::protocol::alice::amounts::OutEvent>,
conn_established: Sender<PeerId>,
send_amounts: Receiver<(ResponseChannel<AliceToBob>, SwapAmounts)>,
send_msg0: Receiver<(ResponseChannel<AliceToBob>, alice::Message0)>,

@ -1,11 +1,12 @@
use libp2p::{
request_response::{
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
RequestResponseEvent, RequestResponseMessage,
RequestResponseEvent, RequestResponseMessage, ResponseChannel,
},
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
NetworkBehaviour,
};
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
task::{Context, Poll},
@ -13,9 +14,11 @@ use std::{
};
use tracing::{debug, error};
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT};
use libp2p::request_response::ResponseChannel;
use xmr_btc::bob;
use crate::{
bitcoin, monero,
network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT},
protocol::bob,
};
#[derive(Debug)]
pub enum OutEvent {
@ -25,18 +28,29 @@ pub enum OutEvent {
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message0 {
pub(crate) A: bitcoin::PublicKey,
pub(crate) S_a_monero: monero::PublicKey,
pub(crate) S_a_bitcoin: bitcoin::PublicKey,
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof,
pub(crate) v_a: monero::PrivateViewKey,
pub(crate) redeem_address: bitcoin::Address,
pub(crate) punish_address: bitcoin::Address,
}
/// A `NetworkBehaviour` that represents send/recv of message 0.
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message0 {
pub struct Message0Behaviour {
rr: RequestResponse<Codec<Message0Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message0 {
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message0) {
impl Message0Behaviour {
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message0) {
let msg = AliceToBob::Message0(Box::new(msg));
self.rr.send_response(channel, msg);
}
@ -53,7 +67,7 @@ impl Message0 {
}
}
impl Default for Message0 {
impl Default for Message0Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -70,7 +84,9 @@ impl Default for Message0 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message0Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -1,3 +1,4 @@
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
use libp2p::{
request_response::{
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
@ -6,6 +7,7 @@ use libp2p::{
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
NetworkBehaviour,
};
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
task::{Context, Poll},
@ -13,8 +15,10 @@ use std::{
};
use tracing::{debug, error};
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT};
use xmr_btc::bob;
use crate::{
network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT},
protocol::bob,
};
#[derive(Debug)]
pub enum OutEvent {
@ -26,18 +30,24 @@ pub enum OutEvent {
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message1 {
pub(crate) tx_cancel_sig: Signature,
pub(crate) tx_refund_encsig: EncryptedSignature,
}
/// A `NetworkBehaviour` that represents send/recv of message 1.
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message1 {
pub struct Message1Behaviour {
rr: RequestResponse<Codec<Message1Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message1 {
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message1) {
impl Message1Behaviour {
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message1) {
let msg = AliceToBob::Message1(Box::new(msg));
self.rr.send_response(channel, msg);
}
@ -55,7 +65,7 @@ impl Message1 {
}
}
impl Default for Message1 {
impl Default for Message1Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -72,7 +82,9 @@ impl Default for Message1 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message1Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -6,6 +6,7 @@ use libp2p::{
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
NetworkBehaviour,
};
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
task::{Context, Poll},
@ -13,8 +14,11 @@ use std::{
};
use tracing::{debug, error};
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT};
use xmr_btc::bob;
use crate::{
monero,
network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT},
protocol::bob,
};
#[derive(Debug)]
pub enum OutEvent {
@ -26,18 +30,23 @@ pub enum OutEvent {
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message2 {
pub tx_lock_proof: monero::TransferProof,
}
/// A `NetworkBehaviour` that represents receiving of message 2 from Bob.
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message2 {
pub struct Message2Behaviour {
rr: RequestResponse<Codec<Message2Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message2 {
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message2) {
impl Message2Behaviour {
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message2) {
let msg = AliceToBob::Message2(msg);
self.rr.send_response(channel, msg);
}
@ -55,7 +64,7 @@ impl Message2 {
}
}
impl Default for Message2 {
impl Default for Message2Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -72,7 +81,9 @@ impl Default for Message2 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message2Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -13,8 +13,10 @@ use std::{
};
use tracing::{debug, error};
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT};
use xmr_btc::bob;
use crate::{
network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT},
protocol::bob,
};
#[derive(Debug)]
pub enum OutEvent {
@ -25,13 +27,13 @@ pub enum OutEvent {
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message3 {
pub struct Message3Behaviour {
rr: RequestResponse<Codec<Message3Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message3 {
impl Message3Behaviour {
fn poll(
&mut self,
_: &mut Context<'_>,
@ -45,7 +47,7 @@ impl Message3 {
}
}
impl Default for Message3 {
impl Default for Message3Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -62,7 +64,9 @@ impl Default for Message3 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message3Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -1,439 +1,25 @@
use crate::{
bitcoin,
bitcoin::{poll_until_block_height_is_gte, BroadcastSignedTransaction, WatchForRawTransaction},
bob, monero,
monero::{CreateWalletForOutput, Transfer},
transport::{ReceiveMessage, SendMessage},
ExpiredTimelocks,
};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use ecdsa_fun::{
adaptor::{Adaptor, EncryptedSignature},
nonce::Deterministic,
};
use futures::{
future::{select, Either},
pin_mut, FutureExt,
};
use genawaiter::sync::{Gen, GenBoxed};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::{
convert::{TryFrom, TryInto},
sync::Arc,
time::Duration,
};
use tokio::{sync::Mutex, time::timeout};
use tracing::{error, info};
pub mod message;
use crate::bitcoin::{
current_epoch, wait_for_cancel_timelock_to_expire, GetBlockHeight, Timelock,
TransactionBlockHeight,
};
pub use message::{Message, Message0, Message1, Message2};
#[derive(Debug)]
pub enum Action {
// This action also includes proving to Bob that this has happened, given that our current
// protocol requires a transfer proof to verify that the coins have been locked on Monero
LockXmr {
amount: monero::Amount,
public_spend_key: monero::PublicKey,
public_view_key: monero::PublicViewKey,
},
RedeemBtc(bitcoin::Transaction),
CreateMoneroWalletForOutput {
spend_key: monero::PrivateKey,
view_key: monero::PrivateViewKey,
},
CancelBtc(bitcoin::Transaction),
PunishBtc(bitcoin::Transaction),
}
// TODO: This could be moved to the bitcoin module
#[async_trait]
pub trait ReceiveBitcoinRedeemEncsig {
async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature;
}
/// Perform the on-chain protocol to swap monero and bitcoin as Alice.
///
/// This is called post handshake, after all the keys, addresses and most of the
/// signatures have been exchanged.
///
/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will
/// wait for Bob, the counterparty, to lock up the bitcoin.
pub fn action_generator<N, B>(
network: Arc<Mutex<N>>,
bitcoin_client: Arc<B>,
// TODO: Replace this with a new, slimmer struct?
State3 {
a,
B,
s_a,
S_b_monero,
S_b_bitcoin,
v,
xmr,
cancel_timelock,
punish_timelock,
refund_address,
redeem_address,
punish_address,
tx_lock,
tx_punish_sig_bob,
tx_cancel_sig_bob,
..
}: State3,
bitcoin_tx_lock_timeout: u64,
) -> GenBoxed<Action, (), ()>
where
N: ReceiveBitcoinRedeemEncsig + Send + 'static,
B: bitcoin::GetBlockHeight
+ bitcoin::TransactionBlockHeight
+ bitcoin::WatchForRawTransaction
+ Send
+ Sync
+ 'static,
{
#[derive(Debug)]
enum SwapFailed {
BeforeBtcLock(Reason),
AfterXmrLock(Reason),
}
/// Reason why the swap has failed.
#[derive(Debug)]
enum Reason {
/// Bob was too slow to lock the bitcoin.
InactiveBob,
/// Bob's encrypted signature on the Bitcoin redeem transaction is
/// invalid.
InvalidEncryptedSignature,
/// The refund timelock has been reached.
BtcExpired,
}
#[derive(Debug)]
enum RefundFailed {
BtcPunishable,
/// Could not find Alice's signature on the refund transaction witness
/// stack.
BtcRefundSignature,
/// Could not recover secret `s_b` from Alice's refund transaction
/// signature.
SecretRecovery,
}
Gen::new_boxed(|co| async move {
let swap_result: Result<(), SwapFailed> = async {
timeout(
Duration::from_secs(bitcoin_tx_lock_timeout),
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
)
.await
.map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?;
let tx_lock_height = bitcoin_client
.transaction_block_height(tx_lock.txid())
.await;
let poll_until_btc_has_expired = poll_until_block_height_is_gte(
bitcoin_client.as_ref(),
tx_lock_height + cancel_timelock,
)
.shared();
pin_mut!(poll_until_btc_has_expired);
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: s_a.into_ed25519(),
});
co.yield_(Action::LockXmr {
amount: xmr,
public_spend_key: S_a + S_b_monero,
public_view_key: v.public(),
})
.await;
// TODO: Watch for LockXmr using watch-only wallet. Doing so will prevent Alice
// from cancelling/refunding unnecessarily.
let tx_redeem_encsig = {
let mut guard = network.as_ref().lock().await;
let tx_redeem_encsig = match select(
guard.receive_bitcoin_redeem_encsig(),
poll_until_btc_has_expired.clone(),
)
.await
{
Either::Left((encsig, _)) => encsig,
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
};
tracing::debug!("select returned redeem encsig from message");
tx_redeem_encsig
};
let (signed_tx_redeem, tx_redeem_txid) = {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address);
bitcoin::verify_encsig(
B,
s_a.into_secp256k1().into(),
&tx_redeem.digest(),
&tx_redeem_encsig,
)
.map_err(|_| SwapFailed::AfterXmrLock(Reason::InvalidEncryptedSignature))?;
let sig_a = a.sign(tx_redeem.digest());
let sig_b =
adaptor.decrypt_signature(&s_a.into_secp256k1(), tx_redeem_encsig.clone());
let tx = tx_redeem
.add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_redeem");
let txid = tx.txid();
(tx, txid)
};
co.yield_(Action::RedeemBtc(signed_tx_redeem)).await;
match select(
bitcoin_client.watch_for_raw_transaction(tx_redeem_txid),
poll_until_btc_has_expired,
)
.await
{
Either::Left(_) => {}
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
};
Ok(())
}
.await;
if let Err(ref err) = swap_result {
error!("swap failed: {:?}", err);
}
if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result {
let refund_result: Result<(), RefundFailed> = async {
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
let signed_tx_cancel = {
let sig_a = a.sign(tx_cancel.digest());
let sig_b = tx_cancel_sig_bob.clone();
tx_cancel
.clone()
.add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel")
};
co.yield_(Action::CancelBtc(signed_tx_cancel)).await;
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,
)
.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(
bitcoin_client.watch_for_raw_transaction(tx_refund.txid()),
poll_until_bob_can_be_punished,
)
.await
{
Either::Left((tx, _)) => tx,
Either::Right(_) => return Err(RefundFailed::BtcPunishable),
};
let s_a = monero::PrivateKey {
scalar: s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(tx_refund_published, a.public())
.map_err(|_| RefundFailed::BtcRefundSignature)?;
let tx_refund_encsig = a.encsign(S_b_bitcoin, tx_refund.digest());
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
.map_err(|_| RefundFailed::SecretRecovery)?;
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
co.yield_(Action::CreateMoneroWalletForOutput {
spend_key: s_a + s_b,
view_key: v,
})
.await;
Ok(())
}
.await;
if let Err(ref err) = refund_result {
error!("refund failed: {:?}", err);
}
// LIMITATION: When approaching the punish scenario, Bob could theoretically
// wake up in between Alice's publication of tx cancel and beat Alice's punish
// transaction with his refund transaction. Alice would then need to carry on
// with the refund on Monero. Doing so may be too verbose with the current,
// linear approach. A different design may be required
if let Err(RefundFailed::BtcPunishable) = refund_result {
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
let tx_punish_txid = tx_punish.txid();
let signed_tx_punish = {
let sig_a = a.sign(tx_punish.digest());
let sig_b = tx_punish_sig_bob;
tx_punish
.add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel")
};
co.yield_(Action::PunishBtc(signed_tx_punish)).await;
let _ = bitcoin_client
.watch_for_raw_transaction(tx_punish_txid)
.await;
}
}
})
}
use tracing::info;
// There are no guarantees that send_message and receive_massage do not block
// the flow of execution. Therefore they must be paired between Alice/Bob, one
// send to one receive in the correct order.
pub async fn next_state<
R: RngCore + CryptoRng,
B: WatchForRawTransaction + BroadcastSignedTransaction,
M: CreateWalletForOutput + Transfer,
T: SendMessage<Message> + ReceiveMessage<bob::Message>,
>(
bitcoin_wallet: &B,
monero_wallet: &M,
transport: &mut T,
state: State,
rng: &mut R,
) -> Result<State> {
match state {
State::State0(state0) => {
let alice_message0 = state0.next_message(rng).into();
let bob_message0 = transport.receive_message().await?.try_into()?;
transport.send_message(alice_message0).await?;
let state1 = state0.receive(bob_message0)?;
Ok(state1.into())
}
State::State1(state1) => {
let bob_message1 = transport.receive_message().await?.try_into()?;
let state2 = state1.receive(bob_message1);
let alice_message1 = state2.next_message();
transport.send_message(alice_message1.into()).await?;
Ok(state2.into())
}
State::State2(state2) => {
let bob_message2 = transport.receive_message().await?.try_into()?;
let state3 = state2.receive(bob_message2)?;
Ok(state3.into())
}
State::State3(state3) => {
tracing::info!("alice is watching for locked btc");
let state4 = state3.watch_for_lock_btc(bitcoin_wallet).await?;
Ok(state4.into())
}
State::State4(state4) => {
let state5 = state4.lock_xmr(monero_wallet).await?;
tracing::info!("alice has locked xmr");
Ok(state5.into())
}
State::State5(state5) => {
transport.send_message(state5.next_message().into()).await?;
// todo: pass in state4b as a parameter somewhere in this call to prevent the
// user from waiting for a message that wont be sent
let message3 = transport.receive_message().await?.try_into()?;
let state6 = state5.receive(message3);
tracing::info!("alice has received bob message 3");
tracing::info!("alice is redeeming btc");
state6.redeem_btc(bitcoin_wallet).await?;
Ok(state6.into())
}
State::State6(state6) => Ok((*state6).into()),
}
}
#[derive(Debug, Deserialize, Serialize)]
pub enum State {
State0(State0),
State1(State1),
State2(State2),
State3(State3),
State4(State4),
State5(State5),
State6(Box<State6>),
}
impl_try_from_parent_enum!(State0, State);
impl_try_from_parent_enum!(State1, State);
impl_try_from_parent_enum!(State2, State);
impl_try_from_parent_enum!(State3, State);
impl_try_from_parent_enum!(State4, State);
impl_try_from_parent_enum!(State5, State);
impl_try_from_parent_enum_for_boxed!(State6, State);
impl_from_child_enum!(State0, State);
impl_from_child_enum!(State1, State);
impl_from_child_enum!(State2, State);
impl_from_child_enum!(State3, State);
impl_from_child_enum!(State4, State);
impl_from_child_enum!(State5, State);
impl_from_child_enum_for_boxed!(State6, State);
impl State {
pub fn new<R: RngCore + CryptoRng>(
rng: &mut R,
btc: bitcoin::Amount,
xmr: monero::Amount,
cancel_timelock: Timelock,
punish_timelock: Timelock,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
) -> Self {
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
Self::State0(State0::new(
a,
s_a,
v_a,
btc,
xmr,
cancel_timelock,
punish_timelock,
redeem_address,
punish_address,
))
}
}
use crate::{
bitcoin,
bitcoin::{
current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire, GetBlockHeight,
TransactionBlockHeight, WatchForRawTransaction,
},
monero,
monero::CreateWalletForOutput,
protocol::{alice, bob},
ExpiredTimelocks,
};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct State0 {
@ -475,11 +61,11 @@ impl State0 {
}
}
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> alice::Message0 {
info!("Producing first message");
let dleq_proof_s_a = cross_curve_dleq::Proof::new(rng, &self.s_a);
Message0 {
alice::Message0 {
A: self.a.public(),
S_a_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: self.s_a.into_ed25519(),
@ -580,7 +166,7 @@ pub struct State2 {
}
impl State2 {
pub fn next_message(&self) -> Message1 {
pub fn next_message(&self) -> alice::Message1 {
let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
@ -593,7 +179,7 @@ impl State2 {
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin, tx_refund.digest());
let tx_cancel_sig = self.a.sign(tx_cancel.digest());
Message1 {
alice::Message1 {
tx_refund_encsig,
tx_cancel_sig,
}
@ -831,8 +417,8 @@ pub struct State5 {
}
impl State5 {
pub fn next_message(&self) -> Message2 {
Message2 {
pub fn next_message(&self) -> alice::Message2 {
alice::Message2 {
tx_lock_proof: self.tx_lock_proof.clone(),
}
}

@ -1,7 +1,3 @@
use crate::{
alice::event_loop::EventLoopHandle, bitcoin, monero, network::request_response::AliceToBob,
SwapAmounts,
};
use anyhow::{bail, Context, Result};
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
use futures::{
@ -14,25 +10,30 @@ use sha2::Sha256;
use std::{sync::Arc, time::Duration};
use tokio::time::timeout;
use tracing::{info, trace};
use xmr_btc::{
alice,
alice::State3,
use crate::{
bitcoin,
bitcoin::{
poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction,
EncryptedSignature, GetBlockHeight, GetRawTransaction, Timelock, TransactionBlockHeight,
TxCancel, TxLock, TxRefund, WaitForTransactionFinality, WatchForRawTransaction,
poll_until_block_height_is_gte,
timelocks::{BlockHeight, Timelock},
BroadcastSignedTransaction, EncryptedSignature, GetBlockHeight, GetRawTransaction,
TransactionBlockHeight, TxCancel, TxLock, TxRefund, WaitForTransactionFinality,
WatchForRawTransaction,
},
config::Config,
cross_curve_dleq,
monero,
monero::Transfer,
network::request_response::AliceToBob,
protocol::{alice, alice::event_loop::EventLoopHandle},
SwapAmounts,
};
pub async fn negotiate(
state0: xmr_btc::alice::State0,
state0: alice::State0,
amounts: SwapAmounts,
event_loop_handle: &mut EventLoopHandle,
config: Config,
) -> Result<(ResponseChannel<AliceToBob>, State3)> {
) -> Result<(ResponseChannel<AliceToBob>, alice::State3)> {
trace!("Starting negotiate");
// todo: we can move this out, we dont need to timeout here
@ -115,7 +116,7 @@ where
pub async fn lock_xmr<W>(
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
state3: alice::State3,
event_loop_handle: &mut EventLoopHandle,
monero_wallet: Arc<W>,
) -> Result<()>

@ -1,20 +1,5 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use crate::{
alice::{
event_loop::EventLoopHandle,
steps::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
},
},
bitcoin::EncryptedSignature,
database::{Database, Swap},
network::request_response::AliceToBob,
SwapAmounts,
};
use anyhow::Result;
use async_recursion::async_recursion;
use futures::{
@ -26,12 +11,28 @@ use rand::{CryptoRng, RngCore};
use std::{fmt, sync::Arc};
use tracing::info;
use uuid::Uuid;
use xmr_btc::{
alice::{State0, State3},
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
use crate::{
bitcoin,
bitcoin::{
EncryptedSignature, TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction,
},
config::Config,
database::{Database, Swap},
monero,
monero::CreateWalletForOutput,
ExpiredTimelocks,
network::request_response::AliceToBob,
protocol::alice::{
event_loop::EventLoopHandle,
state::{State0, State3},
steps::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
},
},
ExpiredTimelocks, SwapAmounts,
};
trait Rng: RngCore + CryptoRng + Send {}
@ -105,8 +106,8 @@ impl fmt::Display for AliceState {
pub async fn swap(
state: AliceState,
event_loop_handle: EventLoopHandle,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
swap_id: Uuid,
db: Database,

@ -1,24 +1,22 @@
//! Run an XMR/BTC swap in the role of Bob.
//! Bob holds BTC and wishes receive XMR.
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
TokioExecutor,
},
SwapAmounts,
};
pub use self::{amounts::*, message0::*, message1::*, message2::*, message3::*, state::*};
use anyhow::Result;
use libp2p::{
core::{identity::Keypair, Multiaddr},
NetworkBehaviour, PeerId,
};
use tracing::{debug, info};
use xmr_btc::{
alice,
use crate::{
bitcoin::EncryptedSignature,
bob::{self},
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
TokioExecutor,
},
protocol::{alice, bob},
SwapAmounts,
};
mod amounts;
@ -27,6 +25,7 @@ mod message0;
mod message1;
mod message2;
mod message3;
pub mod state;
pub mod swap;
pub type Swarm = libp2p::Swarm<Behaviour>;
@ -112,10 +111,10 @@ impl From<message3::OutEvent> for OutEvent {
pub struct Behaviour {
pt: PeerTracker,
amounts: Amounts,
message0: Message0,
message1: Message1,
message2: Message2,
message3: Message3,
message0: Message0Behaviour,
message1: Message1Behaviour,
message2: Message2Behaviour,
message3: Message3Behaviour,
#[behaviour(ignore)]
identity: Keypair,
}
@ -174,10 +173,10 @@ impl Default for Behaviour {
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: Message0::default(),
message1: Message1::default(),
message2: Message2::default(),
message3: Message3::default(),
message0: Message0Behaviour::default(),
message1: Message1Behaviour::default(),
message2: Message2Behaviour::default(),
message3: Message3Behaviour::default(),
identity,
}
}

@ -1,7 +1,3 @@
use crate::{
bob::{Behaviour, OutEvent},
network::{transport::SwapTransport, TokioExecutor},
};
use anyhow::{anyhow, Result};
use futures::FutureExt;
use libp2p::{core::Multiaddr, PeerId};
@ -10,7 +6,15 @@ use tokio::{
sync::mpsc::{Receiver, Sender},
};
use tracing::{debug, error, info};
use xmr_btc::{alice, bitcoin::EncryptedSignature, bob};
use crate::{
bitcoin::EncryptedSignature,
network::{transport::SwapTransport, TokioExecutor},
protocol::{
alice,
bob::{self, Behaviour, OutEvent},
},
};
#[derive(Debug)]
pub struct Channels<T> {

@ -6,6 +6,7 @@ use libp2p::{
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
NetworkBehaviour, PeerId,
};
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
task::{Context, Poll},
@ -13,8 +14,21 @@ use std::{
};
use tracing::{debug, error};
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT};
use xmr_btc::{alice, bob};
use crate::{
bitcoin, monero,
network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT},
protocol::{alice, bob},
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message0 {
pub(crate) B: bitcoin::PublicKey,
pub(crate) S_b_monero: monero::PublicKey,
pub(crate) S_b_bitcoin: bitcoin::PublicKey,
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof,
pub(crate) v_b: monero::PrivateViewKey,
pub(crate) refund_address: bitcoin::Address,
}
#[derive(Debug)]
pub enum OutEvent {
@ -25,13 +39,13 @@ pub enum OutEvent {
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message0 {
pub struct Message0Behaviour {
rr: RequestResponse<Codec<Message0Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message0 {
impl Message0Behaviour {
pub fn send(&mut self, alice: PeerId, msg: bob::Message0) {
let msg = BobToAlice::Message0(Box::new(msg));
let _id = self.rr.send_request(&alice, msg);
@ -50,7 +64,7 @@ impl Message0 {
}
}
impl Default for Message0 {
impl Default for Message0Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -67,7 +81,9 @@ impl Default for Message0 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message0Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -6,15 +6,25 @@ use libp2p::{
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
NetworkBehaviour, PeerId,
};
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
task::{Context, Poll},
time::Duration,
};
use tracing::{debug, error};
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT};
use xmr_btc::{alice, bob};
use crate::{
bitcoin,
network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT},
protocol::alice,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message1 {
pub(crate) tx_lock: bitcoin::TxLock,
}
#[derive(Debug)]
pub enum OutEvent {
@ -25,14 +35,14 @@ pub enum OutEvent {
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message1 {
pub struct Message1Behaviour {
rr: RequestResponse<Codec<Message1Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message1 {
pub fn send(&mut self, alice: PeerId, msg: bob::Message1) {
impl Message1Behaviour {
pub fn send(&mut self, alice: PeerId, msg: Message1) {
let msg = BobToAlice::Message1(msg);
let _id = self.rr.send_request(&alice, msg);
}
@ -50,7 +60,7 @@ impl Message1 {
}
}
impl Default for Message1 {
impl Default for Message1Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -67,7 +77,9 @@ impl Default for Message1 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message1Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -1,3 +1,4 @@
use ecdsa_fun::Signature;
use libp2p::{
request_response::{
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
@ -6,15 +7,25 @@ use libp2p::{
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
NetworkBehaviour, PeerId,
};
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
task::{Context, Poll},
time::Duration,
};
use tracing::{debug, error};
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT};
use xmr_btc::{alice, bob};
use crate::{
network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT},
protocol::alice,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message2 {
pub(crate) tx_punish_sig: Signature,
pub(crate) tx_cancel_sig: Signature,
}
#[derive(Debug)]
pub enum OutEvent {
@ -25,14 +36,14 @@ pub enum OutEvent {
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message2 {
pub struct Message2Behaviour {
rr: RequestResponse<Codec<Message2Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message2 {
pub fn send(&mut self, alice: PeerId, msg: bob::Message2) {
impl Message2Behaviour {
pub fn send(&mut self, alice: PeerId, msg: Message2) {
let msg = BobToAlice::Message2(msg);
let _id = self.rr.send_request(&alice, msg);
}
@ -50,7 +61,7 @@ impl Message2 {
}
}
impl Default for Message2 {
impl Default for Message2Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -67,7 +78,9 @@ impl Default for Message2 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message2Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -6,6 +6,7 @@ use libp2p::{
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
NetworkBehaviour, PeerId,
};
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
task::{Context, Poll},
@ -13,8 +14,15 @@ use std::{
};
use tracing::error;
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT};
use xmr_btc::bob;
use crate::{
bitcoin::EncryptedSignature,
network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT},
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message3 {
pub tx_redeem_encsig: EncryptedSignature,
}
#[derive(Debug, Copy, Clone)]
pub enum OutEvent {
@ -25,14 +33,14 @@ pub enum OutEvent {
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
#[allow(missing_debug_implementations)]
pub struct Message3 {
pub struct Message3Behaviour {
rr: RequestResponse<Codec<Message3Protocol>>,
#[behaviour(ignore)]
events: VecDeque<OutEvent>,
}
impl Message3 {
pub fn send(&mut self, alice: PeerId, msg: bob::Message3) {
impl Message3Behaviour {
pub fn send(&mut self, alice: PeerId, msg: Message3) {
let msg = BobToAlice::Message3(msg);
let _id = self.rr.send_request(&alice, msg);
}
@ -50,7 +58,7 @@ impl Message3 {
}
}
impl Default for Message3 {
impl Default for Message3Behaviour {
fn default() -> Self {
let timeout = Duration::from_secs(TIMEOUT);
let mut config = RequestResponseConfig::default();
@ -67,7 +75,9 @@ impl Default for Message3 {
}
}
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>>
for Message3Behaviour
{
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
match event {
RequestResponseEvent::Message {

@ -1,352 +1,25 @@
use crate::{
alice,
bitcoin::{
self, poll_until_block_height_is_gte, BroadcastSignedTransaction, BuildTxLockPsbt,
SignTxLock, TxCancel, WatchForRawTransaction,
},
monero,
serde::monero_private_key,
transport::{ReceiveMessage, SendMessage},
ExpiredTimelocks,
};
use ::bitcoin::{Transaction, Txid};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use ecdsa_fun::{
adaptor::{Adaptor, EncryptedSignature},
nonce::Deterministic,
Signature,
};
use futures::{
future::{select, Either},
pin_mut, FutureExt,
};
use genawaiter::sync::{Gen, GenBoxed};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::{
convert::{TryFrom, TryInto},
sync::Arc,
time::Duration,
};
use tokio::{sync::Mutex, time::timeout};
use tracing::error;
pub mod message;
use crate::{
bitcoin::{
current_epoch, wait_for_cancel_timelock_to_expire, GetBlockHeight, GetRawTransaction,
Network, Timelock, TransactionBlockHeight,
self, current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire,
BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, GetRawTransaction, Network,
TransactionBlockHeight, TxCancel, WatchForRawTransaction,
},
monero::{CreateWalletForOutput, WatchForTransfer},
monero,
protocol::{alice, bob},
serde::monero_private_key,
ExpiredTimelocks,
};
use ::bitcoin::{Transaction, Txid};
pub use message::{Message, Message0, Message1, Message2, Message3};
#[derive(Debug)]
pub enum Action {
LockBtc(bitcoin::TxLock),
SendBtcRedeemEncsig(bitcoin::EncryptedSignature),
CreateXmrWalletForOutput {
spend_key: monero::PrivateKey,
view_key: monero::PrivateViewKey,
},
CancelBtc(bitcoin::Transaction),
RefundBtc(bitcoin::Transaction),
}
// TODO: This could be moved to the monero module
#[async_trait]
pub trait ReceiveTransferProof {
async fn receive_transfer_proof(&mut self) -> monero::TransferProof;
}
/// Perform the on-chain protocol to swap monero and bitcoin as Bob.
///
/// This is called post handshake, after all the keys, addresses and most of the
/// signatures have been exchanged.
///
/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will
/// wait for Bob, the caller of this function, to lock up the bitcoin.
pub fn action_generator<N, M, B>(
network: Arc<Mutex<N>>,
monero_client: Arc<M>,
bitcoin_client: Arc<B>,
// TODO: Replace this with a new, slimmer struct?
State2 {
A,
b,
s_b,
S_a_monero,
S_a_bitcoin,
v,
xmr,
cancel_timelock,
redeem_address,
refund_address,
tx_lock,
tx_cancel_sig_a,
tx_refund_encsig,
..
}: State2,
bitcoin_tx_lock_timeout: u64,
) -> GenBoxed<Action, (), ()>
where
N: ReceiveTransferProof + Send + 'static,
M: monero::WatchForTransfer + Send + Sync + 'static,
B: bitcoin::GetBlockHeight
+ bitcoin::TransactionBlockHeight
+ bitcoin::WatchForRawTransaction
+ Send
+ Sync
+ 'static,
{
#[derive(Debug)]
enum SwapFailed {
BeforeBtcLock(Reason),
AfterBtcLock(Reason),
AfterBtcRedeem(Reason),
}
/// Reason why the swap has failed.
#[derive(Debug)]
enum Reason {
/// Bob was too slow to lock the bitcoin.
InactiveBob,
/// The refund timelock has been reached.
BtcExpired,
/// Alice did not lock up enough monero in the shared output.
InsufficientXmr(monero::InsufficientFunds),
/// Could not find Bob's signature on the redeem transaction witness
/// stack.
BtcRedeemSignature,
/// Could not recover secret `s_a` from Bob's redeem transaction
/// signature.
SecretRecovery,
}
Gen::new_boxed(|co| async move {
let swap_result: Result<(), SwapFailed> = async {
co.yield_(Action::LockBtc(tx_lock.clone())).await;
timeout(
Duration::from_secs(bitcoin_tx_lock_timeout),
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
)
.await
.map(|tx| tx.txid())
.map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?;
let tx_lock_height = bitcoin_client
.transaction_block_height(tx_lock.txid())
.await;
let poll_until_btc_has_expired = poll_until_block_height_is_gte(
bitcoin_client.as_ref(),
tx_lock_height + cancel_timelock,
)
.shared();
pin_mut!(poll_until_btc_has_expired);
let transfer_proof = {
let mut guard = network.as_ref().lock().await;
let transfer_proof = match select(
guard.receive_transfer_proof(),
poll_until_btc_has_expired.clone(),
)
.await
{
Either::Left((proof, _)) => proof,
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
};
tracing::debug!("select returned transfer proof from message");
transfer_proof
};
let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(
s_b.into_ed25519(),
));
let S = S_a_monero + S_b_monero;
match select(
monero_client.watch_for_transfer(S, v.public(), transfer_proof, xmr, 0),
poll_until_btc_has_expired.clone(),
)
.await
{
Either::Left((Err(e), _)) => {
return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e)))
}
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
_ => {}
}
let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address);
let tx_redeem_encsig = b.encsign(S_a_bitcoin, tx_redeem.digest());
co.yield_(Action::SendBtcRedeemEncsig(tx_redeem_encsig.clone()))
.await;
let tx_redeem_published = match select(
bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()),
poll_until_btc_has_expired,
)
.await
{
Either::Left((tx, _)) => tx,
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
};
let tx_redeem_sig = tx_redeem
.extract_signature_by_key(tx_redeem_published, b.public())
.map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?;
let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)
.map_err(|_| SwapFailed::AfterBtcRedeem(Reason::SecretRecovery))?;
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
let s_b = monero::PrivateKey {
scalar: s_b.into_ed25519(),
};
co.yield_(Action::CreateXmrWalletForOutput {
spend_key: s_a + s_b,
view_key: v,
})
.await;
Ok(())
}
.await;
if let Err(ref err) = swap_result {
error!("swap failed: {:?}", err);
}
if let Err(SwapFailed::AfterBtcLock(_)) = swap_result {
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, A, b.public());
let tx_cancel_txid = tx_cancel.txid();
let signed_tx_cancel = {
let sig_a = tx_cancel_sig_a.clone();
let sig_b = b.sign(tx_cancel.digest());
tx_cancel
.clone()
.add_signatures(&tx_lock, (A, sig_a), (b.public(), 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_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
let tx_refund_txid = tx_refund.txid();
let signed_tx_refund = {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let sig_a =
adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone());
let sig_b = b.sign(tx_refund.digest());
tx_refund
.add_signatures(&tx_cancel, (A, sig_a), (b.public(), sig_b))
.expect("sig_{a,b} to be valid signatures for tx_refund")
};
co.yield_(Action::RefundBtc(signed_tx_refund)).await;
let _ = bitcoin_client
.watch_for_raw_transaction(tx_refund_txid)
.await;
}
})
}
// There are no guarantees that send_message and receive_massage do not block
// the flow of execution. Therefore they must be paired between Alice/Bob, one
// send to one receive in the correct order.
pub async fn next_state<
R: RngCore + CryptoRng,
B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction + Network,
M: CreateWalletForOutput + WatchForTransfer,
T: SendMessage<Message> + ReceiveMessage<alice::Message>,
>(
bitcoin_wallet: &B,
monero_wallet: &M,
transport: &mut T,
state: State,
rng: &mut R,
) -> Result<State> {
match state {
State::State0(state0) => {
transport
.send_message(state0.next_message(rng).into())
.await?;
let message0 = transport.receive_message().await?.try_into()?;
let state1 = state0.receive(bitcoin_wallet, message0).await?;
Ok(state1.into())
}
State::State1(state1) => {
transport.send_message(state1.next_message().into()).await?;
let message1 = transport.receive_message().await?.try_into()?;
let state2 = state1.receive(message1)?;
let message2 = state2.next_message();
transport.send_message(message2.into()).await?;
Ok(state2.into())
}
State::State2(state2) => {
let state3 = state2.lock_btc(bitcoin_wallet).await?;
tracing::info!("bob has locked btc");
Ok(state3.into())
}
State::State3(state3) => {
let message2 = transport.receive_message().await?.try_into()?;
let state4 = state3.watch_for_lock_xmr(monero_wallet, message2).await?;
tracing::info!("bob has seen that alice has locked xmr");
Ok(state4.into())
}
State::State4(state4) => {
transport.send_message(state4.next_message().into()).await?;
tracing::info!("bob is watching for redeem_btc");
let state5 = state4.watch_for_redeem_btc(bitcoin_wallet).await?;
tracing::info!("bob has seen that alice has redeemed btc");
state5.claim_xmr(monero_wallet).await?;
tracing::info!("bob has claimed xmr");
Ok(state5.into())
}
State::State5(state5) => Ok(state5.into()),
}
}
#[derive(Debug, Deserialize, Serialize)]
pub enum State {
State0(State0),
State1(State1),
State2(State2),
State3(State3),
State4(State4),
State5(State5),
}
impl_try_from_parent_enum!(State0, State);
impl_try_from_parent_enum!(State1, State);
impl_try_from_parent_enum!(State2, State);
impl_try_from_parent_enum!(State3, State);
impl_try_from_parent_enum!(State4, State);
impl_try_from_parent_enum!(State5, State);
impl_from_child_enum!(State0, State);
impl_from_child_enum!(State1, State);
impl_from_child_enum!(State2, State);
impl_from_child_enum!(State3, State);
impl_from_child_enum!(State4, State);
impl_from_child_enum!(State5, State);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct State0 {
@ -390,10 +63,10 @@ impl State0 {
}
}
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> bob::Message0 {
let dleq_proof_s_b = cross_curve_dleq::Proof::new(rng, &self.s_b);
Message0 {
bob::Message0 {
B: self.b.public(),
S_b_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: self.s_b.into_ed25519(),
@ -461,8 +134,8 @@ pub struct State1 {
}
impl State1 {
pub fn next_message(&self) -> Message1 {
Message1 {
pub fn next_message(&self) -> bob::Message1 {
bob::Message1 {
tx_lock: self.tx_lock.clone(),
}
}
@ -524,14 +197,14 @@ pub struct State2 {
}
impl State2 {
pub fn next_message(&self) -> Message2 {
pub fn next_message(&self) -> bob::Message2 {
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let tx_cancel_sig = self.b.sign(tx_cancel.digest());
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
let tx_punish_sig = self.b.sign(tx_punish.digest());
Message2 {
bob::Message2 {
tx_punish_sig,
tx_cancel_sig,
}
@ -705,11 +378,11 @@ pub struct State4 {
}
impl State4 {
pub fn next_message(&self) -> Message3 {
pub fn next_message(&self) -> bob::Message3 {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
Message3 { tx_redeem_encsig }
bob::Message3 { tx_redeem_encsig }
}
pub fn tx_redeem_encsig(&self) -> EncryptedSignature {

@ -1,4 +1,3 @@
use crate::{bob::event_loop::EventLoopHandle, database, database::Database, SwapAmounts};
use anyhow::{bail, Result};
use async_recursion::async_recursion;
use rand::{CryptoRng, RngCore};
@ -6,25 +5,28 @@ use std::{fmt, sync::Arc};
use tokio::select;
use tracing::info;
use uuid::Uuid;
use xmr_btc::{
bob::{self, State2},
ExpiredTimelocks,
use crate::{
config::Config,
database::{Database, Swap},
protocol::bob::{self, event_loop::EventLoopHandle, state::*},
ExpiredTimelocks, SwapAmounts,
};
#[derive(Debug, Clone)]
pub enum BobState {
Started {
state0: bob::State0,
state0: State0,
amounts: SwapAmounts,
},
Negotiated(bob::State2),
BtcLocked(bob::State3),
XmrLocked(bob::State4),
EncSigSent(bob::State4),
BtcRedeemed(bob::State5),
CancelTimelockExpired(bob::State4),
BtcCancelled(bob::State4),
BtcRefunded(bob::State4),
Negotiated(State2),
BtcLocked(State3),
XmrLocked(State4),
EncSigSent(State4),
BtcRedeemed(State5),
CancelTimelockExpired(State4),
BtcCancelled(State4),
BtcRefunded(State4),
XmrRedeemed,
BtcPunished,
SafelyAborted,
@ -133,8 +135,7 @@ where
let state = BobState::Negotiated(state2);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
.await?;
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
state,
is_target_state,
@ -155,8 +156,7 @@ where
let state = BobState::BtcLocked(state3);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
.await?;
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
state,
is_target_state,
@ -209,8 +209,7 @@ where
BobState::CancelTimelockExpired(state4)
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
.await?;
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
state,
is_target_state,
@ -251,8 +250,7 @@ where
BobState::CancelTimelockExpired(state)
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
.await?;
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
state,
is_target_state,
@ -287,8 +285,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
.await?;
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
state,
is_target_state,
@ -307,8 +304,7 @@ where
let state = BobState::XmrRedeemed;
let db_state = state.clone().into();
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
.await?;
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
state,
is_target_state,
@ -331,7 +327,7 @@ where
}
let state = BobState::BtcCancelled(state4);
db.insert_latest_state(swap_id, database::Swap::Bob(state.clone().into()))
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
.await?;
run_until(
@ -360,8 +356,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
.await?;
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
state,
is_target_state,
@ -383,12 +378,12 @@ where
}
pub async fn negotiate<R>(
state0: xmr_btc::bob::State0,
state0: crate::protocol::bob::state::State0,
amounts: SwapAmounts,
swarm: &mut EventLoopHandle,
mut rng: R,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
) -> Result<State2>
) -> Result<bob::state::State2>
where
R: RngCore + CryptoRng + Send,
{

@ -6,11 +6,15 @@ use futures::{
use get_port::get_port;
use libp2p::Multiaddr;
use rand::rngs::OsRng;
use swap::{alice, bob};
use swap::{
bitcoin,
config::Config,
monero,
protocol::{alice, bob},
};
use testcontainers::clients::Cli;
use testutils::init_tracing;
use uuid::Uuid;
use xmr_btc::{bitcoin, config::Config};
pub mod testutils;
@ -35,9 +39,9 @@ async fn happy_path() {
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
// 10_000 * 100
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000);
let xmr_alice = xmr_to_swap * 10;
let xmr_bob = xmr_btc::monero::Amount::ZERO;
let xmr_bob = monero::Amount::ZERO;
let port = get_port().expect("Failed to find a free port");
let alice_multiaddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port)

@ -2,12 +2,17 @@ use crate::testutils::{init_alice, init_bob};
use get_port::get_port;
use libp2p::Multiaddr;
use rand::rngs::OsRng;
use swap::{alice, alice::swap::AliceState, bitcoin, bob, database::Database};
use swap::{
bitcoin,
config::Config,
database::Database,
monero,
protocol::{alice, alice::swap::AliceState, bob},
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
use testutils::init_tracing;
use uuid::Uuid;
use xmr_btc::config::Config;
pub mod testutils;
@ -25,7 +30,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() {
) = testutils::init_containers(&cli).await;
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000);
let bob_btc_starting_balance = btc_to_swap * 10;
let alice_xmr_starting_balance = xmr_to_swap * 10;

@ -2,12 +2,17 @@ use crate::testutils::{init_alice, init_bob};
use get_port::get_port;
use libp2p::Multiaddr;
use rand::rngs::OsRng;
use swap::{alice, bitcoin, bob, bob::swap::BobState, database::Database};
use swap::{
bitcoin,
config::Config,
database::Database,
monero,
protocol::{alice, bob, bob::swap::BobState},
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
use testutils::init_tracing;
use uuid::Uuid;
use xmr_btc::config::Config;
pub mod testutils;
@ -25,7 +30,7 @@ async fn given_bob_restarts_after_encsig_is_sent_resume_swap() {
) = testutils::init_containers(&cli).await;
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000);
let bob_btc_starting_balance = btc_to_swap * 10;
let alice_xmr_starting_balance = xmr_to_swap * 10;

@ -2,13 +2,18 @@ use crate::testutils::{init_alice, init_bob};
use get_port::get_port;
use libp2p::Multiaddr;
use rand::rngs::OsRng;
use swap::{alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, database::Database};
use swap::{
bitcoin,
config::Config,
database::Database,
monero,
protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState},
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
use testutils::init_tracing;
use tokio::select;
use uuid::Uuid;
use xmr_btc::config::Config;
pub mod testutils;
@ -26,10 +31,10 @@ async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
) = testutils::init_containers(&cli).await;
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000);
let bob_btc_starting_balance = btc_to_swap * 10;
let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0);
let bob_xmr_starting_balance = monero::Amount::from_piconero(0);
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
let alice_xmr_starting_balance = xmr_to_swap * 10;

@ -6,11 +6,15 @@ use futures::{
use get_port::get_port;
use libp2p::Multiaddr;
use rand::rngs::OsRng;
use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState};
use swap::{
bitcoin,
config::Config,
monero,
protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState},
};
use testcontainers::clients::Cli;
use testutils::init_tracing;
use uuid::Uuid;
use xmr_btc::{bitcoin, config::Config};
pub mod testutils;
@ -30,7 +34,7 @@ async fn alice_punishes_if_bob_never_acts_after_fund() {
) = testutils::init_containers(&cli).await;
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000);
let bob_btc_starting_balance = btc_to_swap * 10;

@ -3,13 +3,18 @@ use futures::future::try_join;
use get_port::get_port;
use libp2p::Multiaddr;
use rand::rngs::OsRng;
use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState, database::Database};
use swap::{
bitcoin,
config::Config,
database::Database,
monero,
protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState},
};
use tempfile::tempdir;
use testcontainers::clients::Cli;
use testutils::init_tracing;
use tokio::select;
use uuid::Uuid;
use xmr_btc::{bitcoin, config::Config};
pub mod testutils;
@ -29,10 +34,10 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() {
) = testutils::init_containers(&cli).await;
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000);
let bob_btc_starting_balance = btc_to_swap * 10;
let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0);
let bob_xmr_starting_balance = monero::Amount::from_piconero(0);
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
let alice_xmr_starting_balance = xmr_to_swap * 10;

@ -4,14 +4,18 @@ use monero_harness::{image, Monero};
use rand::rngs::OsRng;
use std::sync::Arc;
use swap::{
alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, database::Database, monero,
network::transport::build, SwapAmounts,
bitcoin,
config::Config,
database::Database,
monero,
network::transport::build,
protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState},
SwapAmounts,
};
use tempfile::tempdir;
use testcontainers::{clients::Cli, Container};
use tracing_core::dispatcher::DefaultGuard;
use tracing_log::LogTracer;
use xmr_btc::{alice::State0, config::Config, cross_curve_dleq};
pub async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
@ -27,8 +31,8 @@ pub async fn init_wallets(
name: &str,
bitcoind: &Bitcoind<'_>,
monero: &Monero,
btc_starting_balance: Option<xmr_btc::bitcoin::Amount>,
xmr_starting_balance: Option<xmr_btc::monero::Amount>,
btc_starting_balance: Option<::bitcoin::Amount>,
xmr_starting_balance: Option<monero::Amount>,
config: Config,
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
match xmr_starting_balance {
@ -80,12 +84,12 @@ pub async fn init_alice_state(
xmr: xmr_to_swap,
};
let a = crate::bitcoin::SecretKey::new_random(rng);
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
let redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap();
let punish_address = redeem_address.clone();
let state0 = State0::new(
let state0 = alice::State0::new(
a,
s_a,
v_a,
@ -118,7 +122,7 @@ pub async fn init_alice(
monero: &Monero,
btc_to_swap: bitcoin::Amount,
xmr_to_swap: monero::Amount,
xmr_starting_balance: xmr_btc::monero::Amount,
xmr_starting_balance: monero::Amount,
listen: Multiaddr,
config: Config,
) -> (
@ -159,7 +163,7 @@ pub async fn init_alice(
pub async fn init_bob_state(
btc_to_swap: bitcoin::Amount,
xmr_to_swap: xmr_btc::monero::Amount,
xmr_to_swap: monero::Amount,
bob_btc_wallet: Arc<bitcoin::Wallet>,
config: Config,
) -> BobState {
@ -169,7 +173,7 @@ pub async fn init_bob_state(
};
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let state0 = xmr_btc::bob::State0::new(
let state0 = bob::State0::new(
&mut OsRng,
btc_to_swap,
xmr_to_swap,
@ -200,7 +204,7 @@ pub async fn init_bob(
monero: &Monero,
btc_to_swap: bitcoin::Amount,
btc_starting_balance: bitcoin::Amount,
xmr_to_swap: xmr_btc::monero::Amount,
xmr_to_swap: monero::Amount,
config: Config,
) -> (
BobState,

@ -1,31 +0,0 @@
[package]
name = "xmr-btc"
version = "0.1.0"
authors = ["CoBloX Team <team@coblox.tech>"]
edition = "2018"
# TODO: Check for stale dependencies, this looks like its a bit of a mess.
[dependencies]
anyhow = "1"
async-trait = "0.1"
bitcoin = { version = "0.25", features = ["rand", "serde"] }
conquer-once = "0.3"
cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "eddcdea1d1f16fa33ef581d1744014ece535c920", features = ["serde"] }
curve25519-dalek = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] }
ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3
futures = "0.3"
genawaiter = "0.99.1"
miniscript = { version = "4", features = ["serde"] }
monero = { version = "0.9", features = ["serde_support"] }
rand = "0.7"
rust_decimal = "1.8"
serde = { version = "1", features = ["derive"] }
sha2 = "0.9"
thiserror = "1"
tokio = { version = "0.2", default-features = false, features = ["time"] }
tracing = "0.1"
[dev-dependencies]
serde_cbor = "0.11"

@ -1,43 +0,0 @@
use anyhow::Result;
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use crate::{bitcoin, monero};
#[derive(Debug)]
pub enum Message {
Message0(Message0),
Message1(Message1),
Message2(Message2),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message0 {
pub(crate) A: bitcoin::PublicKey,
pub(crate) S_a_monero: monero::PublicKey,
pub(crate) S_a_bitcoin: bitcoin::PublicKey,
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof,
pub(crate) v_a: monero::PrivateViewKey,
pub(crate) redeem_address: bitcoin::Address,
pub(crate) punish_address: bitcoin::Address,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message1 {
pub(crate) tx_cancel_sig: Signature,
pub(crate) tx_refund_encsig: EncryptedSignature,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message2 {
pub tx_lock_proof: monero::TransferProof,
}
impl_try_from_parent_enum!(Message0, Message);
impl_try_from_parent_enum!(Message1, Message);
impl_try_from_parent_enum!(Message2, Message);
impl_from_child_enum!(Message0, Message);
impl_from_child_enum!(Message1, Message);
impl_from_child_enum!(Message2, Message);

@ -1,49 +0,0 @@
use crate::{bitcoin, monero};
use anyhow::Result;
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
#[derive(Clone, Debug)]
pub enum Message {
Message0(Message0),
Message1(Message1),
Message2(Message2),
Message3(Message3),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message0 {
pub(crate) B: bitcoin::PublicKey,
pub(crate) S_b_monero: monero::PublicKey,
pub(crate) S_b_bitcoin: bitcoin::PublicKey,
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof,
pub(crate) v_b: monero::PrivateViewKey,
pub(crate) refund_address: bitcoin::Address,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message1 {
pub(crate) tx_lock: bitcoin::TxLock,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message2 {
pub(crate) tx_punish_sig: Signature,
pub(crate) tx_cancel_sig: Signature,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message3 {
pub tx_redeem_encsig: EncryptedSignature,
}
impl_try_from_parent_enum!(Message0, Message);
impl_try_from_parent_enum!(Message1, Message);
impl_try_from_parent_enum!(Message2, Message);
impl_try_from_parent_enum!(Message3, Message);
impl_from_child_enum!(Message0, Message);
impl_from_child_enum!(Message1, Message);
impl_from_child_enum!(Message2, Message);
impl_from_child_enum!(Message3, Message);

@ -1,91 +0,0 @@
#![warn(
unused_extern_crates,
missing_debug_implementations,
missing_copy_implementations,
rust_2018_idioms,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::fallible_impl_from,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::dbg_macro
)]
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
#![forbid(unsafe_code)]
#![allow(non_snake_case)]
#[derive(Debug, Clone, Copy)]
pub enum ExpiredTimelocks {
None,
Cancel,
Punish,
}
#[macro_use]
mod utils {
macro_rules! impl_try_from_parent_enum {
($type:ident, $parent:ident) => {
impl TryFrom<$parent> for $type {
type Error = anyhow::Error;
fn try_from(from: $parent) -> Result<Self> {
if let $parent::$type(inner) = from {
Ok(inner)
} else {
Err(anyhow::anyhow!(
"Failed to convert parent state to child state"
))
}
}
}
};
}
macro_rules! impl_try_from_parent_enum_for_boxed {
($type:ident, $parent:ident) => {
impl TryFrom<$parent> for $type {
type Error = anyhow::Error;
fn try_from(from: $parent) -> Result<Self> {
if let $parent::$type(inner) = from {
Ok(*inner)
} else {
Err(anyhow::anyhow!(
"Failed to convert parent state to child state"
))
}
}
}
};
}
macro_rules! impl_from_child_enum {
($type:ident, $parent:ident) => {
impl From<$type> for $parent {
fn from(from: $type) -> Self {
$parent::$type(from)
}
}
};
}
macro_rules! impl_from_child_enum_for_boxed {
($type:ident, $parent:ident) => {
impl From<$type> for $parent {
fn from(from: $type) -> Self {
$parent::$type(Box::new(from))
}
}
};
}
}
pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod config;
pub mod monero;
pub mod serde;
pub mod transport;
pub use cross_curve_dleq;

@ -1,274 +0,0 @@
use crate::serde::monero_private_key;
use anyhow::Result;
use async_trait::async_trait;
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use std::ops::{Add, Mul, Sub};
use bitcoin::hashes::core::fmt::Formatter;
pub use curve25519_dalek::scalar::Scalar;
pub use monero::*;
use rust_decimal::{
prelude::{FromPrimitive, ToPrimitive},
Decimal,
};
use std::{fmt::Display, str::FromStr};
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
let scalar = Scalar::random(rng);
PrivateKey::from_scalar(scalar)
}
pub fn private_key_from_secp256k1_scalar(scalar: crate::bitcoin::Scalar) -> PrivateKey {
let mut bytes = scalar.to_bytes();
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
// ed25519 scalar is little endian
bytes.reverse();
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
impl PrivateViewKey {
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let scalar = Scalar::random(rng);
let private_key = PrivateKey::from_scalar(scalar);
Self(private_key)
}
pub fn public(&self) -> PublicViewKey {
PublicViewKey(PublicKey::from_private_key(&self.0))
}
}
impl Add for PrivateViewKey {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl From<PrivateViewKey> for PrivateKey {
fn from(from: PrivateViewKey) -> Self {
from.0
}
}
impl From<PublicViewKey> for PublicKey {
fn from(from: PublicViewKey) -> Self {
from.0
}
}
#[derive(Clone, Copy, Debug)]
pub struct PublicViewKey(PublicKey);
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
pub struct Amount(u64);
impl Amount {
pub const ZERO: Self = Self(0);
/// Create an [Amount] with piconero precision and the given number of
/// piconeros.
///
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
pub fn from_piconero(amount: u64) -> Self {
Amount(amount)
}
pub fn as_piconero(&self) -> u64 {
self.0
}
pub fn parse_monero(amount: &str) -> Result<Self> {
let decimal = Decimal::from_str(amount)?;
let piconeros_dec =
decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
let piconeros = piconeros_dec
.to_u64()
.ok_or_else(|| OverflowError(amount.to_owned()))?;
Ok(Amount(piconeros))
}
}
impl Add for Amount {
type Output = Amount;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for Amount {
type Output = Amount;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Mul<u64> for Amount {
type Output = Amount;
fn mul(self, rhs: u64) -> Self::Output {
Self(self.0 * rhs)
}
}
impl From<Amount> for u64 {
fn from(from: Amount) -> u64 {
from.0
}
}
impl Display for Amount {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut decimal = Decimal::from(self.0);
decimal
.set_scale(12)
.expect("12 is smaller than max precision of 28");
write!(f, "{} XMR", decimal)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TransferProof {
tx_hash: TxHash,
#[serde(with = "monero_private_key")]
tx_key: PrivateKey,
}
impl TransferProof {
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
Self { tx_hash, tx_key }
}
pub fn tx_hash(&self) -> TxHash {
self.tx_hash.clone()
}
pub fn tx_key(&self) -> PrivateKey {
self.tx_key
}
}
// TODO: add constructor/ change String to fixed length byte array
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TxHash(pub String);
impl From<TxHash> for String {
fn from(from: TxHash) -> Self {
from.0
}
}
#[async_trait]
pub trait Transfer {
async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> anyhow::Result<(TransferProof, Amount)>;
}
#[async_trait]
pub trait WatchForTransfer {
async fn watch_for_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
amount: Amount,
expected_confirmations: u32,
) -> Result<(), InsufficientFunds>;
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
pub struct InsufficientFunds {
pub expected: Amount,
pub actual: Amount,
}
#[async_trait]
pub trait CreateWalletForOutput {
async fn create_and_load_wallet_for_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> anyhow::Result<()>;
}
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
#[error("Overflow, cannot convert {0} to u64")]
pub struct OverflowError(pub String);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_monero_min() {
let min_pics = 1;
let amount = Amount::from_piconero(min_pics);
let monero = amount.to_string();
assert_eq!("0.000000000001 XMR", monero);
}
#[test]
fn display_monero_one() {
let min_pics = 1000000000000;
let amount = Amount::from_piconero(min_pics);
let monero = amount.to_string();
assert_eq!("1.000000000000 XMR", monero);
}
#[test]
fn display_monero_max() {
let max_pics = 18_446_744_073_709_551_615;
let amount = Amount::from_piconero(max_pics);
let monero = amount.to_string();
assert_eq!("18446744.073709551615 XMR", monero);
}
#[test]
fn parse_monero_min() {
let monero_min = "0.000000000001";
let amount = Amount::parse_monero(monero_min).unwrap();
let pics = amount.0;
assert_eq!(1, pics);
}
#[test]
fn parse_monero() {
let monero = "123";
let amount = Amount::parse_monero(monero).unwrap();
let pics = amount.0;
assert_eq!(123000000000000, pics);
}
#[test]
fn parse_monero_max() {
let monero = "18446744.073709551615";
let amount = Amount::parse_monero(monero).unwrap();
let pics = amount.0;
assert_eq!(18446744073709551615, pics);
}
#[test]
fn parse_monero_overflows() {
let overflow_pics = "18446744.073709551616";
let error = Amount::parse_monero(overflow_pics).unwrap_err();
assert_eq!(
error.downcast_ref::<OverflowError>().unwrap(),
&OverflowError(overflow_pics.to_owned())
);
}
}

@ -1,12 +0,0 @@
use anyhow::Result;
use async_trait::async_trait;
#[async_trait]
pub trait SendMessage<SendMsg> {
async fn send_message(&mut self, message: SendMsg) -> Result<()>;
}
#[async_trait]
pub trait ReceiveMessage<RecvMsg> {
async fn receive_message(&mut self) -> Result<RecvMsg>;
}
Loading…
Cancel
Save