Skip to main content

ark_core/
contract.rs

1use crate::boarding_output::BoardingOutput;
2use crate::vhtlc::VhtlcOptions;
3use crate::vtxo::Vtxo;
4use crate::Error;
5use bitcoin::absolute;
6use bitcoin::key::Secp256k1;
7use bitcoin::secp256k1::All;
8use bitcoin::taproot::ControlBlock;
9use bitcoin::Network;
10use bitcoin::ScriptBuf;
11use bitcoin::Sequence;
12use bitcoin::XOnlyPublicKey;
13use serde::Deserialize;
14use serde::Serialize;
15use std::fmt;
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct ContractType(String);
19
20impl ContractType {
21    pub fn new(value: impl Into<String>) -> Result<Self, Error> {
22        let value = value.into();
23        if value.is_empty() {
24            return Err(Error::ad_hoc("contract type cannot be empty"));
25        }
26        Ok(Self(value))
27    }
28
29    pub fn as_str(&self) -> &str {
30        &self.0
31    }
32
33    pub fn default_vtxo() -> Self {
34        Self("default".to_string())
35    }
36
37    pub fn delegate_vtxo() -> Self {
38        Self("delegate".to_string())
39    }
40
41    pub fn boarding() -> Self {
42        Self("boarding".to_string())
43    }
44
45    pub fn vhtlc() -> Self {
46        Self("vhtlc".to_string())
47    }
48}
49
50impl fmt::Display for ContractType {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        self.0.fmt(f)
53    }
54}
55
56impl From<&'static str> for ContractType {
57    fn from(value: &'static str) -> Self {
58        Self(value.to_string())
59    }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum ContractState {
65    Active,
66    Inactive,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
70pub struct StoredContract {
71    pub contract_type: ContractType,
72    pub contract_version: u32,
73    pub script_pubkey: ScriptBuf,
74    pub state: ContractState,
75    pub created_at: u64,
76    /// HD derivation index for contracts generated by an index-based key provider.
77    ///
78    /// Static/custom providers may not expose an index, and imported contracts may not have one.
79    pub key_index: Option<u32>,
80    pub data: serde_json::Value,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum SpendPathKind {
86    Forfeit,
87    Exit,
88    Delegate,
89    VhtlcClaim,
90    VhtlcRefund,
91    VhtlcRefundWithoutReceiver,
92    VhtlcUnilateralClaim,
93    VhtlcUnilateralRefund,
94    VhtlcUnilateralRefundWithoutReceiver,
95    Custom(String),
96}
97
98impl SpendPathKind {
99    pub fn from_vhtlc_name(name: String) -> Self {
100        match name.as_str() {
101            "claim" => Self::VhtlcClaim,
102            "refund" => Self::VhtlcRefund,
103            "refund_without_receiver" => Self::VhtlcRefundWithoutReceiver,
104            "unilateral_claim" => Self::VhtlcUnilateralClaim,
105            "unilateral_refund" => Self::VhtlcUnilateralRefund,
106            "unilateral_refund_without_receiver" => Self::VhtlcUnilateralRefundWithoutReceiver,
107            _ => Self::Custom(name),
108        }
109    }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct SpendPath {
114    pub kind: SpendPathKind,
115    pub script: ScriptBuf,
116    pub control_block: ControlBlock,
117}
118
119impl SpendPath {
120    pub fn new(kind: SpendPathKind, script: ScriptBuf, control_block: ControlBlock) -> Self {
121        Self {
122            kind,
123            script,
124            control_block,
125        }
126    }
127
128    pub fn select(self) -> SpendSelection {
129        SpendSelection::new(self)
130    }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct SpendSelection {
135    pub path: SpendPath,
136    pub sequence: Option<Sequence>,
137    pub locktime: Option<absolute::LockTime>,
138    pub extra_witness: Vec<Vec<u8>>,
139}
140
141impl SpendSelection {
142    pub fn new(path: SpendPath) -> Self {
143        Self {
144            path,
145            sequence: None,
146            locktime: None,
147            extra_witness: Vec::new(),
148        }
149    }
150
151    pub fn with_sequence(mut self, sequence: Sequence) -> Self {
152        self.sequence = Some(sequence);
153        self
154    }
155
156    pub fn with_locktime(mut self, locktime: absolute::LockTime) -> Self {
157        self.locktime = Some(locktime);
158        self
159    }
160
161    pub fn with_extra_witness(mut self, extra_witness: Vec<Vec<u8>>) -> Self {
162        self.extra_witness = extra_witness;
163        self
164    }
165
166    pub fn resolved_sequence(&self, default_sequence: Sequence) -> Sequence {
167        self.sequence.unwrap_or(default_sequence)
168    }
169
170    pub fn resolved_spend_info(
171        &self,
172        default_sequence: Sequence,
173    ) -> (Sequence, (ScriptBuf, ControlBlock)) {
174        (self.resolved_sequence(default_sequence), self.spend_info())
175    }
176
177    pub fn spend_info(&self) -> (ScriptBuf, ControlBlock) {
178        (self.path.script.clone(), self.path.control_block.clone())
179    }
180}
181
182#[derive(Clone)]
183pub struct ContractContext {
184    network: Network,
185    secp: Secp256k1<All>,
186}
187
188impl ContractContext {
189    pub fn new(network: Network) -> Self {
190        Self {
191            network,
192            secp: Secp256k1::new(),
193        }
194    }
195
196    pub fn network(&self) -> Network {
197        self.network
198    }
199
200    pub fn secp(&self) -> &Secp256k1<All> {
201        &self.secp
202    }
203}
204
205pub trait ContractSpec:
206    Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static
207{
208    const VERSION: u32;
209
210    fn contract_type() -> ContractType;
211    fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error>;
212    fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error>;
213
214    fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
215        Ok(self
216            .spendable_paths(ctx)?
217            .into_iter()
218            .map(SpendPath::select)
219            .collect())
220    }
221}
222
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct DefaultVtxoContract {
225    pub server: XOnlyPublicKey,
226    pub owner: XOnlyPublicKey,
227    pub exit_delay: Sequence,
228}
229
230impl ContractSpec for DefaultVtxoContract {
231    const VERSION: u32 = 1;
232
233    fn contract_type() -> ContractType {
234        ContractType::default_vtxo()
235    }
236
237    fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
238        Ok(self.vtxo(ctx)?.script_pubkey())
239    }
240
241    fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
242        let vtxo = self.vtxo(ctx)?;
243        let (forfeit_script, forfeit_control_block) = vtxo.forfeit_spend_info()?;
244        let (exit_script, exit_control_block) = vtxo.exit_spend_info()?;
245        Ok(vec![
246            SpendPath::new(
247                SpendPathKind::Forfeit,
248                forfeit_script,
249                forfeit_control_block,
250            ),
251            SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
252        ])
253    }
254
255    fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
256        Ok(self
257            .spendable_paths(ctx)?
258            .into_iter()
259            .map(|path| {
260                if path.kind == SpendPathKind::Exit {
261                    path.select().with_sequence(self.exit_delay)
262                } else {
263                    path.select()
264                }
265            })
266            .collect())
267    }
268}
269
270impl DefaultVtxoContract {
271    pub fn vtxo(&self, ctx: &ContractContext) -> Result<Vtxo, Error> {
272        Vtxo::new_default(
273            ctx.secp(),
274            self.server,
275            self.owner,
276            self.exit_delay,
277            ctx.network(),
278        )
279    }
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283pub struct DelegateVtxoContract {
284    pub server: XOnlyPublicKey,
285    pub owner: XOnlyPublicKey,
286    pub delegator: XOnlyPublicKey,
287    pub exit_delay: Sequence,
288}
289
290impl ContractSpec for DelegateVtxoContract {
291    const VERSION: u32 = 1;
292
293    fn contract_type() -> ContractType {
294        ContractType::delegate_vtxo()
295    }
296
297    fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
298        Ok(self.vtxo(ctx)?.script_pubkey())
299    }
300
301    fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
302        let vtxo = self.vtxo(ctx)?;
303        let (forfeit_script, forfeit_control_block) = vtxo.forfeit_spend_info()?;
304        let (exit_script, exit_control_block) = vtxo.exit_spend_info()?;
305        let (delegate_script, delegate_control_block) = vtxo.delegate_spend_info()?;
306        Ok(vec![
307            SpendPath::new(
308                SpendPathKind::Forfeit,
309                forfeit_script,
310                forfeit_control_block,
311            ),
312            SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
313            SpendPath::new(
314                SpendPathKind::Delegate,
315                delegate_script,
316                delegate_control_block,
317            ),
318        ])
319    }
320
321    fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
322        Ok(self
323            .spendable_paths(ctx)?
324            .into_iter()
325            .map(|path| {
326                if path.kind == SpendPathKind::Exit {
327                    path.select().with_sequence(self.exit_delay)
328                } else {
329                    path.select()
330                }
331            })
332            .collect())
333    }
334}
335
336impl DelegateVtxoContract {
337    pub fn vtxo(&self, ctx: &ContractContext) -> Result<Vtxo, Error> {
338        Vtxo::new_with_delegator(
339            ctx.secp(),
340            self.server,
341            self.owner,
342            self.delegator,
343            self.exit_delay,
344            ctx.network(),
345        )
346    }
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
350pub struct BoardingContract {
351    pub server: XOnlyPublicKey,
352    pub owner: XOnlyPublicKey,
353    pub exit_delay: Sequence,
354}
355
356impl ContractSpec for BoardingContract {
357    const VERSION: u32 = 1;
358
359    fn contract_type() -> ContractType {
360        ContractType::boarding()
361    }
362
363    fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
364        Ok(self.boarding_output(ctx)?.script_pubkey())
365    }
366
367    fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
368        let boarding_output = self.boarding_output(ctx)?;
369        let (forfeit_script, forfeit_control_block) = boarding_output.forfeit_spend_info();
370        let (exit_script, exit_control_block) = boarding_output.exit_spend_info();
371        Ok(vec![
372            SpendPath::new(
373                SpendPathKind::Forfeit,
374                forfeit_script,
375                forfeit_control_block,
376            ),
377            SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
378        ])
379    }
380
381    fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
382        Ok(self
383            .spendable_paths(ctx)?
384            .into_iter()
385            .map(|path| {
386                if path.kind == SpendPathKind::Exit {
387                    path.select().with_sequence(self.exit_delay)
388                } else {
389                    path.select()
390                }
391            })
392            .collect())
393    }
394}
395
396impl BoardingContract {
397    pub fn boarding_output(&self, ctx: &ContractContext) -> Result<BoardingOutput, Error> {
398        BoardingOutput::new(
399            ctx.secp(),
400            self.server,
401            self.owner,
402            self.exit_delay,
403            ctx.network(),
404        )
405    }
406}
407
408#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
409pub struct VhtlcContract {
410    pub options: VhtlcOptions,
411}
412
413impl ContractSpec for VhtlcContract {
414    const VERSION: u32 = 1;
415
416    fn contract_type() -> ContractType {
417        ContractType::vhtlc()
418    }
419
420    fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
421        let script = crate::vhtlc::VhtlcScript::new(self.options.clone(), ctx.network())
422            .map_err(|e| Error::ad_hoc(format!("failed to build vhtlc: {e}")))?;
423        Ok(script.script_pubkey())
424    }
425
426    fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
427        let script = crate::vhtlc::VhtlcScript::new(self.options.clone(), ctx.network())
428            .map_err(|e| Error::ad_hoc(format!("failed to build vhtlc: {e}")))?;
429        script
430            .get_script_map()
431            .into_iter()
432            .map(|(name, tapscript)| {
433                let control_block = script
434                    .taproot_spend_info()
435                    .control_block(&(tapscript.clone(), bitcoin::taproot::LeafVersion::TapScript))
436                    .ok_or_else(|| Error::ad_hoc("missing vhtlc control block"))?;
437                Ok(SpendPath {
438                    kind: SpendPathKind::from_vhtlc_name(name),
439                    script: tapscript,
440                    control_block,
441                })
442            })
443            .collect()
444    }
445
446    fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
447        Ok(self
448            .spendable_paths(ctx)?
449            .into_iter()
450            .filter_map(|path| match path.kind {
451                SpendPathKind::VhtlcClaim | SpendPathKind::VhtlcUnilateralClaim => None,
452                SpendPathKind::VhtlcRefundWithoutReceiver => Some(path.select().with_locktime(
453                    absolute::LockTime::from_consensus(self.options.refund_locktime),
454                )),
455                SpendPathKind::VhtlcUnilateralRefund => Some(
456                    path.select()
457                        .with_sequence(self.options.unilateral_refund_delay),
458                ),
459                SpendPathKind::VhtlcUnilateralRefundWithoutReceiver => Some(
460                    path.select()
461                        .with_sequence(self.options.unilateral_refund_without_receiver_delay),
462                ),
463                SpendPathKind::Forfeit
464                | SpendPathKind::Exit
465                | SpendPathKind::Delegate
466                | SpendPathKind::VhtlcRefund
467                | SpendPathKind::Custom(_) => Some(path.select()),
468            })
469            .collect())
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use bitcoin::hashes::ripemd160;
477    use bitcoin::hashes::Hash;
478    use std::str::FromStr;
479
480    fn test_key(hex: &str) -> XOnlyPublicKey {
481        XOnlyPublicKey::from_str(hex).unwrap()
482    }
483
484    fn vhtlc_contract() -> VhtlcContract {
485        VhtlcContract {
486            options: VhtlcOptions {
487                sender: test_key(
488                    "f874c4fd782a63a2b078b424f9f4e5edae622880a29fe572bd71bee45ab55ea0",
489                ),
490                receiver: test_key(
491                    "b02b1a956cd0ed91d1a06b65ae976470ffd4b8c9215f446f266bb74fedd153b6",
492                ),
493                server: test_key(
494                    "e35799157be4b37565bb5afe4d04e6a0fa0a4b6a4f4e48b0d904685d253cdbdb",
495                ),
496                preimage_hash: ripemd160::Hash::hash(b"preimage"),
497                refund_locktime: 500,
498                unilateral_claim_delay: Sequence::from_height(10),
499                unilateral_refund_delay: Sequence::from_height(20),
500                unilateral_refund_without_receiver_delay: Sequence::from_height(30),
501            },
502        }
503    }
504
505    #[test]
506    fn vhtlc_spendable_selections_include_required_constraints() {
507        let ctx = ContractContext::new(Network::Regtest);
508        let selections = vhtlc_contract().spendable_selections(&ctx).unwrap();
509
510        assert!(!selections
511            .iter()
512            .any(|selection| selection.path.kind == SpendPathKind::VhtlcClaim));
513        assert!(!selections
514            .iter()
515            .any(|selection| selection.path.kind == SpendPathKind::VhtlcUnilateralClaim));
516
517        let refund_without_receiver = selections
518            .iter()
519            .find(|selection| selection.path.kind == SpendPathKind::VhtlcRefundWithoutReceiver)
520            .unwrap();
521        assert_eq!(
522            refund_without_receiver.locktime,
523            Some(absolute::LockTime::from_consensus(500))
524        );
525
526        let unilateral_refund = selections
527            .iter()
528            .find(|selection| selection.path.kind == SpendPathKind::VhtlcUnilateralRefund)
529            .unwrap();
530        assert_eq!(unilateral_refund.sequence, Some(Sequence::from_height(20)));
531
532        let unilateral_refund_without_receiver = selections
533            .iter()
534            .find(|selection| {
535                selection.path.kind == SpendPathKind::VhtlcUnilateralRefundWithoutReceiver
536            })
537            .unwrap();
538        assert_eq!(
539            unilateral_refund_without_receiver.sequence,
540            Some(Sequence::from_height(30))
541        );
542    }
543}