diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 0160696c..35475ab1 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -12,6 +12,7 @@ cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", curve25519-dalek = "2" ecdsa_fun = { version = "0.3.1", features = ["libsecp_compat"] } ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3 +genawaiter = "0.99.1" miniscript = "1" monero = "0.9" rand = "0.7" diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 5bad1f9d..43622cb6 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -6,9 +6,8 @@ use bitcoin::{ hashes::{hex::ToHex, Hash}, secp256k1, util::psbt::PartiallySignedTransaction, - SigHash, Transaction, + SigHash, }; -pub use bitcoin::{Address, Amount, OutPoint, Txid}; use ecdsa_fun::{ adaptor::Adaptor, fun::{ @@ -18,13 +17,14 @@ use ecdsa_fun::{ nonce::Deterministic, ECDSA, }; -pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; use miniscript::{Descriptor, Segwitv0}; use rand::{CryptoRng, RngCore}; use sha2::Sha256; use std::str::FromStr; pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund}; +pub use bitcoin::{Address, Amount, OutPoint, Transaction, Txid}; +pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; pub const TX_FEE: u64 = 10_000; diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index f3c38157..75c13bd3 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -255,22 +255,22 @@ impl State1 { #[derive(Debug)] pub struct State2 { - A: bitcoin::PublicKey, - b: bitcoin::SecretKey, - s_b: cross_curve_dleq::Scalar, - S_a_monero: monero::PublicKey, - S_a_bitcoin: bitcoin::PublicKey, - v: monero::PrivateViewKey, + pub A: bitcoin::PublicKey, + pub b: bitcoin::SecretKey, + pub s_b: cross_curve_dleq::Scalar, + pub S_a_monero: monero::PublicKey, + pub S_a_bitcoin: bitcoin::PublicKey, + pub v: monero::PrivateViewKey, btc: bitcoin::Amount, - xmr: monero::Amount, - refund_timelock: u32, + pub xmr: monero::Amount, + pub refund_timelock: u32, punish_timelock: u32, - refund_address: bitcoin::Address, - redeem_address: bitcoin::Address, + pub refund_address: bitcoin::Address, + pub redeem_address: bitcoin::Address, punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, - tx_cancel_sig_a: Signature, - tx_refund_encsig: EncryptedSignature, + pub tx_lock: bitcoin::TxLock, + pub tx_cancel_sig_a: Signature, + pub tx_refund_encsig: EncryptedSignature, } impl State2 { diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 790cb477..a26269a9 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -50,3 +50,151 @@ pub mod bitcoin; pub mod bob; pub mod monero; pub mod transport; + +use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use genawaiter::sync::{Gen, GenBoxed}; +use sha2::Sha256; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum Action { + LockBitcoin(bitcoin::TxLock), + SendBitcoinRedeemEncsig(bitcoin::EncryptedSignature), + CreateMoneroWalletForOutput { + spend_key: monero::PrivateKey, + view_key: monero::PrivateViewKey, + }, + RefundBitcoin { + tx_cancel: bitcoin::Transaction, + tx_refund: bitcoin::Transaction, + }, +} + +// TODO: This could be moved to the monero module +pub trait ReceiveTransferProof { + fn receive_transfer_proof(&self) -> monero::TransferProof; +} + +/// Perform the on-chain protocol to swap monero and bitcoin as Bob. +/// +/// This is called post handshake, after all the keys, addresses and most of the +/// signatures have been exchanged. +pub fn action_generator_bob( + network: &'static N, + monero_ledger: &'static M, + bitcoin_ledger: &'static B, + // TODO: Replace this with a new, slimmer struct? + bob::State2 { + A, + b, + s_b, + S_a_monero, + S_a_bitcoin, + v, + xmr, + refund_timelock, + redeem_address, + refund_address, + tx_lock, + tx_cancel_sig_a, + tx_refund_encsig, + .. + }: bob::State2, +) -> GenBoxed +where + N: ReceiveTransferProof + Send + Sync, + M: monero::CheckTransfer + Send + Sync, + B: bitcoin::WatchForRawTransaction + Send + Sync, +{ + Gen::new_boxed(|co| async move { + let swap_result: Result<(), ()> = { + co.yield_(Action::LockBitcoin(tx_lock.clone())).await; + + // the source of this could be the database, this layer doesn't care + let transfer_proof = network.receive_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; + + // TODO: We should require a specific number of confirmations on the lock + // transaction + monero_ledger + .check_transfer(S, v.public(), transfer_proof, xmr) + .await + .expect("TODO: implementor of this trait must make it infallible by retrying"); + + let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); + let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest()); + + co.yield_(Action::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone())) + .await; + + let tx_redeem_published = bitcoin_ledger + .watch_for_raw_transaction(tx_redeem.txid()) + .await + .expect("TODO: implementor of this trait must make it infallible by retrying"); + + // NOTE: If any of this fails, Bob will never be able to take the monero. + // Therefore, there is no way to handle these errors other than aborting + let tx_redeem_sig = tx_redeem + .extract_signature_by_key(tx_redeem_published, b.public()) + .expect("redeem transaction must include signature from us"); + let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig).expect( + "alice can only produce our signature by decrypting our encrypted signature", + ); + let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order( + s_a.to_bytes(), + )); + + let s_b = monero::PrivateKey { + scalar: s_b.into_ed25519(), + }; + + co.yield_(Action::CreateMoneroWalletForOutput { + spend_key: s_a + s_b, + view_key: v, + }) + .await; + + Ok(()) + }; + + // NOTE: swap result should only be `Err` if we have reached the + // `refund_timelock`. Therefore, we should always yield the refund action + if swap_result.is_err() { + let tx_cancel = + bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); + + 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.clone(), sig_a), (b.public(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel") + }; + + let signed_tx_refund = { + let adaptor = Adaptor::>::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.clone(), sig_a), (b.public(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_refund") + }; + + co.yield_(Action::RefundBitcoin { + tx_cancel: signed_tx_cancel, + tx_refund: signed_tx_refund, + }) + .await; + } + }) +} diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs new file mode 100644 index 00000000..d8d9e59d --- /dev/null +++ b/xmr-btc/tests/on_chain.rs @@ -0,0 +1,67 @@ +mod harness; + +use anyhow::Result; +use genawaiter::GeneratorState; +use harness::wallet::{bitcoin, monero}; +use xmr_btc::{ + action_generator_bob, + bitcoin::{BroadcastSignedTransaction, SignTxLock}, + bob, + monero::CreateWalletForOutput, + Action, ReceiveTransferProof, +}; + +struct Network; + +impl ReceiveTransferProof for Network { + fn receive_transfer_proof(&self) -> xmr_btc::monero::TransferProof { + todo!("use libp2p") + } +} + +async fn swap_as_bob( + network: &'static Network, + monero_wallet: &'static monero::BobWallet<'static>, + bitcoin_wallet: &'static bitcoin::Wallet, + state: bob::State2, +) -> Result<()> { + let mut action_generator = action_generator_bob(network, monero_wallet, bitcoin_wallet, state); + + loop { + match action_generator.async_resume().await { + GeneratorState::Yielded(Action::LockBitcoin(tx_lock)) => { + let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?; + let _ = bitcoin_wallet + .broadcast_signed_transaction(signed_tx_lock) + .await?; + } + GeneratorState::Yielded(Action::SendBitcoinRedeemEncsig(_tx_redeem_encsig)) => { + todo!("use libp2p") + } + GeneratorState::Yielded(Action::CreateMoneroWalletForOutput { + spend_key, + view_key, + }) => { + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + } + GeneratorState::Yielded(Action::RefundBitcoin { + tx_cancel, + tx_refund, + }) => { + let _ = bitcoin_wallet + .broadcast_signed_transaction(tx_cancel) + .await?; + + let _ = bitcoin_wallet + .broadcast_signed_transaction(tx_refund) + .await?; + } + GeneratorState::Complete(()) => return Ok(()), + } + } +} + +#[test] +fn on_chain_happy_path() {}