diff --git a/swap/src/database/bob.rs b/swap/src/database/bob.rs index 943957dd..322e21ba 100644 --- a/swap/src/database/bob.rs +++ b/swap/src/database/bob.rs @@ -29,8 +29,8 @@ pub enum Bob { state4: bob::State4, }, BtcRedeemed(bob::State5), - CancelTimelockExpired(bob::State4), - BtcCancelled(bob::State4), + CancelTimelockExpired(bob::State6), + BtcCancelled(bob::State6), Done(BobEndState), } @@ -38,7 +38,7 @@ pub enum Bob { pub enum BobEndState { SafelyAborted, XmrRedeemed { tx_lock_id: bitcoin::Txid }, - BtcRefunded(Box), + BtcRefunded(Box), BtcPunished { tx_lock_id: bitcoin::Txid }, } @@ -60,9 +60,9 @@ impl From for Bob { BobState::XmrLocked(state4) => Bob::XmrLocked { state4 }, BobState::EncSigSent(state4) => Bob::EncSigSent { state4 }, BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5), - BobState::CancelTimelockExpired(state4) => Bob::CancelTimelockExpired(state4), - BobState::BtcCancelled(state4) => Bob::BtcCancelled(state4), - BobState::BtcRefunded(state4) => Bob::Done(BobEndState::BtcRefunded(Box::new(state4))), + BobState::CancelTimelockExpired(state6) => Bob::CancelTimelockExpired(state6), + BobState::BtcCancelled(state6) => Bob::BtcCancelled(state6), + BobState::BtcRefunded(state6) => Bob::Done(BobEndState::BtcRefunded(Box::new(state6))), BobState::XmrRedeemed { tx_lock_id } => { Bob::Done(BobEndState::XmrRedeemed { tx_lock_id }) } @@ -92,12 +92,12 @@ impl From for BobState { Bob::XmrLocked { state4 } => BobState::XmrLocked(state4), Bob::EncSigSent { state4 } => BobState::EncSigSent(state4), Bob::BtcRedeemed(state5) => BobState::BtcRedeemed(state5), - Bob::CancelTimelockExpired(state4) => BobState::CancelTimelockExpired(state4), - Bob::BtcCancelled(state4) => BobState::BtcCancelled(state4), + Bob::CancelTimelockExpired(state6) => BobState::CancelTimelockExpired(state6), + Bob::BtcCancelled(state6) => BobState::BtcCancelled(state6), Bob::Done(end_state) => match end_state { BobEndState::SafelyAborted => BobState::SafelyAborted, BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, - BobEndState::BtcRefunded(state4) => BobState::BtcRefunded(*state4), + BobEndState::BtcRefunded(state6) => BobState::BtcRefunded(*state6), BobEndState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id }, }, } diff --git a/swap/src/protocol/bob/cancel.rs b/swap/src/protocol/bob/cancel.rs index 90209383..ea9f2d9d 100644 --- a/swap/src/protocol/bob/cancel.rs +++ b/swap/src/protocol/bob/cancel.rs @@ -20,12 +20,12 @@ pub async fn cancel( db: Database, force: bool, ) -> Result> { - let state4 = match state { + let state6 = match state { BobState::BtcLocked(state3) => state3.cancel(), BobState::XmrLockProofReceived { state, .. } => state.cancel(), - BobState::XmrLocked(state4) => state4, - BobState::EncSigSent(state4) => state4, - BobState::CancelTimelockExpired(state4) => state4, + BobState::XmrLocked(state4) => state4.cancel(), + BobState::EncSigSent(state4) => state4.cancel(), + BobState::CancelTimelockExpired(state6) => state6, _ => bail!( "Cannot cancel swap {} because it is in state {} which is not refundable.", swap_id, @@ -34,16 +34,16 @@ pub async fn cancel( }; if !force { - if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? { + if let ExpiredTimelocks::None = state6.expired_timelock(bitcoin_wallet.as_ref()).await? { return Ok(Err(Error::CancelTimelockNotExpiredYet)); } - if state4 + if state6 .check_for_tx_cancel(bitcoin_wallet.as_ref()) .await .is_ok() { - let state = BobState::BtcCancelled(state4); + let state = BobState::BtcCancelled(state6); let db_state = state.into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; @@ -51,9 +51,9 @@ pub async fn cancel( } } - let txid = state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; + let txid = state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; - let state = BobState::BtcCancelled(state4); + let state = BobState::BtcCancelled(state6); let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index b1390045..4fa96778 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -16,14 +16,14 @@ pub async fn refund( db: Database, force: bool, ) -> Result> { - let state4 = if force { + let state6 = if force { match state { BobState::BtcLocked(state3) => state3.cancel(), BobState::XmrLockProofReceived { state, .. } => state.cancel(), - BobState::XmrLocked(state4) => state4, - BobState::EncSigSent(state4) => state4, - BobState::CancelTimelockExpired(state4) => state4, - BobState::BtcCancelled(state4) => state4, + BobState::XmrLocked(state4) => state4.cancel(), + BobState::EncSigSent(state4) => state4.cancel(), + BobState::CancelTimelockExpired(state6) => state6, + BobState::BtcCancelled(state6) => state6, _ => bail!( "Cannot refund swap {} because it is in state {} which is not refundable.", swap_id, @@ -32,16 +32,16 @@ pub async fn refund( } } else { match state { - BobState::BtcCancelled(state4) => state4, + BobState::BtcCancelled(state6) => state6, _ => { return Ok(Err(SwapNotCancelledYet(swap_id))); } } }; - state4.refund_btc(bitcoin_wallet.as_ref()).await?; + state6.refund_btc(bitcoin_wallet.as_ref()).await?; - let state = BobState::BtcRefunded(state4); + let state = BobState::BtcRefunded(state6); let db_state = state.clone().into(); db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 9db3f852..e1f3c2d6 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -34,9 +34,9 @@ pub enum BobState { XmrLocked(State4), EncSigSent(State4), BtcRedeemed(State5), - CancelTimelockExpired(State4), - BtcCancelled(State4), - BtcRefunded(State4), + CancelTimelockExpired(State6), + BtcCancelled(State6), + BtcRefunded(State6), XmrRedeemed { tx_lock_id: bitcoin::Txid, }, @@ -357,23 +357,17 @@ impl State3 { Ok(()) } - pub fn cancel(&self) -> State4 { - State4 { + pub fn cancel(&self) -> State6 { + State6 { A: self.A, b: self.b.clone(), 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.clone(), - redeem_address: self.redeem_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(), - // For cancel scenarios the monero wallet rescan blockchain height is irrelevant for - // Bob, because Bob's cancel can only lead to refunding on Bitcoin - monero_wallet_restore_blockheight: BlockHeight { height: 0 }, } } @@ -428,29 +422,6 @@ impl State4 { self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()) } - pub async fn check_for_tx_cancel( - &self, - bitcoin_wallet: &bitcoin::Wallet, - ) -> Result { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - - 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 { - let transaction = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()) - .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 watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); @@ -505,29 +476,18 @@ impl State4 { )) } - pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); - - let adaptor = Adaptor::, Deterministic>::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))?; - - let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; - - finality.await?; - - Ok(()) - } - - pub fn tx_lock_id(&self) -> bitcoin::Txid { - self.tx_lock.txid() + 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, + } } } @@ -559,3 +519,83 @@ impl State5 { 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, +} + +impl State6 { + pub async fn expired_timelock( + &self, + bitcoin_wallet: &bitcoin::Wallet, + ) -> Result { + let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + + 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 { + let tx_cancel = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + + 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 { + let transaction = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()) + .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 refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> { + let tx_cancel = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); + + let adaptor = Adaptor::, Deterministic>::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))?; + + let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; + + finality.await?; + + Ok(()) + } + + pub fn tx_lock_id(&self) -> bitcoin::Txid { + self.tx_lock.txid() + } +} diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 4a6cc598..1e26e04c 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -184,11 +184,11 @@ async fn run_until_internal( BobState::EncSigSent(state) }, _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { - BobState::CancelTimelockExpired(state) + BobState::CancelTimelockExpired(state.cancel()) } } } else { - BobState::CancelTimelockExpired(state) + BobState::CancelTimelockExpired(state.cancel()) } } BobState::EncSigSent(state) => { @@ -198,11 +198,11 @@ async fn run_until_internal( BobState::BtcRedeemed(state5?) }, _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { - BobState::CancelTimelockExpired(state) + BobState::CancelTimelockExpired(state.cancel()) } } } else { - BobState::CancelTimelockExpired(state) + BobState::CancelTimelockExpired(state.cancel()) } } BobState::BtcRedeemed(state) => {