mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
[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] e584d2b14f/nectar/src/swap.rs (L716-L751)
.
This commit is contained in:
parent
1ee060b535
commit
5daa3ea9a8
@ -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"
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<N, M, B>(
|
||||
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<Action, (), ()>
|
||||
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::<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.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;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
67
xmr-btc/tests/on_chain.rs
Normal file
67
xmr-btc/tests/on_chain.rs
Normal file
@ -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() {}
|
Loading…
Reference in New Issue
Block a user