diff --git a/ddk-manager/src/contract/ser.rs b/ddk-manager/src/contract/ser.rs index 6524cc0b..70cc175d 100644 --- a/ddk-manager/src/contract/ser.rs +++ b/ddk-manager/src/contract/ser.rs @@ -15,6 +15,8 @@ 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::{ @@ -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..e26e2b7c 100644 --- a/dlc-messages/src/lib.rs +++ b/dlc-messages/src/lib.rs @@ -399,24 +399,81 @@ 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 (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, + 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