mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-07 05:52:31 -04:00
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:
parent
818e522bd4
commit
1f99cf001c
27 changed files with 3977 additions and 0 deletions
516
xmr-btc/src/alice.rs
Normal file
516
xmr-btc/src/alice.rs
Normal 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
209
xmr-btc/src/bitcoin.rs
Normal 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)
|
||||
}
|
498
xmr-btc/src/bitcoin/transactions.rs
Normal file
498
xmr-btc/src/bitcoin/transactions.rs
Normal 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)
|
||||
}
|
||||
}
|
116
xmr-btc/src/bitcoin/wallet.rs
Normal file
116
xmr-btc/src/bitcoin/wallet.rs
Normal 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
472
xmr-btc/src/bob.rs
Normal 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
384
xmr-btc/src/lib.rs
Normal 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
123
xmr-btc/src/monero.rs
Normal 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<()>;
|
||||
}
|
125
xmr-btc/src/monero/wallet.rs
Normal file
125
xmr-btc/src/monero/wallet.rs
Normal 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(())
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue