Skip to main content

ddk_manager/contract/
contract_input.rs

1//! #ContractInput
2
3use crate::error::Error;
4
5use super::ContractDescriptor;
6use bitcoin::Amount;
7use secp256k1_zkp::XOnlyPublicKey;
8#[cfg(feature = "use-serde")]
9use serde::{Deserialize, Serialize};
10
11const DUST_LIMIT: Amount = Amount::from_sat(1000);
12
13/// Oracle information required for the initial creation of a contract.
14#[derive(Debug, Clone)]
15#[cfg_attr(
16    feature = "use-serde",
17    derive(Serialize, Deserialize),
18    serde(rename_all = "camelCase")
19)]
20pub struct OracleInput {
21    /// The set of public keys for each of the used oracles.
22    pub public_keys: Vec<XOnlyPublicKey>,
23    /// The id of the event being used for the contract. Note that at the moment
24    /// a single event id is used, while multiple ids would be preferable.
25    pub event_id: String,
26    /// The number of oracles that need to provide attestations satisfying the
27    /// contract conditions to be able to close the contract.
28    pub threshold: u16,
29}
30
31impl OracleInput {
32    /// Checks whether the data within the struct is consistent.
33    pub fn validate(&self) -> Result<(), Error> {
34        if self.public_keys.is_empty() {
35            return Err(Error::InvalidParameters(
36                "OracleInput must have at least one public key.".to_string(),
37            ));
38        }
39
40        if self.threshold > self.public_keys.len() as u16 {
41            return Err(Error::InvalidParameters(
42                "Threshold cannot be larger than number of oracles.".to_string(),
43            ));
44        }
45
46        if self.threshold == 0 {
47            return Err(Error::InvalidParameters(
48                "Threshold cannot be zero.".to_string(),
49            ));
50        }
51
52        Ok(())
53    }
54}
55
56/// Represents the contract specifications.
57#[derive(Debug, Clone)]
58#[cfg_attr(
59    feature = "use-serde",
60    derive(Serialize, Deserialize),
61    serde(rename_all = "camelCase")
62)]
63pub struct ContractInputInfo {
64    /// The contract conditions.
65    pub contract_descriptor: ContractDescriptor,
66    /// The oracle information.
67    pub oracles: OracleInput,
68}
69
70#[derive(Debug, Clone)]
71#[cfg_attr(
72    feature = "use-serde",
73    derive(Serialize, Deserialize),
74    serde(rename_all = "camelCase")
75)]
76/// Contains all the information necessary for the initialization of a DLC.
77pub struct ContractInput {
78    /// The collateral for the offering party.
79    pub offer_collateral: Amount,
80    /// The collateral for the accepting party.
81    pub accept_collateral: Amount,
82    /// The fee rate used to construct the transactions.
83    pub fee_rate: u64,
84    /// Feature flags for the contract (bit 0: refund to accepter).
85    #[cfg_attr(feature = "use-serde", serde(default))]
86    pub contract_flags: u8,
87    /// The set of contract that make up the DLC (a single DLC can be based
88    /// on multiple contracts).
89    pub contract_infos: Vec<ContractInputInfo>,
90}
91
92impl ContractInput {
93    /// Validate the contract input parameters
94    pub fn validate(&self) -> Result<(), Error> {
95        // Allow 0 collateral for single-funded DLCs, but non-zero must exceed dust limit
96        if self.offer_collateral > Amount::ZERO && self.offer_collateral < DUST_LIMIT {
97            return Err(Error::InvalidParameters(
98                "Non-zero offer collateral must be greater than dust limit.".to_string(),
99            ));
100        }
101
102        let total_collateral = self.offer_collateral + self.accept_collateral;
103        if total_collateral < DUST_LIMIT {
104            return Err(Error::InvalidParameters(
105                "Total collateral must be greater than dust limit.".to_string(),
106            ));
107        }
108
109        if self.contract_infos.is_empty() {
110            return Err(Error::InvalidParameters(
111                "Need at least one contract info".to_string(),
112            ));
113        }
114
115        for contract_info in &self.contract_infos {
116            contract_info.oracles.validate()?;
117        }
118
119        ddk_dlc::util::validate_fee_rate(self.fee_rate)
120            .map_err(|_| Error::InvalidParameters("Fee rate too high.".to_string()))
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use ddk_dlc::{EnumerationPayout, Payout};
127    use secp256k1_zkp::{Keypair, SecretKey, SECP256K1};
128
129    use crate::contract::enum_descriptor::EnumDescriptor;
130
131    use super::*;
132
133    fn get_base_input() -> ContractInput {
134        ContractInput {
135            offer_collateral: Amount::from_sat(1000000),
136            accept_collateral: Amount::from_sat(2000000),
137            fee_rate: 1234,
138            contract_flags: 0,
139            contract_infos: vec![ContractInputInfo {
140                contract_descriptor: ContractDescriptor::Enum(EnumDescriptor {
141                    outcome_payouts: vec![
142                        EnumerationPayout {
143                            outcome: "A".to_string(),
144                            payout: Payout {
145                                offer: Amount::from_sat(3000000),
146                                accept: Amount::ZERO,
147                            },
148                        },
149                        EnumerationPayout {
150                            outcome: "B".to_string(),
151                            payout: Payout {
152                                offer: Amount::ZERO,
153                                accept: Amount::from_sat(3000000),
154                            },
155                        },
156                    ],
157                }),
158                oracles: OracleInput {
159                    public_keys: vec![
160                        XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(
161                            SECP256K1,
162                            &SecretKey::from_slice(&secp256k1_zkp::constants::ONE).unwrap(),
163                        ))
164                        .0,
165                    ],
166                    event_id: "1234".to_string(),
167                    threshold: 1,
168                },
169            }],
170        }
171    }
172
173    #[test]
174    fn valid_contract_input_is_valid() {
175        let input = get_base_input();
176        input.validate().expect("the contract input to be valid.");
177    }
178
179    #[test]
180    fn no_contract_info_contract_input_is_not_valid() {
181        let mut input = get_base_input();
182        input.contract_infos.clear();
183        input
184            .validate()
185            .expect_err("the contract input to be invalid.");
186    }
187
188    #[test]
189    fn invalid_fee_rate_contract_input_is_not_valid() {
190        let mut input = get_base_input();
191        input.fee_rate = 251 * 25;
192        input
193            .validate()
194            .expect_err("the contract input to be invalid.");
195    }
196
197    #[test]
198    fn no_public_keys_oracle_input_contract_input_is_not_valid() {
199        let mut input = get_base_input();
200        input.contract_infos[0].oracles.public_keys.clear();
201        input
202            .validate()
203            .expect_err("the contract input to be invalid.");
204    }
205
206    #[test]
207    fn invalid_oracle_info_threshold_oracle_input_contract_input_is_not_valid() {
208        let mut input = get_base_input();
209        input.contract_infos[0].oracles.threshold = 2;
210        input
211            .validate()
212            .expect_err("the contract input to be invalid.");
213    }
214
215    #[test]
216    fn invalid_oracle_info_threshold_zero() {
217        let mut input = get_base_input();
218        input.contract_infos[0].oracles.threshold = 0;
219        input
220            .validate()
221            .expect_err("the contract input to be invalid.");
222    }
223}