From 7c26d7898e7a5f561803ab6a64a5b54a7649197f Mon Sep 17 00:00:00 2001 From: Matt Black Date: Fri, 26 Jun 2026 12:37:45 -0500 Subject: [PATCH 1/3] fix: add backward compat for pre-contract_flags serialization Replace macro-generated Readable impls with manual ones that detect old format (no protocol_version/contract_flags) vs new format. OfferDlc: peek first 4 bytes as u32 - if 1-10 it's protocol_version (new format), otherwise it's contract_flags+chain_hash (old format). OfferedContract: peek byte after refund_locktime - if 0x02/0x03 it's a pubkey prefix (old format, no contract_flags), if 0x00/0x01 it's contract_flags (new format). --- ddk-manager/src/contract/ser.rs | 88 +++++++++++++++++++++++++----- ddk/src/storage/sled/contract.rs | 24 ++++++++ dlc-messages/src/lib.rs | 94 ++++++++++++++++++++++++++------ 3 files changed, 173 insertions(+), 33 deletions(-) diff --git a/ddk-manager/src/contract/ser.rs b/ddk-manager/src/contract/ser.rs index 6524cc0b..aee6d013 100644 --- a/ddk-manager/src/contract/ser.rs +++ b/ddk-manager/src/contract/ser.rs @@ -11,6 +11,8 @@ use crate::contract::AdaptorInfo; use crate::contract::{ ClosedContract, ContractDescriptor, FailedAcceptContract, FailedSignContract, PreClosedContract, }; +use crate::KeysId; +use bitcoin::Amount; use crate::payout_curve::{ HyperbolaPayoutCurvePiece, PayoutFunction, PayoutFunctionPiece, PayoutPoint, PolynomialPayoutCurvePiece, RoundingInterval, RoundingIntervals, @@ -87,21 +89,77 @@ impl_dlc_writeable!(EnumDescriptor, { {vec_cb, ddk_messages::ser_impls::enum_payout::write, ddk_messages::ser_impls::enum_payout::read} ) }); -impl_dlc_writeable!(OfferedContract, { - (id, writeable), - (is_offer_party, writeable), - (contract_info, vec), - (offer_params, { cb_writeable, ddk_messages::ser_impls::party_params::write, ddk_messages::ser_impls::party_params::read }), - (total_collateral, writeable), - (funding_inputs, vec), - (fund_output_serial_id, writeable), - (fee_rate_per_vb, writeable), - (cet_locktime, writeable), - (refund_locktime, writeable), - (contract_flags, writeable), - (counter_party, writeable), - (keys_id, writeable) -}); +impl Writeable for OfferedContract { + fn write(&self, w: &mut W) -> Result<(), lightning::io::Error> { + self.id.write(w)?; + self.is_offer_party.write(w)?; + write_vec(&self.contract_info, w)?; + ddk_messages::ser_impls::party_params::write(&self.offer_params, w)?; + self.total_collateral.write(w)?; + write_vec(&self.funding_inputs, w)?; + self.fund_output_serial_id.write(w)?; + self.fee_rate_per_vb.write(w)?; + self.cet_locktime.write(w)?; + self.refund_locktime.write(w)?; + self.contract_flags.write(w)?; + self.counter_party.write(w)?; + self.keys_id.write(w)?; + Ok(()) + } +} + +impl Readable for OfferedContract { + fn read(r: &mut R) -> Result { + let id: [u8; 32] = Readable::read(r)?; + let is_offer_party: bool = Readable::read(r)?; + let contract_info = read_vec(r)?; + let offer_params = ddk_messages::ser_impls::party_params::read(r)?; + let total_collateral: Amount = Readable::read(r)?; + let funding_inputs = read_vec(r)?; + let fund_output_serial_id: u64 = Readable::read(r)?; + let fee_rate_per_vb: u64 = Readable::read(r)?; + let cet_locktime: u32 = Readable::read(r)?; + let refund_locktime: u32 = Readable::read(r)?; + + // Backward compatibility: contract_flags (u8) was inserted between + // refund_locktime and counter_party. Peek one byte: 0x02/0x03 means + // old format (compressed pubkey prefix), 0x00/0x01 means new format + // (contract_flags value). + let mut peek = [0u8; 1]; + r.read_exact(&mut peek)?; + let (contract_flags, counter_party) = if peek[0] == 0x02 || peek[0] == 0x03 { + // Old format: this byte is the start of counter_party pubkey + let mut pubkey_bytes = [0u8; 33]; + pubkey_bytes[0] = peek[0]; + r.read_exact(&mut pubkey_bytes[1..])?; + let pk = secp256k1_zkp::PublicKey::from_slice(&pubkey_bytes) + .map_err(|_| DecodeError::InvalidValue)?; + (0u8, pk) + } else { + // New format: this byte is contract_flags + let counter_party: secp256k1_zkp::PublicKey = Readable::read(r)?; + (peek[0], counter_party) + }; + + let keys_id: KeysId = Readable::read(r)?; + + Ok(Self { + id, + is_offer_party, + contract_info, + offer_params, + total_collateral, + funding_inputs, + fund_output_serial_id, + fee_rate_per_vb, + cet_locktime, + refund_locktime, + contract_flags, + counter_party, + keys_id, + }) + } +} impl_dlc_writeable_external!(RangeInfo, range_info, { (cet_index, usize), (adaptor_index, usize)}); impl_dlc_writeable_enum!(AdaptorInfo,;; (0, Numerical, write_multi_oracle_trie, read_multi_oracle_trie), (1, NumericalWithDifference, write_multi_oracle_trie_with_diff, read_multi_oracle_trie_with_diff); (2, Enum)); impl_dlc_writeable_external!( diff --git a/ddk/src/storage/sled/contract.rs b/ddk/src/storage/sled/contract.rs index 43d647ce..14a7528b 100644 --- a/ddk/src/storage/sled/contract.rs +++ b/ddk/src/storage/sled/contract.rs @@ -550,4 +550,28 @@ mod tests { assert_eq!(chain_monitor, retrieved); } ); + + #[test] + fn old_format_offered_contract_deserializes() { + let serialized = include_bytes!("../../../../testconfig/contract_binaries/old/Offered"); + let contract = deserialize_contract(&serialized.to_vec()) + .expect("Old format Offered binary should deserialize"); + if let Contract::Offered(offered) = contract { + assert_eq!(offered.contract_flags, 0); + } else { + panic!("Expected Offered contract"); + } + } + + #[test] + fn new_format_offered_contract_deserializes() { + let serialized = include_bytes!("../../../../testconfig/contract_binaries/Offered"); + let contract = deserialize_contract(&serialized.to_vec()) + .expect("New format Offered binary should deserialize"); + if let Contract::Offered(offered) = contract { + assert_eq!(offered.contract_flags, 0); + } else { + panic!("Expected Offered contract"); + } + } } diff --git a/dlc-messages/src/lib.rs b/dlc-messages/src/lib.rs index 495f77e3..b3692c00 100644 --- a/dlc-messages/src/lib.rs +++ b/dlc-messages/src/lib.rs @@ -399,24 +399,82 @@ impl OfferDlc { } } -impl_dlc_writeable!(OfferDlc, OFFER_TYPE, { - (protocol_version, writeable), - (contract_flags, writeable), - (chain_hash, writeable), - (temporary_contract_id, writeable), - (contract_info, writeable), - (funding_pubkey, writeable), - (payout_spk, writeable), - (payout_serial_id, writeable), - (offer_collateral, writeable), - (funding_inputs, vec), - (change_spk, writeable), - (change_serial_id, writeable), - (fund_output_serial_id, writeable), - (fee_rate_per_vb, writeable), - (cet_locktime, writeable), - (refund_locktime, writeable) -}); +impl Writeable for OfferDlc { + fn write(&self, w: &mut W) -> Result<(), lightning::io::Error> { + OFFER_TYPE.write(w)?; + self.protocol_version.write(w)?; + self.contract_flags.write(w)?; + self.chain_hash.write(w)?; + self.temporary_contract_id.write(w)?; + self.contract_info.write(w)?; + self.funding_pubkey.write(w)?; + self.payout_spk.write(w)?; + self.payout_serial_id.write(w)?; + self.offer_collateral.write(w)?; + crate::ser_impls::write_vec(&self.funding_inputs, w)?; + self.change_spk.write(w)?; + self.change_serial_id.write(w)?; + self.fund_output_serial_id.write(w)?; + self.fee_rate_per_vb.write(w)?; + self.cet_locktime.write(w)?; + self.refund_locktime.write(w)?; + Ok(()) + } +} + +impl Readable for OfferDlc { + fn read(r: &mut R) -> Result { + let type_id: u16 = Readable::read(r)?; + if type_id != OFFER_TYPE { + return Err(DecodeError::UnknownRequiredFeature); + } + + // Backward compatibility: detect old format (no protocol_version) vs new format. + // New format: [protocol_version: 4 bytes][contract_flags: 1 byte][chain_hash: 32 bytes] + // Old format: [contract_flags: 1 byte][chain_hash: 32 bytes] + // + // Read 5 bytes. Interpret first 4 as u32. If 1-10, it's protocol_version (new format). + // In old format, these 4 bytes are contract_flags (0x00/0x01) + 3 chain_hash bytes, + // which produces values far outside 1-10 for any Bitcoin network. + let mut peek = [0u8; 5]; + r.read_exact(&mut peek)?; + let possible_pv = u32::from_be_bytes([peek[0], peek[1], peek[2], peek[3]]); + + let (protocol_version, contract_flags, chain_hash) = + if possible_pv >= 1 && possible_pv <= 10 { + // New format: peek[0..4] = protocol_version, peek[4] = contract_flags + let chain_hash: [u8; 32] = Readable::read(r)?; + (possible_pv, peek[4], chain_hash) + } else { + // Old format: peek[0] = contract_flags, peek[1..5] = first 4 chain_hash bytes + let mut remaining = [0u8; 28]; + r.read_exact(&mut remaining)?; + let mut chain_hash = [0u8; 32]; + chain_hash[..4].copy_from_slice(&peek[1..5]); + chain_hash[4..].copy_from_slice(&remaining); + (1u32, peek[0], chain_hash) + }; + + Ok(Self { + protocol_version, + contract_flags, + chain_hash, + temporary_contract_id: Readable::read(r)?, + contract_info: Readable::read(r)?, + funding_pubkey: Readable::read(r)?, + payout_spk: Readable::read(r)?, + payout_serial_id: Readable::read(r)?, + offer_collateral: Readable::read(r)?, + funding_inputs: crate::ser_impls::read_vec(r)?, + change_spk: Readable::read(r)?, + change_serial_id: Readable::read(r)?, + fund_output_serial_id: Readable::read(r)?, + fee_rate_per_vb: Readable::read(r)?, + cet_locktime: Readable::read(r)?, + refund_locktime: Readable::read(r)?, + }) + } +} /// Contains information about a party wishing to accept a DLC offer. The contained /// information is sufficient for the offering party to re-build the set of From 4c77d7da59b6611b3b4b891470e887aac05759cd Mon Sep 17 00:00:00 2001 From: Matt Black Date: Fri, 26 Jun 2026 12:43:27 -0500 Subject: [PATCH 2/3] fix: use contains() for clippy manual_range_contains --- dlc-messages/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlc-messages/src/lib.rs b/dlc-messages/src/lib.rs index b3692c00..0a7a29ea 100644 --- a/dlc-messages/src/lib.rs +++ b/dlc-messages/src/lib.rs @@ -441,7 +441,7 @@ impl Readable for OfferDlc { let possible_pv = u32::from_be_bytes([peek[0], peek[1], peek[2], peek[3]]); let (protocol_version, contract_flags, chain_hash) = - if possible_pv >= 1 && possible_pv <= 10 { + if (1..=10).contains(&possible_pv) { // New format: peek[0..4] = protocol_version, peek[4] = contract_flags let chain_hash: [u8; 32] = Readable::read(r)?; (possible_pv, peek[4], chain_hash) From 915204adbf41ef9e4c91fe62a6c6c0a52777f35a Mon Sep 17 00:00:00 2001 From: Matt Black Date: Fri, 26 Jun 2026 12:44:46 -0500 Subject: [PATCH 3/3] style: cargo fmt --- ddk-manager/src/contract/ser.rs | 4 ++-- dlc-messages/src/lib.rs | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/ddk-manager/src/contract/ser.rs b/ddk-manager/src/contract/ser.rs index aee6d013..70cc175d 100644 --- a/ddk-manager/src/contract/ser.rs +++ b/ddk-manager/src/contract/ser.rs @@ -11,12 +11,12 @@ use crate::contract::AdaptorInfo; use crate::contract::{ ClosedContract, ContractDescriptor, FailedAcceptContract, FailedSignContract, PreClosedContract, }; -use crate::KeysId; -use bitcoin::Amount; use crate::payout_curve::{ HyperbolaPayoutCurvePiece, PayoutFunction, PayoutFunctionPiece, PayoutPoint, PolynomialPayoutCurvePiece, RoundingInterval, RoundingIntervals, }; +use crate::KeysId; +use bitcoin::Amount; use ddk_dlc::DlcTransactions; use ddk_messages::impl_dlc_writeable; use ddk_messages::ser_impls::{ diff --git a/dlc-messages/src/lib.rs b/dlc-messages/src/lib.rs index 0a7a29ea..e26e2b7c 100644 --- a/dlc-messages/src/lib.rs +++ b/dlc-messages/src/lib.rs @@ -440,20 +440,19 @@ impl Readable for OfferDlc { r.read_exact(&mut peek)?; let possible_pv = u32::from_be_bytes([peek[0], peek[1], peek[2], peek[3]]); - let (protocol_version, contract_flags, chain_hash) = - if (1..=10).contains(&possible_pv) { - // New format: peek[0..4] = protocol_version, peek[4] = contract_flags - let chain_hash: [u8; 32] = Readable::read(r)?; - (possible_pv, peek[4], chain_hash) - } else { - // Old format: peek[0] = contract_flags, peek[1..5] = first 4 chain_hash bytes - let mut remaining = [0u8; 28]; - r.read_exact(&mut remaining)?; - let mut chain_hash = [0u8; 32]; - chain_hash[..4].copy_from_slice(&peek[1..5]); - chain_hash[4..].copy_from_slice(&remaining); - (1u32, peek[0], chain_hash) - }; + let (protocol_version, contract_flags, chain_hash) = if (1..=10).contains(&possible_pv) { + // New format: peek[0..4] = protocol_version, peek[4] = contract_flags + let chain_hash: [u8; 32] = Readable::read(r)?; + (possible_pv, peek[4], chain_hash) + } else { + // Old format: peek[0] = contract_flags, peek[1..5] = first 4 chain_hash bytes + let mut remaining = [0u8; 28]; + r.read_exact(&mut remaining)?; + let mut chain_hash = [0u8; 32]; + chain_hash[..4].copy_from_slice(&peek[1..5]); + chain_hash[4..].copy_from_slice(&remaining); + (1u32, peek[0], chain_hash) + }; Ok(Self { protocol_version,