Bitcoin transaction published state

This improves the error handling on the ASB.
Once the Bitcoin redeem transaction is seen in mempool, the state machine cannot transition to a cancel scenario anymore because at that point the CLI will have redeemed the Monero.
The additional state then waits for transaction finality.
This commit is contained in:
Daniel Karzel 2021-05-23 12:59:21 +10:00
parent db319d0a90
commit 01af9a5676
No known key found for this signature in database
GPG Key ID: 30C3FC2E438ADB6E
9 changed files with 73 additions and 9 deletions

View File

@ -45,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3. ASB is running in resume-only mode and does not accept incoming swap requests 3. ASB is running in resume-only mode and does not accept incoming swap requests
- An issue where the monero daemon port used by the `monero-wallet-rpc` could not be specified. - An issue where the monero daemon port used by the `monero-wallet-rpc` could not be specified.
The CLI parameter `--monero-daemon-host` was changed to `--monero-daemon-address` where host and port have to be specified. The CLI parameter `--monero-daemon-host` was changed to `--monero-daemon-address` where host and port have to be specified.
- An issue where an ASB redeem scenario can transition to a cancel and publish scenario that will fail.
This is a breaking change for the ASB, because it introduces a new state into the database.
### Changed ### Changed

View File

@ -39,6 +39,9 @@ pub enum Alice {
encrypted_signature: EncryptedSignature, encrypted_signature: EncryptedSignature,
state3: alice::State3, state3: alice::State3,
}, },
BtcRedeemTransactionPublished {
state3: alice::State3,
},
CancelTimelockExpired { CancelTimelockExpired {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof, transfer_proof: TransferProof,
@ -119,6 +122,11 @@ impl From<&AliceState> for Alice {
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
encrypted_signature: *encrypted_signature.clone(), encrypted_signature: *encrypted_signature.clone(),
}, },
AliceState::BtcRedeemTransactionPublished { state3 } => {
Alice::BtcRedeemTransactionPublished {
state3: state3.as_ref().clone(),
}
}
AliceState::BtcRedeemed => Alice::Done(AliceEndState::BtcRedeemed), AliceState::BtcRedeemed => Alice::Done(AliceEndState::BtcRedeemed),
AliceState::BtcCancelled { AliceState::BtcCancelled {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
@ -212,6 +220,11 @@ impl From<Alice> for AliceState {
state3: Box::new(state), state3: Box::new(state),
encrypted_signature: Box::new(encrypted_signature), encrypted_signature: Box::new(encrypted_signature),
}, },
Alice::BtcRedeemTransactionPublished { state3 } => {
AliceState::BtcRedeemTransactionPublished {
state3: Box::new(state3),
}
}
Alice::CancelTimelockExpired { Alice::CancelTimelockExpired {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof, transfer_proof,
@ -271,12 +284,15 @@ impl Display for Alice {
Alice::XmrLockTransferProofSent { .. } => { Alice::XmrLockTransferProofSent { .. } => {
f.write_str("Monero lock transfer proof sent") f.write_str("Monero lock transfer proof sent")
} }
Alice::EncSigLearned { .. } => f.write_str("Encrypted signature learned"),
Alice::BtcRedeemTransactionPublished { .. } => {
f.write_str("Bitcoin redeem transaction published")
}
Alice::CancelTimelockExpired { .. } => f.write_str("Cancel timelock is expired"), Alice::CancelTimelockExpired { .. } => f.write_str("Cancel timelock is expired"),
Alice::BtcCancelled { .. } => f.write_str("Bitcoin cancel transaction published"), Alice::BtcCancelled { .. } => f.write_str("Bitcoin cancel transaction published"),
Alice::BtcPunishable { .. } => f.write_str("Bitcoin punishable"), Alice::BtcPunishable { .. } => f.write_str("Bitcoin punishable"),
Alice::BtcRefunded { .. } => f.write_str("Monero refundable"), Alice::BtcRefunded { .. } => f.write_str("Monero refundable"),
Alice::Done(end_state) => write!(f, "Done: {}", end_state), Alice::Done(end_state) => write!(f, "Done: {}", end_state),
Alice::EncSigLearned { .. } => f.write_str("Encrypted signature learned"),
} }
} }
} }

View File

@ -34,6 +34,9 @@ pub async fn cancel(
(monero_wallet_restore_blockheight, transfer_proof, state3) (monero_wallet_restore_blockheight, transfer_proof, state3)
} }
// The redeem transaction was already published, it is not safe to cancel anymore
AliceState::BtcRedeemTransactionPublished { .. } => bail!(" The redeem transaction was already published, it is not safe to cancel anymore"),
// The cancel tx was already published, but Alice not yet in final state // The cancel tx was already published, but Alice not yet in final state
AliceState::BtcCancelled { .. } AliceState::BtcCancelled { .. }
| AliceState::BtcRefunded { .. } | AliceState::BtcRefunded { .. }
@ -43,7 +46,7 @@ pub async fn cancel(
| AliceState::BtcRedeemed | AliceState::BtcRedeemed
| AliceState::XmrRefunded | AliceState::XmrRefunded
| AliceState::BtcPunished | AliceState::BtcPunished
| AliceState::SafelyAborted => bail!("Cannot cancel swap {} because it is in state {} which is not cancelable", swap_id, state), | AliceState::SafelyAborted => bail!("Swap is is in state {} which is not cancelable", state),
}; };
tracing::info!(%swap_id, "Trying to manually cancel swap"); tracing::info!(%swap_id, "Trying to manually cancel swap");

View File

@ -50,7 +50,8 @@ pub async fn punish(
} }
// If the swap was refunded it cannot be punished // If the swap was refunded it cannot be punished
AliceState::BtcRefunded {..} AliceState::BtcRedeemTransactionPublished { .. }
| AliceState::BtcRefunded {..}
// Alice already in final state // Alice already in final state
| AliceState::BtcRedeemed | AliceState::BtcRedeemed
| AliceState::XmrRefunded | AliceState::XmrRefunded

View File

@ -50,19 +50,39 @@ pub async fn redeem(
let redeem_tx = state3.signed_redeem_transaction(*encrypted_signature)?; let redeem_tx = state3.signed_redeem_transaction(*encrypted_signature)?;
let (txid, subscription) = bitcoin_wallet.broadcast(redeem_tx, "redeem").await?; let (txid, subscription) = bitcoin_wallet.broadcast(redeem_tx, "redeem").await?;
subscription.wait_until_seen().await?;
let state = AliceState::BtcRedeemTransactionPublished { state3 };
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
if let Finality::Await = finality { if let Finality::Await = finality {
subscription.wait_until_final().await?; subscription.wait_until_final().await?;
} }
let state = AliceState::BtcRedeemed; let state = AliceState::BtcRedeemed;
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?; .await?;
Ok((txid, state)) Ok((txid, state))
} }
AliceState::BtcRedeemTransactionPublished { state3 } => {
let subscription = bitcoin_wallet.subscribe_to(state3.tx_redeem()).await;
if let Finality::Await = finality {
subscription.wait_until_final().await?;
}
let state = AliceState::BtcRedeemed;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
let txid = state3.tx_redeem().txid();
Ok((txid, state))
}
AliceState::Started { .. } AliceState::Started { .. }
| AliceState::BtcLocked { .. } | AliceState::BtcLocked { .. }
| AliceState::XmrLockTransactionSent { .. } | AliceState::XmrLockTransactionSent { .. }

View File

@ -56,7 +56,8 @@ pub async fn refund(
} }
// Alice already in final state // Alice already in final state
AliceState::BtcRedeemed AliceState::BtcRedeemTransactionPublished { .. }
| AliceState::BtcRedeemed
| AliceState::XmrRefunded | AliceState::XmrRefunded
| AliceState::BtcPunished | AliceState::BtcPunished
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)), | AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),

View File

@ -22,6 +22,7 @@ pub async fn safely_abort(swap_id: Uuid, db: Arc<Database>) -> Result<AliceState
| AliceState::XmrLocked { .. } | AliceState::XmrLocked { .. }
| AliceState::XmrLockTransferProofSent { .. } | AliceState::XmrLockTransferProofSent { .. }
| AliceState::EncSigLearned { .. } | AliceState::EncSigLearned { .. }
| AliceState::BtcRedeemTransactionPublished { .. }
| AliceState::CancelTimelockExpired { .. } | AliceState::CancelTimelockExpired { .. }
| AliceState::BtcCancelled { .. } | AliceState::BtcCancelled { .. }
| AliceState::BtcRefunded { .. } | AliceState::BtcRefunded { .. }

View File

@ -1,6 +1,6 @@
use crate::bitcoin::{ use crate::bitcoin::{
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
TxPunish, TxRefund, Txid, TxPunish, TxRedeem, TxRefund, Txid,
}; };
use crate::env::Config; use crate::env::Config;
use crate::monero::wallet::{TransferRequest, WatchRequest}; use crate::monero::wallet::{TransferRequest, WatchRequest};
@ -45,6 +45,9 @@ pub enum AliceState {
encrypted_signature: Box<bitcoin::EncryptedSignature>, encrypted_signature: Box<bitcoin::EncryptedSignature>,
state3: Box<State3>, state3: Box<State3>,
}, },
BtcRedeemTransactionPublished {
state3: Box<State3>,
},
BtcRedeemed, BtcRedeemed,
BtcCancelled { BtcCancelled {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
@ -83,6 +86,9 @@ impl fmt::Display for AliceState {
write!(f, "xmr lock transfer proof sent") write!(f, "xmr lock transfer proof sent")
} }
AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"), AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"),
AliceState::BtcRedeemTransactionPublished { .. } => {
write!(f, "bitcoin redeem transaction published")
}
AliceState::BtcRedeemed => write!(f, "btc is redeemed"), AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"), AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),
AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"), AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"),
@ -449,6 +455,10 @@ impl State3 {
bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address, self.tx_refund_fee) bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address, self.tx_refund_fee)
} }
pub fn tx_redeem(&self) -> TxRedeem {
TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee)
}
pub fn extract_monero_private_key( pub fn extract_monero_private_key(
&self, &self,
published_refund_tx: bitcoin::Transaction, published_refund_tx: bitcoin::Transaction,

View File

@ -204,10 +204,10 @@ where
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
match state3.signed_redeem_transaction(*encrypted_signature) { match state3.signed_redeem_transaction(*encrypted_signature) {
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok((_, subscription)) => match subscription.wait_until_final().await { Ok((_, subscription)) => match subscription.wait_until_seen().await {
Ok(_) => AliceState::BtcRedeemed, Ok(_) => AliceState::BtcRedeemTransactionPublished { state3 },
Err(e) => { Err(e) => {
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e) bail!("Waiting for Bitcoin redeem transaction to be in mempool failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
} }
}, },
Err(error) => { Err(error) => {
@ -247,6 +247,16 @@ where
state3, state3,
}, },
}, },
AliceState::BtcRedeemTransactionPublished { state3 } => {
let subscription = bitcoin_wallet.subscribe_to(state3.tx_redeem()).await;
match subscription.wait_until_final().await {
Ok(_) => AliceState::BtcRedeemed,
Err(e) => {
bail!("The Bitcoin redeem transaction was seen in mempool, but waiting for finality timed out with {}. Manual investigation might be needed to ensure that the transaction was included.", e)
}
}
}
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof, transfer_proof,