diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs index ae2e502..18696ac 100644 --- a/lib/core/src/recover/recoverer.rs +++ b/lib/core/src/recover/recoverer.rs @@ -18,6 +18,7 @@ use crate::wallet::OnchainWallet; use crate::{ chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService}, recover::model::{BtcScript, HistoryTxId, LBtcScript}, + utils, }; pub(crate) struct Recoverer { @@ -44,10 +45,10 @@ impl Recoverer { }) } - async fn recover_preimages<'a>( + async fn recover_preimages( &self, - claim_tx_ids_by_swap_id: HashMap<&'a String, Txid>, - ) -> Result> { + claim_tx_ids_by_swap_id: HashMap<&String, Txid>, + ) -> Result> { let claim_tx_ids: Vec = claim_tx_ids_by_swap_id.values().copied().collect(); let claim_txs = self @@ -70,8 +71,17 @@ impl Recoverer { let mut preimages = HashMap::new(); for (swap_id, claim_tx) in claim_txs_by_swap_id { - if let Ok(preimage) = Self::get_send_swap_preimage_from_claim_tx(swap_id, &claim_tx) { - preimages.insert(swap_id, preimage); + match Self::get_send_swap_preimage_from_claim_tx(swap_id, &claim_tx) { + Ok(preimage) => { + preimages.insert(swap_id.to_string(), preimage); + } + Err(e) => { + debug!( + "Couldn't get swap preimage from claim tx {} for swap {swap_id}: {e} - \ + could be a cooperative claim tx", + claim_tx.txid() + ); + } } } Ok(preimages) @@ -121,7 +131,7 @@ impl Recoverer { let swaps_list = swaps.to_vec().try_into()?; let histories = self.fetch_swaps_histories(&swaps_list).await?; - let recovered_send_data = self.recover_send_swap_tx_ids(&tx_map, histories.send)?; + let mut recovered_send_data = self.recover_send_swap_tx_ids(&tx_map, histories.send)?; let recovered_send_with_claim_tx = recovered_send_data .iter() .filter_map(|(swap_id, send_data)| { @@ -132,6 +142,34 @@ impl Recoverer { }) .collect::>(); let mut recovered_preimages = self.recover_preimages(recovered_send_with_claim_tx).await?; + // Keep only verified preimages + recovered_preimages.retain(|swap_id, preimage| { + if let Some(Swap::Send(send_swap)) = swaps.iter().find(|s| s.id() == *swap_id) { + match utils::verify_payment_hash(preimage, &send_swap.invoice) { + Ok(_) => true, + Err(e) => { + error!("Failed to verify recovered preimage for swap {swap_id}: {e}"); + false + } + } + } else { + false + } + }); + // Keep only claim tx for which there is a recovered or synced preimage + for (swap_id, send_data) in recovered_send_data.iter_mut() { + if let Some(Swap::Send(send_swap)) = swaps.iter().find(|s| s.id() == *swap_id) { + if send_data.claim_tx_id.is_some() + && !recovered_preimages.contains_key(swap_id) + && send_swap.preimage.is_none() + { + error!( + "Seemingly found a claim tx but no preimage for swap {swap_id}. Ignoring claim tx." + ); + send_data.claim_tx_id = None; + } + } + } let recovered_receive_data = self.recover_receive_swap_tx_ids( &tx_map, diff --git a/lib/core/src/send_swap.rs b/lib/core/src/send_swap.rs index 248afe2..7470e65 100644 --- a/lib/core/src/send_swap.rs +++ b/lib/core/src/send_swap.rs @@ -5,8 +5,6 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use boltz_client::swaps::boltz; use boltz_client::swaps::{boltz::CreateSubmarineResponse, boltz::SubSwapStates}; -use boltz_client::util::secrets::Preimage; -use boltz_client::Bolt11Invoice; use futures_util::TryFutureExt; use log::{debug, error, info, warn}; use lwk_wollet::elements::{LockTime, Transaction}; @@ -431,7 +429,7 @@ impl SendSwapHandler { invoice: &str, preimage: &str, ) -> Result<(), PaymentError> { - Self::verify_payment_hash(preimage, invoice)?; + utils::verify_payment_hash(preimage, invoice)?; info!("Preimage is valid for Send Swap {swap_id}"); Ok(()) } @@ -598,18 +596,6 @@ impl SendSwapHandler { }), } } - - fn verify_payment_hash(preimage: &str, invoice: &str) -> Result<(), PaymentError> { - let preimage = Preimage::from_str(preimage)?; - let preimage_hash = preimage.sha256.to_string(); - let invoice = Bolt11Invoice::from_str(invoice) - .map_err(|err| PaymentError::invalid_invoice(&err.to_string()))?; - let invoice_payment_hash = invoice.payment_hash(); - - (invoice_payment_hash.to_string() == preimage_hash) - .then_some(()) - .ok_or(PaymentError::InvalidPreimage) - } } #[cfg(test)] diff --git a/lib/core/src/swapper/boltz/liquid.rs b/lib/core/src/swapper/boltz/liquid.rs index 3f930bb..dd1fc37 100644 --- a/lib/core/src/swapper/boltz/liquid.rs +++ b/lib/core/src/swapper/boltz/liquid.rs @@ -4,7 +4,7 @@ use boltz_client::{ boltz::SwapTxKind, elements::Transaction, util::{liquid_genesis_hash, secrets::Preimage}, - Amount, Bolt11Invoice, ElementsAddress as Address, LBtcSwapTx, + Amount, ElementsAddress as Address, LBtcSwapTx, }; use log::info; @@ -15,6 +15,7 @@ use crate::{ ChainSwap, Direction, LiquidNetwork, ReceiveSwap, Swap, Utxo, LOWBALL_FEE_RATE_SAT_PER_VBYTE, STANDARD_FEE_RATE_SAT_PER_VBYTE, }, + utils, }; use super::BoltzSwapper; @@ -26,33 +27,11 @@ impl BoltzSwapper { invoice: &str, preimage: &str, ) -> Result<(), PaymentError> { - Self::verify_payment_hash(preimage, invoice)?; + utils::verify_payment_hash(preimage, invoice)?; info!("Preimage is valid for Send Swap {swap_id}"); Ok(()) } - pub(crate) fn verify_payment_hash(preimage: &str, invoice: &str) -> Result<(), PaymentError> { - let preimage = Preimage::from_str(preimage)?; - let preimage_hash = preimage.sha256.to_string(); - - let invoice_payment_hash = match Bolt11Invoice::from_str(invoice) { - Ok(invoice) => Ok(invoice.payment_hash().to_string()), - Err(_) => match crate::utils::parse_bolt12_invoice(invoice) { - Ok(invoice) => Ok(invoice.payment_hash().to_string()), - Err(e) => Err(PaymentError::Generic { - err: format!("Could not parse invoice: {e:?}"), - }), - }, - }?; - - ensure_sdk!( - invoice_payment_hash == preimage_hash, - PaymentError::InvalidPreimage - ); - - Ok(()) - } - pub(crate) fn new_receive_claim_tx( &self, swap: &ReceiveSwap, diff --git a/lib/core/src/utils.rs b/lib/core/src/utils.rs index 73af84a..3a600d3 100644 --- a/lib/core/src/utils.rs +++ b/lib/core/src/utils.rs @@ -1,8 +1,10 @@ use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::ensure_sdk; use crate::error::{PaymentError, SdkResult}; use anyhow::{anyhow, ensure, Result}; +use boltz_client::util::secrets::Preimage; use boltz_client::ToHex; use lwk_wollet::elements::encode::deserialize; use lwk_wollet::elements::hex::FromHex; @@ -81,3 +83,65 @@ pub(crate) fn get_invoice_destination_pubkey(invoice: &str, is_bolt12: bool) -> .map_err(Into::into) } } + +/// Verifies a BOLT11/12 invoice against a preimage +pub(crate) fn verify_payment_hash( + preimage: &str, + invoice: &str, +) -> std::result::Result<(), PaymentError> { + let preimage = Preimage::from_str(preimage)?; + let preimage_hash = preimage.sha256.to_string(); + + let invoice_payment_hash = match Bolt11Invoice::from_str(invoice) { + Ok(invoice) => Ok(invoice.payment_hash().to_string()), + Err(_) => match parse_bolt12_invoice(invoice) { + Ok(invoice) => Ok(invoice.payment_hash().to_string()), + Err(e) => Err(PaymentError::InvalidInvoice { + err: format!("Could not parse invoice: {e:?}"), + }), + }, + }?; + + ensure_sdk!( + invoice_payment_hash == preimage_hash, + PaymentError::InvalidPreimage + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::error::PaymentError; + use crate::utils::verify_payment_hash; + + #[test] + fn test_verify_payment_hash() -> anyhow::Result<()> { + let bolt11_invoice = "lnbc10u1pnczjaupp55392fur38rc2y9vzmhdy0tclvfels0lvlmzgvmhpg6q2mndxzmrsdqqcqzzsxqyz5vqsp5ya6pvchlsvl3mzqh3zw4hg3tz5pww77q6rcwfr52qchyrp7s6krs9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgqgnp0sskk0ljjew8vkc3udhzgquzs79evf5wezfaex9q4gjk5qcn8m3luauyte93lgassd8skh5m90glhtt52ry2wtftzrjn4h076z7sqdjry3d"; + let bolt11_preimage = "c17a0a28d0523596ec909c2d439c0c2315b5bd996bf4ff48be50b2df08fb8ac1"; + let bolt12_invoice = "lni1qqg274t4yefgrj0pn3cwjz4vaacayyxvqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7quxqu8s2dggmw92army03kdxt825vqkawcz33kvrennr6g2fu6f7vpqx40040703nghrcj3pp9gax3y2my9v4kd5gtzv5k68k7vnxa3y7dlqqee9gty2hdrprsd8t04jz5nea79phgs9vyp9ruexzczwfhzg57c3yl345x3jy3kqpgr3e3u54fu0vewzjv2jq4lvj8gghjf5h8kgpw23c8tugua4qh432mylsdj3ac260fwvptzgcqpq486c0qlz6aqrj7q7k804w5mv92jqv85yszcyypft03wvpgapj7t0h58t6da6tdwx69admya2p0dl435la7wq4ljk79ql5qe5quxfmcztl0gldv8mxy3sm8x5jscdz27u39fy6luxu8zcdn9j73l3upa3vjg727ft7cwfkg4yqxyctm98hq6utue2k5k5at05azu2wgw57szq2qztaq2rnqjt6ugna4em3uj2el3cr7gj2glzuwkm346qpx93y9ruqz9fkumys35w9jdqxs45qzec44fhpy7lldwzt80y3q33sk09nkgf7h9r6etd45zp80snmz5x4uquqk7a0cusp0sluhku8md0eaxejqvkdd6mcp0gxqr6hsfwsxu4vx6lx08axqpj3fe87jeqfvdmetqxcaadn993vv3fe3qpny568lpz00dj3w6rag6gv3jyj9nnqmh6455l4h7ewe4zstwprmumemut8fexnnmgqmfzj0xwgr3mmwygw59jjqqv0h9vgc8vhkcx4g3s3av4kd48w4p4qs29zggh4vz924t23m7va0am4d7d4uur96uypayuchcgs2wxm0ktsaadewffys0jdlz245saetsd4f2m7ljp3tdxmt45qw64slkmlwaeak0h7508hftjdh6vyzr7skx2eucwwmgce0pydvgx5egmv4fnu0e7383ygyhwa0vwd4gy6zsez6kktvdezn79ejh2n8zmdtk998jvzuq7syv4gsuqqqq86qqqqqxgqfqqqqqqqqqqqp7sqqqqqqqq85ysqqqpfqyv7pxql4xqvq4rq9gyztzsk4ktppyn45peyvhfpl6lv8ewjr666gkzttspjkp8zn0g2n9f2srpapyptsrqgqqpvppq2lkfr5ytey6tnmyqh9gur47yww6st6c4dj0cxeg7u9d85hxq43yduzqrxguu82stp5egwzefmhvm9k63r0nxemf0pg54j3hdfzgt068te3dv5s089p54gcplnk778kcnfhkn6l8tggjqmgyc88vrgr6gc3gx7q"; + let bolt12_preimage = "443c900d61ed8e90a7bfbb7958f1485a7f57e74adacd3e216deba03f8326a392"; + + // Test valid inputs + verify_payment_hash(bolt11_preimage, bolt11_invoice)?; + verify_payment_hash(bolt12_preimage, bolt12_invoice)?; + + // Test invalid preimages + assert!(matches!( + verify_payment_hash(bolt12_preimage, bolt11_invoice), + Err(PaymentError::InvalidPreimage { .. }) + )); + assert!(matches!( + verify_payment_hash(bolt11_preimage, bolt12_invoice), + Err(PaymentError::InvalidPreimage { .. }) + )); + + // Test invalid invoice + assert!(matches!( + verify_payment_hash(bolt11_preimage, "not an invoice"), + Err(PaymentError::InvalidInvoice { .. }) + )); + + Ok(()) + } +}