[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:
Lucas Soriano del Pino 2020-10-12 17:17:22 +11:00
parent 1ee060b535
commit 5daa3ea9a8
5 changed files with 232 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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