Skip to content

Commit def648e

Browse files
fix: distinguish dropped txs from mempool txs in esplora confirmations
Root cause: get_transaction_confirmations returned Ok(0) for both mempool txs and dropped/never-broadcast txs. With NB_CONFIRMATIONS=0, the manager's `0 >= 0` check always passed, promoting contracts through states even when their funding tx was never mined. esplora.rs: when the tx is unconfirmed, call get_tx to check if it's actually in the mempool. If the tx is gone, return a TransactionNotFound error instead of Ok(0). This lets consumers distinguish mempool (Ok(0)) from dropped (Err). manager.rs: - close_contract / check_and_broadcast_refund / check_refund: use unwrap_or(0) since the tx may not exist yet (pre-broadcast) - check_confirmed_contract pending_close_txs: skip dropped txs - check_for_spliced_contract: only skip pre-close when the splice fund tx is dropped (Err), not when it is in mempool (Ok(0)) - check_preclosed_contract: on confirmation error for splice closes (no attestations), revert to Confirmed instead of propagating - recover_incorrectly_closed_contracts: new periodic check that finds splice-closed contracts whose closing tx is gone and reverts them
1 parent 0d8aa46 commit def648e

2 files changed

Lines changed: 119 additions & 17 deletions

File tree

ddk-manager/src/manager.rs

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ where
535535
self.check_signed_contracts(&signed_contracts).await?;
536536
self.check_confirmed_contracts().await?;
537537
self.check_preclosed_contracts().await?;
538+
self.recover_incorrectly_closed_contracts().await?;
538539

539540
if check_channels {
540541
self.channel_checks().await?;
@@ -890,6 +891,30 @@ where
890891

891892
let contract_id = funding_input.dlc_input.as_ref().unwrap().contract_id;
892893

894+
// Skip pre-close if the splice fund tx was dropped from the
895+
// mempool (get_transaction_confirmations returns Err for
896+
// dropped txs after the esplora fix). Mempool txs (Ok(0))
897+
// and confirmed txs (Ok(n)) proceed normally.
898+
let splice_fund_txid = contract
899+
.accepted_contract
900+
.dlc_transactions
901+
.fund
902+
.compute_txid();
903+
if self
904+
.blockchain
905+
.get_transaction_confirmations(&splice_fund_txid)
906+
.await
907+
.is_err()
908+
{
909+
log_debug!(
910+
self.logger,
911+
"Splice fund tx not found, skipping pre-close of previous contract. splice_fund_txid={} contract_id={}",
912+
splice_fund_txid,
913+
contract_id.to_lower_hex_string(),
914+
);
915+
continue;
916+
}
917+
893918
let confirmed_contract_in_splice = match get_contract_in_state!(
894919
self,
895920
&contract_id,
@@ -899,14 +924,14 @@ where
899924
Ok(c) => Ok(c),
900925
Err(Error::InvalidState(e)) => {
901926
log_trace!(self.logger,
902-
"The previouse contract referenced in a splice transaction is in an unexpected state. contract_id={} error={}",
927+
"The previouse contract referenced in a splice transaction is in an unexpected state. contract_id={} error={}",
903928
contract_id.to_lower_hex_string(), e.to_string(),
904929
);
905930
continue;
906931
}
907932
Err(e) => {
908933
log_debug!(self.logger,
909-
"The previouse contract referenced in a splice transaction failed to retrieve. contract_id={} error={}",
934+
"The previouse contract referenced in a splice transaction failed to retrieve. contract_id={} error={}",
910935
contract_id.to_lower_hex_string(), e.to_string(),
911936
);
912937
Err(e)
@@ -920,8 +945,9 @@ where
920945
};
921946

922947
log_debug!(self.logger,
923-
"The previous contract that was spliced is now closed because the splice contract is confirmed. contract_id={}",
948+
"The previous contract that was spliced is now closed because the splice contract is confirmed. contract_id={} splice_fund_txid={}",
924949
contract_id.to_lower_hex_string(),
950+
splice_fund_txid,
925951
);
926952

927953
self.store
@@ -1118,10 +1144,14 @@ where
11181144
.dlc_transactions
11191145
.pending_close_txs
11201146
{
1121-
let confirmations = self
1147+
let confirmations = match self
11221148
.blockchain
11231149
.get_transaction_confirmations(&pending_close_tx.compute_txid())
1124-
.await?;
1150+
.await
1151+
{
1152+
Ok(c) => c,
1153+
Err(_) => continue,
1154+
};
11251155

11261156
log_debug!(
11271157
self.logger,
@@ -1300,10 +1330,32 @@ where
13001330
#[tracing::instrument(skip_all, level = "debug")]
13011331
async fn check_preclosed_contract(&self, contract: &PreClosedContract) -> Result<(), Error> {
13021332
let broadcasted_txid = contract.signed_cet.compute_txid();
1303-
let confirmations = self
1333+
let confirmations = match self
13041334
.blockchain
13051335
.get_transaction_confirmations(&broadcasted_txid)
1306-
.await?;
1336+
.await
1337+
{
1338+
Ok(c) => c,
1339+
Err(e) => {
1340+
// For splice pre-closes (no attestations), a tx-not-found
1341+
// error means the splice funding tx was dropped from the
1342+
// mempool. Revert back to Confirmed.
1343+
if contract.attestations.is_none() {
1344+
log_info!(
1345+
self.logger,
1346+
"Pre-closed splice contract closing tx not found, reverting to confirmed. broadcasted_txid={} contract_id={} error={}",
1347+
broadcasted_txid,
1348+
contract.signed_contract.accepted_contract.get_contract_id_string(),
1349+
e
1350+
);
1351+
self.store
1352+
.update_contract(&Contract::Confirmed(contract.signed_contract.clone()))
1353+
.await?;
1354+
return Ok(());
1355+
}
1356+
return Err(e);
1357+
}
1358+
};
13071359
log_debug!(
13081360
self.logger,
13091361
"Checking pre-closed contract. broadcasted_txid={} contract_id={} confirmations={}",
@@ -1314,9 +1366,10 @@ where
13141366
.get_contract_id_string(),
13151367
confirmations
13161368
);
1369+
13171370
if confirmations >= *NB_CONFIRMATIONS {
13181371
log_debug!(self.logger,
1319-
"Pre-closed contract is fully confirmed. Moving to closed. broadcasted_txid={} contract_id={}",
1372+
"Pre-closed contract is fully confirmed. Moving to closed. broadcasted_txid={} contract_id={}",
13201373
broadcasted_txid.to_string(),
13211374
contract.signed_contract.accepted_contract.get_contract_id_string()
13221375
);
@@ -1356,6 +1409,39 @@ where
13561409
Ok(())
13571410
}
13581411

1412+
/// Recover splice-closed contracts whose closing tx was dropped from
1413+
/// the mempool. Only reverts contracts closed via splice (attestations
1414+
/// is None). Uses get_transaction to verify the tx is truly gone — not
1415+
/// just unconfirmed in the mempool.
1416+
#[tracing::instrument(skip_all, level = "debug")]
1417+
async fn recover_incorrectly_closed_contracts(&self) -> Result<(), Error> {
1418+
let contracts = self.store.get_contracts().await?;
1419+
for contract in contracts {
1420+
let Contract::Closed(ref closed) = contract else {
1421+
continue;
1422+
};
1423+
if closed.attestations.is_some() {
1424+
continue;
1425+
}
1426+
let Some(ref cet) = closed.signed_cet else {
1427+
continue;
1428+
};
1429+
let txid = cet.compute_txid();
1430+
if self.blockchain.get_transaction(&txid).await.is_err() {
1431+
log_info!(
1432+
self.logger,
1433+
"Splice-closed contract closing tx not found, reverting to confirmed. closing_txid={} contract_id={}",
1434+
txid,
1435+
closed.contract_id.to_lower_hex_string(),
1436+
);
1437+
self.store
1438+
.update_contract(&Contract::Confirmed(closed.signed_contract.clone()))
1439+
.await?;
1440+
}
1441+
}
1442+
Ok(())
1443+
}
1444+
13591445
#[tracing::instrument(skip_all, level = "debug")]
13601446
async fn close_contract(
13611447
&self,
@@ -1366,7 +1452,8 @@ where
13661452
let confirmations = self
13671453
.blockchain
13681454
.get_transaction_confirmations(&signed_cet.compute_txid())
1369-
.await?;
1455+
.await
1456+
.unwrap_or(0);
13701457

13711458
if confirmations < 1 {
13721459
log_info!(
@@ -1448,7 +1535,8 @@ where
14481535
let confirmations = self
14491536
.blockchain
14501537
.get_transaction_confirmations(&refund.compute_txid())
1451-
.await?;
1538+
.await
1539+
.unwrap_or(0);
14521540
if confirmations == 0 {
14531541
log_debug!(self.logger,
14541542
"Refund transaction has not been broadcast yet. Sending transaction txid={} contract_id={}",
@@ -1494,7 +1582,8 @@ where
14941582
let confirmations = self
14951583
.blockchain
14961584
.get_transaction_confirmations(&refund_txid)
1497-
.await?;
1585+
.await
1586+
.unwrap_or(0);
14981587

14991588
if confirmations > 0 {
15001589
// Counterparty (or we) already broadcast the refund tx. Update state.

ddk/src/chain/esplora.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,12 @@ impl ddk_manager::Blockchain for EsploraClient {
173173
.get_tx_status(tx_id)
174174
.await
175175
.map_err(esplora_err_to_manager_err)?;
176-
let tip_height = self
177-
.async_client
178-
.get_height()
179-
.await
180-
.map_err(esplora_err_to_manager_err)?;
181176
if txn.confirmed {
177+
let tip_height = self
178+
.async_client
179+
.get_height()
180+
.await
181+
.map_err(esplora_err_to_manager_err)?;
182182
match txn.block_height {
183183
Some(height) => Ok(tip_height
184184
.checked_sub(height)
@@ -187,7 +187,20 @@ impl ddk_manager::Blockchain for EsploraClient {
187187
None => Ok(0),
188188
}
189189
} else {
190-
Ok(0)
190+
// tx is unconfirmed — check if it's in the mempool or truly gone
191+
match self.async_client.get_tx(tx_id).await {
192+
Ok(Some(_)) => Ok(0),
193+
_ => {
194+
log_warn!(
195+
self.logger,
196+
"Transaction not found in mempool or on-chain. txid={}",
197+
tx_id,
198+
);
199+
Err(esplora_err_to_manager_err(
200+
EsploraError::TransactionNotFound(*tx_id),
201+
))
202+
}
203+
}
191204
}
192205
}
193206
}

0 commit comments

Comments
 (0)