You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wow-btc-swap/swap/src/protocol/bob/state.rs

683 lines
22 KiB

use crate::bitcoin::wallet::EstimateFeeRate;
use crate::bitcoin::{
self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
TxLock, Txid,
};
use crate::monero;
use crate::monero::wallet::WatchRequest;
use crate::monero::{monero_private_key, TransferProof};
use crate::monero_ext::ScalarExt;
use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM};
use anyhow::{anyhow, bail, Context, Result};
use bdk::database::BatchDatabase;
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::nonce::Deterministic;
use ecdsa_fun::Signature;
use monero_rpc::wallet::BlockHeight;
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub enum BobState {
Started {
btc_amount: bitcoin::Amount,
},
ExecutionSetupDone(State2),
BtcLocked(State3),
XmrLockProofReceived {
state: State3,
lock_transfer_proof: TransferProof,
monero_wallet_restore_blockheight: BlockHeight,
},
XmrLocked(State4),
EncSigSent(State4),
BtcRedeemed(State5),
CancelTimelockExpired(State6),
BtcCancelled(State6),
BtcRefunded(State6),
XmrRedeemed {
tx_lock_id: bitcoin::Txid,
},
BtcPunished {
tx_lock_id: bitcoin::Txid,
},
SafelyAborted,
}
impl fmt::Display for BobState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BobState::Started { .. } => write!(f, "quote has been requested"),
BobState::ExecutionSetupDone(..) => write!(f, "execution setup done"),
BobState::BtcLocked(..) => write!(f, "btc is locked"),
BobState::XmrLockProofReceived { .. } => {
write!(f, "XMR lock transaction transfer proof received")
}
BobState::XmrLocked(..) => write!(f, "xmr is locked"),
BobState::EncSigSent(..) => write!(f, "encrypted signature is sent"),
BobState::BtcRedeemed(..) => write!(f, "btc is redeemed"),
BobState::CancelTimelockExpired(..) => write!(f, "cancel timelock is expired"),
BobState::BtcCancelled(..) => write!(f, "btc is cancelled"),
BobState::BtcRefunded(..) => write!(f, "btc is refunded"),
BobState::XmrRedeemed { .. } => write!(f, "xmr is redeemed"),
BobState::BtcPunished { .. } => write!(f, "btc is punished"),
BobState::SafelyAborted => write!(f, "safely aborted"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct State0 {
swap_id: Uuid,
b: bitcoin::SecretKey,
s_b: monero::Scalar,
S_b_monero: monero::PublicKey,
S_b_bitcoin: bitcoin::PublicKey,
v_b: monero::PrivateViewKey,
dleq_proof_s_b: CrossCurveDLEQProof,
btc: bitcoin::Amount,
xmr: monero::Amount,
cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
min_monero_confirmations: u64,
tx_refund_fee: bitcoin::Amount,
tx_cancel_fee: bitcoin::Amount,
}
impl State0 {
#[allow(clippy::too_many_arguments)]
pub fn new<R: RngCore + CryptoRng>(
swap_id: Uuid,
rng: &mut R,
btc: bitcoin::Amount,
xmr: monero::Amount,
cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
min_monero_confirmations: u64,
tx_refund_fee: bitcoin::Amount,
tx_cancel_fee: bitcoin::Amount,
) -> Self {
let b = bitcoin::SecretKey::new_random(rng);
let s_b = monero::Scalar::random(rng);
let v_b = monero::PrivateViewKey::new_random(rng);
let (dleq_proof_s_b, (S_b_bitcoin, S_b_monero)) = CROSS_CURVE_PROOF_SYSTEM.prove(&s_b, rng);
Self {
swap_id,
b,
s_b,
v_b,
S_b_bitcoin: bitcoin::PublicKey::from(S_b_bitcoin),
S_b_monero: monero::PublicKey {
point: S_b_monero.compress(),
},
btc,
xmr,
dleq_proof_s_b,
cancel_timelock,
punish_timelock,
refund_address,
min_monero_confirmations,
tx_refund_fee,
tx_cancel_fee,
}
}
pub fn next_message(&self) -> Message0 {
Message0 {
swap_id: self.swap_id,
B: self.b.public(),
S_b_monero: self.S_b_monero,
S_b_bitcoin: self.S_b_bitcoin,
dleq_proof_s_b: self.dleq_proof_s_b.clone(),
v_b: self.v_b,
refund_address: self.refund_address.clone(),
tx_refund_fee: self.tx_refund_fee,
tx_cancel_fee: self.tx_cancel_fee,
}
}
pub async fn receive<B, D, C>(
self,
wallet: &bitcoin::Wallet<B, D, C>,
msg: Message1,
) -> Result<State1>
where
C: EstimateFeeRate,
D: BatchDatabase,
{
let valid = CROSS_CURVE_PROOF_SYSTEM.verify(
&msg.dleq_proof_s_a,
(
msg.S_a_bitcoin.into(),
msg.S_a_monero
.point
.decompress()
.ok_or_else(|| anyhow!("S_a is not a monero curve point"))?,
),
);
if !valid {
bail!("Alice's dleq proof doesn't verify")
}
let tx_lock = bitcoin::TxLock::new(wallet, self.btc, msg.A, 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,
xmr: self.xmr,
cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: msg.redeem_address,
punish_address: msg.punish_address,
tx_lock,
min_monero_confirmations: self.min_monero_confirmations,
tx_redeem_fee: msg.tx_redeem_fee,
tx_refund_fee: self.tx_refund_fee,
tx_punish_fee: msg.tx_punish_fee,
tx_cancel_fee: self.tx_cancel_fee,
})
}
}
#[derive(Debug)]
pub struct State1 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_b: monero::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
xmr: monero::Amount,
cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
min_monero_confirmations: u64,
tx_redeem_fee: bitcoin::Amount,
tx_refund_fee: bitcoin::Amount,
tx_punish_fee: bitcoin::Amount,
tx_cancel_fee: bitcoin::Amount,
}
impl State1 {
pub fn next_message(&self) -> Message2 {
Message2 {
psbt: self.tx_lock.clone().into(),
}
}
pub fn receive(self, msg: Message3) -> Result<State2> {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
);
let tx_refund =
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
bitcoin::verify_sig(&self.A, &tx_cancel.digest(), &msg.tx_cancel_sig)?;
bitcoin::verify_encsig(
self.A,
bitcoin::PublicKey::from(self.s_b.to_secpfun_scalar()),
&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,
xmr: self.xmr,
cancel_timelock: self.cancel_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,
min_monero_confirmations: self.min_monero_confirmations,
tx_redeem_fee: self.tx_redeem_fee,
tx_refund_fee: self.tx_refund_fee,
tx_punish_fee: self.tx_punish_fee,
tx_cancel_fee: self.tx_cancel_fee,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct State2 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_b: monero::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
xmr: monero::Amount,
cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: bitcoin::EncryptedSignature,
min_monero_confirmations: u64,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_redeem_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_punish_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_refund_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_cancel_fee: bitcoin::Amount,
}
impl State2 {
pub fn next_message(&self) -> Message4 {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
);
let tx_cancel_sig = self.b.sign(tx_cancel.digest());
let tx_punish = bitcoin::TxPunish::new(
&tx_cancel,
&self.punish_address,
self.punish_timelock,
self.tx_punish_fee,
);
let tx_punish_sig = self.b.sign(tx_punish.digest());
Message4 {
tx_punish_sig,
tx_cancel_sig,
}
}
pub async fn lock_btc(self) -> Result<(State3, TxLock)> {
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,
xmr: self.xmr,
cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
tx_lock: self.tx_lock.clone(),
tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
min_monero_confirmations: self.min_monero_confirmations,
tx_redeem_fee: self.tx_redeem_fee,
tx_refund_fee: self.tx_refund_fee,
tx_cancel_fee: self.tx_cancel_fee,
},
self.tx_lock,
))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct State3 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_b: monero::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
xmr: monero::Amount,
pub cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
pub tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: bitcoin::EncryptedSignature,
min_monero_confirmations: u64,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_redeem_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_refund_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_cancel_fee: bitcoin::Amount,
}
impl State3 {
pub fn lock_xmr_watch_request(&self, transfer_proof: TransferProof) -> WatchRequest {
let S_b_monero =
monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b));
let S = self.S_a_monero + S_b_monero;
WatchRequest {
public_spend_key: S,
public_view_key: self.v.public(),
transfer_proof,
conf_target: self.min_monero_confirmations,
expected: self.xmr,
}
}
pub fn xmr_locked(self, monero_wallet_restore_blockheight: BlockHeight) -> State4 {
State4 {
A: self.A,
b: self.b,
s_b: self.s_b,
S_a_bitcoin: self.S_a_bitcoin,
v: self.v,
cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
tx_lock: self.tx_lock,
tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
monero_wallet_restore_blockheight,
tx_redeem_fee: self.tx_redeem_fee,
tx_refund_fee: self.tx_refund_fee,
tx_cancel_fee: self.tx_cancel_fee,
}
}
pub fn cancel(&self) -> State6 {
State6 {
A: self.A,
b: self.b.clone(),
s_b: self.s_b,
cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address.clone(),
tx_lock: self.tx_lock.clone(),
tx_cancel_sig_a: self.tx_cancel_sig_a.clone(),
tx_refund_encsig: self.tx_refund_encsig.clone(),
tx_refund_fee: self.tx_refund_fee,
tx_cancel_fee: self.tx_cancel_fee,
}
}
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
pub async fn current_epoch(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
);
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
Ok(current_epoch(
self.cancel_timelock,
self.punish_timelock,
tx_lock_status,
tx_cancel_status,
))
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct State4 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_b: monero::Scalar,
S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
pub cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
pub tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: bitcoin::EncryptedSignature,
monero_wallet_restore_blockheight: BlockHeight,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_redeem_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_refund_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
tx_cancel_fee: bitcoin::Amount,
}
impl State4 {
pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature {
let tx_redeem =
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee);
self.b.encsign(self.S_a_bitcoin, tx_redeem.digest())
}
pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State5> {
let tx_redeem =
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
bitcoin_wallet
.subscribe_to(tx_redeem.clone())
.await
.wait_until_seen()
.await?;
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, tx_redeem_sig, tx_redeem_encsig)?;
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
Ok(State5 {
s_a,
s_b: self.s_b,
v: self.v,
tx_lock: self.tx_lock.clone(),
monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight,
})
}
pub async fn expired_timelock(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
);
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
Ok(current_epoch(
self.cancel_timelock,
self.punish_timelock,
tx_lock_status,
tx_cancel_status,
))
}
pub fn cancel(self) -> State6 {
State6 {
A: self.A,
b: self.b,
s_b: self.s_b,
cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
tx_lock: self.tx_lock,
tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
tx_refund_fee: self.tx_refund_fee,
tx_cancel_fee: self.tx_cancel_fee,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct State5 {
#[serde(with = "monero_private_key")]
s_a: monero::PrivateKey,
s_b: monero::Scalar,
v: monero::PrivateViewKey,
tx_lock: bitcoin::TxLock,
pub monero_wallet_restore_blockheight: BlockHeight,
}
impl State5 {
pub fn xmr_keys(&self) -> (monero::PrivateKey, monero::PrivateViewKey) {
let s_b = monero::PrivateKey { scalar: self.s_b };
let s = self.s_a + s_b;
(s, self.v)
}
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct State6 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_b: monero::Scalar,
cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: bitcoin::EncryptedSignature,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub tx_refund_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub tx_cancel_fee: bitcoin::Amount,
}
impl State6 {
pub async fn expired_timelock(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> {
let tx_cancel = TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
);
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
Ok(current_epoch(
self.cancel_timelock,
self.punish_timelock,
tx_lock_status,
tx_cancel_status,
))
}
pub async fn check_for_tx_cancel(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<Transaction> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
);
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx)
}
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
let transaction = bitcoin::TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
)
.complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone())
.context("Failed to complete Bitcoin cancel transaction")?;
let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
Ok(tx_id)
}
pub async fn publish_refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
let signed_tx_refund = self.signed_refund_transaction()?;
let (_, subscription) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
subscription.wait_until_final().await?;
Ok(())
}
pub fn signed_refund_transaction(&self) -> Result<Transaction> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
);
let tx_refund =
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
let sig_b = self.b.sign(tx_refund.digest());
let sig_a =
adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone());
let signed_tx_refund =
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
Ok(signed_tx_refund)
}
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
}