Swap Monero for Bitcoin

Co-authored-by: rishflab <rishflab@hotmail.com>
Co-authored-by: Philipp Hoenisch <philipp@hoenisch.at>
Co-authored-by: Tobin C. Harding <tobin@coblox.tech>
This commit is contained in:
Lucas Soriano del Pino 2020-09-28 16:18:50 +10:00
parent 818e522bd4
commit 1f99cf001c
27 changed files with 3977 additions and 0 deletions

516
xmr-btc/src/alice.rs Normal file
View file

@ -0,0 +1,516 @@
use anyhow::{anyhow, Result};
use ecdsa_fun::adaptor::{Adaptor, EncryptedSignature};
use rand::{CryptoRng, RngCore};
use crate::{bitcoin, bitcoin::GetRawTransaction, bob, monero, monero::ImportOutput};
use ecdsa_fun::{nonce::Deterministic, Signature};
use sha2::Sha256;
#[derive(Debug)]
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(Debug)]
pub struct Message1 {
pub(crate) tx_cancel_sig: Signature,
pub(crate) tx_refund_encsig: EncryptedSignature,
}
#[derive(Debug)]
pub struct Message2 {
pub(crate) tx_lock_proof: monero::TransferProof,
}
#[derive(Debug)]
pub struct State0 {
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
}
impl State0 {
pub fn new<R: RngCore + CryptoRng>(
rng: &mut R,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
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 {
a,
s_a,
v_a,
redeem_address,
punish_address,
btc,
xmr,
refund_timelock,
punish_timelock,
}
}
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
let dleq_proof_s_a = cross_curve_dleq::Proof::new(rng, &self.s_a);
Message0 {
A: self.a.public(),
S_a_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: self.s_a.into_ed25519(),
}),
S_a_bitcoin: self.s_a.into_secp256k1().into(),
dleq_proof_s_a,
v_a: self.v_a,
redeem_address: self.redeem_address.clone(),
punish_address: self.punish_address.clone(),
}
}
pub fn receive(self, msg: bob::Message0) -> Result<State1> {
msg.dleq_proof_s_b.verify(
&msg.S_b_bitcoin.clone().into(),
msg.S_b_monero
.point
.decompress()
.ok_or_else(|| anyhow!("S_b is not a monero curve point"))?,
)?;
let v = self.v_a + msg.v_b;
Ok(State1 {
a: self.a,
B: msg.B,
s_a: self.s_a,
S_b_monero: msg.S_b_monero,
S_b_bitcoin: msg.S_b_bitcoin,
v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: msg.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
})
}
}
#[derive(Debug)]
pub struct State1 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
}
impl State1 {
pub fn receive(self, msg: bob::Message1) -> State2 {
State2 {
a: self.a,
B: self.B,
s_a: self.s_a,
S_b_monero: self.S_b_monero,
S_b_bitcoin: self.S_b_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: msg.tx_lock,
}
}
}
#[derive(Debug)]
pub struct State2 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
}
impl State2 {
pub fn next_message(&self) -> Message1 {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.a.public(),
self.B.clone(),
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
// Alice encsigns the refund transaction(bitcoin) digest with Bob's monero
// pubkey(S_b). The refund transaction spends the output of
// tx_lock_bitcoin to Bob's refund address.
// recover(encsign(a, S_b, d), sign(a, d), S_b) = s_b where d is a digest, (a,
// A) is alice's keypair and (s_b, S_b) is bob's keypair.
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin.clone(), tx_refund.digest());
let tx_cancel_sig = self.a.sign(tx_cancel.digest());
Message1 {
tx_refund_encsig,
tx_cancel_sig,
}
}
pub fn receive(self, msg: bob::Message2) -> Result<State3> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.a.public(),
self.B.clone(),
);
bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig)?;
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
bitcoin::verify_sig(&self.B, &tx_punish.digest(), &msg.tx_punish_sig)?;
Ok(State3 {
a: self.a,
B: self.B,
s_a: self.s_a,
S_b_monero: self.S_b_monero,
S_b_bitcoin: self.S_b_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_punish_sig_bob: msg.tx_punish_sig,
tx_cancel_sig_bob: msg.tx_cancel_sig,
})
}
}
#[derive(Debug)]
pub struct State3 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_punish_sig_bob: bitcoin::Signature,
tx_cancel_sig_bob: bitcoin::Signature,
}
impl State3 {
pub async fn watch_for_lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State4>
where
W: bitcoin::GetRawTransaction,
{
let _ = bitcoin_wallet
.get_raw_transaction(self.tx_lock.txid())
.await?;
Ok(State4 {
a: self.a,
B: self.B,
s_a: self.s_a,
S_b_monero: self.S_b_monero,
S_b_bitcoin: self.S_b_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_punish_sig_bob: self.tx_punish_sig_bob,
tx_cancel_sig_bob: self.tx_cancel_sig_bob,
})
}
}
#[derive(Debug)]
pub struct State4 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_punish_sig_bob: bitcoin::Signature,
tx_cancel_sig_bob: bitcoin::Signature,
}
impl State4 {
pub async fn lock_xmr<W>(self, monero_wallet: &W) -> Result<(State4b, monero::Amount)>
where
W: monero::Transfer,
{
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: self.s_a.into_ed25519(),
});
let S_b = self.S_b_monero;
let (tx_lock_proof, fee) = monero_wallet
.transfer(S_a + S_b, self.v.public(), self.xmr)
.await?;
Ok((
State4b {
a: self.a,
B: self.B,
s_a: self.s_a,
S_b_monero: self.S_b_monero,
S_b_bitcoin: self.S_b_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_lock_proof,
tx_punish_sig_bob: self.tx_punish_sig_bob,
tx_cancel_sig_bob: self.tx_cancel_sig_bob,
},
fee,
))
}
pub async fn punish<W: bitcoin::BroadcastSignedTransaction>(
&self,
bitcoin_wallet: &W,
) -> Result<()> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.a.public(),
self.B.clone(),
);
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
{
let sig_a = self.a.sign(tx_cancel.digest());
let sig_b = self.tx_cancel_sig_bob.clone();
let signed_tx_cancel = tx_cancel.clone().add_signatures(
&self.tx_lock,
(self.a.public(), sig_a),
(self.B.clone(), sig_b),
)?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_cancel)
.await?;
}
{
let sig_a = self.a.sign(tx_punish.digest());
let sig_b = self.tx_punish_sig_bob.clone();
let signed_tx_punish = tx_punish.add_signatures(
&tx_cancel,
(self.a.public(), sig_a),
(self.B.clone(), sig_b),
)?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_punish)
.await?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct State4b {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_lock_proof: monero::TransferProof,
tx_punish_sig_bob: bitcoin::Signature,
tx_cancel_sig_bob: bitcoin::Signature,
}
impl State4b {
pub fn next_message(&self) -> Message2 {
Message2 {
tx_lock_proof: self.tx_lock_proof.clone(),
}
}
pub fn receive(self, msg: bob::Message3) -> State5 {
State5 {
a: self.a,
B: self.B,
s_a: self.s_a,
S_b_monero: self.S_b_monero,
S_b_bitcoin: self.S_b_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_punish_sig_bob: self.tx_punish_sig_bob,
tx_redeem_encsig: msg.tx_redeem_encsig,
}
}
// watch for refund on btc, recover s_b and refund xmr
pub async fn refund_xmr<B, M>(self, bitcoin_wallet: &B, monero_wallet: &M) -> Result<()>
where
B: GetRawTransaction,
M: ImportOutput,
{
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.a.public(),
self.B.clone(),
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin.clone(), tx_refund.digest());
let tx_refund_candidate = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
let tx_refund_sig =
tx_refund.extract_signature_by_key(tx_refund_candidate, self.a.public())?;
let s_b = bitcoin::recover(self.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
let s_b =
monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_b.to_bytes()));
let s = s_b.scalar + self.s_a.into_ed25519();
// NOTE: This actually generates and opens a new wallet, closing the currently
// open one.
monero_wallet
.import_output(monero::PrivateKey::from_scalar(s), self.v)
.await?;
Ok(())
}
}
#[derive(Debug)]
pub struct State5 {
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_punish_sig_bob: bitcoin::Signature,
tx_redeem_encsig: EncryptedSignature,
}
impl State5 {
pub async fn redeem_btc<W: bitcoin::BroadcastSignedTransaction>(
&self,
bitcoin_wallet: &W,
) -> Result<()> {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let sig_a = self.a.sign(tx_redeem.digest());
let sig_b =
adaptor.decrypt_signature(&self.s_a.into_secp256k1(), self.tx_redeem_encsig.clone());
let sig_tx_redeem = tx_redeem.add_signatures(
&self.tx_lock,
(self.a.public(), sig_a),
(self.B.clone(), sig_b),
)?;
bitcoin_wallet
.broadcast_signed_transaction(sig_tx_redeem)
.await?;
Ok(())
}
}

209
xmr-btc/src/bitcoin.rs Normal file
View file

@ -0,0 +1,209 @@
pub mod transactions;
#[cfg(test)]
pub mod wallet;
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use bitcoin::{
hashes::{hex::ToHex, Hash},
secp256k1,
util::psbt::PartiallySignedTransaction,
SigHash, Transaction,
};
use ecdsa_fun::{
adaptor::Adaptor,
fun::{
marker::{Jacobian, Mark},
Point, Scalar,
},
nonce::Deterministic,
ECDSA,
};
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, Txid};
pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
#[cfg(test)]
pub use wallet::{make_wallet, Wallet};
pub const TX_FEE: u64 = 10_000;
#[derive(Debug, Clone)]
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.clone())
}
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, Clone)]
pub struct PublicKey(Point);
impl From<PublicKey> for Point<Jacobian> {
fn from(from: PublicKey) -> Self {
from.0.mark::<Jacobian>()
}
}
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<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 GetRawTransaction {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
}
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)
}

View file

@ -0,0 +1,498 @@
use crate::bitcoin::{
build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, OutPoint, PublicKey, Txid, TX_FEE,
};
use anyhow::{bail, Context, Result};
use bitcoin::{
util::{bip143::SighashComponents, psbt::PartiallySignedTransaction},
Address, Amount, Network, SigHash, Transaction, TxIn, TxOut,
};
use ecdsa_fun::Signature;
use miniscript::Descriptor;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TxLock {
inner: Transaction,
output_descriptor: Descriptor<::bitcoin::PublicKey>,
}
impl TxLock {
pub async fn new<W>(wallet: &W, amount: Amount, A: PublicKey, B: PublicKey) -> Result<Self>
where
W: BuildTxLockPsbt,
{
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
let address = lock_output_descriptor
.address(Network::Regtest)
.expect("can derive address from descriptor");
// We construct a psbt for convenience
let psbt = wallet.build_tx_lock_psbt(address, amount).await?;
// We don't take advantage of psbt functionality yet, instead we convert to a
// raw transaction
let inner = psbt.extract_tx();
Ok(Self {
inner,
output_descriptor: lock_output_descriptor,
})
}
pub fn lock_amount(&self) -> Amount {
Amount::from_sat(self.inner.output[self.lock_output_vout()].value)
}
pub fn txid(&self) -> Txid {
self.inner.txid()
}
pub fn as_outpoint(&self) -> OutPoint {
#[allow(clippy::cast_possible_truncation)]
OutPoint::new(self.inner.txid(), self.lock_output_vout() as u32)
}
/// Retreive the index of the locked output in the transaction outputs
/// vector
fn lock_output_vout(&self) -> usize {
self.inner
.output
.iter()
.position(|output| output.script_pubkey == self.output_descriptor.script_pubkey())
.expect("transaction contains lock output")
}
fn build_spend_transaction(
&self,
spend_address: &Address,
sequence: Option<u32>,
) -> (Transaction, TxIn) {
let previous_output = self.as_outpoint();
let tx_in = TxIn {
previous_output,
script_sig: Default::default(),
sequence: sequence.unwrap_or(0xFFFF_FFFF),
witness: Vec::new(),
};
let tx_out = TxOut {
value: self.inner.output[self.lock_output_vout()].value - TX_FEE,
script_pubkey: spend_address.script_pubkey(),
};
let transaction = Transaction {
version: 2,
lock_time: 0,
input: vec![tx_in.clone()],
output: vec![tx_out],
};
(transaction, tx_in)
}
}
impl From<TxLock> for PartiallySignedTransaction {
fn from(from: TxLock) -> Self {
PartiallySignedTransaction::from_unsigned_tx(from.inner).expect("to be unsigned")
}
}
#[derive(Debug, Clone)]
pub struct TxRedeem {
inner: Transaction,
digest: SigHash,
}
impl TxRedeem {
pub fn new(tx_lock: &TxLock, redeem_address: &Address) -> Self {
// lock_input is the shared output that is now being used as an input for the
// redeem transaction
let (tx_redeem, lock_input) = tx_lock.build_spend_transaction(redeem_address, None);
let digest = SighashComponents::new(&tx_redeem).sighash_all(
&lock_input,
&tx_lock.output_descriptor.witness_script(),
tx_lock.lock_amount().as_sat(),
);
Self {
inner: tx_redeem,
digest,
}
}
pub fn txid(&self) -> Txid {
self.inner.txid()
}
pub fn digest(&self) -> SigHash {
self.digest
}
pub fn add_signatures(
self,
tx_lock: &TxLock,
(A, sig_a): (PublicKey, Signature),
(B, sig_b): (PublicKey, Signature),
) -> Result<Transaction> {
let satisfier = {
let mut satisfier = HashMap::with_capacity(2);
let A = ::bitcoin::PublicKey {
compressed: true,
key: A.0.into(),
};
let B = ::bitcoin::PublicKey {
compressed: true,
key: B.0.into(),
};
// The order in which these are inserted doesn't matter
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
satisfier.insert(B, (sig_b.into(), ::bitcoin::SigHashType::All));
satisfier
};
let mut tx_redeem = self.inner;
tx_lock
.output_descriptor
.satisfy(&mut tx_redeem.input[0], satisfier)?;
Ok(tx_redeem)
}
pub fn extract_signature_by_key(
&self,
candidate_transaction: Transaction,
B: PublicKey,
) -> Result<Signature> {
let input = match candidate_transaction.input.as_slice() {
[input] => input,
[] => bail!(NoInputs),
[inputs @ ..] => bail!(TooManyInputs(inputs.len())),
};
let sigs = match input
.witness
.iter()
.map(|vec| vec.as_slice())
.collect::<Vec<_>>()
.as_slice()
{
[sig_1, sig_2, _script] => [sig_1, sig_2]
.iter()
.map(|sig| {
bitcoin::secp256k1::Signature::from_der(&sig[..sig.len() - 1])
.map(Signature::from)
})
.collect::<std::result::Result<Vec<_>, _>>(),
[] => bail!(EmptyWitnessStack),
[witnesses @ ..] => bail!(NotThreeWitnesses(witnesses.len())),
}?;
let sig = sigs
.into_iter()
.find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok())
.context("neither signature on witness stack verifies against B")?;
Ok(sig)
}
}
#[derive(Clone, Copy, thiserror::Error, Debug)]
#[error("transaction does not spend anything")]
pub struct NoInputs;
#[derive(Clone, Copy, thiserror::Error, Debug)]
#[error("transaction has {0} inputs, expected 1")]
pub struct TooManyInputs(usize);
#[derive(Clone, Copy, thiserror::Error, Debug)]
#[error("empty witness stack")]
pub struct EmptyWitnessStack;
#[derive(Clone, Copy, thiserror::Error, Debug)]
#[error("input has {0} witnesses, expected 3")]
pub struct NotThreeWitnesses(usize);
#[derive(Debug, Clone)]
pub struct TxCancel {
inner: Transaction,
digest: SigHash,
output_descriptor: Descriptor<::bitcoin::PublicKey>,
}
impl TxCancel {
pub fn new(tx_lock: &TxLock, cancel_timelock: u32, A: PublicKey, B: PublicKey) -> Self {
let cancel_output_descriptor = build_shared_output_descriptor(A.0, B.0);
let tx_in = TxIn {
previous_output: tx_lock.as_outpoint(),
script_sig: Default::default(),
sequence: cancel_timelock,
witness: Vec::new(),
};
let tx_out = TxOut {
value: tx_lock.lock_amount().as_sat() - TX_FEE,
script_pubkey: cancel_output_descriptor.script_pubkey(),
};
let transaction = Transaction {
version: 2,
lock_time: 0,
input: vec![tx_in.clone()],
output: vec![tx_out],
};
let digest = SighashComponents::new(&transaction).sighash_all(
&tx_in,
&tx_lock.output_descriptor.witness_script(),
tx_lock.lock_amount().as_sat(),
);
Self {
inner: transaction,
digest,
output_descriptor: cancel_output_descriptor,
}
}
pub fn digest(&self) -> SigHash {
self.digest
}
fn amount(&self) -> Amount {
Amount::from_sat(self.inner.output[0].value)
}
pub fn as_outpoint(&self) -> OutPoint {
OutPoint::new(self.inner.txid(), 0)
}
pub fn add_signatures(
self,
tx_lock: &TxLock,
(A, sig_a): (PublicKey, Signature),
(B, sig_b): (PublicKey, Signature),
) -> Result<Transaction> {
let satisfier = {
let mut satisfier = HashMap::with_capacity(2);
let A = ::bitcoin::PublicKey {
compressed: true,
key: A.0.into(),
};
let B = ::bitcoin::PublicKey {
compressed: true,
key: B.0.into(),
};
// The order in which these are inserted doesn't matter
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
satisfier.insert(B, (sig_b.into(), ::bitcoin::SigHashType::All));
satisfier
};
let mut tx_cancel = self.inner;
tx_lock
.output_descriptor
.satisfy(&mut tx_cancel.input[0], satisfier)?;
Ok(tx_cancel)
}
fn build_spend_transaction(
&self,
spend_address: &Address,
sequence: Option<u32>,
) -> (Transaction, TxIn) {
let previous_output = self.as_outpoint();
let tx_in = TxIn {
previous_output,
script_sig: Default::default(),
sequence: sequence.unwrap_or(0xFFFF_FFFF),
witness: Vec::new(),
};
let tx_out = TxOut {
value: self.amount().as_sat() - TX_FEE,
script_pubkey: spend_address.script_pubkey(),
};
let transaction = Transaction {
version: 2,
lock_time: 0,
input: vec![tx_in.clone()],
output: vec![tx_out],
};
(transaction, tx_in)
}
}
#[derive(Debug)]
pub struct TxRefund {
inner: Transaction,
digest: SigHash,
}
impl TxRefund {
pub fn new(tx_cancel: &TxCancel, refund_address: &Address) -> Self {
let (tx_punish, cancel_input) = tx_cancel.build_spend_transaction(refund_address, None);
let digest = SighashComponents::new(&tx_punish).sighash_all(
&cancel_input,
&tx_cancel.output_descriptor.witness_script(),
tx_cancel.amount().as_sat(),
);
Self {
inner: tx_punish,
digest,
}
}
pub fn txid(&self) -> Txid {
self.inner.txid()
}
pub fn digest(&self) -> SigHash {
self.digest
}
pub fn add_signatures(
self,
tx_cancel: &TxCancel,
(A, sig_a): (PublicKey, Signature),
(B, sig_b): (PublicKey, Signature),
) -> Result<Transaction> {
let satisfier = {
let mut satisfier = HashMap::with_capacity(2);
let A = ::bitcoin::PublicKey {
compressed: true,
key: A.0.into(),
};
let B = ::bitcoin::PublicKey {
compressed: true,
key: B.0.into(),
};
// The order in which these are inserted doesn't matter
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
satisfier.insert(B, (sig_b.into(), ::bitcoin::SigHashType::All));
satisfier
};
let mut tx_refund = self.inner;
tx_cancel
.output_descriptor
.satisfy(&mut tx_refund.input[0], satisfier)?;
Ok(tx_refund)
}
pub fn extract_signature_by_key(
&self,
candidate_transaction: Transaction,
B: PublicKey,
) -> Result<Signature> {
let input = match candidate_transaction.input.as_slice() {
[input] => input,
[] => bail!(NoInputs),
[inputs @ ..] => bail!(TooManyInputs(inputs.len())),
};
let sigs = match input
.witness
.iter()
.map(|vec| vec.as_slice())
.collect::<Vec<_>>()
.as_slice()
{
[sig_1, sig_2, _script] => [sig_1, sig_2]
.iter()
.map(|sig| {
bitcoin::secp256k1::Signature::from_der(&sig[..sig.len() - 1])
.map(Signature::from)
})
.collect::<std::result::Result<Vec<_>, _>>(),
[] => bail!(EmptyWitnessStack),
[witnesses @ ..] => bail!(NotThreeWitnesses(witnesses.len())),
}?;
let sig = sigs
.into_iter()
.find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok())
.context("neither signature on witness stack verifies against B")?;
Ok(sig)
}
}
#[derive(Debug)]
pub struct TxPunish {
inner: Transaction,
digest: SigHash,
}
impl TxPunish {
pub fn new(tx_cancel: &TxCancel, punish_address: &Address, punish_timelock: u32) -> Self {
let (tx_punish, lock_input) =
tx_cancel.build_spend_transaction(punish_address, Some(punish_timelock));
let digest = SighashComponents::new(&tx_punish).sighash_all(
&lock_input,
&tx_cancel.output_descriptor.witness_script(),
tx_cancel.amount().as_sat(),
);
Self {
inner: tx_punish,
digest,
}
}
pub fn digest(&self) -> SigHash {
self.digest
}
pub fn add_signatures(
self,
tx_cancel: &TxCancel,
(A, sig_a): (PublicKey, Signature),
(B, sig_b): (PublicKey, Signature),
) -> Result<Transaction> {
let satisfier = {
let mut satisfier = HashMap::with_capacity(2);
let A = ::bitcoin::PublicKey {
compressed: true,
key: A.0.into(),
};
let B = ::bitcoin::PublicKey {
compressed: true,
key: B.0.into(),
};
// The order in which these are inserted doesn't matter
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
satisfier.insert(B, (sig_b.into(), ::bitcoin::SigHashType::All));
satisfier
};
let mut tx_punish = self.inner;
tx_cancel
.output_descriptor
.satisfy(&mut tx_punish.input[0], satisfier)?;
Ok(tx_punish)
}
}

View file

@ -0,0 +1,116 @@
use crate::bitcoin::{
BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxLock,
};
use anyhow::Result;
use async_trait::async_trait;
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind};
use reqwest::Url;
use std::time::Duration;
use tokio::time;
#[derive(Debug)]
pub struct Wallet(pub bitcoin_harness::Wallet);
impl Wallet {
pub async fn new(name: &str, url: &Url) -> Result<Self> {
let wallet = bitcoin_harness::Wallet::new(name, url.clone()).await?;
Ok(Self(wallet))
}
pub async fn balance(&self) -> Result<Amount> {
let balance = self.0.balance().await?;
Ok(balance)
}
pub async fn new_address(&self) -> Result<Address> {
self.0.new_address().await.map_err(Into::into)
}
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
let fee = self
.0
.get_wallet_transaction(txid)
.await
.map(|res| bitcoin::Amount::from_btc(-res.fee))??;
Ok(fee)
}
}
pub async fn make_wallet(
name: &str,
bitcoind: &Bitcoind<'_>,
fund_amount: Amount,
) -> Result<Wallet> {
let wallet = Wallet::new(name, &bitcoind.node_url).await?;
let buffer = Amount::from_btc(1.0).unwrap();
let amount = fund_amount + buffer;
let address = wallet.0.new_address().await.unwrap();
bitcoind.mint(address, amount).await.unwrap();
Ok(wallet)
}
#[async_trait]
impl BuildTxLockPsbt for Wallet {
async fn build_tx_lock_psbt(
&self,
output_address: Address,
output_amount: Amount,
) -> Result<PartiallySignedTransaction> {
let psbt = self.0.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.0.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.0.send_raw_transaction(transaction).await?;
// TODO: Instead of guessing how long it will take for the transaction to be
// mined we should ask bitcoind for the number of confirmations on `txid`
// give time for transaction to be mined
time::delay_for(Duration::from_millis(1100)).await;
Ok(txid)
}
}
#[async_trait]
impl GetRawTransaction for Wallet {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
let tx = self.0.get_raw_transaction(txid).await?;
Ok(tx)
}
}

472
xmr-btc/src/bob.rs Normal file
View file

@ -0,0 +1,472 @@
use crate::{
alice,
bitcoin::{self, BuildTxLockPsbt, GetRawTransaction, TxCancel},
monero,
};
use anyhow::{anyhow, Result};
use ecdsa_fun::{
adaptor::{Adaptor, EncryptedSignature},
nonce::Deterministic,
Signature,
};
use rand::{CryptoRng, RngCore};
use sha2::Sha256;
#[derive(Debug)]
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)]
pub struct Message1 {
pub(crate) tx_lock: bitcoin::TxLock,
}
#[derive(Debug)]
pub struct Message2 {
pub(crate) tx_punish_sig: Signature,
pub(crate) tx_cancel_sig: Signature,
}
#[derive(Debug)]
pub struct Message3 {
pub(crate) tx_redeem_encsig: EncryptedSignature,
}
#[derive(Debug)]
pub struct State0 {
b: bitcoin::SecretKey,
s_b: cross_curve_dleq::Scalar,
v_b: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
}
impl State0 {
pub fn new<R: RngCore + CryptoRng>(
rng: &mut R,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
) -> Self {
let b = bitcoin::SecretKey::new_random(rng);
let s_b = cross_curve_dleq::Scalar::random(rng);
let v_b = monero::PrivateViewKey::new_random(rng);
Self {
b,
s_b,
v_b,
btc,
xmr,
refund_timelock,
punish_timelock,
refund_address,
}
}
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
let dleq_proof_s_b = cross_curve_dleq::Proof::new(rng, &self.s_b);
Message0 {
B: self.b.public(),
S_b_monero: monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: self.s_b.into_ed25519(),
}),
S_b_bitcoin: self.s_b.into_secp256k1().into(),
dleq_proof_s_b,
v_b: self.v_b,
refund_address: self.refund_address.clone(),
}
}
pub async fn receive<W>(self, wallet: &W, msg: alice::Message0) -> anyhow::Result<State1>
where
W: BuildTxLockPsbt,
{
msg.dleq_proof_s_a.verify(
&msg.S_a_bitcoin.clone().into(),
msg.S_a_monero
.point
.decompress()
.ok_or_else(|| anyhow!("S_a is not a monero curve point"))?,
)?;
let tx_lock =
bitcoin::TxLock::new(wallet, self.btc, msg.A.clone(), self.b.public()).await?;
let v = msg.v_a + self.v_b;
Ok(State1 {
A: msg.A,
b: self.b,
s_b: self.s_b,
S_a_monero: msg.S_a_monero,
S_a_bitcoin: msg.S_a_bitcoin,
v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: msg.redeem_address,
punish_address: msg.punish_address,
tx_lock,
})
}
}
#[derive(Debug)]
pub struct State1 {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
}
impl State1 {
pub fn next_message(&self) -> Message1 {
Message1 {
tx_lock: self.tx_lock.clone(),
}
}
pub fn receive(self, msg: alice::Message1) -> Result<State2> {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
self.b.public(),
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
bitcoin::verify_sig(&self.A, &tx_cancel.digest(), &msg.tx_cancel_sig)?;
bitcoin::verify_encsig(
self.A.clone(),
self.s_b.into_secp256k1().into(),
&tx_refund.digest(),
&msg.tx_refund_encsig,
)?;
Ok(State2 {
A: self.A,
b: self.b,
s_b: self.s_b,
S_a_monero: self.S_a_monero,
S_a_bitcoin: self.S_a_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_cancel_sig_a: msg.tx_cancel_sig,
tx_refund_encsig: msg.tx_refund_encsig,
})
}
}
#[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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
impl State2 {
pub fn next_message(&self) -> Message2 {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
self.b.public(),
);
let tx_cancel_sig = self.b.sign(tx_cancel.digest());
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock);
let tx_punish_sig = self.b.sign(tx_punish.digest());
Message2 {
tx_punish_sig,
tx_cancel_sig,
}
}
pub async fn lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State2b>
where
W: bitcoin::SignTxLock + bitcoin::BroadcastSignedTransaction,
{
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(self.tx_lock.clone()).await?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_lock)
.await?;
Ok(State2b {
A: self.A,
b: self.b,
s_b: self.s_b,
S_a_monero: self.S_a_monero,
S_a_bitcoin: self.S_a_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
})
}
}
#[derive(Debug, Clone)]
pub struct State2b {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
impl State2b {
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State3>
where
W: monero::CheckTransfer,
{
let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(
self.s_b.into_ed25519(),
));
let S = self.S_a_monero + S_b_monero;
xmr_wallet
.check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr)
.await?;
Ok(State3 {
A: self.A,
b: self.b,
s_b: self.s_b,
S_a_monero: self.S_a_monero,
S_a_bitcoin: self.S_a_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
})
}
pub async fn refund_btc<W: bitcoin::BroadcastSignedTransaction>(
&self,
bitcoin_wallet: &W,
) -> Result<()> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
self.b.public(),
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
{
let sig_b = self.b.sign(tx_cancel.digest());
let sig_a = self.tx_cancel_sig_a.clone();
let signed_tx_cancel = tx_cancel.clone().add_signatures(
&self.tx_lock,
(self.A.clone(), sig_a),
(self.b.public(), sig_b),
)?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_cancel)
.await?;
}
{
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let sig_b = self.b.sign(tx_refund.digest());
let sig_a = adaptor
.decrypt_signature(&self.s_b.into_secp256k1(), self.tx_refund_encsig.clone());
let signed_tx_refund = tx_refund.add_signatures(
&tx_cancel.clone(),
(self.A.clone(), sig_a),
(self.b.public(), sig_b),
)?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_refund)
.await?;
}
Ok(())
}
#[cfg(test)]
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
}
#[derive(Debug, Clone)]
pub struct State3 {
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,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
impl State3 {
pub fn next_message(&self) -> Message3 {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest());
Message3 { tx_redeem_encsig }
}
pub async fn watch_for_redeem_btc<W>(self, bitcoin_wallet: &W) -> Result<State4>
where
W: GetRawTransaction,
{
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest());
let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?;
let tx_redeem_sig =
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
let s_a = bitcoin::recover(self.S_a_bitcoin.clone(), tx_redeem_sig, tx_redeem_encsig)?;
let s_a =
monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_a.to_bytes()));
Ok(State4 {
A: self.A,
b: self.b,
s_a,
s_b: self.s_b,
S_a_monero: self.S_a_monero,
S_a_bitcoin: self.S_a_bitcoin,
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_refund_encsig: self.tx_refund_encsig,
tx_cancel_sig: self.tx_cancel_sig_a,
})
}
}
#[derive(Debug)]
pub struct State4 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_a: monero::PrivateKey,
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_refund_encsig: EncryptedSignature,
tx_cancel_sig: Signature,
}
impl State4 {
pub async fn claim_xmr<W>(&self, monero_wallet: &W) -> Result<()>
where
W: monero::ImportOutput,
{
let s_b = monero::PrivateKey {
scalar: self.s_b.into_ed25519(),
};
let s = self.s_a + s_b;
// NOTE: This actually generates and opens a new wallet, closing the currently
// open one.
monero_wallet.import_output(s, self.v).await?;
Ok(())
}
}

384
xmr-btc/src/lib.rs Normal file
View file

@ -0,0 +1,384 @@
#![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)]
pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod monero;
#[cfg(test)]
mod tests {
use crate::{
alice, bitcoin,
bitcoin::{Amount, TX_FEE},
bob, monero,
};
use bitcoin_harness::Bitcoind;
use monero_harness::Monero;
use rand::rngs::OsRng;
use testcontainers::clients::Cli;
const TEN_XMR: u64 = 10_000_000_000_000;
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
let _ = bitcoind.init(5).await;
bitcoind
}
#[tokio::test]
async fn happy_path() {
let cli = Cli::default();
let monero = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let fund_alice = TEN_XMR;
let fund_bob = 0;
monero.init(fund_alice, fund_bob).await.unwrap();
let alice_monero_wallet = monero::AliceWallet(&monero);
let bob_monero_wallet = monero::BobWallet(&monero);
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
let alice_initial_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let refund_timelock = 1;
let punish_timelock = 1;
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
redeem_address,
punish_address,
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
refund_address.clone(),
);
let alice_message0 = alice_state0.next_message(&mut OsRng);
let bob_message0 = bob_state0.next_message(&mut OsRng);
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
let bob_state1 = bob_state0
.receive(&bob_btc_wallet, alice_message0)
.await
.unwrap();
let bob_message1 = bob_state1.next_message();
let alice_state2 = alice_state1.receive(bob_message1);
let alice_message1 = alice_state2.next_message();
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
let bob_message2 = bob_state2.next_message();
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
let lock_txid = bob_state2b.tx_lock_id();
let alice_state4 = alice_state3
.watch_for_lock_btc(&alice_btc_wallet)
.await
.unwrap();
let (alice_state4b, lock_tx_monero_fee) =
alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap();
let alice_message2 = alice_state4b.next_message();
let bob_state3 = bob_state2b
.watch_for_lock_xmr(&bob_monero_wallet, alice_message2)
.await
.unwrap();
let bob_message3 = bob_state3.next_message();
let alice_state5 = alice_state4b.receive(bob_message3);
alice_state5.redeem_btc(&alice_btc_wallet).await.unwrap();
let bob_state4 = bob_state3
.watch_for_redeem_btc(&bob_btc_wallet)
.await
.unwrap();
bob_state4.claim_xmr(&bob_monero_wallet).await.unwrap();
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
let lock_tx_bitcoin_fee = bob_btc_wallet.transaction_fee(lock_txid).await.unwrap();
assert_eq!(
alice_final_btc_balance,
alice_initial_btc_balance + btc_amount - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee
);
let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
bob_monero_wallet
.0
.wait_for_bob_wallet_block_height()
.await
.unwrap();
let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
assert_eq!(
alice_final_xmr_balance,
alice_initial_xmr_balance - u64::from(xmr_amount) - u64::from(lock_tx_monero_fee)
);
assert_eq!(
bob_final_xmr_balance,
bob_initial_xmr_balance + u64::from(xmr_amount)
);
}
#[tokio::test]
async fn both_refund() {
let cli = Cli::default();
let monero = Monero::new(&cli);
let bitcoind = init_bitcoind(&cli).await;
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let fund_alice = TEN_XMR;
let fund_bob = 0;
monero.init(fund_alice, fund_bob).await.unwrap();
let alice_monero_wallet = monero::AliceWallet(&monero);
let bob_monero_wallet = monero::BobWallet(&monero);
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let refund_timelock = 1;
let punish_timelock = 1;
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
redeem_address,
punish_address,
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
refund_address.clone(),
);
let alice_message0 = alice_state0.next_message(&mut OsRng);
let bob_message0 = bob_state0.next_message(&mut OsRng);
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
let bob_state1 = bob_state0
.receive(&bob_btc_wallet, alice_message0)
.await
.unwrap();
let bob_message1 = bob_state1.next_message();
let alice_state2 = alice_state1.receive(bob_message1);
let alice_message1 = alice_state2.next_message();
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
let bob_message2 = bob_state2.next_message();
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
let alice_state4 = alice_state3
.watch_for_lock_btc(&alice_btc_wallet)
.await
.unwrap();
let (alice_state4b, _lock_tx_monero_fee) =
alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap();
bob_state2b.refund_btc(&bob_btc_wallet).await.unwrap();
alice_state4b
.refund_xmr(&alice_btc_wallet, &alice_monero_wallet)
.await
.unwrap();
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
// to TX_FEE
let lock_tx_bitcoin_fee = bob_btc_wallet
.transaction_fee(bob_state2b.tx_lock_id())
.await
.unwrap();
assert_eq!(alice_final_btc_balance, alice_initial_btc_balance);
assert_eq!(
bob_final_btc_balance,
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
bob_initial_btc_balance - Amount::from_sat(2 * TX_FEE) - lock_tx_bitcoin_fee
);
alice_monero_wallet
.0
.wait_for_alice_wallet_block_height()
.await
.unwrap();
let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
// Because we create a new wallet when claiming Monero, we can only assert on
// this new wallet owning all of `xmr_amount` after refund
assert_eq!(alice_final_xmr_balance, u64::from(xmr_amount));
assert_eq!(bob_final_xmr_balance, bob_initial_xmr_balance);
}
#[tokio::test]
async fn alice_punishes() {
let cli = Cli::default();
let bitcoind = init_bitcoind(&cli).await;
// must be bigger than our hardcoded fee of 10_000
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await
.unwrap();
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
.await
.unwrap();
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
let punish_address = redeem_address.clone();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let refund_timelock = 1;
let punish_timelock = 1;
let alice_state0 = alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
redeem_address,
punish_address,
);
let bob_state0 = bob::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock,
punish_timelock,
refund_address.clone(),
);
let alice_message0 = alice_state0.next_message(&mut OsRng);
let bob_message0 = bob_state0.next_message(&mut OsRng);
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
let bob_state1 = bob_state0
.receive(&bob_btc_wallet, alice_message0)
.await
.unwrap();
let bob_message1 = bob_state1.next_message();
let alice_state2 = alice_state1.receive(bob_message1);
let alice_message1 = alice_state2.next_message();
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
let bob_message2 = bob_state2.next_message();
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
let alice_state4 = alice_state3
.watch_for_lock_btc(&alice_btc_wallet)
.await
.unwrap();
alice_state4.punish(&alice_btc_wallet).await.unwrap();
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
// to TX_FEE
let lock_tx_bitcoin_fee = bob_btc_wallet
.transaction_fee(bob_state2b.tx_lock_id())
.await
.unwrap();
assert_eq!(
alice_final_btc_balance,
alice_initial_btc_balance + btc_amount - Amount::from_sat(2 * TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee
);
}
}

123
xmr-btc/src/monero.rs Normal file
View file

@ -0,0 +1,123 @@
#[cfg(test)]
pub mod wallet;
use std::ops::Add;
use anyhow::Result;
use async_trait::async_trait;
use rand::{CryptoRng, RngCore};
pub use curve25519_dalek::scalar::Scalar;
pub use monero::{Address, PrivateKey, PublicKey};
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
let scalar = Scalar::random(rng);
PrivateKey::from_scalar(scalar)
}
#[cfg(test)]
pub use wallet::{AliceWallet, BobWallet};
#[derive(Clone, Copy, Debug)]
pub struct PrivateViewKey(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)]
pub struct Amount(u64);
impl Amount {
/// 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)
}
}
impl From<Amount> for u64 {
fn from(from: Amount) -> u64 {
from.0
}
}
#[derive(Clone, Debug)]
pub struct TransferProof {
tx_hash: TxHash,
tx_key: PrivateKey,
}
#[derive(Clone, Debug)]
pub struct TxHash(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,
) -> Result<(TransferProof, Amount)>;
}
#[async_trait]
pub trait CheckTransfer {
async fn check_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
amount: Amount,
) -> Result<()>;
}
#[async_trait]
pub trait ImportOutput {
async fn import_output(
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
) -> Result<()>;
}

View file

@ -0,0 +1,125 @@
use crate::monero::{
Amount, CheckTransfer, ImportOutput, PrivateViewKey, PublicKey, PublicViewKey, Transfer,
TransferProof, TxHash,
};
use anyhow::{bail, Result};
use async_trait::async_trait;
use monero::{Address, Network, PrivateKey};
use monero_harness::Monero;
use std::str::FromStr;
#[derive(Debug)]
pub struct AliceWallet<'c>(pub &'c Monero<'c>);
#[async_trait]
impl Transfer for AliceWallet<'_> {
async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> Result<(TransferProof, Amount)> {
let destination_address =
Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
let res = self
.0
.transfer_from_alice(amount.0, &destination_address.to_string())
.await?;
let tx_hash = TxHash(res.tx_hash);
let tx_key = PrivateKey::from_str(&res.tx_key)?;
let fee = Amount::from_piconero(res.fee);
Ok((TransferProof { tx_hash, tx_key }, fee))
}
}
#[derive(Debug)]
pub struct BobWallet<'c>(pub &'c Monero<'c>);
#[async_trait]
impl CheckTransfer for BobWallet<'_> {
async fn check_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
amount: Amount,
) -> Result<()> {
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
let cli = self.0.bob_wallet_rpc_client();
let res = cli
.check_tx_key(
&String::from(transfer_proof.tx_hash),
&transfer_proof.tx_key.to_string(),
&address.to_string(),
)
.await?;
if res.received != u64::from(amount) {
bail!(
"tx_lock doesn't pay enough: expected {:?}, got {:?}",
res.received,
amount
)
}
Ok(())
}
}
#[async_trait]
impl ImportOutput for BobWallet<'_> {
async fn import_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(Network::Mainnet, public_spend_key, public_view_key);
let _ = self
.0
.bob_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
Ok(())
}
}
#[async_trait]
impl ImportOutput for AliceWallet<'_> {
async fn import_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(Network::Mainnet, public_spend_key, public_view_key);
let _ = self
.0
.alice_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
Ok(())
}
}