From 564ce597b9622267d7e17d2ce144bb9545d3b167 Mon Sep 17 00:00:00 2001 From: Matthew Black Date: Wed, 20 May 2026 15:39:01 -0500 Subject: [PATCH 1/2] feat: use contract_flags bit 0 to redirect refund to accepter --- ddk-manager/benches/benchmarks.rs | 2 +- ddk-manager/src/channel/offered_channel.rs | 3 +- ddk-manager/src/channel_updater.rs | 5 ++ ddk-manager/src/contract/offered_contract.rs | 7 +- ddk-manager/src/contract/ser.rs | 1 + ddk-manager/src/contract_updater.rs | 4 + dlc-messages/src/compatibility_tests.rs | 2 + dlc/src/channel/mod.rs | 4 + dlc/src/lib.rs | 88 +++++++++++++++++++- 9 files changed, 111 insertions(+), 5 deletions(-) diff --git a/ddk-manager/benches/benchmarks.rs b/ddk-manager/benches/benchmarks.rs index eb69134..429d6af 100644 --- a/ddk-manager/benches/benchmarks.rs +++ b/ddk-manager/benches/benchmarks.rs @@ -206,7 +206,7 @@ fn create_transactions(payouts: &[Payout]) -> DlcTransactions { collateral: Amount::from_sat(100000000), dlc_inputs: vec![], }; - create_dlc_transactions(&offer_params, &accept_params, payouts, 1000, 2, 0, 1000, 3).unwrap() + create_dlc_transactions(&offer_params, &accept_params, payouts, 1000, 2, 0, 1000, 3, 0).unwrap() } fn accept_seckey() -> SecretKey { diff --git a/ddk-manager/src/channel/offered_channel.rs b/ddk-manager/src/channel/offered_channel.rs index 344d0bc..5c7e505 100644 --- a/ddk-manager/src/channel/offered_channel.rs +++ b/ddk-manager/src/channel/offered_channel.rs @@ -47,7 +47,7 @@ impl OfferedChannel { let party_points = &self.party_points; OfferChannel { protocol_version: crate::conversion_utils::PROTOCOL_VERSION, - contract_flags: 0, + contract_flags: offered_contract.contract_flags, chain_hash: crate::conversion_utils::BITCOIN_CHAINHASH, temporary_contract_id: offered_contract.id, temporary_channel_id: self.temporary_channel_id, @@ -122,6 +122,7 @@ impl OfferedChannel { fund_output_serial_id: offer_channel.fund_output_serial_id, funding_inputs: offer_channel.funding_inputs.clone(), total_collateral: offer_channel.contract_info.get_total_collateral(), + contract_flags: offer_channel.contract_flags, keys_id, }; diff --git a/ddk-manager/src/channel_updater.rs b/ddk-manager/src/channel_updater.rs index 036fabd..6dc9c30 100644 --- a/ddk-manager/src/channel_updater.rs +++ b/ddk-manager/src/channel_updater.rs @@ -222,6 +222,7 @@ where offered_contract.cet_locktime, offered_contract.fund_output_serial_id, Sequence(offered_channel.cet_nsequence), + offered_contract.contract_flags, )?; let own_base_secret_key = @@ -368,6 +369,7 @@ where offered_contract.cet_locktime, offered_contract.fund_output_serial_id, Sequence(cet_nsequence), + offered_contract.contract_flags, )?; let channel_id = crate::utils::compute_id( @@ -1246,6 +1248,7 @@ where fee_rate_per_vb: signed_channel.fee_rate_per_vb, cet_locktime: renew_offer.cet_locktime, refund_locktime: renew_offer.refund_locktime, + contract_flags: 0, keys_id, }; @@ -1338,6 +1341,7 @@ where offered_contract.fee_rate_per_vb, 0, Sequence(cet_nsequence), + offered_contract.contract_flags, )?; let own_secret_key = derive_private_key(secp, &accept_per_update_point, &own_base_secret_key); @@ -1452,6 +1456,7 @@ where offered_contract.fee_rate_per_vb, 0, Sequence(cet_nsequence), + offered_contract.contract_flags, )?; let _offer_own_sk = derive_private_key(secp, &offer_per_update_point, &own_base_secret_key); diff --git a/ddk-manager/src/contract/offered_contract.rs b/ddk-manager/src/contract/offered_contract.rs index b632850..4eee106 100644 --- a/ddk-manager/src/contract/offered_contract.rs +++ b/ddk-manager/src/contract/offered_contract.rs @@ -47,6 +47,9 @@ pub struct OfferedContract { pub cet_locktime: u32, /// The time at which the contract becomes refundable. pub refund_locktime: u32, + /// Feature flags for the contract (bit 0: refund to accepter). + #[cfg_attr(feature = "use-serde", serde(default))] + pub contract_flags: u8, /// Keys Id for generating the signers pub(crate) keys_id: KeysId, } @@ -120,6 +123,7 @@ impl OfferedContract { fee_rate_per_vb: contract.fee_rate, cet_locktime, refund_locktime: latest_maturity + refund_delay, + contract_flags: 0, counter_party: *counter_party, keys_id, } @@ -157,6 +161,7 @@ impl OfferedContract { fund_output_serial_id: offer_dlc.fund_output_serial_id, funding_inputs: offer_dlc.funding_inputs.clone(), total_collateral: offer_dlc.contract_info.get_total_collateral(), + contract_flags: offer_dlc.contract_flags, counter_party, keys_id, }) @@ -168,7 +173,7 @@ impl From<&OfferedContract> for OfferDlc { OfferDlc { protocol_version: PROTOCOL_VERSION, temporary_contract_id: offered_contract.id, - contract_flags: 0, + contract_flags: offered_contract.contract_flags, chain_hash: BITCOIN_CHAINHASH, contract_info: offered_contract.into(), funding_pubkey: offered_contract.offer_params.fund_pubkey, diff --git a/ddk-manager/src/contract/ser.rs b/ddk-manager/src/contract/ser.rs index feeb135..6524cc0 100644 --- a/ddk-manager/src/contract/ser.rs +++ b/ddk-manager/src/contract/ser.rs @@ -98,6 +98,7 @@ impl_dlc_writeable!(OfferedContract, { (fee_rate_per_vb, writeable), (cet_locktime, writeable), (refund_locktime, writeable), + (contract_flags, writeable), (counter_party, writeable), (keys_id, writeable) }); diff --git a/ddk-manager/src/contract_updater.rs b/ddk-manager/src/contract_updater.rs index e864015..34e44e0 100644 --- a/ddk-manager/src/contract_updater.rs +++ b/ddk-manager/src/contract_updater.rs @@ -175,6 +175,7 @@ where 0, offered_contract.cet_locktime, offered_contract.fund_output_serial_id, + offered_contract.contract_flags, )? } else { log_debug!(logger, "Creating DLC transactions without splicing."); @@ -187,6 +188,7 @@ where 0, offered_contract.cet_locktime, offered_contract.fund_output_serial_id, + offered_contract.contract_flags, )? }; @@ -404,6 +406,7 @@ where 0, offered_contract.cet_locktime, offered_contract.fund_output_serial_id, + offered_contract.contract_flags, )? } else { log_debug!(logger, "Creating DLC transactions without splicing."); @@ -416,6 +419,7 @@ where 0, offered_contract.cet_locktime, offered_contract.fund_output_serial_id, + offered_contract.contract_flags, )? }; diff --git a/dlc-messages/src/compatibility_tests.rs b/dlc-messages/src/compatibility_tests.rs index 6704e5b..20cf9b1 100644 --- a/dlc-messages/src/compatibility_tests.rs +++ b/dlc-messages/src/compatibility_tests.rs @@ -435,6 +435,7 @@ fn test_single(case: TestCase, secp: &secp256k1::Secp256k1) { 0, params.contract_maturity_bound, 0, + 0, ) .unwrap(); @@ -732,6 +733,7 @@ fn test_dlc_txs() { 0, params.contract_maturity_bound, 0, + 0, ) .unwrap(); let test_txs = test_case.txs.unwrap(); diff --git a/dlc/src/channel/mod.rs b/dlc/src/channel/mod.rs index 5255c5f..2b35038 100644 --- a/dlc/src/channel/mod.rs +++ b/dlc/src/channel/mod.rs @@ -243,6 +243,7 @@ pub fn create_channel_transactions( cet_lock_time: u32, fund_output_serial_id: u64, cet_nsequence: Sequence, + contract_flags: u8, ) -> Result { let extra_fee = super::util::weight_to_fee(BUFFER_TX_WEIGHT + CET_EXTRA_WEIGHT, fee_rate_per_vb)?; @@ -267,6 +268,7 @@ pub fn create_channel_transactions( fee_rate_per_vb, cet_lock_time, cet_nsequence, + contract_flags, ) } @@ -285,6 +287,7 @@ pub fn create_renewal_channel_transactions( fee_rate_per_vb: u64, cet_lock_time: u32, cet_nsequence: Sequence, + contract_flags: u8, ) -> Result { let extra_fee = super::util::weight_to_fee(BUFFER_TX_WEIGHT + CET_EXTRA_WEIGHT, fee_rate_per_vb)?; @@ -327,6 +330,7 @@ pub fn create_renewal_channel_transactions( refund_lock_time, cet_lock_time, Some(cet_nsequence), + contract_flags, )?; Ok(DlcChannelTransactions { diff --git a/dlc/src/lib.rs b/dlc/src/lib.rs index b6f49e9..9349e72 100644 --- a/dlc/src/lib.rs +++ b/dlc/src/lib.rs @@ -55,6 +55,9 @@ pub mod util; /// See: https://github.com/discreetlogcontracts/dlcspecs/blob/master/Transactions.md#change-outputs const DUST_LIMIT: Amount = Amount::from_sat(1000); +/// Bit 0 of `contract_flags`: redirect all refund proceeds to the accepter. +pub const REFUND_TO_ACCEPTER_FLAG: u8 = 0x01; + /// The transaction version /// See: https://github.com/discreetlogcontracts/dlcspecs/blob/master/Transactions.md#funding-transaction const TX_VERSION: Version = Version::TWO; @@ -436,6 +439,7 @@ pub fn create_spliced_dlc_transactions( fund_lock_time: u32, cet_lock_time: u32, fund_output_serial_id: u64, + contract_flags: u8, ) -> Result { // Create enhanced party parameters that include DLC inputs as regular inputs let mut enhanced_offer_params = offer_params.clone(); @@ -480,6 +484,7 @@ pub fn create_spliced_dlc_transactions( fund_lock_time, cet_lock_time, fund_output_serial_id, + contract_flags, ) } @@ -494,6 +499,7 @@ pub fn create_dlc_transactions( fund_lock_time: u32, cet_lock_time: u32, fund_output_serial_id: u64, + contract_flags: u8, ) -> Result { let (fund_tx, funding_script_pubkey) = create_fund_transaction_with_fees( offer_params, @@ -517,6 +523,7 @@ pub fn create_dlc_transactions( refund_lock_time, cet_lock_time, None, + contract_flags, )?; Ok(DlcTransactions { @@ -602,6 +609,7 @@ pub fn create_cets_and_refund_tx( refund_lock_time: u32, cet_lock_time: u32, cet_nsequence: Option, + contract_flags: u8, ) -> Result<(Vec, Transaction), Error> { let total_collateral = checked_add!(offer_params.collateral, accept_params.collateral)?; @@ -637,13 +645,23 @@ pub fn create_cets_and_refund_tx( cet_lock_time, ); + let (offer_refund_value, accept_refund_value) = + if contract_flags & REFUND_TO_ACCEPTER_FLAG != 0 { + ( + Amount::ZERO, + checked_add!(offer_params.collateral, accept_params.collateral)?, + ) + } else { + (offer_params.collateral, accept_params.collateral) + }; + let offer_refund_output = TxOut { - value: offer_params.collateral, + value: offer_refund_value, script_pubkey: offer_params.payout_script_pubkey.clone(), }; let accept_refund_ouput = TxOut { - value: accept_params.collateral, + value: accept_refund_value, script_pubkey: accept_params.payout_script_pubkey.clone(), }; @@ -1495,6 +1513,7 @@ mod tests { 10, 10, 0, + 0, ) .unwrap(); @@ -1532,6 +1551,7 @@ mod tests { 10, 10, 0, + 0, ) .unwrap(); @@ -1716,6 +1736,7 @@ mod tests { 10, 10, case.serials[0], + 0, ) .unwrap(); @@ -1760,4 +1781,67 @@ mod tests { .expect("Could not find fund output"); } } + + #[test] + fn refund_to_accepter_flag_redirects_all_funds() { + let secp = Secp256k1::new(); + let mut rng = secp256k1_zkp::rand::thread_rng(); + + let offer_collateral = Amount::from_sat(100000000); + let accept_collateral = Amount::ZERO; + let total_collateral = offer_collateral + accept_collateral; + + let (offer_party_params, _) = get_party_params( + Amount::from_sat(1000000000), + offer_collateral, + None, + ); + + let accept_fund_privkey = SecretKey::new(&mut rng); + let accept_party_params = PartyParams { + fund_pubkey: PublicKey::from_secret_key(&secp, &accept_fund_privkey), + change_script_pubkey: get_p2wpkh_script_pubkey(&secp, &mut rng), + change_serial_id: 2, + payout_script_pubkey: get_p2wpkh_script_pubkey(&secp, &mut rng), + payout_serial_id: 2, + input_amount: Amount::ZERO, + collateral: accept_collateral, + inputs: vec![], + dlc_inputs: vec![], + }; + + let dlc_txs = create_dlc_transactions( + &offer_party_params, + &accept_party_params, + &[Payout { + offer: total_collateral, + accept: Amount::ZERO, + }], + 100, + 4, + 10, + 10, + 0, + REFUND_TO_ACCEPTER_FLAG, + ) + .unwrap(); + + let refund_tx = &dlc_txs.refund; + + assert_eq!( + refund_tx.output.len(), + 1, + "Refund TX should have exactly one output when flag is set" + ); + assert_eq!( + refund_tx.output[0].value, + total_collateral, + "The single output should receive total collateral" + ); + assert_eq!( + refund_tx.output[0].script_pubkey, + accept_party_params.payout_script_pubkey, + "Output should pay to the accepter's payout SPK" + ); + } } From 50177c572bfae9fb529760cdceb0dac047242a42 Mon Sep 17 00:00:00 2001 From: Matthew Black Date: Wed, 20 May 2026 15:49:23 -0500 Subject: [PATCH 2/2] fix: allow too_many_arguments on create_cets_and_refund_tx --- dlc/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/dlc/src/lib.rs b/dlc/src/lib.rs index 9349e72..64cc359 100644 --- a/dlc/src/lib.rs +++ b/dlc/src/lib.rs @@ -601,6 +601,7 @@ pub fn create_fund_transaction_with_fees( } /// Create the contract execution transactions and refund transaction. +#[allow(clippy::too_many_arguments)] pub fn create_cets_and_refund_tx( offer_params: &PartyParams, accept_params: &PartyParams,