mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-03 12:06:17 -04:00
Merge pull request #125 from comit-network/reorganise-modules-2
Merge xmr_btc crate
This commit is contained in:
commit
64ba8d6a87
53 changed files with 1505 additions and 2413 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -34,7 +34,6 @@ jobs:
|
||||||
- name: Check Cargo.toml formatting
|
- name: Check Cargo.toml formatting
|
||||||
run: |
|
run: |
|
||||||
cargo tomlfmt -d -p Cargo.toml
|
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 monero-harness/Cargo.toml
|
||||||
cargo tomlfmt -d -p swap/Cargo.toml
|
cargo tomlfmt -d -p swap/Cargo.toml
|
||||||
|
|
||||||
|
|
105
Cargo.lock
generated
105
Cargo.lock
generated
|
@ -1089,36 +1089,6 @@ version = "0.3.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
|
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]]
|
[[package]]
|
||||||
name = "generator"
|
name = "generator"
|
||||||
version = "0.6.23"
|
version = "0.6.23"
|
||||||
|
@ -2379,45 +2349,19 @@ dependencies = [
|
||||||
"uint 0.8.5",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "proc-macro-error"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-error-attr 1.0.4",
|
"proc-macro-error-attr",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro-error-attr"
|
name = "proc-macro-error-attr"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -3283,7 +3227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
|
checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro-error 1.0.4",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
@ -3335,21 +3279,25 @@ dependencies = [
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"bitcoin-harness",
|
"bitcoin-harness",
|
||||||
"conquer-once",
|
"conquer-once",
|
||||||
|
"cross-curve-dleq",
|
||||||
|
"curve25519-dalek 2.1.0",
|
||||||
"derivative",
|
"derivative",
|
||||||
"ecdsa_fun",
|
"ecdsa_fun",
|
||||||
|
"ed25519-dalek",
|
||||||
"futures",
|
"futures",
|
||||||
"genawaiter",
|
|
||||||
"get-port",
|
"get-port",
|
||||||
"hyper",
|
"hyper",
|
||||||
"libp2p",
|
"libp2p",
|
||||||
"libp2p-tokio-socks5",
|
"libp2p-tokio-socks5",
|
||||||
"log",
|
"log",
|
||||||
|
"miniscript",
|
||||||
"monero",
|
"monero",
|
||||||
"monero-harness",
|
"monero-harness",
|
||||||
"port_check",
|
"port_check",
|
||||||
"prettytable-rs",
|
"prettytable-rs",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rust_decimal",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_cbor",
|
"serde_cbor",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
@ -3361,6 +3309,7 @@ dependencies = [
|
||||||
"strum",
|
"strum",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"testcontainers",
|
"testcontainers",
|
||||||
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -3371,7 +3320,6 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"void",
|
"void",
|
||||||
"xmr-btc",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3385,17 +3333,6 @@ dependencies = [
|
||||||
"unicode-xid",
|
"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]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
|
@ -4044,32 +3981,6 @@ dependencies = [
|
||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "yamux"
|
name = "yamux"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["monero-harness", "xmr-btc", "swap"]
|
members = ["monero-harness", "swap"]
|
||||||
|
|
|
@ -15,18 +15,22 @@ base64 = "0.12"
|
||||||
bitcoin = { version = "0.25", features = ["rand", "use-serde"] }
|
bitcoin = { version = "0.25", features = ["rand", "use-serde"] }
|
||||||
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "864b55fcba2e770105f135781dd2e3002c503d12" }
|
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "864b55fcba2e770105f135781dd2e3002c503d12" }
|
||||||
conquer-once = "0.3"
|
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"
|
derivative = "2"
|
||||||
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] }
|
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 }
|
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 = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] }
|
||||||
libp2p-tokio-socks5 = "0.4"
|
libp2p-tokio-socks5 = "0.4"
|
||||||
log = { version = "0.4", features = ["serde"] }
|
log = { version = "0.4", features = ["serde"] }
|
||||||
|
miniscript = { version = "4", features = ["serde"] }
|
||||||
monero = { version = "0.9", features = ["serde_support"] }
|
monero = { version = "0.9", features = ["serde_support"] }
|
||||||
monero-harness = { path = "../monero-harness" }
|
monero-harness = { path = "../monero-harness" }
|
||||||
prettytable-rs = "0.8"
|
prettytable-rs = "0.8"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
|
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
|
||||||
|
rust_decimal = "1.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_cbor = "0.11"
|
serde_cbor = "0.11"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
@ -36,6 +40,7 @@ sled = "0.34"
|
||||||
structopt = "0.3"
|
structopt = "0.3"
|
||||||
strum = { version = "0.20", features = ["derive"] }
|
strum = { version = "0.20", features = ["derive"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
thiserror = "1"
|
||||||
time = "0.2"
|
time = "0.2"
|
||||||
tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] }
|
tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] }
|
||||||
tracing = { version = "0.1", features = ["attributes"] }
|
tracing = { version = "0.1", features = ["attributes"] }
|
||||||
|
@ -46,12 +51,12 @@ tracing-subscriber = { version = "0.2", default-features = false, features = ["f
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||||
void = "1"
|
void = "1"
|
||||||
xmr-btc = { path = "../xmr-btc" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
get-port = "3"
|
get-port = "3"
|
||||||
hyper = "0.13"
|
hyper = "0.13"
|
||||||
port_check = "0.1"
|
port_check = "0.1"
|
||||||
|
serde_cbor = "0.11"
|
||||||
spectral = "0.6"
|
spectral = "0.6"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
testcontainers = "0.11"
|
testcontainers = "0.11"
|
||||||
|
|
|
@ -1,202 +1,295 @@
|
||||||
use anyhow::{Context, Result};
|
use ::bitcoin::{
|
||||||
use async_trait::async_trait;
|
hashes::{hex::ToHex, Hash},
|
||||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
secp256k1,
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
util::psbt::PartiallySignedTransaction,
|
||||||
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi};
|
SigHash,
|
||||||
use reqwest::Url;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::time::interval;
|
|
||||||
use xmr_btc::{
|
|
||||||
bitcoin::{
|
|
||||||
BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, SignTxLock,
|
|
||||||
TransactionBlockHeight, WatchForRawTransaction,
|
|
||||||
},
|
|
||||||
config::Config,
|
|
||||||
};
|
};
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
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};
|
use crate::{bitcoin::timelocks::BlockHeight, config::Config, ExpiredTimelocks};
|
||||||
pub use xmr_btc::bitcoin::*;
|
|
||||||
|
|
||||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
pub use crate::bitcoin::{
|
||||||
|
timelocks::Timelock,
|
||||||
|
transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund},
|
||||||
|
};
|
||||||
|
pub use ::bitcoin::{util::amount::Amount, Address, Network, Transaction, Txid};
|
||||||
|
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
||||||
|
pub use wallet::Wallet;
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub mod timelocks;
|
||||||
pub struct Wallet {
|
pub mod transactions;
|
||||||
pub inner: bitcoin_harness::Wallet,
|
pub mod wallet;
|
||||||
pub network: bitcoin::Network,
|
|
||||||
|
// 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 {
|
impl SecretKey {
|
||||||
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
|
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||||
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
|
let scalar = Scalar::random(rng);
|
||||||
|
|
||||||
Ok(Self {
|
let ecdsa = ECDSA::<()>::default();
|
||||||
inner: wallet,
|
let public = ecdsa.verification_key_for(&scalar);
|
||||||
network,
|
|
||||||
})
|
Self {
|
||||||
|
inner: scalar,
|
||||||
|
public,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn balance(&self) -> Result<Amount> {
|
pub fn public(&self) -> PublicKey {
|
||||||
let balance = self.inner.balance().await?;
|
PublicKey(self.public)
|
||||||
Ok(balance)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new_address(&self) -> Result<Address> {
|
pub fn to_bytes(&self) -> [u8; 32] {
|
||||||
self.inner.new_address().await.map_err(Into::into)
|
self.inner.to_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
pub fn sign(&self, digest: SigHash) -> Signature {
|
||||||
let fee = self
|
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
||||||
.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)
|
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]
|
#[async_trait]
|
||||||
impl BuildTxLockPsbt for Wallet {
|
pub trait BuildTxLockPsbt {
|
||||||
async fn build_tx_lock_psbt(
|
async fn build_tx_lock_psbt(
|
||||||
&self,
|
&self,
|
||||||
output_address: Address,
|
output_address: Address,
|
||||||
output_amount: Amount,
|
output_amount: Amount,
|
||||||
) -> Result<PartiallySignedTransaction> {
|
) -> 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]
|
#[async_trait]
|
||||||
impl SignTxLock for Wallet {
|
pub trait SignTxLock {
|
||||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
|
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]
|
#[async_trait]
|
||||||
impl BroadcastSignedTransaction for Wallet {
|
pub trait BroadcastSignedTransaction {
|
||||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
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]
|
#[async_trait]
|
||||||
impl GetRawTransaction for Wallet {
|
pub trait WatchForRawTransaction {
|
||||||
// todo: potentially replace with option
|
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
|
||||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
|
||||||
Ok(self.inner.get_raw_transaction(txid).await?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl GetBlockHeight for Wallet {
|
pub trait WaitForTransactionFinality {
|
||||||
async fn get_block_height(&self) -> BlockHeight {
|
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
|
||||||
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]
|
#[async_trait]
|
||||||
impl TransactionBlockHeight for Wallet {
|
pub trait GetBlockHeight {
|
||||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight {
|
async fn get_block_height(&self) -> 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]
|
#[async_trait]
|
||||||
impl WaitForTransactionFinality for Wallet {
|
pub trait TransactionBlockHeight {
|
||||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> {
|
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
|
||||||
// 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
|
#[async_trait]
|
||||||
// on.
|
pub trait WaitForBlockHeight {
|
||||||
let mut interval = interval(config.bitcoin_avg_block_time / 4);
|
async fn wait_for_block_height(&self, height: BlockHeight);
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
#[async_trait]
|
||||||
let tx = self.inner.client.get_raw_transaction_verbose(txid).await?;
|
pub trait GetRawTransaction {
|
||||||
if let Some(confirmations) = tx.confirmations {
|
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
||||||
if confirmations >= config.bitcoin_finality_confirmations {
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interval.tick().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait GetNetwork {
|
||||||
|
fn get_network(&self) -> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
Ok(())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Network for Wallet {
|
|
||||||
fn get_network(&self) -> bitcoin::Network {
|
|
||||||
self.network
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
use crate::bitcoin::{
|
use ::bitcoin::{
|
||||||
build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, Network, OutPoint, PublicKey,
|
util::{bip143::SigHashCache, psbt::PartiallySignedTransaction},
|
||||||
Timelock, Txid, TX_FEE,
|
OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use bitcoin::{
|
|
||||||
util::{bip143::SigHashCache, psbt::PartiallySignedTransaction},
|
|
||||||
Address, Amount, SigHash, SigHashType, Transaction, TxIn, TxOut,
|
|
||||||
};
|
|
||||||
use ecdsa_fun::Signature;
|
use ecdsa_fun::Signature;
|
||||||
use miniscript::{Descriptor, NullCtx};
|
use miniscript::{Descriptor, NullCtx};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::bitcoin::{
|
||||||
|
build_shared_output_descriptor, timelocks::Timelock, verify_sig, Address, Amount,
|
||||||
|
BuildTxLockPsbt, GetNetwork, PublicKey, Transaction, TX_FEE,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct TxLock {
|
pub struct TxLock {
|
||||||
inner: Transaction,
|
inner: Transaction,
|
||||||
|
@ -21,7 +22,7 @@ pub struct TxLock {
|
||||||
impl TxLock {
|
impl TxLock {
|
||||||
pub async fn new<W>(wallet: &W, amount: Amount, A: PublicKey, B: PublicKey) -> Result<Self>
|
pub async fn new<W>(wallet: &W, amount: Amount, A: PublicKey, B: PublicKey) -> Result<Self>
|
||||||
where
|
where
|
||||||
W: BuildTxLockPsbt + Network,
|
W: BuildTxLockPsbt + GetNetwork,
|
||||||
{
|
{
|
||||||
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||||
let address = lock_output_descriptor
|
let address = lock_output_descriptor
|
201
swap/src/bitcoin/wallet.rs
Normal file
201
swap/src/bitcoin/wallet.rs
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
use ::bitcoin::{util::psbt::PartiallySignedTransaction, 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, Address, Amount, BroadcastSignedTransaction, BuildTxLockPsbt,
|
||||||
|
GetBlockHeight, GetNetwork, GetRawTransaction, SignTxLock, Transaction,
|
||||||
|
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 GetNetwork for Wallet {
|
||||||
|
fn get_network(&self) -> bitcoin::Network {
|
||||||
|
self.network
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,8 @@ use libp2p::{core::Multiaddr, PeerId};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{bitcoin, monero};
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
// TODO: Default value should points to proper configuration folder in home folder
|
// TODO: Default value should points to proper configuration folder in home folder
|
||||||
|
@ -13,7 +15,7 @@ pub struct Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
#[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 {
|
pub enum Command {
|
||||||
SellXmr {
|
SellXmr {
|
||||||
#[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")]
|
#[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")]
|
||||||
|
@ -32,7 +34,7 @@ pub enum Command {
|
||||||
listen_addr: Multiaddr,
|
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))]
|
#[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))]
|
#[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,
|
||||||
|
@ -60,7 +62,7 @@ pub enum Command {
|
||||||
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))]
|
#[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,
|
History,
|
||||||
Resume(Resume),
|
Resume(Resume),
|
||||||
|
@ -116,7 +118,7 @@ fn parse_btc(str: &str) -> anyhow::Result<bitcoin::Amount> {
|
||||||
Ok(amount)
|
Ok(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_xmr(str: &str) -> anyhow::Result<xmr_btc::monero::Amount> {
|
fn parse_xmr(str: &str) -> anyhow::Result<monero::Amount> {
|
||||||
let amount = xmr_btc::monero::Amount::parse_monero(str)?;
|
let amount = monero::Amount::parse_monero(str)?;
|
||||||
Ok(amount)
|
Ok(amount)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ pub struct Config {
|
||||||
pub monero_finality_confirmations: u32,
|
pub monero_finality_confirmations: u32,
|
||||||
pub bitcoin_cancel_timelock: Timelock,
|
pub bitcoin_cancel_timelock: Timelock,
|
||||||
pub bitcoin_punish_timelock: Timelock,
|
pub bitcoin_punish_timelock: Timelock,
|
||||||
pub bitcoin_network: ::bitcoin::Network,
|
pub bitcoin_network: bitcoin::Network,
|
||||||
pub monero_network: ::monero::Network,
|
pub monero_network: monero::Network,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -28,8 +28,8 @@ impl Config {
|
||||||
monero_finality_confirmations: mainnet::MONERO_FINALITY_CONFIRMATIONS,
|
monero_finality_confirmations: mainnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||||
bitcoin_cancel_timelock: mainnet::BITCOIN_CANCEL_TIMELOCK,
|
bitcoin_cancel_timelock: mainnet::BITCOIN_CANCEL_TIMELOCK,
|
||||||
bitcoin_punish_timelock: mainnet::BITCOIN_PUNISH_TIMELOCK,
|
bitcoin_punish_timelock: mainnet::BITCOIN_PUNISH_TIMELOCK,
|
||||||
bitcoin_network: ::bitcoin::Network::Bitcoin,
|
bitcoin_network: bitcoin::Network::Bitcoin,
|
||||||
monero_network: ::monero::Network::Mainnet,
|
monero_network: monero::Network::Mainnet,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ impl Config {
|
||||||
monero_finality_confirmations: testnet::MONERO_FINALITY_CONFIRMATIONS,
|
monero_finality_confirmations: testnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||||
bitcoin_cancel_timelock: testnet::BITCOIN_CANCEL_TIMELOCK,
|
bitcoin_cancel_timelock: testnet::BITCOIN_CANCEL_TIMELOCK,
|
||||||
bitcoin_punish_timelock: testnet::BITCOIN_PUNISH_TIMELOCK,
|
bitcoin_punish_timelock: testnet::BITCOIN_PUNISH_TIMELOCK,
|
||||||
bitcoin_network: ::bitcoin::Network::Testnet,
|
bitcoin_network: bitcoin::Network::Testnet,
|
||||||
monero_network: ::monero::Network::Stagenet,
|
monero_network: monero::Network::Stagenet,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,8 +62,8 @@ impl Config {
|
||||||
monero_finality_confirmations: regtest::MONERO_FINALITY_CONFIRMATIONS,
|
monero_finality_confirmations: regtest::MONERO_FINALITY_CONFIRMATIONS,
|
||||||
bitcoin_cancel_timelock: regtest::BITCOIN_CANCEL_TIMELOCK,
|
bitcoin_cancel_timelock: regtest::BITCOIN_CANCEL_TIMELOCK,
|
||||||
bitcoin_punish_timelock: regtest::BITCOIN_PUNISH_TIMELOCK,
|
bitcoin_punish_timelock: regtest::BITCOIN_PUNISH_TIMELOCK,
|
||||||
bitcoin_network: ::bitcoin::Network::Regtest,
|
bitcoin_network: bitcoin::Network::Regtest,
|
||||||
monero_network: ::monero::Network::default(),
|
monero_network: monero::Network::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,10 +6,36 @@ use uuid::Uuid;
|
||||||
mod alice;
|
mod alice;
|
||||||
mod bob;
|
mod bob;
|
||||||
|
|
||||||
pub use alice::*;
|
pub use alice::Alice;
|
||||||
pub use bob::*;
|
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);
|
pub struct Database(sled::Db);
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
|
@ -85,37 +111,13 @@ where
|
||||||
Ok(serde_cbor::from_slice(&v)?)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::database::{Alice, AliceEndState, Bob, BobEndState};
|
use crate::database::{
|
||||||
|
alice::{Alice, AliceEndState},
|
||||||
|
bob::{Bob, BobEndState},
|
||||||
|
};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_write_and_read_to_multiple_keys() {
|
async fn can_write_and_read_to_multiple_keys() {
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use crate::{alice::swap::AliceState, SwapAmounts};
|
use ::bitcoin::hashes::core::fmt::Display;
|
||||||
use bitcoin::hashes::core::fmt::Display;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use xmr_btc::{
|
|
||||||
alice,
|
use crate::{
|
||||||
bitcoin::{EncryptedSignature, TxCancel, TxRefund},
|
bitcoin::{EncryptedSignature, TxCancel, TxRefund},
|
||||||
monero,
|
monero,
|
||||||
serde::monero_private_key,
|
monero::monero_private_key,
|
||||||
|
protocol::{alice, alice::AliceState},
|
||||||
|
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.
|
// and is dropped once written in DB.
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use crate::{bob::swap::BobState, SwapAmounts};
|
use ::bitcoin::hashes::core::fmt::Display;
|
||||||
use bitcoin::hashes::core::fmt::Display;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use xmr_btc::bob;
|
|
||||||
|
use crate::{
|
||||||
|
protocol::{bob, bob::BobState},
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
pub enum Bob {
|
pub enum Bob {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#![warn(
|
#![warn(
|
||||||
unused_extern_crates,
|
unused_extern_crates,
|
||||||
missing_debug_implementations,
|
|
||||||
missing_copy_implementations,
|
|
||||||
rust_2018_idioms,
|
rust_2018_idioms,
|
||||||
clippy::cast_possible_truncation,
|
clippy::cast_possible_truncation,
|
||||||
clippy::cast_sign_loss,
|
clippy::cast_sign_loss,
|
||||||
|
@ -10,19 +8,24 @@
|
||||||
clippy::cast_possible_wrap,
|
clippy::cast_possible_wrap,
|
||||||
clippy::dbg_macro
|
clippy::dbg_macro
|
||||||
)]
|
)]
|
||||||
|
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(
|
||||||
|
non_snake_case,
|
||||||
|
missing_debug_implementations,
|
||||||
|
missing_copy_implementations
|
||||||
|
)]
|
||||||
|
|
||||||
use ::serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
pub mod alice;
|
|
||||||
pub mod bitcoin;
|
pub mod bitcoin;
|
||||||
pub mod bob;
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
pub mod config;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod monero;
|
pub mod monero;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod protocol;
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
|
|
||||||
pub type Never = std::convert::Infallible;
|
pub type Never = std::convert::Infallible;
|
||||||
|
@ -48,7 +51,7 @@ pub struct SwapAmounts {
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
pub btc: bitcoin::Amount,
|
pub btc: bitcoin::Amount,
|
||||||
/// Amount of XMR to swap.
|
/// Amount of XMR to swap.
|
||||||
#[serde(with = "xmr_btc::serde::monero_amount")]
|
#[serde(with = "monero::monero_amount")]
|
||||||
pub xmr: monero::Amount,
|
pub xmr: monero::Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,3 +66,10 @@ impl Display for SwapAmounts {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ExpiredTimelocks {
|
||||||
|
None,
|
||||||
|
Cancel,
|
||||||
|
Punish,
|
||||||
|
}
|
||||||
|
|
|
@ -20,20 +20,18 @@ use rand::rngs::OsRng;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use swap::{
|
use swap::{
|
||||||
alice,
|
bitcoin,
|
||||||
alice::swap::AliceState,
|
|
||||||
bitcoin, bob,
|
|
||||||
bob::swap::BobState,
|
|
||||||
cli::{Command, Options, Resume},
|
cli::{Command, Options, Resume},
|
||||||
|
config::Config,
|
||||||
database::{Database, Swap},
|
database::{Database, Swap},
|
||||||
monero,
|
monero,
|
||||||
network::transport::build,
|
network::transport::build,
|
||||||
|
protocol::{alice, alice::AliceState, bob, bob::BobState},
|
||||||
trace::init_tracing,
|
trace::init_tracing,
|
||||||
SwapAmounts,
|
SwapAmounts,
|
||||||
};
|
};
|
||||||
use tracing::{info, log::LevelFilter};
|
use tracing::{info, log::LevelFilter};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::{alice::State0, config::Config, cross_curve_dleq};
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate prettytable;
|
extern crate prettytable;
|
||||||
|
@ -76,10 +74,10 @@ async fn main() -> Result<()> {
|
||||||
let rng = &mut OsRng;
|
let rng = &mut OsRng;
|
||||||
let a = bitcoin::SecretKey::new_random(rng);
|
let a = bitcoin::SecretKey::new_random(rng);
|
||||||
let s_a = cross_curve_dleq::Scalar::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 redeem_address = bitcoin_wallet.as_ref().new_address().await?;
|
||||||
let punish_address = redeem_address.clone();
|
let punish_address = redeem_address.clone();
|
||||||
let state0 = State0::new(
|
let state0 = alice::state::State0::new(
|
||||||
a,
|
a,
|
||||||
s_a,
|
s_a,
|
||||||
v_a,
|
v_a,
|
||||||
|
@ -129,7 +127,7 @@ async fn main() -> Result<()> {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let refund_address = bitcoin_wallet.new_address().await?;
|
let refund_address = bitcoin_wallet.new_address().await?;
|
||||||
let state0 = xmr_btc::bob::State0::new(
|
let state0 = bob::state::State0::new(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
send_bitcoin,
|
send_bitcoin,
|
||||||
receive_monero,
|
receive_monero,
|
||||||
|
@ -248,9 +246,10 @@ async fn setup_wallets(
|
||||||
bitcoin_wallet_name: &str,
|
bitcoin_wallet_name: &str,
|
||||||
monero_wallet_rpc_url: url::Url,
|
monero_wallet_rpc_url: url::Url,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> Result<(Arc<bitcoin::Wallet>, Arc<monero::Wallet>)> {
|
) -> Result<(Arc<swap::bitcoin::Wallet>, Arc<swap::monero::Wallet>)> {
|
||||||
let bitcoin_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?;
|
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
||||||
info!(
|
info!(
|
||||||
"Connection to Bitcoin wallet succeeded, balance: {}",
|
"Connection to Bitcoin wallet succeeded, balance: {}",
|
||||||
|
@ -273,8 +272,8 @@ async fn alice_swap(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
state: AliceState,
|
state: AliceState,
|
||||||
listen_addr: Multiaddr,
|
listen_addr: Multiaddr,
|
||||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<monero::Wallet>,
|
monero_wallet: Arc<swap::monero::Wallet>,
|
||||||
config: Config,
|
config: Config,
|
||||||
db: Database,
|
db: Database,
|
||||||
) -> Result<AliceState> {
|
) -> Result<AliceState> {
|
||||||
|
@ -306,8 +305,8 @@ async fn alice_swap(
|
||||||
async fn bob_swap(
|
async fn bob_swap(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
state: BobState,
|
state: BobState,
|
||||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<monero::Wallet>,
|
monero_wallet: Arc<swap::monero::Wallet>,
|
||||||
db: Database,
|
db: Database,
|
||||||
alice_peer_id: PeerId,
|
alice_peer_id: PeerId,
|
||||||
alice_addr: Multiaddr,
|
alice_addr: Multiaddr,
|
||||||
|
|
|
@ -1,143 +1,377 @@
|
||||||
|
pub mod wallet;
|
||||||
|
|
||||||
|
use ::bitcoin::hashes::core::fmt::Formatter;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
use rand::{CryptoRng, RngCore};
|
||||||
use monero_harness::rpc::wallet;
|
use rust_decimal::{
|
||||||
use std::{str::FromStr, time::Duration};
|
prelude::{FromPrimitive, ToPrimitive},
|
||||||
use url::Url;
|
Decimal,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
ops::{Add, Mul, Sub},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
pub use xmr_btc::monero::*;
|
use crate::bitcoin;
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub use ::monero::{Network, PrivateKey, PublicKey};
|
||||||
pub struct Wallet {
|
pub use curve25519_dalek::scalar::Scalar;
|
||||||
pub inner: wallet::Client,
|
pub use wallet::Wallet;
|
||||||
pub network: Network,
|
|
||||||
|
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 private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey {
|
||||||
pub fn new(url: Url, network: Network) -> Self {
|
let mut bytes = scalar.to_bytes();
|
||||||
Self {
|
|
||||||
inner: wallet::Client::new(url),
|
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
|
||||||
network,
|
// 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 fn public(&self) -> PublicViewKey {
|
||||||
pub async fn get_balance(&self) -> Result<Amount> {
|
PublicViewKey(PublicKey::from_private_key(&self.0))
|
||||||
let amount = self.inner.get_balance(0).await?;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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]
|
#[async_trait]
|
||||||
impl Transfer for Wallet {
|
pub trait Transfer {
|
||||||
async fn transfer(
|
async fn transfer(
|
||||||
&self,
|
&self,
|
||||||
public_spend_key: PublicKey,
|
public_spend_key: PublicKey,
|
||||||
public_view_key: PublicViewKey,
|
public_view_key: PublicViewKey,
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
) -> Result<(TransferProof, Amount)> {
|
) -> anyhow::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]
|
#[async_trait]
|
||||||
impl CreateWalletForOutput for Wallet {
|
pub trait WatchForTransfer {
|
||||||
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(
|
async fn watch_for_transfer(
|
||||||
&self,
|
&self,
|
||||||
public_spend_key: PublicKey,
|
public_spend_key: PublicKey,
|
||||||
public_view_key: PublicViewKey,
|
public_view_key: PublicViewKey,
|
||||||
transfer_proof: TransferProof,
|
transfer_proof: TransferProof,
|
||||||
expected_amount: Amount,
|
amount: Amount,
|
||||||
expected_confirmations: u32,
|
expected_confirmations: u32,
|
||||||
) -> Result<(), InsufficientFunds> {
|
) -> Result<(), InsufficientFunds>;
|
||||||
enum Error {
|
}
|
||||||
TxNotFound,
|
|
||||||
InsufficientConfirmations,
|
|
||||||
InsufficientFunds { expected: Amount, actual: Amount },
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
#[async_trait]
|
||||||
// NOTE: Currently, this is conflating IO errors with the transaction not being
|
pub trait CreateWalletForOutput {
|
||||||
// in the blockchain yet, or not having enough confirmations on it. All these
|
async fn create_and_load_wallet_for_output(
|
||||||
// errors warrant a retry, but the strategy should probably differ per case
|
&self,
|
||||||
let proof = self
|
private_spend_key: PrivateKey,
|
||||||
.inner
|
private_view_key: PrivateViewKey,
|
||||||
.check_tx_key(
|
) -> anyhow::Result<()>;
|
||||||
&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() {
|
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
|
||||||
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
|
#[error("Overflow, cannot convert {0} to u64")]
|
||||||
expected: expected_amount,
|
pub struct OverflowError(pub String);
|
||||||
actual: Amount::from_piconero(proof.received),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if proof.confirmations < expected_confirmations {
|
pub mod monero_private_key {
|
||||||
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
|
use monero::{
|
||||||
}
|
consensus::{Decodable, Encodable},
|
||||||
|
PrivateKey,
|
||||||
Ok(proof)
|
|
||||||
})
|
|
||||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
|
||||||
return Err(InsufficientFunds { expected, actual });
|
|
||||||
};
|
};
|
||||||
|
use serde::{de, de::Visitor, ser::Error, Deserializer, Serializer};
|
||||||
|
use std::{fmt, io::Cursor};
|
||||||
|
|
||||||
Ok(())
|
struct BytesVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for BytesVisitor {
|
||||||
|
type Value = PrivateKey;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(formatter, "a byte array representing a Monero private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
let mut s = s;
|
||||||
|
PrivateKey::consensus_decode(&mut s).map_err(|err| E::custom(format!("{:?}", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S>(x: &PrivateKey, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut bytes = Cursor::new(vec![]);
|
||||||
|
x.consensus_encode(&mut bytes)
|
||||||
|
.map_err(|err| S::Error::custom(format!("{:?}", err)))?;
|
||||||
|
s.serialize_bytes(bytes.into_inner().as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<PrivateKey, <D as Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let key = deserializer.deserialize_bytes(BytesVisitor)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod monero_amount {
|
||||||
|
use crate::monero::Amount;
|
||||||
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
s.serialize_u64(x.as_piconero())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let picos = u64::deserialize(deserializer)?;
|
||||||
|
let amount = Amount::from_piconero(picos);
|
||||||
|
|
||||||
|
Ok(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serde_monero_private_key() {
|
||||||
|
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
||||||
|
crate::monero::Scalar::random(&mut OsRng),
|
||||||
|
));
|
||||||
|
let encoded = serde_cbor::to_vec(&key).unwrap();
|
||||||
|
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
||||||
|
assert_eq!(key, decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serde_monero_amount() {
|
||||||
|
let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000));
|
||||||
|
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
||||||
|
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
||||||
|
assert_eq!(amount, decoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
147
swap/src/monero/wallet.rs
Normal file
147
swap/src/monero/wallet.rs
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
use ::monero::{Address, Network, PrivateKey, PublicKey};
|
||||||
|
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 crate::monero::{
|
||||||
|
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicViewKey, Transfer,
|
||||||
|
TransferProof, TxHash, WatchForTransfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Wallet {
|
||||||
|
pub inner: wallet::Client,
|
||||||
|
pub network: Network,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wallet {
|
||||||
|
pub fn new(url: Url, network: Network) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: wallet::Client::new(url),
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the balance of the primary account.
|
||||||
|
pub async fn get_balance(&self) -> Result<Amount> {
|
||||||
|
let amount = self.inner.get_balance(0).await?;
|
||||||
|
|
||||||
|
Ok(Amount::from_piconero(amount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Transfer for Wallet {
|
||||||
|
async fn transfer(
|
||||||
|
&self,
|
||||||
|
public_spend_key: PublicKey,
|
||||||
|
public_view_key: PublicViewKey,
|
||||||
|
amount: Amount,
|
||||||
|
) -> Result<(TransferProof, Amount)> {
|
||||||
|
let destination_address =
|
||||||
|
Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.inner
|
||||||
|
.transfer(0, amount.as_piconero(), &destination_address.to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let tx_hash = TxHash(res.tx_hash);
|
||||||
|
tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash);
|
||||||
|
let tx_key = PrivateKey::from_str(&res.tx_key)?;
|
||||||
|
|
||||||
|
let fee = Amount::from_piconero(res.fee);
|
||||||
|
|
||||||
|
let transfer_proof = TransferProof::new(tx_hash, tx_key);
|
||||||
|
tracing::debug!(" Transfer proof: {:?}", transfer_proof);
|
||||||
|
|
||||||
|
Ok((transfer_proof, fee))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CreateWalletForOutput for Wallet {
|
||||||
|
async fn create_and_load_wallet_for_output(
|
||||||
|
&self,
|
||||||
|
private_spend_key: PrivateKey,
|
||||||
|
private_view_key: PrivateViewKey,
|
||||||
|
) -> Result<()> {
|
||||||
|
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
|
||||||
|
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
|
||||||
|
|
||||||
|
let address = Address::standard(self.network, public_spend_key, public_view_key);
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.inner
|
||||||
|
.generate_from_keys(
|
||||||
|
&address.to_string(),
|
||||||
|
&private_spend_key.to_string(),
|
||||||
|
&PrivateKey::from(private_view_key).to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||||
|
// to `ConstantBackoff`.
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WatchForTransfer for Wallet {
|
||||||
|
async fn watch_for_transfer(
|
||||||
|
&self,
|
||||||
|
public_spend_key: PublicKey,
|
||||||
|
public_view_key: PublicViewKey,
|
||||||
|
transfer_proof: TransferProof,
|
||||||
|
expected_amount: Amount,
|
||||||
|
expected_confirmations: u32,
|
||||||
|
) -> Result<(), InsufficientFunds> {
|
||||||
|
enum Error {
|
||||||
|
TxNotFound,
|
||||||
|
InsufficientConfirmations,
|
||||||
|
InsufficientFunds { expected: Amount, actual: Amount },
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||||
|
|
||||||
|
let res = (|| async {
|
||||||
|
// NOTE: Currently, this is conflating IO errors with the transaction not being
|
||||||
|
// in the blockchain yet, or not having enough confirmations on it. All these
|
||||||
|
// errors warrant a retry, but the strategy should probably differ per case
|
||||||
|
let proof = self
|
||||||
|
.inner
|
||||||
|
.check_tx_key(
|
||||||
|
&String::from(transfer_proof.tx_hash()),
|
||||||
|
&transfer_proof.tx_key().to_string(),
|
||||||
|
&address.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
|
||||||
|
|
||||||
|
if proof.received != expected_amount.as_piconero() {
|
||||||
|
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
|
||||||
|
expected: expected_amount,
|
||||||
|
actual: Amount::from_piconero(proof.received),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if proof.confirmations < expected_confirmations {
|
||||||
|
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(proof)
|
||||||
|
})
|
||||||
|
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
||||||
|
return Err(InsufficientFunds { expected, actual });
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::monero;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
|
@ -8,8 +9,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::{fmt::Debug, io, marker::PhantomData};
|
use std::{fmt::Debug, io, marker::PhantomData};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::SwapAmounts;
|
use crate::{
|
||||||
use xmr_btc::{alice, bob, monero};
|
protocol::{alice, bob},
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
|
||||||
/// Time to wait for a response back once we send a request.
|
/// Time to wait for a response back once we send a request.
|
||||||
pub const TIMEOUT: u64 = 3600; // One hour.
|
pub const TIMEOUT: u64 = 3600; // One hour.
|
||||||
|
|
2
swap/src/protocol.rs
Normal file
2
swap/src/protocol.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod alice;
|
||||||
|
pub mod bob;
|
|
@ -1,15 +1,5 @@
|
||||||
//! Run an XMR/BTC swap in the role of Alice.
|
//! Run an XMR/BTC swap in the role of Alice.
|
||||||
//! Alice holds XMR and wishes receive BTC.
|
//! 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,
|
|
||||||
};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
core::{identity::Keypair, Multiaddr},
|
core::{identity::Keypair, Multiaddr},
|
||||||
|
@ -17,7 +7,19 @@ use libp2p::{
|
||||||
NetworkBehaviour, PeerId,
|
NetworkBehaviour, PeerId,
|
||||||
};
|
};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
use xmr_btc::bob;
|
|
||||||
|
use crate::{
|
||||||
|
network::{
|
||||||
|
peer_tracker::{self, PeerTracker},
|
||||||
|
request_response::AliceToBob,
|
||||||
|
transport::SwapTransport,
|
||||||
|
TokioExecutor,
|
||||||
|
},
|
||||||
|
protocol::bob,
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use self::{amounts::*, message0::Message0, message1::Message1, message2::Message2, state::*};
|
||||||
|
|
||||||
mod amounts;
|
mod amounts;
|
||||||
pub mod event_loop;
|
pub mod event_loop;
|
||||||
|
@ -25,6 +27,7 @@ mod message0;
|
||||||
mod message1;
|
mod message1;
|
||||||
mod message2;
|
mod message2;
|
||||||
mod message3;
|
mod message3;
|
||||||
|
pub mod state;
|
||||||
mod steps;
|
mod steps;
|
||||||
pub mod swap;
|
pub mod swap;
|
||||||
|
|
||||||
|
@ -133,10 +136,10 @@ impl From<message3::OutEvent> for OutEvent {
|
||||||
pub struct Behaviour {
|
pub struct Behaviour {
|
||||||
pt: PeerTracker,
|
pt: PeerTracker,
|
||||||
amounts: Amounts,
|
amounts: Amounts,
|
||||||
message0: Message0,
|
message0: message0::Behaviour,
|
||||||
message1: Message1,
|
message1: message1::Behaviour,
|
||||||
message2: Message2,
|
message2: message2::Behaviour,
|
||||||
message3: Message3,
|
message3: message3::Behaviour,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
identity: Keypair,
|
identity: Keypair,
|
||||||
}
|
}
|
||||||
|
@ -158,31 +161,19 @@ impl Behaviour {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send Message0 to Bob in response to receiving his Message0.
|
/// Send Message0 to Bob in response to receiving his Message0.
|
||||||
pub fn send_message0(
|
pub fn send_message0(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message0) {
|
||||||
&mut self,
|
|
||||||
channel: ResponseChannel<AliceToBob>,
|
|
||||||
msg: xmr_btc::alice::Message0,
|
|
||||||
) {
|
|
||||||
self.message0.send(channel, msg);
|
self.message0.send(channel, msg);
|
||||||
debug!("Sent Message0");
|
debug!("Sent Message0");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send Message1 to Bob in response to receiving his Message1.
|
/// Send Message1 to Bob in response to receiving his Message1.
|
||||||
pub fn send_message1(
|
pub fn send_message1(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message1) {
|
||||||
&mut self,
|
|
||||||
channel: ResponseChannel<AliceToBob>,
|
|
||||||
msg: xmr_btc::alice::Message1,
|
|
||||||
) {
|
|
||||||
self.message1.send(channel, msg);
|
self.message1.send(channel, msg);
|
||||||
debug!("Sent Message1");
|
debug!("Sent Message1");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send Message2 to Bob in response to receiving his Message2.
|
/// Send Message2 to Bob in response to receiving his Message2.
|
||||||
pub fn send_message2(
|
pub fn send_message2(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message2) {
|
||||||
&mut self,
|
|
||||||
channel: ResponseChannel<AliceToBob>,
|
|
||||||
msg: xmr_btc::alice::Message2,
|
|
||||||
) {
|
|
||||||
self.message2.send(channel, msg);
|
self.message2.send(channel, msg);
|
||||||
debug!("Sent Message2");
|
debug!("Sent Message2");
|
||||||
}
|
}
|
||||||
|
@ -195,10 +186,10 @@ impl Default for Behaviour {
|
||||||
Self {
|
Self {
|
||||||
pt: PeerTracker::default(),
|
pt: PeerTracker::default(),
|
||||||
amounts: Amounts::default(),
|
amounts: Amounts::default(),
|
||||||
message0: Message0::default(),
|
message0: message0::Behaviour::default(),
|
||||||
message1: Message1::default(),
|
message1: message1::Behaviour::default(),
|
||||||
message2: Message2::default(),
|
message2: message2::Behaviour::default(),
|
||||||
message3: Message3::default(),
|
message3: message3::Behaviour::default(),
|
||||||
identity,
|
identity,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,8 +14,8 @@ use std::{
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
alice::amounts,
|
|
||||||
network::request_response::{AliceToBob, AmountsProtocol, BobToAlice, Codec, TIMEOUT},
|
network::request_response::{AliceToBob, AmountsProtocol, BobToAlice, Codec, TIMEOUT},
|
||||||
|
protocol::alice::amounts,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
|
@ -1,15 +1,19 @@
|
||||||
use crate::{
|
|
||||||
alice::{Behaviour, OutEvent},
|
|
||||||
network::{request_response::AliceToBob, transport::SwapTransport, TokioExecutor},
|
|
||||||
SwapAmounts,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
core::Multiaddr, futures::StreamExt, request_response::ResponseChannel, PeerId, Swarm,
|
core::Multiaddr, futures::StreamExt, request_response::ResponseChannel, PeerId, Swarm,
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
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)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Channels<T> {
|
pub struct Channels<T> {
|
||||||
|
@ -36,7 +40,7 @@ pub struct EventLoopHandle {
|
||||||
msg1: Receiver<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
msg1: Receiver<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
||||||
msg2: Receiver<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
msg2: Receiver<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
||||||
msg3: Receiver<bob::Message3>,
|
msg3: Receiver<bob::Message3>,
|
||||||
request: Receiver<crate::alice::amounts::OutEvent>,
|
request: Receiver<crate::protocol::alice::amounts::OutEvent>,
|
||||||
conn_established: Receiver<PeerId>,
|
conn_established: Receiver<PeerId>,
|
||||||
send_amounts: Sender<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
send_amounts: Sender<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
||||||
send_msg0: Sender<(ResponseChannel<AliceToBob>, alice::Message0)>,
|
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"))
|
.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
|
self.request
|
||||||
.recv()
|
.recv()
|
||||||
.await
|
.await
|
||||||
|
@ -131,7 +135,7 @@ pub struct EventLoop {
|
||||||
msg1: Sender<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
msg1: Sender<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
||||||
msg2: Sender<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
msg2: Sender<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
||||||
msg3: Sender<bob::Message3>,
|
msg3: Sender<bob::Message3>,
|
||||||
request: Sender<crate::alice::amounts::OutEvent>,
|
request: Sender<crate::protocol::alice::amounts::OutEvent>,
|
||||||
conn_established: Sender<PeerId>,
|
conn_established: Sender<PeerId>,
|
||||||
send_amounts: Receiver<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
send_amounts: Receiver<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
||||||
send_msg0: Receiver<(ResponseChannel<AliceToBob>, alice::Message0)>,
|
send_msg0: Receiver<(ResponseChannel<AliceToBob>, alice::Message0)>,
|
|
@ -1,11 +1,12 @@
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
request_response::{
|
request_response::{
|
||||||
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||||
RequestResponseEvent, RequestResponseMessage,
|
RequestResponseEvent, RequestResponseMessage, ResponseChannel,
|
||||||
},
|
},
|
||||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
NetworkBehaviour,
|
NetworkBehaviour,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
@ -13,9 +14,11 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT};
|
use crate::{
|
||||||
use libp2p::request_response::ResponseChannel;
|
bitcoin, monero,
|
||||||
use xmr_btc::bob;
|
network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT},
|
||||||
|
protocol::bob,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
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.
|
/// A `NetworkBehaviour` that represents send/recv of message 0.
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message0 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message0Protocol>>,
|
rr: RequestResponse<Codec<Message0Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message0 {
|
impl Behaviour {
|
||||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message0) {
|
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message0) {
|
||||||
let msg = AliceToBob::Message0(Box::new(msg));
|
let msg = AliceToBob::Message0(Box::new(msg));
|
||||||
self.rr.send_response(channel, msg);
|
self.rr.send_response(channel, msg);
|
||||||
}
|
}
|
||||||
|
@ -53,7 +67,7 @@ impl Message0 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message0 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -70,7 +84,7 @@ impl Default for Message0 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -1,3 +1,4 @@
|
||||||
|
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
request_response::{
|
request_response::{
|
||||||
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||||
|
@ -6,6 +7,7 @@ use libp2p::{
|
||||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
NetworkBehaviour,
|
NetworkBehaviour,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
@ -13,8 +15,10 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT};
|
use crate::{
|
||||||
use xmr_btc::bob;
|
network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT},
|
||||||
|
protocol::bob,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
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.
|
/// A `NetworkBehaviour` that represents send/recv of message 1.
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message1 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message1Protocol>>,
|
rr: RequestResponse<Codec<Message1Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message1 {
|
impl Behaviour {
|
||||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message1) {
|
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message1) {
|
||||||
let msg = AliceToBob::Message1(Box::new(msg));
|
let msg = AliceToBob::Message1(Box::new(msg));
|
||||||
self.rr.send_response(channel, msg);
|
self.rr.send_response(channel, msg);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +65,7 @@ impl Message1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message1 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -72,7 +82,7 @@ impl Default for Message1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -6,6 +6,7 @@ use libp2p::{
|
||||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
NetworkBehaviour,
|
NetworkBehaviour,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
@ -13,8 +14,11 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT};
|
use crate::{
|
||||||
use xmr_btc::bob;
|
monero,
|
||||||
|
network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT},
|
||||||
|
protocol::bob,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
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.
|
/// A `NetworkBehaviour` that represents receiving of message 2 from Bob.
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message2 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message2Protocol>>,
|
rr: RequestResponse<Codec<Message2Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message2 {
|
impl Behaviour {
|
||||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message2) {
|
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message2) {
|
||||||
let msg = AliceToBob::Message2(msg);
|
let msg = AliceToBob::Message2(msg);
|
||||||
self.rr.send_response(channel, msg);
|
self.rr.send_response(channel, msg);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +64,7 @@ impl Message2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message2 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -72,7 +81,7 @@ impl Default for Message2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -13,8 +13,10 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT};
|
use crate::{
|
||||||
use xmr_btc::bob;
|
network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT},
|
||||||
|
protocol::bob,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
pub enum OutEvent {
|
||||||
|
@ -25,13 +27,13 @@ pub enum OutEvent {
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message3 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message3Protocol>>,
|
rr: RequestResponse<Codec<Message3Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message3 {
|
impl Behaviour {
|
||||||
fn poll(
|
fn poll(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &mut Context<'_>,
|
_: &mut Context<'_>,
|
||||||
|
@ -45,7 +47,7 @@ impl Message3 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message3 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -62,7 +64,7 @@ impl Default for Message3 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -1,437 +1,89 @@
|
||||||
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 anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
|
||||||
use ecdsa_fun::{
|
use ecdsa_fun::{
|
||||||
adaptor::{Adaptor, EncryptedSignature},
|
adaptor::{Adaptor, EncryptedSignature},
|
||||||
nonce::Deterministic,
|
nonce::Deterministic,
|
||||||
};
|
};
|
||||||
use futures::{
|
use libp2p::request_response::ResponseChannel;
|
||||||
future::{select, Either},
|
|
||||||
pin_mut, FutureExt,
|
|
||||||
};
|
|
||||||
use genawaiter::sync::{Gen, GenBoxed};
|
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::{
|
use std::fmt;
|
||||||
convert::{TryFrom, TryInto},
|
use tracing::info;
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
use crate::{
|
||||||
|
bitcoin,
|
||||||
|
bitcoin::{
|
||||||
|
current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire, GetBlockHeight,
|
||||||
|
TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction,
|
||||||
|
},
|
||||||
|
monero,
|
||||||
|
monero::CreateWalletForOutput,
|
||||||
|
network::request_response::AliceToBob,
|
||||||
|
protocol::{alice, bob},
|
||||||
|
ExpiredTimelocks, SwapAmounts,
|
||||||
};
|
};
|
||||||
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)]
|
#[derive(Debug)]
|
||||||
pub enum Action {
|
pub enum AliceState {
|
||||||
// This action also includes proving to Bob that this has happened, given that our current
|
Started {
|
||||||
// protocol requires a transfer proof to verify that the coins have been locked on Monero
|
amounts: SwapAmounts,
|
||||||
LockXmr {
|
state0: State0,
|
||||||
amount: monero::Amount,
|
|
||||||
public_spend_key: monero::PublicKey,
|
|
||||||
public_view_key: monero::PublicViewKey,
|
|
||||||
},
|
},
|
||||||
RedeemBtc(bitcoin::Transaction),
|
Negotiated {
|
||||||
CreateMoneroWalletForOutput {
|
channel: Option<ResponseChannel<AliceToBob>>,
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
state3: Box<State3>,
|
||||||
|
},
|
||||||
|
BtcLocked {
|
||||||
|
channel: Option<ResponseChannel<AliceToBob>>,
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
state3: Box<State3>,
|
||||||
|
},
|
||||||
|
XmrLocked {
|
||||||
|
state3: Box<State3>,
|
||||||
|
},
|
||||||
|
EncSigLearned {
|
||||||
|
encrypted_signature: EncryptedSignature,
|
||||||
|
state3: Box<State3>,
|
||||||
|
},
|
||||||
|
BtcRedeemed,
|
||||||
|
BtcCancelled {
|
||||||
|
tx_cancel: TxCancel,
|
||||||
|
state3: Box<State3>,
|
||||||
|
},
|
||||||
|
BtcRefunded {
|
||||||
spend_key: monero::PrivateKey,
|
spend_key: monero::PrivateKey,
|
||||||
view_key: monero::PrivateViewKey,
|
state3: Box<State3>,
|
||||||
},
|
},
|
||||||
CancelBtc(bitcoin::Transaction),
|
BtcPunishable {
|
||||||
PunishBtc(bitcoin::Transaction),
|
tx_refund: TxRefund,
|
||||||
|
state3: Box<State3>,
|
||||||
|
},
|
||||||
|
XmrRefunded,
|
||||||
|
CancelTimelockExpired {
|
||||||
|
state3: Box<State3>,
|
||||||
|
},
|
||||||
|
BtcPunished,
|
||||||
|
SafelyAborted,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This could be moved to the bitcoin module
|
impl fmt::Display for AliceState {
|
||||||
#[async_trait]
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
pub trait ReceiveBitcoinRedeemEncsig {
|
match self {
|
||||||
async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature;
|
AliceState::Started { .. } => write!(f, "started"),
|
||||||
}
|
AliceState::Negotiated { .. } => write!(f, "negotiated"),
|
||||||
|
AliceState::BtcLocked { .. } => write!(f, "btc is locked"),
|
||||||
/// Perform the on-chain protocol to swap monero and bitcoin as Alice.
|
AliceState::XmrLocked { .. } => write!(f, "xmr is locked"),
|
||||||
///
|
AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"),
|
||||||
/// This is called post handshake, after all the keys, addresses and most of the
|
AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
|
||||||
/// signatures have been exchanged.
|
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),
|
||||||
///
|
AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"),
|
||||||
/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will
|
AliceState::BtcPunished => write!(f, "btc is punished"),
|
||||||
/// wait for Bob, the counterparty, to lock up the bitcoin.
|
AliceState::SafelyAborted => write!(f, "safely aborted"),
|
||||||
pub fn action_generator<N, B>(
|
AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"),
|
||||||
network: Arc<Mutex<N>>,
|
AliceState::XmrRefunded => write!(f, "xmr is refunded"),
|
||||||
bitcoin_client: Arc<B>,
|
AliceState::CancelTimelockExpired { .. } => write!(f, "cancel timelock is expired"),
|
||||||
// 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,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,11 +127,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");
|
info!("Producing first message");
|
||||||
let dleq_proof_s_a = cross_curve_dleq::Proof::new(rng, &self.s_a);
|
let dleq_proof_s_a = cross_curve_dleq::Proof::new(rng, &self.s_a);
|
||||||
|
|
||||||
Message0 {
|
alice::Message0 {
|
||||||
A: self.a.public(),
|
A: self.a.public(),
|
||||||
S_a_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
|
S_a_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||||
scalar: self.s_a.into_ed25519(),
|
scalar: self.s_a.into_ed25519(),
|
||||||
|
@ -580,7 +232,7 @@ pub struct State2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State2 {
|
impl State2 {
|
||||||
pub fn next_message(&self) -> Message1 {
|
pub fn next_message(&self) -> alice::Message1 {
|
||||||
let tx_cancel =
|
let tx_cancel =
|
||||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
|
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
|
||||||
|
|
||||||
|
@ -593,7 +245,7 @@ impl State2 {
|
||||||
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin, tx_refund.digest());
|
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin, tx_refund.digest());
|
||||||
|
|
||||||
let tx_cancel_sig = self.a.sign(tx_cancel.digest());
|
let tx_cancel_sig = self.a.sign(tx_cancel.digest());
|
||||||
Message1 {
|
alice::Message1 {
|
||||||
tx_refund_encsig,
|
tx_refund_encsig,
|
||||||
tx_cancel_sig,
|
tx_cancel_sig,
|
||||||
}
|
}
|
||||||
|
@ -831,8 +483,8 @@ pub struct State5 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State5 {
|
impl State5 {
|
||||||
pub fn next_message(&self) -> Message2 {
|
pub fn next_message(&self) -> alice::Message2 {
|
||||||
Message2 {
|
alice::Message2 {
|
||||||
tx_lock_proof: self.tx_lock_proof.clone(),
|
tx_lock_proof: self.tx_lock_proof.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
use crate::{
|
|
||||||
alice::event_loop::EventLoopHandle, bitcoin, monero, network::request_response::AliceToBob,
|
|
||||||
SwapAmounts,
|
|
||||||
};
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
||||||
use futures::{
|
use futures::{
|
||||||
|
@ -14,25 +10,30 @@ use sha2::Sha256;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tracing::{info, trace};
|
use tracing::{info, trace};
|
||||||
use xmr_btc::{
|
|
||||||
alice,
|
use crate::{
|
||||||
alice::State3,
|
bitcoin,
|
||||||
bitcoin::{
|
bitcoin::{
|
||||||
poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction,
|
poll_until_block_height_is_gte,
|
||||||
EncryptedSignature, GetBlockHeight, GetRawTransaction, Timelock, TransactionBlockHeight,
|
timelocks::{BlockHeight, Timelock},
|
||||||
TxCancel, TxLock, TxRefund, WaitForTransactionFinality, WatchForRawTransaction,
|
BroadcastSignedTransaction, EncryptedSignature, GetBlockHeight, GetRawTransaction,
|
||||||
|
TransactionBlockHeight, TxCancel, TxLock, TxRefund, WaitForTransactionFinality,
|
||||||
|
WatchForRawTransaction,
|
||||||
},
|
},
|
||||||
config::Config,
|
config::Config,
|
||||||
cross_curve_dleq,
|
monero,
|
||||||
monero::Transfer,
|
monero::Transfer,
|
||||||
|
network::request_response::AliceToBob,
|
||||||
|
protocol::{alice, alice::event_loop::EventLoopHandle},
|
||||||
|
SwapAmounts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn negotiate(
|
pub async fn negotiate(
|
||||||
state0: xmr_btc::alice::State0,
|
state0: alice::State0,
|
||||||
amounts: SwapAmounts,
|
amounts: SwapAmounts,
|
||||||
event_loop_handle: &mut EventLoopHandle,
|
event_loop_handle: &mut EventLoopHandle,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> Result<(ResponseChannel<AliceToBob>, State3)> {
|
) -> Result<(ResponseChannel<AliceToBob>, alice::State3)> {
|
||||||
trace!("Starting negotiate");
|
trace!("Starting negotiate");
|
||||||
|
|
||||||
// todo: we can move this out, we dont need to timeout here
|
// todo: we can move this out, we dont need to timeout here
|
||||||
|
@ -115,7 +116,7 @@ where
|
||||||
pub async fn lock_xmr<W>(
|
pub async fn lock_xmr<W>(
|
||||||
channel: ResponseChannel<AliceToBob>,
|
channel: ResponseChannel<AliceToBob>,
|
||||||
amounts: SwapAmounts,
|
amounts: SwapAmounts,
|
||||||
state3: State3,
|
state3: alice::State3,
|
||||||
event_loop_handle: &mut EventLoopHandle,
|
event_loop_handle: &mut EventLoopHandle,
|
||||||
monero_wallet: Arc<W>,
|
monero_wallet: Arc<W>,
|
||||||
) -> Result<()>
|
) -> Result<()>
|
|
@ -1,7 +1,24 @@
|
||||||
//! Run an XMR/BTC swap in the role of Alice.
|
//! Run an XMR/BTC swap in the role of Alice.
|
||||||
//! Alice holds XMR and wishes receive BTC.
|
//! Alice holds XMR and wishes receive BTC.
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_recursion::async_recursion;
|
||||||
|
use futures::{
|
||||||
|
future::{select, Either},
|
||||||
|
pin_mut,
|
||||||
|
};
|
||||||
|
use rand::{CryptoRng, RngCore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
alice::{
|
bitcoin,
|
||||||
|
bitcoin::{TransactionBlockHeight, WatchForRawTransaction},
|
||||||
|
config::Config,
|
||||||
|
database::{Database, Swap},
|
||||||
|
monero,
|
||||||
|
monero::CreateWalletForOutput,
|
||||||
|
protocol::alice::{
|
||||||
event_loop::EventLoopHandle,
|
event_loop::EventLoopHandle,
|
||||||
steps::{
|
steps::{
|
||||||
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
|
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
|
||||||
|
@ -9,28 +26,8 @@ use crate::{
|
||||||
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
|
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
|
||||||
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
||||||
},
|
},
|
||||||
|
AliceState,
|
||||||
},
|
},
|
||||||
bitcoin::EncryptedSignature,
|
|
||||||
database::{Database, Swap},
|
|
||||||
network::request_response::AliceToBob,
|
|
||||||
SwapAmounts,
|
|
||||||
};
|
|
||||||
use anyhow::Result;
|
|
||||||
use async_recursion::async_recursion;
|
|
||||||
use futures::{
|
|
||||||
future::{select, Either},
|
|
||||||
pin_mut,
|
|
||||||
};
|
|
||||||
use libp2p::request_response::ResponseChannel;
|
|
||||||
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},
|
|
||||||
config::Config,
|
|
||||||
monero::CreateWalletForOutput,
|
|
||||||
ExpiredTimelocks,
|
ExpiredTimelocks,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,75 +35,11 @@ trait Rng: RngCore + CryptoRng + Send {}
|
||||||
|
|
||||||
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
|
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum AliceState {
|
|
||||||
Started {
|
|
||||||
amounts: SwapAmounts,
|
|
||||||
state0: State0,
|
|
||||||
},
|
|
||||||
Negotiated {
|
|
||||||
channel: Option<ResponseChannel<AliceToBob>>,
|
|
||||||
amounts: SwapAmounts,
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
BtcLocked {
|
|
||||||
channel: Option<ResponseChannel<AliceToBob>>,
|
|
||||||
amounts: SwapAmounts,
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
XmrLocked {
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
EncSigLearned {
|
|
||||||
encrypted_signature: EncryptedSignature,
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
BtcRedeemed,
|
|
||||||
BtcCancelled {
|
|
||||||
tx_cancel: TxCancel,
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
BtcRefunded {
|
|
||||||
spend_key: monero::PrivateKey,
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
BtcPunishable {
|
|
||||||
tx_refund: TxRefund,
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
XmrRefunded,
|
|
||||||
CancelTimelockExpired {
|
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
BtcPunished,
|
|
||||||
SafelyAborted,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for AliceState {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
AliceState::Started { .. } => write!(f, "started"),
|
|
||||||
AliceState::Negotiated { .. } => write!(f, "negotiated"),
|
|
||||||
AliceState::BtcLocked { .. } => write!(f, "btc is locked"),
|
|
||||||
AliceState::XmrLocked { .. } => write!(f, "xmr is locked"),
|
|
||||||
AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"),
|
|
||||||
AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
|
|
||||||
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),
|
|
||||||
AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"),
|
|
||||||
AliceState::BtcPunished => write!(f, "btc is punished"),
|
|
||||||
AliceState::SafelyAborted => write!(f, "safely aborted"),
|
|
||||||
AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"),
|
|
||||||
AliceState::XmrRefunded => write!(f, "xmr is refunded"),
|
|
||||||
AliceState::CancelTimelockExpired { .. } => write!(f, "cancel timelock is expired"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn swap(
|
pub async fn swap(
|
||||||
state: AliceState,
|
state: AliceState,
|
||||||
event_loop_handle: EventLoopHandle,
|
event_loop_handle: EventLoopHandle,
|
||||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<crate::monero::Wallet>,
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
config: Config,
|
config: Config,
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
db: Database,
|
db: Database,
|
|
@ -1,24 +1,26 @@
|
||||||
//! Run an XMR/BTC swap in the role of Bob.
|
//! Run an XMR/BTC swap in the role of Bob.
|
||||||
//! Bob holds BTC and wishes receive XMR.
|
//! 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,
|
|
||||||
};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
core::{identity::Keypair, Multiaddr},
|
core::{identity::Keypair, Multiaddr},
|
||||||
NetworkBehaviour, PeerId,
|
NetworkBehaviour, PeerId,
|
||||||
};
|
};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
use xmr_btc::{
|
|
||||||
alice,
|
use crate::{
|
||||||
bitcoin::EncryptedSignature,
|
bitcoin::EncryptedSignature,
|
||||||
bob::{self},
|
network::{
|
||||||
|
peer_tracker::{self, PeerTracker},
|
||||||
|
transport::SwapTransport,
|
||||||
|
TokioExecutor,
|
||||||
|
},
|
||||||
|
protocol::{alice, bob},
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
amounts::*, message0::Message0, message1::Message1, message2::Message2, message3::Message3,
|
||||||
|
state::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod amounts;
|
mod amounts;
|
||||||
|
@ -27,6 +29,7 @@ mod message0;
|
||||||
mod message1;
|
mod message1;
|
||||||
mod message2;
|
mod message2;
|
||||||
mod message3;
|
mod message3;
|
||||||
|
pub mod state;
|
||||||
pub mod swap;
|
pub mod swap;
|
||||||
|
|
||||||
pub type Swarm = libp2p::Swarm<Behaviour>;
|
pub type Swarm = libp2p::Swarm<Behaviour>;
|
||||||
|
@ -112,10 +115,10 @@ impl From<message3::OutEvent> for OutEvent {
|
||||||
pub struct Behaviour {
|
pub struct Behaviour {
|
||||||
pt: PeerTracker,
|
pt: PeerTracker,
|
||||||
amounts: Amounts,
|
amounts: Amounts,
|
||||||
message0: Message0,
|
message0: message0::Behaviour,
|
||||||
message1: Message1,
|
message1: message1::Behaviour,
|
||||||
message2: Message2,
|
message2: message2::Behaviour,
|
||||||
message3: Message3,
|
message3: message3::Behaviour,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
identity: Keypair,
|
identity: Keypair,
|
||||||
}
|
}
|
||||||
|
@ -174,10 +177,10 @@ impl Default for Behaviour {
|
||||||
Self {
|
Self {
|
||||||
pt: PeerTracker::default(),
|
pt: PeerTracker::default(),
|
||||||
amounts: Amounts::default(),
|
amounts: Amounts::default(),
|
||||||
message0: Message0::default(),
|
message0: message0::Behaviour::default(),
|
||||||
message1: Message1::default(),
|
message1: message1::Behaviour::default(),
|
||||||
message2: Message2::default(),
|
message2: message2::Behaviour::default(),
|
||||||
message3: Message3::default(),
|
message3: message3::Behaviour::default(),
|
||||||
identity,
|
identity,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
use crate::{
|
|
||||||
bob::{Behaviour, OutEvent},
|
|
||||||
network::{transport::SwapTransport, TokioExecutor},
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use libp2p::{core::Multiaddr, PeerId};
|
use libp2p::{core::Multiaddr, PeerId};
|
||||||
|
@ -10,7 +6,15 @@ use tokio::{
|
||||||
sync::mpsc::{Receiver, Sender},
|
sync::mpsc::{Receiver, Sender},
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info};
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct Channels<T> {
|
pub struct Channels<T> {
|
|
@ -6,6 +6,7 @@ use libp2p::{
|
||||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
NetworkBehaviour, PeerId,
|
NetworkBehaviour, PeerId,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
@ -13,8 +14,21 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT};
|
use crate::{
|
||||||
use xmr_btc::{alice, bob};
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
pub enum OutEvent {
|
||||||
|
@ -25,13 +39,13 @@ pub enum OutEvent {
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message0 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message0Protocol>>,
|
rr: RequestResponse<Codec<Message0Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message0 {
|
impl Behaviour {
|
||||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message0) {
|
pub fn send(&mut self, alice: PeerId, msg: bob::Message0) {
|
||||||
let msg = BobToAlice::Message0(Box::new(msg));
|
let msg = BobToAlice::Message0(Box::new(msg));
|
||||||
let _id = self.rr.send_request(&alice, msg);
|
let _id = self.rr.send_request(&alice, msg);
|
||||||
|
@ -50,7 +64,7 @@ impl Message0 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message0 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -67,7 +81,7 @@ impl Default for Message0 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -6,6 +6,7 @@ use libp2p::{
|
||||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
NetworkBehaviour, PeerId,
|
NetworkBehaviour, PeerId,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
@ -13,8 +14,16 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT};
|
use crate::{
|
||||||
use xmr_btc::{alice, bob};
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
pub enum OutEvent {
|
||||||
|
@ -25,14 +34,14 @@ pub enum OutEvent {
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message1 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message1Protocol>>,
|
rr: RequestResponse<Codec<Message1Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message1 {
|
impl Behaviour {
|
||||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message1) {
|
pub fn send(&mut self, alice: PeerId, msg: Message1) {
|
||||||
let msg = BobToAlice::Message1(msg);
|
let msg = BobToAlice::Message1(msg);
|
||||||
let _id = self.rr.send_request(&alice, msg);
|
let _id = self.rr.send_request(&alice, msg);
|
||||||
}
|
}
|
||||||
|
@ -50,7 +59,7 @@ impl Message1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message1 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -67,7 +76,7 @@ impl Default for Message1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -1,3 +1,4 @@
|
||||||
|
use ecdsa_fun::Signature;
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
request_response::{
|
request_response::{
|
||||||
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||||
|
@ -6,6 +7,7 @@ use libp2p::{
|
||||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
NetworkBehaviour, PeerId,
|
NetworkBehaviour, PeerId,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
@ -13,8 +15,16 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT};
|
use crate::{
|
||||||
use xmr_btc::{alice, bob};
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
pub enum OutEvent {
|
||||||
|
@ -25,14 +35,14 @@ pub enum OutEvent {
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message2 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message2Protocol>>,
|
rr: RequestResponse<Codec<Message2Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message2 {
|
impl Behaviour {
|
||||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message2) {
|
pub fn send(&mut self, alice: PeerId, msg: Message2) {
|
||||||
let msg = BobToAlice::Message2(msg);
|
let msg = BobToAlice::Message2(msg);
|
||||||
let _id = self.rr.send_request(&alice, msg);
|
let _id = self.rr.send_request(&alice, msg);
|
||||||
}
|
}
|
||||||
|
@ -50,7 +60,7 @@ impl Message2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message2 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -67,7 +77,7 @@ impl Default for Message2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -6,6 +6,7 @@ use libp2p::{
|
||||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||||
NetworkBehaviour, PeerId,
|
NetworkBehaviour, PeerId,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
@ -13,8 +14,15 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT};
|
use crate::{
|
||||||
use xmr_btc::bob;
|
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)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum OutEvent {
|
pub enum OutEvent {
|
||||||
|
@ -25,14 +33,14 @@ pub enum OutEvent {
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Message3 {
|
pub struct Behaviour {
|
||||||
rr: RequestResponse<Codec<Message3Protocol>>,
|
rr: RequestResponse<Codec<Message3Protocol>>,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
events: VecDeque<OutEvent>,
|
events: VecDeque<OutEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message3 {
|
impl Behaviour {
|
||||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message3) {
|
pub fn send(&mut self, alice: PeerId, msg: Message3) {
|
||||||
let msg = BobToAlice::Message3(msg);
|
let msg = BobToAlice::Message3(msg);
|
||||||
let _id = self.rr.send_request(&alice, msg);
|
let _id = self.rr.send_request(&alice, msg);
|
||||||
}
|
}
|
||||||
|
@ -50,7 +58,7 @@ impl Message3 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Message3 {
|
impl Default for Behaviour {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(TIMEOUT);
|
||||||
let mut config = RequestResponseConfig::default();
|
let mut config = RequestResponseConfig::default();
|
||||||
|
@ -67,7 +75,7 @@ impl Default for Message3 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
|
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||||
match event {
|
match event {
|
||||||
RequestResponseEvent::Message {
|
RequestResponseEvent::Message {
|
|
@ -1,353 +1,64 @@
|
||||||
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 anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
|
||||||
use ecdsa_fun::{
|
use ecdsa_fun::{
|
||||||
adaptor::{Adaptor, EncryptedSignature},
|
adaptor::{Adaptor, EncryptedSignature},
|
||||||
nonce::Deterministic,
|
nonce::Deterministic,
|
||||||
Signature,
|
Signature,
|
||||||
};
|
};
|
||||||
use futures::{
|
|
||||||
future::{select, Either},
|
|
||||||
pin_mut, FutureExt,
|
|
||||||
};
|
|
||||||
use genawaiter::sync::{Gen, GenBoxed};
|
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::{
|
use std::fmt;
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use tokio::{sync::Mutex, time::timeout};
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
pub mod message;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::{
|
bitcoin::{
|
||||||
current_epoch, wait_for_cancel_timelock_to_expire, GetBlockHeight, GetRawTransaction,
|
self, current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire,
|
||||||
Network, Timelock, TransactionBlockHeight,
|
BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, GetNetwork, GetRawTransaction,
|
||||||
|
Transaction, TransactionBlockHeight, TxCancel, Txid, WatchForRawTransaction,
|
||||||
},
|
},
|
||||||
monero::{CreateWalletForOutput, WatchForTransfer},
|
monero,
|
||||||
|
monero::monero_private_key,
|
||||||
|
protocol::{alice, bob},
|
||||||
|
ExpiredTimelocks, SwapAmounts,
|
||||||
};
|
};
|
||||||
use ::bitcoin::{Transaction, Txid};
|
|
||||||
pub use message::{Message, Message0, Message1, Message2, Message3};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Action {
|
pub enum BobState {
|
||||||
LockBtc(bitcoin::TxLock),
|
Started {
|
||||||
SendBtcRedeemEncsig(bitcoin::EncryptedSignature),
|
state0: State0,
|
||||||
CreateXmrWalletForOutput {
|
amounts: SwapAmounts,
|
||||||
spend_key: monero::PrivateKey,
|
|
||||||
view_key: monero::PrivateViewKey,
|
|
||||||
},
|
},
|
||||||
CancelBtc(bitcoin::Transaction),
|
Negotiated(State2),
|
||||||
RefundBtc(bitcoin::Transaction),
|
BtcLocked(State3),
|
||||||
|
XmrLocked(State4),
|
||||||
|
EncSigSent(State4),
|
||||||
|
BtcRedeemed(State5),
|
||||||
|
CancelTimelockExpired(State4),
|
||||||
|
BtcCancelled(State4),
|
||||||
|
BtcRefunded(State4),
|
||||||
|
XmrRedeemed,
|
||||||
|
BtcPunished,
|
||||||
|
SafelyAborted,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This could be moved to the monero module
|
impl fmt::Display for BobState {
|
||||||
#[async_trait]
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
pub trait ReceiveTransferProof {
|
match self {
|
||||||
async fn receive_transfer_proof(&mut self) -> monero::TransferProof;
|
BobState::Started { .. } => write!(f, "started"),
|
||||||
}
|
BobState::Negotiated(..) => write!(f, "negotiated"),
|
||||||
|
BobState::BtcLocked(..) => write!(f, "btc is locked"),
|
||||||
/// Perform the on-chain protocol to swap monero and bitcoin as Bob.
|
BobState::XmrLocked(..) => write!(f, "xmr is locked"),
|
||||||
///
|
BobState::EncSigSent(..) => write!(f, "encrypted signature is sent"),
|
||||||
/// This is called post handshake, after all the keys, addresses and most of the
|
BobState::BtcRedeemed(..) => write!(f, "btc is redeemed"),
|
||||||
/// signatures have been exchanged.
|
BobState::CancelTimelockExpired(..) => write!(f, "cancel timelock is expired"),
|
||||||
///
|
BobState::BtcCancelled(..) => write!(f, "btc is cancelled"),
|
||||||
/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will
|
BobState::BtcRefunded(..) => write!(f, "btc is refunded"),
|
||||||
/// wait for Bob, the caller of this function, to lock up the bitcoin.
|
BobState::XmrRedeemed => write!(f, "xmr is redeemed"),
|
||||||
pub fn action_generator<N, M, B>(
|
BobState::BtcPunished => write!(f, "btc is punished"),
|
||||||
network: Arc<Mutex<N>>,
|
BobState::SafelyAborted => write!(f, "safely aborted"),
|
||||||
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)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct State0 {
|
pub struct State0 {
|
||||||
b: bitcoin::SecretKey,
|
b: bitcoin::SecretKey,
|
||||||
|
@ -390,10 +101,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);
|
let dleq_proof_s_b = cross_curve_dleq::Proof::new(rng, &self.s_b);
|
||||||
|
|
||||||
Message0 {
|
bob::Message0 {
|
||||||
B: self.b.public(),
|
B: self.b.public(),
|
||||||
S_b_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
|
S_b_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||||
scalar: self.s_b.into_ed25519(),
|
scalar: self.s_b.into_ed25519(),
|
||||||
|
@ -407,7 +118,7 @@ impl State0 {
|
||||||
|
|
||||||
pub async fn receive<W>(self, wallet: &W, msg: alice::Message0) -> anyhow::Result<State1>
|
pub async fn receive<W>(self, wallet: &W, msg: alice::Message0) -> anyhow::Result<State1>
|
||||||
where
|
where
|
||||||
W: BuildTxLockPsbt + Network,
|
W: BuildTxLockPsbt + GetNetwork,
|
||||||
{
|
{
|
||||||
msg.dleq_proof_s_a.verify(
|
msg.dleq_proof_s_a.verify(
|
||||||
msg.S_a_bitcoin.clone().into(),
|
msg.S_a_bitcoin.clone().into(),
|
||||||
|
@ -461,8 +172,8 @@ pub struct State1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State1 {
|
impl State1 {
|
||||||
pub fn next_message(&self) -> Message1 {
|
pub fn next_message(&self) -> bob::Message1 {
|
||||||
Message1 {
|
bob::Message1 {
|
||||||
tx_lock: self.tx_lock.clone(),
|
tx_lock: self.tx_lock.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -524,14 +235,14 @@ pub struct State2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 = 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_cancel_sig = self.b.sign(tx_cancel.digest());
|
||||||
let tx_punish =
|
let tx_punish =
|
||||||
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
|
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
|
||||||
let tx_punish_sig = self.b.sign(tx_punish.digest());
|
let tx_punish_sig = self.b.sign(tx_punish.digest());
|
||||||
|
|
||||||
Message2 {
|
bob::Message2 {
|
||||||
tx_punish_sig,
|
tx_punish_sig,
|
||||||
tx_cancel_sig,
|
tx_cancel_sig,
|
||||||
}
|
}
|
||||||
|
@ -705,11 +416,11 @@ pub struct State4 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
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 {
|
pub fn tx_redeem_encsig(&self) -> EncryptedSignature {
|
|
@ -1,54 +1,17 @@
|
||||||
use crate::{bob::event_loop::EventLoopHandle, database, database::Database, SwapAmounts};
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
use std::{fmt, sync::Arc};
|
use std::sync::Arc;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::{
|
|
||||||
bob::{self, State2},
|
use crate::{
|
||||||
ExpiredTimelocks,
|
database::{Database, Swap},
|
||||||
|
protocol::bob::{self, event_loop::EventLoopHandle, state::*},
|
||||||
|
ExpiredTimelocks, SwapAmounts,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum BobState {
|
|
||||||
Started {
|
|
||||||
state0: bob::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),
|
|
||||||
XmrRedeemed,
|
|
||||||
BtcPunished,
|
|
||||||
SafelyAborted,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for BobState {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
BobState::Started { .. } => write!(f, "started"),
|
|
||||||
BobState::Negotiated(..) => write!(f, "negotiated"),
|
|
||||||
BobState::BtcLocked(..) => write!(f, "btc is locked"),
|
|
||||||
BobState::XmrLocked(..) => write!(f, "xmr is locked"),
|
|
||||||
BobState::EncSigSent(..) => write!(f, "encrypted signature is sent"),
|
|
||||||
BobState::BtcRedeemed(..) => write!(f, "btc is redeemed"),
|
|
||||||
BobState::CancelTimelockExpired(..) => write!(f, "cancel timelock is expired"),
|
|
||||||
BobState::BtcCancelled(..) => write!(f, "btc is cancelled"),
|
|
||||||
BobState::BtcRefunded(..) => write!(f, "btc is refunded"),
|
|
||||||
BobState::XmrRedeemed => write!(f, "xmr is redeemed"),
|
|
||||||
BobState::BtcPunished => write!(f, "btc is punished"),
|
|
||||||
BobState::SafelyAborted => write!(f, "safely aborted"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(Franck): Make this a method on a struct
|
// TODO(Franck): Make this a method on a struct
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn swap<R>(
|
pub async fn swap<R>(
|
||||||
|
@ -133,8 +96,7 @@ where
|
||||||
|
|
||||||
let state = BobState::Negotiated(state2);
|
let state = BobState::Negotiated(state2);
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
.await?;
|
|
||||||
run_until(
|
run_until(
|
||||||
state,
|
state,
|
||||||
is_target_state,
|
is_target_state,
|
||||||
|
@ -155,8 +117,7 @@ where
|
||||||
|
|
||||||
let state = BobState::BtcLocked(state3);
|
let state = BobState::BtcLocked(state3);
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
.await?;
|
|
||||||
run_until(
|
run_until(
|
||||||
state,
|
state,
|
||||||
is_target_state,
|
is_target_state,
|
||||||
|
@ -209,8 +170,7 @@ where
|
||||||
BobState::CancelTimelockExpired(state4)
|
BobState::CancelTimelockExpired(state4)
|
||||||
};
|
};
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
.await?;
|
|
||||||
run_until(
|
run_until(
|
||||||
state,
|
state,
|
||||||
is_target_state,
|
is_target_state,
|
||||||
|
@ -251,8 +211,7 @@ where
|
||||||
BobState::CancelTimelockExpired(state)
|
BobState::CancelTimelockExpired(state)
|
||||||
};
|
};
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
.await?;
|
|
||||||
run_until(
|
run_until(
|
||||||
state,
|
state,
|
||||||
is_target_state,
|
is_target_state,
|
||||||
|
@ -287,8 +246,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
.await?;
|
|
||||||
run_until(
|
run_until(
|
||||||
state,
|
state,
|
||||||
is_target_state,
|
is_target_state,
|
||||||
|
@ -307,8 +265,7 @@ where
|
||||||
|
|
||||||
let state = BobState::XmrRedeemed;
|
let state = BobState::XmrRedeemed;
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
.await?;
|
|
||||||
run_until(
|
run_until(
|
||||||
state,
|
state,
|
||||||
is_target_state,
|
is_target_state,
|
||||||
|
@ -331,7 +288,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = BobState::BtcCancelled(state4);
|
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?;
|
.await?;
|
||||||
|
|
||||||
run_until(
|
run_until(
|
||||||
|
@ -360,8 +317,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
db.insert_latest_state(swap_id, database::Swap::Bob(db_state))
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
.await?;
|
|
||||||
run_until(
|
run_until(
|
||||||
state,
|
state,
|
||||||
is_target_state,
|
is_target_state,
|
||||||
|
@ -383,12 +339,12 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn negotiate<R>(
|
pub async fn negotiate<R>(
|
||||||
state0: xmr_btc::bob::State0,
|
state0: crate::protocol::bob::state::State0,
|
||||||
amounts: SwapAmounts,
|
amounts: SwapAmounts,
|
||||||
swarm: &mut EventLoopHandle,
|
swarm: &mut EventLoopHandle,
|
||||||
mut rng: R,
|
mut rng: R,
|
||||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||||
) -> Result<State2>
|
) -> Result<bob::state::State2>
|
||||||
where
|
where
|
||||||
R: RngCore + CryptoRng + Send,
|
R: RngCore + CryptoRng + Send,
|
||||||
{
|
{
|
|
@ -6,11 +6,15 @@ use futures::{
|
||||||
use get_port::get_port;
|
use get_port::get_port;
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use swap::{alice, bob};
|
use swap::{
|
||||||
|
bitcoin,
|
||||||
|
config::Config,
|
||||||
|
monero,
|
||||||
|
protocol::{alice, bob},
|
||||||
|
};
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use testutils::init_tracing;
|
use testutils::init_tracing;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::{bitcoin, config::Config};
|
|
||||||
|
|
||||||
pub mod testutils;
|
pub mod testutils;
|
||||||
|
|
||||||
|
@ -35,9 +39,9 @@ async fn happy_path() {
|
||||||
|
|
||||||
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
|
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
|
||||||
// 10_000 * 100
|
// 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_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 port = get_port().expect("Failed to find a free port");
|
||||||
let alice_multiaddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port)
|
let alice_multiaddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port)
|
||||||
|
|
|
@ -2,12 +2,17 @@ use crate::testutils::{init_alice, init_bob};
|
||||||
use get_port::get_port;
|
use get_port::get_port;
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use rand::rngs::OsRng;
|
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::AliceState, bob},
|
||||||
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use testutils::init_tracing;
|
use testutils::init_tracing;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::config::Config;
|
|
||||||
|
|
||||||
pub mod testutils;
|
pub mod testutils;
|
||||||
|
|
||||||
|
@ -25,7 +30,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() {
|
||||||
) = testutils::init_containers(&cli).await;
|
) = testutils::init_containers(&cli).await;
|
||||||
|
|
||||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
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_btc_starting_balance = btc_to_swap * 10;
|
||||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||||
|
|
|
@ -2,12 +2,17 @@ use crate::testutils::{init_alice, init_bob};
|
||||||
use get_port::get_port;
|
use get_port::get_port;
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use rand::rngs::OsRng;
|
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::BobState},
|
||||||
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use testutils::init_tracing;
|
use testutils::init_tracing;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::config::Config;
|
|
||||||
|
|
||||||
pub mod testutils;
|
pub mod testutils;
|
||||||
|
|
||||||
|
@ -25,7 +30,7 @@ async fn given_bob_restarts_after_encsig_is_sent_resume_swap() {
|
||||||
) = testutils::init_containers(&cli).await;
|
) = testutils::init_containers(&cli).await;
|
||||||
|
|
||||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
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_btc_starting_balance = btc_to_swap * 10;
|
||||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||||
|
|
|
@ -2,13 +2,18 @@ use crate::testutils::{init_alice, init_bob};
|
||||||
use get_port::get_port;
|
use get_port::get_port;
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use rand::rngs::OsRng;
|
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::AliceState, bob, bob::BobState},
|
||||||
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use testutils::init_tracing;
|
use testutils::init_tracing;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::config::Config;
|
|
||||||
|
|
||||||
pub mod testutils;
|
pub mod testutils;
|
||||||
|
|
||||||
|
@ -26,10 +31,10 @@ async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
|
||||||
) = testutils::init_containers(&cli).await;
|
) = testutils::init_containers(&cli).await;
|
||||||
|
|
||||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
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_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_btc_starting_balance = bitcoin::Amount::ZERO;
|
||||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||||
|
|
|
@ -6,11 +6,15 @@ use futures::{
|
||||||
use get_port::get_port;
|
use get_port::get_port;
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState};
|
use swap::{
|
||||||
|
bitcoin,
|
||||||
|
config::Config,
|
||||||
|
monero,
|
||||||
|
protocol::{alice, alice::AliceState, bob, bob::BobState},
|
||||||
|
};
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use testutils::init_tracing;
|
use testutils::init_tracing;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::{bitcoin, config::Config};
|
|
||||||
|
|
||||||
pub mod testutils;
|
pub mod testutils;
|
||||||
|
|
||||||
|
@ -30,7 +34,7 @@ async fn alice_punishes_if_bob_never_acts_after_fund() {
|
||||||
) = testutils::init_containers(&cli).await;
|
) = testutils::init_containers(&cli).await;
|
||||||
|
|
||||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
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_btc_starting_balance = btc_to_swap * 10;
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,18 @@ use futures::future::try_join;
|
||||||
use get_port::get_port;
|
use get_port::get_port;
|
||||||
use libp2p::Multiaddr;
|
use libp2p::Multiaddr;
|
||||||
use rand::rngs::OsRng;
|
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::AliceState, bob, bob::BobState},
|
||||||
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use testutils::init_tracing;
|
use testutils::init_tracing;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xmr_btc::{bitcoin, config::Config};
|
|
||||||
|
|
||||||
pub mod testutils;
|
pub mod testutils;
|
||||||
|
|
||||||
|
@ -29,10 +34,10 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() {
|
||||||
) = testutils::init_containers(&cli).await;
|
) = testutils::init_containers(&cli).await;
|
||||||
|
|
||||||
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
|
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_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_btc_starting_balance = bitcoin::Amount::ZERO;
|
||||||
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
let alice_xmr_starting_balance = xmr_to_swap * 10;
|
||||||
|
|
|
@ -4,14 +4,18 @@ use monero_harness::{image, Monero};
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use swap::{
|
use swap::{
|
||||||
alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, database::Database, monero,
|
bitcoin,
|
||||||
network::transport::build, SwapAmounts,
|
config::Config,
|
||||||
|
database::Database,
|
||||||
|
monero,
|
||||||
|
network::transport::build,
|
||||||
|
protocol::{alice, alice::AliceState, bob, bob::BobState},
|
||||||
|
SwapAmounts,
|
||||||
};
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use testcontainers::{clients::Cli, Container};
|
use testcontainers::{clients::Cli, Container};
|
||||||
use tracing_core::dispatcher::DefaultGuard;
|
use tracing_core::dispatcher::DefaultGuard;
|
||||||
use tracing_log::LogTracer;
|
use tracing_log::LogTracer;
|
||||||
use xmr_btc::{alice::State0, config::Config, cross_curve_dleq};
|
|
||||||
|
|
||||||
pub async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
|
pub async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
|
||||||
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
|
||||||
|
@ -27,8 +31,8 @@ pub async fn init_wallets(
|
||||||
name: &str,
|
name: &str,
|
||||||
bitcoind: &Bitcoind<'_>,
|
bitcoind: &Bitcoind<'_>,
|
||||||
monero: &Monero,
|
monero: &Monero,
|
||||||
btc_starting_balance: Option<xmr_btc::bitcoin::Amount>,
|
btc_starting_balance: Option<::bitcoin::Amount>,
|
||||||
xmr_starting_balance: Option<xmr_btc::monero::Amount>,
|
xmr_starting_balance: Option<monero::Amount>,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
||||||
match xmr_starting_balance {
|
match xmr_starting_balance {
|
||||||
|
@ -80,12 +84,12 @@ pub async fn init_alice_state(
|
||||||
xmr: xmr_to_swap,
|
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 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 redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap();
|
||||||
let punish_address = redeem_address.clone();
|
let punish_address = redeem_address.clone();
|
||||||
let state0 = State0::new(
|
let state0 = alice::State0::new(
|
||||||
a,
|
a,
|
||||||
s_a,
|
s_a,
|
||||||
v_a,
|
v_a,
|
||||||
|
@ -118,7 +122,7 @@ pub async fn init_alice(
|
||||||
monero: &Monero,
|
monero: &Monero,
|
||||||
btc_to_swap: bitcoin::Amount,
|
btc_to_swap: bitcoin::Amount,
|
||||||
xmr_to_swap: monero::Amount,
|
xmr_to_swap: monero::Amount,
|
||||||
xmr_starting_balance: xmr_btc::monero::Amount,
|
xmr_starting_balance: monero::Amount,
|
||||||
listen: Multiaddr,
|
listen: Multiaddr,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> (
|
) -> (
|
||||||
|
@ -159,7 +163,7 @@ pub async fn init_alice(
|
||||||
|
|
||||||
pub async fn init_bob_state(
|
pub async fn init_bob_state(
|
||||||
btc_to_swap: bitcoin::Amount,
|
btc_to_swap: bitcoin::Amount,
|
||||||
xmr_to_swap: xmr_btc::monero::Amount,
|
xmr_to_swap: monero::Amount,
|
||||||
bob_btc_wallet: Arc<bitcoin::Wallet>,
|
bob_btc_wallet: Arc<bitcoin::Wallet>,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> BobState {
|
) -> BobState {
|
||||||
|
@ -169,7 +173,7 @@ pub async fn init_bob_state(
|
||||||
};
|
};
|
||||||
|
|
||||||
let refund_address = bob_btc_wallet.new_address().await.unwrap();
|
let refund_address = bob_btc_wallet.new_address().await.unwrap();
|
||||||
let state0 = xmr_btc::bob::State0::new(
|
let state0 = bob::State0::new(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
btc_to_swap,
|
btc_to_swap,
|
||||||
xmr_to_swap,
|
xmr_to_swap,
|
||||||
|
@ -200,7 +204,7 @@ pub async fn init_bob(
|
||||||
monero: &Monero,
|
monero: &Monero,
|
||||||
btc_to_swap: bitcoin::Amount,
|
btc_to_swap: bitcoin::Amount,
|
||||||
btc_starting_balance: bitcoin::Amount,
|
btc_starting_balance: bitcoin::Amount,
|
||||||
xmr_to_swap: xmr_btc::monero::Amount,
|
xmr_to_swap: monero::Amount,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> (
|
) -> (
|
||||||
BobState,
|
BobState,
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "xmr-btc"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["CoBloX Team <team@coblox.tech>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# TODO: Check for stale dependencies, this looks like its a bit of a mess.
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1"
|
|
||||||
async-trait = "0.1"
|
|
||||||
bitcoin = { version = "0.25", features = ["rand", "serde"] }
|
|
||||||
conquer-once = "0.3"
|
|
||||||
cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "eddcdea1d1f16fa33ef581d1744014ece535c920", features = ["serde"] }
|
|
||||||
curve25519-dalek = "2"
|
|
||||||
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] }
|
|
||||||
ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3
|
|
||||||
futures = "0.3"
|
|
||||||
genawaiter = "0.99.1"
|
|
||||||
miniscript = { version = "4", features = ["serde"] }
|
|
||||||
monero = { version = "0.9", features = ["serde_support"] }
|
|
||||||
rand = "0.7"
|
|
||||||
rust_decimal = "1.8"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
sha2 = "0.9"
|
|
||||||
thiserror = "1"
|
|
||||||
tokio = { version = "0.2", default-features = false, features = ["time"] }
|
|
||||||
tracing = "0.1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serde_cbor = "0.11"
|
|
|
@ -1,43 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
use crate::{bitcoin, monero};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Message {
|
|
||||||
Message0(Message0),
|
|
||||||
Message1(Message1),
|
|
||||||
Message2(Message2),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message0 {
|
|
||||||
pub(crate) A: bitcoin::PublicKey,
|
|
||||||
pub(crate) S_a_monero: monero::PublicKey,
|
|
||||||
pub(crate) S_a_bitcoin: bitcoin::PublicKey,
|
|
||||||
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof,
|
|
||||||
pub(crate) v_a: monero::PrivateViewKey,
|
|
||||||
pub(crate) redeem_address: bitcoin::Address,
|
|
||||||
pub(crate) punish_address: bitcoin::Address,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message1 {
|
|
||||||
pub(crate) tx_cancel_sig: Signature,
|
|
||||||
pub(crate) tx_refund_encsig: EncryptedSignature,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message2 {
|
|
||||||
pub tx_lock_proof: monero::TransferProof,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_try_from_parent_enum!(Message0, Message);
|
|
||||||
impl_try_from_parent_enum!(Message1, Message);
|
|
||||||
impl_try_from_parent_enum!(Message2, Message);
|
|
||||||
|
|
||||||
impl_from_child_enum!(Message0, Message);
|
|
||||||
impl_from_child_enum!(Message1, Message);
|
|
||||||
impl_from_child_enum!(Message2, Message);
|
|
|
@ -1,285 +0,0 @@
|
||||||
mod timelocks;
|
|
||||||
pub mod transactions;
|
|
||||||
|
|
||||||
use crate::{config::Config, ExpiredTimelocks};
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
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::{util::psbt::PartiallySignedTransaction, *};
|
|
||||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
|
||||||
pub use timelocks::*;
|
|
||||||
pub use transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
|
||||||
|
|
||||||
// TODO: Configurable tx-fee (note: parties have to agree prior to swapping)
|
|
||||||
// Current reasoning:
|
|
||||||
// tx with largest weight (as determined by get_weight() upon broadcast in e2e
|
|
||||||
// test) = 609 assuming segwit and 60 sat/vB:
|
|
||||||
// (609 / 4) * 60 (sat/vB) = 9135 sats
|
|
||||||
// Recommended: Overpay a bit to ensure we don't have to wait too long for test
|
|
||||||
// runs.
|
|
||||||
pub const TX_FEE: u64 = 15_000;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
|
||||||
pub struct SecretKey {
|
|
||||||
inner: Scalar,
|
|
||||||
public: Point,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SecretKey {
|
|
||||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
|
||||||
let scalar = Scalar::random(rng);
|
|
||||||
|
|
||||||
let ecdsa = ECDSA::<()>::default();
|
|
||||||
let public = ecdsa.verification_key_for(&scalar);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
inner: scalar,
|
|
||||||
public,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn public(&self) -> PublicKey {
|
|
||||||
PublicKey(self.public)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> [u8; 32] {
|
|
||||||
self.inner.to_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sign(&self, digest: SigHash) -> Signature {
|
|
||||||
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
|
||||||
|
|
||||||
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]
|
|
||||||
pub trait BuildTxLockPsbt {
|
|
||||||
async fn build_tx_lock_psbt(
|
|
||||||
&self,
|
|
||||||
output_address: Address,
|
|
||||||
output_amount: Amount,
|
|
||||||
) -> Result<PartiallySignedTransaction>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait SignTxLock {
|
|
||||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait BroadcastSignedTransaction {
|
|
||||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WatchForRawTransaction {
|
|
||||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WaitForTransactionFinality {
|
|
||||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait GetBlockHeight {
|
|
||||||
async fn get_block_height(&self) -> BlockHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait TransactionBlockHeight {
|
|
||||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WaitForBlockHeight {
|
|
||||||
async fn wait_for_block_height(&self, height: BlockHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait GetRawTransaction {
|
|
||||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Network {
|
|
||||||
fn get_network(&self) -> bitcoin::Network;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
|
||||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
|
||||||
|
|
||||||
let s = adaptor
|
|
||||||
.recover_decryption_key(&S.0, &sig, &encsig)
|
|
||||||
.map(SecretKey::from)
|
|
||||||
.ok_or_else(|| anyhow!("secret recovery failure"))?;
|
|
||||||
|
|
||||||
Ok(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn poll_until_block_height_is_gte<B>(client: &B, target: BlockHeight)
|
|
||||||
where
|
|
||||||
B: GetBlockHeight,
|
|
||||||
{
|
|
||||||
while client.get_block_height().await < target {
|
|
||||||
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
use crate::{bitcoin, monero};
|
|
||||||
use anyhow::Result;
|
|
||||||
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum Message {
|
|
||||||
Message0(Message0),
|
|
||||||
Message1(Message1),
|
|
||||||
Message2(Message2),
|
|
||||||
Message3(Message3),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message0 {
|
|
||||||
pub(crate) B: bitcoin::PublicKey,
|
|
||||||
pub(crate) S_b_monero: monero::PublicKey,
|
|
||||||
pub(crate) S_b_bitcoin: bitcoin::PublicKey,
|
|
||||||
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof,
|
|
||||||
pub(crate) v_b: monero::PrivateViewKey,
|
|
||||||
pub(crate) refund_address: bitcoin::Address,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message1 {
|
|
||||||
pub(crate) tx_lock: bitcoin::TxLock,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message2 {
|
|
||||||
pub(crate) tx_punish_sig: Signature,
|
|
||||||
pub(crate) tx_cancel_sig: Signature,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message3 {
|
|
||||||
pub tx_redeem_encsig: EncryptedSignature,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_try_from_parent_enum!(Message0, Message);
|
|
||||||
impl_try_from_parent_enum!(Message1, Message);
|
|
||||||
impl_try_from_parent_enum!(Message2, Message);
|
|
||||||
impl_try_from_parent_enum!(Message3, Message);
|
|
||||||
|
|
||||||
impl_from_child_enum!(Message0, Message);
|
|
||||||
impl_from_child_enum!(Message1, Message);
|
|
||||||
impl_from_child_enum!(Message2, Message);
|
|
||||||
impl_from_child_enum!(Message3, Message);
|
|
|
@ -1,91 +0,0 @@
|
||||||
#![warn(
|
|
||||||
unused_extern_crates,
|
|
||||||
missing_debug_implementations,
|
|
||||||
missing_copy_implementations,
|
|
||||||
rust_2018_idioms,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_sign_loss,
|
|
||||||
clippy::fallible_impl_from,
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::dbg_macro
|
|
||||||
)]
|
|
||||||
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum ExpiredTimelocks {
|
|
||||||
None,
|
|
||||||
Cancel,
|
|
||||||
Punish,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
mod utils {
|
|
||||||
|
|
||||||
macro_rules! impl_try_from_parent_enum {
|
|
||||||
($type:ident, $parent:ident) => {
|
|
||||||
impl TryFrom<$parent> for $type {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
fn try_from(from: $parent) -> Result<Self> {
|
|
||||||
if let $parent::$type(inner) = from {
|
|
||||||
Ok(inner)
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Failed to convert parent state to child state"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_try_from_parent_enum_for_boxed {
|
|
||||||
($type:ident, $parent:ident) => {
|
|
||||||
impl TryFrom<$parent> for $type {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(from: $parent) -> Result<Self> {
|
|
||||||
if let $parent::$type(inner) = from {
|
|
||||||
Ok(*inner)
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Failed to convert parent state to child state"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_from_child_enum {
|
|
||||||
($type:ident, $parent:ident) => {
|
|
||||||
impl From<$type> for $parent {
|
|
||||||
fn from(from: $type) -> Self {
|
|
||||||
$parent::$type(from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_from_child_enum_for_boxed {
|
|
||||||
($type:ident, $parent:ident) => {
|
|
||||||
impl From<$type> for $parent {
|
|
||||||
fn from(from: $type) -> Self {
|
|
||||||
$parent::$type(Box::new(from))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod alice;
|
|
||||||
pub mod bitcoin;
|
|
||||||
pub mod bob;
|
|
||||||
pub mod config;
|
|
||||||
pub mod monero;
|
|
||||||
pub mod serde;
|
|
||||||
pub mod transport;
|
|
||||||
|
|
||||||
pub use cross_curve_dleq;
|
|
|
@ -1,274 +0,0 @@
|
||||||
use crate::serde::monero_private_key;
|
|
||||||
use anyhow::Result;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use rand::{CryptoRng, RngCore};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::ops::{Add, Mul, Sub};
|
|
||||||
|
|
||||||
use bitcoin::hashes::core::fmt::Formatter;
|
|
||||||
pub use curve25519_dalek::scalar::Scalar;
|
|
||||||
pub use monero::*;
|
|
||||||
use rust_decimal::{
|
|
||||||
prelude::{FromPrimitive, ToPrimitive},
|
|
||||||
Decimal,
|
|
||||||
};
|
|
||||||
use std::{fmt::Display, str::FromStr};
|
|
||||||
|
|
||||||
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
|
||||||
|
|
||||||
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
|
||||||
let scalar = Scalar::random(rng);
|
|
||||||
|
|
||||||
PrivateKey::from_scalar(scalar)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn private_key_from_secp256k1_scalar(scalar: crate::bitcoin::Scalar) -> PrivateKey {
|
|
||||||
let mut bytes = scalar.to_bytes();
|
|
||||||
|
|
||||||
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
|
|
||||||
// ed25519 scalar is little endian
|
|
||||||
bytes.reverse();
|
|
||||||
|
|
||||||
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
|
|
||||||
|
|
||||||
impl PrivateViewKey {
|
|
||||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
|
||||||
let scalar = Scalar::random(rng);
|
|
||||||
let private_key = PrivateKey::from_scalar(scalar);
|
|
||||||
|
|
||||||
Self(private_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn public(&self) -> PublicViewKey {
|
|
||||||
PublicViewKey(PublicKey::from_private_key(&self.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Add for PrivateViewKey {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 + rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PrivateViewKey> for PrivateKey {
|
|
||||||
fn from(from: PrivateViewKey) -> Self {
|
|
||||||
from.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PublicViewKey> for PublicKey {
|
|
||||||
fn from(from: PublicViewKey) -> Self {
|
|
||||||
from.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct PublicViewKey(PublicKey);
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
|
|
||||||
pub struct Amount(u64);
|
|
||||||
|
|
||||||
impl Amount {
|
|
||||||
pub const ZERO: Self = Self(0);
|
|
||||||
/// Create an [Amount] with piconero precision and the given number of
|
|
||||||
/// piconeros.
|
|
||||||
///
|
|
||||||
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
|
|
||||||
pub fn from_piconero(amount: u64) -> Self {
|
|
||||||
Amount(amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_piconero(&self) -> u64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_monero(amount: &str) -> Result<Self> {
|
|
||||||
let decimal = Decimal::from_str(amount)?;
|
|
||||||
let piconeros_dec =
|
|
||||||
decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
|
||||||
let piconeros = piconeros_dec
|
|
||||||
.to_u64()
|
|
||||||
.ok_or_else(|| OverflowError(amount.to_owned()))?;
|
|
||||||
Ok(Amount(piconeros))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Add for Amount {
|
|
||||||
type Output = Amount;
|
|
||||||
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 + rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sub for Amount {
|
|
||||||
type Output = Amount;
|
|
||||||
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 - rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mul<u64> for Amount {
|
|
||||||
type Output = Amount;
|
|
||||||
|
|
||||||
fn mul(self, rhs: u64) -> Self::Output {
|
|
||||||
Self(self.0 * rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Amount> for u64 {
|
|
||||||
fn from(from: Amount) -> u64 {
|
|
||||||
from.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Amount {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let mut decimal = Decimal::from(self.0);
|
|
||||||
decimal
|
|
||||||
.set_scale(12)
|
|
||||||
.expect("12 is smaller than max precision of 28");
|
|
||||||
write!(f, "{} XMR", decimal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct TransferProof {
|
|
||||||
tx_hash: TxHash,
|
|
||||||
#[serde(with = "monero_private_key")]
|
|
||||||
tx_key: PrivateKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransferProof {
|
|
||||||
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
|
|
||||||
Self { tx_hash, tx_key }
|
|
||||||
}
|
|
||||||
pub fn tx_hash(&self) -> TxHash {
|
|
||||||
self.tx_hash.clone()
|
|
||||||
}
|
|
||||||
pub fn tx_key(&self) -> PrivateKey {
|
|
||||||
self.tx_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add constructor/ change String to fixed length byte array
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct TxHash(pub String);
|
|
||||||
|
|
||||||
impl From<TxHash> for String {
|
|
||||||
fn from(from: TxHash) -> Self {
|
|
||||||
from.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Transfer {
|
|
||||||
async fn transfer(
|
|
||||||
&self,
|
|
||||||
public_spend_key: PublicKey,
|
|
||||||
public_view_key: PublicViewKey,
|
|
||||||
amount: Amount,
|
|
||||||
) -> anyhow::Result<(TransferProof, Amount)>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WatchForTransfer {
|
|
||||||
async fn watch_for_transfer(
|
|
||||||
&self,
|
|
||||||
public_spend_key: PublicKey,
|
|
||||||
public_view_key: PublicViewKey,
|
|
||||||
transfer_proof: TransferProof,
|
|
||||||
amount: Amount,
|
|
||||||
expected_confirmations: u32,
|
|
||||||
) -> Result<(), InsufficientFunds>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
|
||||||
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
|
|
||||||
pub struct InsufficientFunds {
|
|
||||||
pub expected: Amount,
|
|
||||||
pub actual: Amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait CreateWalletForOutput {
|
|
||||||
async fn create_and_load_wallet_for_output(
|
|
||||||
&self,
|
|
||||||
private_spend_key: PrivateKey,
|
|
||||||
private_view_key: PrivateViewKey,
|
|
||||||
) -> anyhow::Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
|
|
||||||
#[error("Overflow, cannot convert {0} to u64")]
|
|
||||||
pub struct OverflowError(pub String);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_monero_min() {
|
|
||||||
let min_pics = 1;
|
|
||||||
let amount = Amount::from_piconero(min_pics);
|
|
||||||
let monero = amount.to_string();
|
|
||||||
assert_eq!("0.000000000001 XMR", monero);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_monero_one() {
|
|
||||||
let min_pics = 1000000000000;
|
|
||||||
let amount = Amount::from_piconero(min_pics);
|
|
||||||
let monero = amount.to_string();
|
|
||||||
assert_eq!("1.000000000000 XMR", monero);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_monero_max() {
|
|
||||||
let max_pics = 18_446_744_073_709_551_615;
|
|
||||||
let amount = Amount::from_piconero(max_pics);
|
|
||||||
let monero = amount.to_string();
|
|
||||||
assert_eq!("18446744.073709551615 XMR", monero);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_monero_min() {
|
|
||||||
let monero_min = "0.000000000001";
|
|
||||||
let amount = Amount::parse_monero(monero_min).unwrap();
|
|
||||||
let pics = amount.0;
|
|
||||||
assert_eq!(1, pics);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_monero() {
|
|
||||||
let monero = "123";
|
|
||||||
let amount = Amount::parse_monero(monero).unwrap();
|
|
||||||
let pics = amount.0;
|
|
||||||
assert_eq!(123000000000000, pics);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_monero_max() {
|
|
||||||
let monero = "18446744.073709551615";
|
|
||||||
let amount = Amount::parse_monero(monero).unwrap();
|
|
||||||
let pics = amount.0;
|
|
||||||
assert_eq!(18446744073709551615, pics);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_monero_overflows() {
|
|
||||||
let overflow_pics = "18446744.073709551616";
|
|
||||||
let error = Amount::parse_monero(overflow_pics).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
error.downcast_ref::<OverflowError>().unwrap(),
|
|
||||||
&OverflowError(overflow_pics.to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
pub mod monero_private_key {
|
|
||||||
use monero::{
|
|
||||||
consensus::{Decodable, Encodable},
|
|
||||||
PrivateKey,
|
|
||||||
};
|
|
||||||
use serde::{de, de::Visitor, ser::Error, Deserializer, Serializer};
|
|
||||||
use std::{fmt, io::Cursor};
|
|
||||||
|
|
||||||
struct BytesVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for BytesVisitor {
|
|
||||||
type Value = PrivateKey;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(formatter, "a byte array representing a Monero private key")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
let mut s = s;
|
|
||||||
PrivateKey::consensus_decode(&mut s).map_err(|err| E::custom(format!("{:?}", err)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize<S>(x: &PrivateKey, s: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let mut bytes = Cursor::new(vec![]);
|
|
||||||
x.consensus_encode(&mut bytes)
|
|
||||||
.map_err(|err| S::Error::custom(format!("{:?}", err)))?;
|
|
||||||
s.serialize_bytes(bytes.into_inner().as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<PrivateKey, <D as Deserializer<'de>>::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let key = deserializer.deserialize_bytes(BytesVisitor)?;
|
|
||||||
Ok(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod monero_amount {
|
|
||||||
use crate::monero::Amount;
|
|
||||||
use serde::{Deserialize, Deserializer, Serializer};
|
|
||||||
|
|
||||||
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
s.serialize_u64(x.as_piconero())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let picos = u64::deserialize(deserializer)?;
|
|
||||||
let amount = Amount::from_piconero(picos);
|
|
||||||
|
|
||||||
Ok(amount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rand::rngs::OsRng;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serde_monero_private_key() {
|
|
||||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
|
||||||
crate::monero::Scalar::random(&mut OsRng),
|
|
||||||
));
|
|
||||||
let encoded = serde_cbor::to_vec(&key).unwrap();
|
|
||||||
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
|
||||||
assert_eq!(key, decoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serde_monero_amount() {
|
|
||||||
let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000));
|
|
||||||
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
|
||||||
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
|
||||||
assert_eq!(amount, decoded);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait SendMessage<SendMsg> {
|
|
||||||
async fn send_message(&mut self, message: SendMsg) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ReceiveMessage<RecvMsg> {
|
|
||||||
async fn receive_message(&mut self) -> Result<RecvMsg>;
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue