Skip to content

Commit 44e7831

Browse files
fix: prevent incorrect contract closure from mempool-only splice txs
Three fixes for splice contracts whose fund tx enters the mempool but is never mined: 1. check_for_spliced_contract: only pre-close the old contract when the splice fund tx has real on-chain confirmations, not just mempool presence. 2. check_preclosed_contract: for splice pre-closes (no attestations), require at least 1 confirmation before moving to Closed. Revert splice pre-closes to Confirmed when the closing tx has 0 confirmations. CET closes (with attestations) use the configured NB_CONFIRMATIONS threshold unchanged. 3. recover_incorrectly_closed_contracts: new periodic check that finds splice-closed contracts (no attestations) whose closing tx is not on-chain and reverts them to Confirmed.
1 parent 0d8aa46 commit 44e7831

1 file changed

Lines changed: 94 additions & 6 deletions

File tree

ddk-manager/src/manager.rs

Lines changed: 94 additions & 6 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,29 @@ where
890891

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

894+
// Only pre-close the old contract when the splice fund tx is
895+
// actually confirmed on-chain. A Signed splice contract may have
896+
// a fund tx that is still in the mempool or was dropped entirely.
897+
let splice_fund_txid = contract
898+
.accepted_contract
899+
.dlc_transactions
900+
.fund
901+
.compute_txid();
902+
let splice_confirmations = self
903+
.blockchain
904+
.get_transaction_confirmations(&splice_fund_txid)
905+
.await
906+
.unwrap_or(0);
907+
if splice_confirmations == 0 {
908+
log_debug!(
909+
self.logger,
910+
"Splice fund tx not yet confirmed, skipping pre-close of previous contract. splice_fund_txid={} contract_id={}",
911+
splice_fund_txid,
912+
contract_id.to_lower_hex_string(),
913+
);
914+
continue;
915+
}
916+
893917
let confirmed_contract_in_splice = match get_contract_in_state!(
894918
self,
895919
&contract_id,
@@ -899,14 +923,14 @@ where
899923
Ok(c) => Ok(c),
900924
Err(Error::InvalidState(e)) => {
901925
log_trace!(self.logger,
902-
"The previouse contract referenced in a splice transaction is in an unexpected state. contract_id={} error={}",
926+
"The previouse contract referenced in a splice transaction is in an unexpected state. contract_id={} error={}",
903927
contract_id.to_lower_hex_string(), e.to_string(),
904928
);
905929
continue;
906930
}
907931
Err(e) => {
908932
log_debug!(self.logger,
909-
"The previouse contract referenced in a splice transaction failed to retrieve. contract_id={} error={}",
933+
"The previouse contract referenced in a splice transaction failed to retrieve. contract_id={} error={}",
910934
contract_id.to_lower_hex_string(), e.to_string(),
911935
);
912936
Err(e)
@@ -920,8 +944,9 @@ where
920944
};
921945

922946
log_debug!(self.logger,
923-
"The previous contract that was spliced is now closed because the splice contract is confirmed. contract_id={}",
947+
"The previous contract that was spliced is now closed because the splice contract is confirmed. contract_id={} splice_fund_txid={}",
924948
contract_id.to_lower_hex_string(),
949+
splice_fund_txid,
925950
);
926951

927952
self.store
@@ -1303,7 +1328,8 @@ where
13031328
let confirmations = self
13041329
.blockchain
13051330
.get_transaction_confirmations(&broadcasted_txid)
1306-
.await?;
1331+
.await
1332+
.unwrap_or(0);
13071333
log_debug!(
13081334
self.logger,
13091335
"Checking pre-closed contract. broadcasted_txid={} contract_id={} confirmations={}",
@@ -1314,9 +1340,21 @@ where
13141340
.get_contract_id_string(),
13151341
confirmations
13161342
);
1317-
if confirmations >= *NB_CONFIRMATIONS {
1343+
1344+
let is_splice_close = contract.attestations.is_none();
1345+
// For splice pre-closes (no attestations), require at least 1 real
1346+
// confirmation. With NB_CONFIRMATIONS=0, esplora returns 0 for both
1347+
// mempool and dropped txs — a splice funding tx that was never mined
1348+
// would pass `0 >= 0` and incorrectly close the contract.
1349+
// CET closes (with attestations) use the configured threshold.
1350+
let required = if is_splice_close {
1351+
std::cmp::max(*NB_CONFIRMATIONS, 1)
1352+
} else {
1353+
*NB_CONFIRMATIONS
1354+
};
1355+
if confirmations >= required {
13181356
log_debug!(self.logger,
1319-
"Pre-closed contract is fully confirmed. Moving to closed. broadcasted_txid={} contract_id={}",
1357+
"Pre-closed contract is fully confirmed. Moving to closed. broadcasted_txid={} contract_id={}",
13201358
broadcasted_txid.to_string(),
13211359
contract.signed_contract.accepted_contract.get_contract_id_string()
13221360
);
@@ -1351,11 +1389,61 @@ where
13511389
self.store
13521390
.update_contract(&Contract::Closed(closed_contract))
13531391
.await?;
1392+
} else if is_splice_close && confirmations == 0 {
1393+
// Splice closing tx not confirmed — it may have been dropped from
1394+
// the mempool. Revert back to Confirmed so the contract can be
1395+
// used as a DLC input again.
1396+
log_info!(
1397+
self.logger,
1398+
"Pre-closed splice contract closing tx not confirmed, reverting to confirmed. broadcasted_txid={} contract_id={}",
1399+
broadcasted_txid.to_string(),
1400+
contract.signed_contract.accepted_contract.get_contract_id_string()
1401+
);
1402+
self.store
1403+
.update_contract(&Contract::Confirmed(contract.signed_contract.clone()))
1404+
.await?;
13541405
}
13551406

13561407
Ok(())
13571408
}
13581409

1410+
/// Recover splice-closed contracts whose closing tx was never mined.
1411+
/// Only reverts contracts closed via splice (attestations is None) — CET
1412+
/// closes are legitimate even if the tx hasn't confirmed yet.
1413+
#[tracing::instrument(skip_all, level = "debug")]
1414+
async fn recover_incorrectly_closed_contracts(&self) -> Result<(), Error> {
1415+
let contracts = self.store.get_contracts().await?;
1416+
for contract in contracts {
1417+
let Contract::Closed(ref closed) = contract else {
1418+
continue;
1419+
};
1420+
if closed.attestations.is_some() {
1421+
continue;
1422+
}
1423+
let Some(ref cet) = closed.signed_cet else {
1424+
continue;
1425+
};
1426+
let txid = cet.compute_txid();
1427+
let confirmations = self
1428+
.blockchain
1429+
.get_transaction_confirmations(&txid)
1430+
.await
1431+
.unwrap_or(0);
1432+
if confirmations == 0 {
1433+
log_info!(
1434+
self.logger,
1435+
"Splice-closed contract closing tx not on-chain, reverting to confirmed. closing_txid={} contract_id={}",
1436+
txid,
1437+
closed.contract_id.to_lower_hex_string(),
1438+
);
1439+
self.store
1440+
.update_contract(&Contract::Confirmed(closed.signed_contract.clone()))
1441+
.await?;
1442+
}
1443+
}
1444+
Ok(())
1445+
}
1446+
13591447
#[tracing::instrument(skip_all, level = "debug")]
13601448
async fn close_contract(
13611449
&self,

0 commit comments

Comments
 (0)