Skip to main content

ark_core/
vtxo.rs

1use crate::ark_address::ArkAddress;
2use crate::script::csv_sig_script;
3use crate::script::multisig_3_of_3_script;
4use crate::script::multisig_script;
5use crate::script::tr_script_pubkey;
6use crate::Error;
7use crate::ErrorContext;
8use crate::ExitDelayKind;
9use crate::UNSPENDABLE_KEY;
10use bitcoin::key::PublicKey;
11use bitcoin::key::Secp256k1;
12use bitcoin::key::Verification;
13use bitcoin::taproot;
14use bitcoin::taproot::LeafVersion;
15use bitcoin::taproot::TaprootBuilder;
16use bitcoin::taproot::TaprootSpendInfo;
17use bitcoin::Address;
18use bitcoin::Network;
19use bitcoin::ScriptBuf;
20use bitcoin::XOnlyPublicKey;
21use std::time::Duration;
22
23/// All the information needed to _spend_ a VTXO.
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub struct Vtxo {
26    server_forfeit: XOnlyPublicKey,
27    owner: XOnlyPublicKey,
28    owner_unilateral_exit: XOnlyPublicKey,
29    /// The delegator's public key, if this VTXO has a delegate spending path.
30    delegator: Option<XOnlyPublicKey>,
31    spend_info: TaprootSpendInfo,
32    /// All the scripts in this VTXO's Taproot tree.
33    tapscripts: Vec<ScriptBuf>,
34    address: Address,
35    exit_delay: bitcoin::Sequence,
36    exit_delay_kind: ExitDelayKind,
37    network: Network,
38}
39
40impl Vtxo {
41    /// Build a VTXO, by providing all the scripts to be included in the Taproot tree.
42    ///
43    /// The provided `scripts` must follow the following rules:
44    ///
45    /// - All unilateral spend paths MUST be timelocked.
46    /// - All other spend paths MUST involve the Ark server's signature.
47    pub fn new_with_custom_scripts<C>(
48        secp: &Secp256k1<C>,
49        server_forfeit: XOnlyPublicKey,
50        owner: XOnlyPublicKey,
51        // TODO: Verify the validity of these scripts before constructing the `Vtxo`.
52        scripts: Vec<ScriptBuf>,
53        exit_delay: bitcoin::Sequence,
54        network: Network,
55    ) -> Result<Self, Error>
56    where
57        C: Verification,
58    {
59        let vtxo = Self::new_with_custom_scripts_and_split_owner_keys(
60            secp,
61            server_forfeit,
62            owner,
63            owner,
64            scripts,
65            exit_delay,
66            network,
67        )?;
68
69        Ok(vtxo)
70    }
71
72    pub fn new_with_custom_scripts_and_split_owner_keys<C>(
73        secp: &Secp256k1<C>,
74        server_forfeit: XOnlyPublicKey,
75        owner: XOnlyPublicKey,
76        owner_unilateral_exit: XOnlyPublicKey,
77        scripts: Vec<ScriptBuf>,
78        exit_delay: bitcoin::Sequence,
79        network: Network,
80    ) -> Result<Self, Error>
81    where
82        C: Verification,
83    {
84        let unspendable_key: PublicKey = UNSPENDABLE_KEY
85            .parse()
86            .map_err(|e| Error::ad_hoc(format!("invalid unspendable key: {e}")))?;
87        let (unspendable_key, _) = unspendable_key.inner.x_only_public_key();
88
89        let leaf_distribution = calculate_leaf_depths(scripts.len());
90
91        let mut builder = TaprootBuilder::new();
92        for (script, depth) in scripts.iter().zip(leaf_distribution.iter()) {
93            builder = builder
94                .add_leaf(*depth as u8, script.clone())
95                .map_err(Error::ad_hoc)?;
96        }
97
98        let spend_info = builder
99            .finalize(secp, unspendable_key)
100            .map_err(|_| Error::ad_hoc("failed to finalize Taproot tree"))?;
101
102        let exit_delay_kind = ExitDelayKind::from_sequence(exit_delay)?;
103
104        let script_pubkey = tr_script_pubkey(&spend_info);
105        let address = Address::from_script(&script_pubkey, network)
106            .map_err(|e| Error::ad_hoc(format!("invalid script: {e}")))?;
107
108        Ok(Self {
109            server_forfeit,
110            owner,
111            owner_unilateral_exit,
112            delegator: None,
113            spend_info,
114            tapscripts: scripts,
115            address,
116            exit_delay,
117            exit_delay_kind,
118            network,
119        })
120    }
121
122    /// Build a default VTXO.
123    pub fn new_default<C>(
124        secp: &Secp256k1<C>,
125        server_signer: XOnlyPublicKey,
126        owner: XOnlyPublicKey,
127        exit_delay: bitcoin::Sequence,
128        network: Network,
129    ) -> Result<Self, Error>
130    where
131        C: Verification,
132    {
133        let forfeit_script = multisig_script(server_signer, owner);
134        let redeem_script = csv_sig_script(exit_delay, owner);
135
136        Self::new_with_custom_scripts(
137            secp,
138            server_signer,
139            owner,
140            vec![forfeit_script, redeem_script],
141            exit_delay,
142            network,
143        )
144    }
145
146    /// Build a VTXO with a delegate spending path.
147    ///
148    /// This creates a 3-leaf Taproot tree:
149    /// 1. **Forfeit**: 2-of-2 multisig (server + owner)
150    /// 2. **Exit**: CSV-timelocked owner signature
151    /// 3. **Delegate**: 3-of-3 multisig (owner + delegator + server)
152    ///
153    /// The delegate path allows a third-party delegator service to cooperate with the owner and
154    /// the server to renew VTXOs before they expire.
155    pub fn new_with_delegator<C>(
156        secp: &Secp256k1<C>,
157        server_signer: XOnlyPublicKey,
158        owner: XOnlyPublicKey,
159        delegator: XOnlyPublicKey,
160        exit_delay: bitcoin::Sequence,
161        network: Network,
162    ) -> Result<Self, Error>
163    where
164        C: Verification,
165    {
166        let forfeit_script = multisig_script(server_signer, owner);
167        let redeem_script = csv_sig_script(exit_delay, owner);
168        let delegate_script = multisig_3_of_3_script(owner, delegator, server_signer);
169
170        let mut vtxo = Self::new_with_custom_scripts(
171            secp,
172            server_signer,
173            owner,
174            vec![forfeit_script, redeem_script, delegate_script],
175            exit_delay,
176            network,
177        )?;
178
179        vtxo.delegator = Some(delegator);
180
181        Ok(vtxo)
182    }
183
184    pub fn script_pubkey(&self) -> ScriptBuf {
185        self.address.script_pubkey()
186    }
187
188    pub fn address(&self) -> &Address {
189        &self.address
190    }
191
192    pub fn owner_pk(&self) -> XOnlyPublicKey {
193        self.owner
194    }
195
196    pub fn server_pk(&self) -> XOnlyPublicKey {
197        self.server_forfeit
198    }
199
200    pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
201        self.delegator
202    }
203
204    pub fn exit_delay(&self) -> bitcoin::Sequence {
205        self.exit_delay
206    }
207
208    pub fn to_ark_address(&self) -> ArkAddress {
209        let vtxo_tap_key = self.spend_info.output_key();
210        ArkAddress::new(self.network, self.server_forfeit, vtxo_tap_key)
211    }
212
213    /// The spend info of an arbitrary branch of a VTXO.
214    pub fn get_spend_info(&self, script: ScriptBuf) -> Result<taproot::ControlBlock, Error> {
215        let control_block = self
216            .spend_info
217            .control_block(&(script, LeafVersion::TapScript))
218            .ok_or(Error::ad_hoc("could not build control block for script"))?;
219
220        Ok(control_block)
221    }
222
223    /// The spend info for the forfeit branch of a _default_ VTXO.
224    ///
225    /// This method can fail because [`Vtxo`]s constructed with the method
226    /// [`Vtxo::new_with_custom_scripts`] may not contain this script exactly.
227    pub fn forfeit_spend_info(&self) -> Result<(ScriptBuf, taproot::ControlBlock), Error> {
228        let forfeit_script = multisig_script(self.server_forfeit, self.owner);
229
230        let control_block = self
231            .get_spend_info(forfeit_script.clone())
232            .context("missing default forfeit script")?;
233
234        Ok((forfeit_script, control_block))
235    }
236
237    /// The spend info for the unilateral exit branch of a _default_ VTXO.
238    ///
239    /// This method can fail because [`Vtxo`]s constructed with the method
240    /// [`Vtxo::new_with_custom_scripts`] may not contain this script exactly.
241    pub fn exit_spend_info(&self) -> Result<(ScriptBuf, taproot::ControlBlock), Error> {
242        let exit_script = csv_sig_script(self.exit_delay, self.owner_unilateral_exit);
243
244        let control_block = self
245            .get_spend_info(exit_script.clone())
246            .context("missing default exit script")?;
247
248        Ok((exit_script, control_block))
249    }
250
251    /// The spend info for the delegate branch of a VTXO constructed with
252    /// [`Vtxo::new_with_delegator`].
253    ///
254    /// Returns an error if the VTXO was not built with a delegator.
255    pub fn delegate_spend_info(&self) -> Result<(ScriptBuf, taproot::ControlBlock), Error> {
256        let delegator = self
257            .delegator
258            .ok_or(Error::ad_hoc("VTXO has no delegate path"))?;
259
260        let delegate_script = multisig_3_of_3_script(self.owner, delegator, self.server_forfeit);
261
262        let control_block = self
263            .get_spend_info(delegate_script.clone())
264            .context("missing delegate script")?;
265
266        Ok((delegate_script, control_block))
267    }
268
269    pub fn tapscripts(&self) -> Vec<ScriptBuf> {
270        self.tapscripts.clone()
271    }
272
273    /// Whether the VTXO can be claimed unilaterally by the owner or not, given the
274    /// `confirmation_blocktime` of the transaction that included this VTXO as an output.
275    pub fn can_be_claimed_unilaterally_by_owner(
276        &self,
277        now: Duration,
278        confirmation_blocktime: Duration,
279        confirmations: u64,
280    ) -> bool {
281        match self.exit_delay_kind {
282            ExitDelayKind::Time(seconds) => {
283                let exit_path_time = confirmation_blocktime + seconds;
284
285                now > exit_path_time
286            }
287            ExitDelayKind::Blocks(confirmations_required) => {
288                confirmations >= confirmations_required
289            }
290        }
291    }
292}
293
294fn calculate_leaf_depths(n: usize) -> Vec<usize> {
295    // Handle edge cases
296    if n == 0 {
297        return vec![];
298    }
299    if n == 1 {
300        return vec![0]; // A single node has depth 0
301    }
302    if n == 2 {
303        return vec![1, 1];
304    }
305
306    // Calculate the minimum depth required for n leaves
307    let min_depth = (n as f64).log2().ceil() as usize;
308
309    // Calculate the number of nodes at the deepest level
310    let nodes_at_max_depth = n - (1 << (min_depth - 1)) + 1;
311    let nodes_at_min_depth = (1 << min_depth) - nodes_at_max_depth;
312
313    // Create the result vector with the appropriate depths
314    let mut result = Vec::with_capacity(n);
315
316    // Add the deeper nodes first
317    for _ in 0..nodes_at_max_depth {
318        result.push(min_depth);
319    }
320
321    // Add the less deep nodes
322    for _ in 0..nodes_at_min_depth {
323        result.push(min_depth - 1);
324    }
325
326    result
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use bitcoin::secp256k1::Secp256k1;
333    use std::str::FromStr;
334
335    fn test_keys() -> (XOnlyPublicKey, XOnlyPublicKey, XOnlyPublicKey) {
336        let server = XOnlyPublicKey::from_str(
337            "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
338        )
339        .unwrap();
340        let owner = XOnlyPublicKey::from_str(
341            "28845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
342        )
343        .unwrap();
344        let delegator = XOnlyPublicKey::from_str(
345            "38845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
346        )
347        .unwrap();
348        (server, owner, delegator)
349    }
350
351    #[test]
352    fn new_with_delegator_has_three_tapscripts() {
353        let secp = Secp256k1::new();
354        let (server, owner, delegator) = test_keys();
355        let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
356
357        let vtxo = Vtxo::new_with_delegator(
358            &secp,
359            server,
360            owner,
361            delegator,
362            exit_delay,
363            Network::Regtest,
364        )
365        .unwrap();
366
367        assert_eq!(vtxo.tapscripts().len(), 3);
368        assert_eq!(vtxo.delegator_pk(), Some(delegator));
369    }
370
371    #[test]
372    fn delegator_vtxo_all_spend_paths_resolve() {
373        let secp = Secp256k1::new();
374        let (server, owner, delegator) = test_keys();
375        let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
376
377        let vtxo = Vtxo::new_with_delegator(
378            &secp,
379            server,
380            owner,
381            delegator,
382            exit_delay,
383            Network::Regtest,
384        )
385        .unwrap();
386
387        // All three spend paths should produce valid spend info.
388        let (forfeit_script, _cb) = vtxo.forfeit_spend_info().unwrap();
389        let (exit_script, _cb) = vtxo.exit_spend_info().unwrap();
390        let (delegate_script, _cb) = vtxo.delegate_spend_info().unwrap();
391
392        // Scripts should be distinct.
393        assert_ne!(forfeit_script, exit_script);
394        assert_ne!(forfeit_script, delegate_script);
395        assert_ne!(exit_script, delegate_script);
396    }
397
398    #[test]
399    fn default_vtxo_has_no_delegate_path() {
400        let secp = Secp256k1::new();
401        let (server, owner, _) = test_keys();
402        let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
403
404        let vtxo = Vtxo::new_default(&secp, server, owner, exit_delay, Network::Regtest).unwrap();
405
406        assert!(vtxo.delegator_pk().is_none());
407        assert!(vtxo.delegate_spend_info().is_err());
408    }
409
410    #[test]
411    fn delegator_vtxo_address_differs_from_default() {
412        let secp = Secp256k1::new();
413        let (server, owner, delegator) = test_keys();
414        let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
415
416        let default =
417            Vtxo::new_default(&secp, server, owner, exit_delay, Network::Regtest).unwrap();
418        let with_delegator = Vtxo::new_with_delegator(
419            &secp,
420            server,
421            owner,
422            delegator,
423            exit_delay,
424            Network::Regtest,
425        )
426        .unwrap();
427
428        // Different taproot trees should produce different addresses.
429        assert_ne!(default.address(), with_delegator.address());
430    }
431}