From 5daa3ea9a8a78d0333142c203a302d3634c42b93 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 12 Oct 2020 17:17:22 +1100 Subject: [PATCH] [WIP] Generate actions for Bob's on-chain protocol Mimics what @thomaseizinger did here [1] and here [2]. This has the advantage that the consumer has more freedom to execute `Action`s without having to implement particular traits. The error handling required inside this protocol-executing function is also reduced. As discussed with Thomas, for this approach to work well, the trait functions such as `receive_transfer_proof` should be infallible, and the implementer should be forced to hide IO errors behind a retry mechanism. All of these asynchronous calls need to be "raced" against the abort condition (determined by the `refund_timelock`), which is missing in the current state of the implementation. The initial handshake of the protocol has not been included here, because it may not be easy to integrate this approach with libp2p, but a couple of messages still need to exchanged. I need @tcharding to tell me if it's feasible/good to do it like this. [1] https://github.com/comit-network/comit-rs/blob/move-nectar-swap-to-comit/nectar/src/swap/comit/herc20_hbit.rs#L57-L184. [2] https://github.com/comit-network/comit-rs/blob/e584d2b14f3602e2657d09b989e8ea1d483a0626/nectar/src/swap.rs#L716-L751. --- xmr-btc/Cargo.toml | 1 + xmr-btc/src/bitcoin.rs | 6 +- xmr-btc/src/bob.rs | 26 +++---- xmr-btc/src/lib.rs | 148 ++++++++++++++++++++++++++++++++++++++ xmr-btc/tests/on_chain.rs | 67 +++++++++++++++++ 5 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 xmr-btc/tests/on_chain.rs 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() {}