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
This commit is contained in:
rishflab 2021-01-05 14:08:36 +11:00
parent a0d859147a
commit c900d12593
52 changed files with 1372 additions and 1933 deletions

View File

@ -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
View File

@ -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,45 +2349,19 @@ 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-macro-error-attr",
"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-macro2",
"quote",
"syn",
"syn-mid",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
@ -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"

View File

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

View File

@ -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"

View File

@ -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,
};
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;
pub use ::bitcoin::{Address, Transaction};
pub use xmr_btc::bitcoin::*;
use crate::{config::Config, ExpiredTimelocks};
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
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;
#[derive(Debug)]
pub struct Wallet {
pub inner: bitcoin_harness::Wallet,
pub network: bitcoin::Network,
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 Wallet {
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
impl SecretKey {
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let scalar = Scalar::random(rng);
Ok(Self {
inner: wallet,
network,
})
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")?;
pub fn sign(&self, digest: SigHash) -> Signature {
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
Ok(fee)
ecdsa.sign(&self.inner, &digest.into_inner())
}
// 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
// produced (by Alice) encsig - published (by Bob) sig = s_b (it's not really
// subtraction, it's recover)
// 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())
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct PublicKey(Point);
impl From<PublicKey> for Point {
fn from(from: PublicKey) -> Self {
from.0
}
}
impl From<Scalar> for SecretKey {
fn from(scalar: Scalar) -> Self {
let ecdsa = ECDSA::<()>::default();
let public = ecdsa.verification_key_for(&scalar);
Self {
inner: scalar,
public,
}
}
}
impl From<SecretKey> for Scalar {
fn from(sk: SecretKey) -> Self {
sk.inner
}
}
impl From<Scalar> for PublicKey {
fn from(scalar: Scalar) -> Self {
let ecdsa = ECDSA::<()>::default();
PublicKey(ecdsa.verification_key_for(&scalar))
}
}
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)
}
}
#[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 BuildTxLockPsbt for Wallet {
pub trait BuildTxLockPsbt {
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)
}
) -> Result<PartiallySignedTransaction>;
}
#[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)
}
pub trait SignTxLock {
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
}
#[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")
}
pub trait BroadcastSignedTransaction {
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
}
#[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 WatchForRawTransaction {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
}
#[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 WaitForTransactionFinality {
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
}
#[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)
}
pub trait GetBlockHeight {
async fn get_block_height(&self) -> 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
pub trait TransactionBlockHeight {
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
}
// 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);
#[async_trait]
pub trait WaitForBlockHeight {
async fn wait_for_block_height(&self, height: BlockHeight);
}
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;
}
#[async_trait]
pub trait GetRawTransaction {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
}
Ok(())
#[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(())
}

View File

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

View File

@ -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,

201
swap/src/bitcoin/wallet.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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)]

View File

@ -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 {

View File

@ -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,
}

View File

@ -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,

View File

@ -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,
};
pub use xmr_btc::monero::*;
use crate::{bitcoin, serde::monero_private_key};
#[derive(Debug)]
pub struct Wallet {
pub inner: wallet::Client,
pub network: Network,
pub use curve25519_dalek::scalar::Scalar;
pub use monero::*;
pub use wallet::Wallet;
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)
}
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)
}
/// Get the balance of the primary account.
pub async fn get_balance(&self) -> Result<Amount> {
let amount = self.inner.get_balance(0).await?;
pub fn public(&self) -> PublicViewKey {
PublicViewKey(PublicKey::from_private_key(&self.0))
}
}
Ok(Amount::from_piconero(amount))
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]
impl Transfer for Wallet {
pub trait Transfer {
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))
}
) -> anyhow::Result<(TransferProof, Amount)>;
}
#[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 {
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 },
}
) -> Result<(), InsufficientFunds>;
}
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
#[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,
}
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))?;
#[async_trait]
pub trait CreateWalletForOutput {
async fn create_and_load_wallet_for_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> anyhow::Result<()>;
}
if proof.received != expected_amount.as_piconero() {
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
expected: expected_amount,
actual: Amount::from_piconero(proof.received),
}));
}
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
#[error("Overflow, cannot convert {0} to u64")]
pub struct OverflowError(pub String);
if proof.confirmations < expected_confirmations {
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
}
#[cfg(test)]
mod tests {
use super::*;
Ok(proof)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await;
#[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);
}
if let Err(Error::InsufficientFunds { expected, actual }) = res {
return Err(InsufficientFunds { expected, actual });
};
#[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);
}
Ok(())
#[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())
);
}
}

147
swap/src/monero/wallet.rs Normal file
View File

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

View File

@ -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.

2
swap/src/protocol.rs Normal file
View File

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

View File

@ -1,15 +1,6 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
network::{
peer_tracker::{self, PeerTracker},
request_response::AliceToBob,
transport::SwapTransport,
TokioExecutor,
},
SwapAmounts,
};
pub use self::{amounts::*, message0::*, message1::*, message2::*, message3::*, state::*};
use anyhow::Result;
use libp2p::{
core::{identity::Keypair, Multiaddr},
@ -17,7 +8,17 @@ use libp2p::{
NetworkBehaviour, PeerId,
};
use tracing::{debug, info};
use xmr_btc::bob;
use crate::{
network::{
peer_tracker::{self, PeerTracker},
request_response::AliceToBob,
transport::SwapTransport,
TokioExecutor,
},
protocol::bob,
SwapAmounts,
};
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,
}
}

View File

@ -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)]

View File

@ -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)>,

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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,
use tracing::info;
use crate::{
bitcoin,
bitcoin::{
current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire, GetBlockHeight,
TransactionBlockHeight, WatchForRawTransaction,
},
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;
}
}
})
}
// 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,
))
}
}
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(),
}
}

View File

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

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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> {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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,
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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