Skip to main content

ark_core/
boarding_output.rs

1use crate::script::csv_sig_script;
2use crate::script::multisig_script;
3use crate::script::tr_script_pubkey;
4use crate::Error;
5use crate::ExitDelayKind;
6use crate::ExplorerUtxo;
7use crate::UNSPENDABLE_KEY;
8use bitcoin::key::PublicKey;
9use bitcoin::key::Secp256k1;
10use bitcoin::key::Verification;
11use bitcoin::taproot;
12use bitcoin::taproot::LeafVersion;
13use bitcoin::taproot::TaprootBuilder;
14use bitcoin::taproot::TaprootSpendInfo;
15use bitcoin::Address;
16use bitcoin::Amount;
17use bitcoin::Network;
18use bitcoin::OutPoint;
19use bitcoin::ScriptBuf;
20use bitcoin::XOnlyPublicKey;
21use std::time::Duration;
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24pub struct BoardingOutput {
25    server: XOnlyPublicKey,
26    owner: XOnlyPublicKey,
27    spend_info: TaprootSpendInfo,
28    address: Address,
29    exit_delay: bitcoin::Sequence,
30    exit_delay_kind: ExitDelayKind,
31}
32
33impl BoardingOutput {
34    pub fn new<C>(
35        secp: &Secp256k1<C>,
36        server: XOnlyPublicKey,
37        owner: XOnlyPublicKey,
38        exit_delay: bitcoin::Sequence,
39        network: Network,
40    ) -> Result<Self, Error>
41    where
42        C: Verification,
43    {
44        let unspendable_key: PublicKey = UNSPENDABLE_KEY
45            .parse()
46            .map_err(|e| Error::ad_hoc(format!("invalid unspendable key: {e}")))?;
47        let (unspendable_key, _) = unspendable_key.inner.x_only_public_key();
48
49        let multisig_script = multisig_script(server, owner);
50        let exit_script = csv_sig_script(exit_delay, owner);
51
52        let spend_info = TaprootBuilder::new()
53            .add_leaf(1, multisig_script)
54            .map_err(|e| Error::ad_hoc(format!("invalid multisig leaf: {e}")))?
55            .add_leaf(1, exit_script)
56            .map_err(|e| Error::ad_hoc(format!("invalid exit leaf: {e}")))?
57            .finalize(secp, unspendable_key)
58            .map_err(|_| Error::ad_hoc("failed to finalize taproot builder"))?;
59
60        let exit_delay_kind = ExitDelayKind::from_sequence(exit_delay)?;
61
62        let script_pubkey = tr_script_pubkey(&spend_info);
63        let address = Address::from_script(&script_pubkey, network)
64            .map_err(|e| Error::ad_hoc(format!("invalid script: {e}")))?;
65
66        Ok(Self {
67            server,
68            owner,
69            spend_info,
70            address,
71            exit_delay,
72            exit_delay_kind,
73        })
74    }
75
76    pub fn address(&self) -> &Address {
77        &self.address
78    }
79
80    pub fn owner_pk(&self) -> XOnlyPublicKey {
81        self.owner
82    }
83
84    pub fn script_pubkey(&self) -> ScriptBuf {
85        self.address.script_pubkey()
86    }
87
88    pub fn forfeit_spend_info(&self) -> (ScriptBuf, taproot::ControlBlock) {
89        // It's kind of rubbish that we need to reconstruct the script every time we want a
90        // `ControlBlock`. It would be nicer to just get the `ControlBlock` for the left leaf and
91        // the right leaf, knowing which one is which.
92
93        let forfeit_script = self.forfeit_script();
94
95        let control_block = self
96            .spend_info
97            .control_block(&(forfeit_script.clone(), LeafVersion::TapScript))
98            .expect("forfeit script");
99
100        (forfeit_script, control_block)
101    }
102
103    pub fn exit_spend_info(&self) -> (ScriptBuf, taproot::ControlBlock) {
104        let exit_script = self.exit_script();
105
106        let control_block = self
107            .spend_info
108            .control_block(&(exit_script.clone(), LeafVersion::TapScript))
109            .expect("exit script");
110
111        (exit_script, control_block)
112    }
113
114    pub fn exit_delay(&self) -> bitcoin::Sequence {
115        self.exit_delay
116    }
117
118    pub fn output_key(&self) -> bitcoin::key::TweakedPublicKey {
119        self.spend_info.output_key()
120    }
121
122    pub fn to_ark_address(&self, network: Network, server: XOnlyPublicKey) -> crate::ArkAddress {
123        crate::ArkAddress::new(network, server, self.output_key())
124    }
125
126    pub fn tapscripts(&self) -> Vec<ScriptBuf> {
127        let (exit_script, _) = self.exit_spend_info();
128        let (forfeit_script, _) = self.forfeit_spend_info();
129
130        vec![exit_script, forfeit_script]
131    }
132
133    /// Whether the boarding output can be claimed unilaterally by the owner or not, given the
134    /// `confirmation_blocktime` of the transaction that included this boarding output as an output.
135    pub fn can_be_claimed_unilaterally_by_owner(
136        &self,
137        now: Duration,
138        confirmation_blocktime: Duration,
139        confirmations: u64,
140    ) -> bool {
141        match self.exit_delay_kind {
142            ExitDelayKind::Time(seconds) => {
143                let exit_path_time = confirmation_blocktime + seconds;
144
145                now > exit_path_time
146            }
147            ExitDelayKind::Blocks(confirmations_required) => {
148                confirmations >= confirmations_required
149            }
150        }
151    }
152
153    fn forfeit_script(&self) -> ScriptBuf {
154        multisig_script(self.server, self.owner)
155    }
156
157    fn exit_script(&self) -> ScriptBuf {
158        csv_sig_script(self.exit_delay, self.owner)
159    }
160}
161
162/// The on-chain status of a collection of boarding outputs.
163#[derive(Debug, Clone, Default)]
164pub struct BoardingOutpoints {
165    /// Boarding outputs that can be converted into VTXOs in collaboration with the Ark server.
166    pub spendable: Vec<(OutPoint, Amount, BoardingOutput)>,
167    /// Boarding outputs that should only be spent unilaterally.
168    pub expired: Vec<(OutPoint, Amount, BoardingOutput)>,
169    /// Boarding outputs that are not yet confirmed on-chain.
170    pub pending: Vec<(OutPoint, Amount, BoardingOutput)>,
171    /// Boarding outputs that were already spent.
172    pub spent: Vec<(OutPoint, Amount)>,
173}
174
175impl BoardingOutpoints {
176    pub fn spendable_balance(&self) -> Amount {
177        self.spendable.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
178    }
179
180    pub fn expired_balance(&self) -> Amount {
181        self.expired.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
182    }
183
184    pub fn pending_balance(&self) -> Amount {
185        self.pending.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
186    }
187}
188
189/// Given a list of [`BoardingOutput`]s, determine their on-chain status.
190pub fn list_boarding_outpoints<F>(
191    find_outpoints_fn: F,
192    boarding_outputs: &[BoardingOutput],
193) -> Result<BoardingOutpoints, Error>
194where
195    F: Fn(&Address) -> Result<Vec<ExplorerUtxo>, Error>,
196{
197    let mut spendable = Vec::new();
198    let mut expired = Vec::new();
199    let mut pending = Vec::new();
200    let mut spent = Vec::new();
201    for boarding_output in boarding_outputs.iter() {
202        let boarding_address = boarding_output.address();
203
204        // The boarding outputs corresponding to this address that we can find on-chain.
205        let boarding_utxos = find_outpoints_fn(boarding_address)?;
206
207        for boarding_utxo in boarding_utxos.iter() {
208            match *boarding_utxo {
209                // The boarding output can be found on-chain.
210                ExplorerUtxo {
211                    confirmation_blocktime: Some(confirmation_blocktime),
212                    confirmations,
213                    outpoint,
214                    amount,
215                    is_spent: false,
216                } => {
217                    let now = std::time::UNIX_EPOCH.elapsed().map_err(Error::ad_hoc)?;
218
219                    // If the boarding output is on-chain can be spent unilaterally, it has expired.
220                    if boarding_output.can_be_claimed_unilaterally_by_owner(
221                        now,
222                        Duration::from_secs(confirmation_blocktime),
223                        confirmations,
224                    ) {
225                        expired.push((outpoint, amount, boarding_output.clone()));
226                    }
227                    // If the boarding output is on-chain and cannot be spent unilaterally, it is
228                    // spendable.
229                    else {
230                        spendable.push((outpoint, amount, boarding_output.clone()));
231                    }
232                }
233                // The boarding output is still pending confirmation.
234                ExplorerUtxo {
235                    confirmation_blocktime: None,
236                    outpoint,
237                    amount,
238                    is_spent: false,
239                    ..
240                } => {
241                    pending.push((outpoint, amount, boarding_output.clone()));
242                }
243                // The boarding output was spent.
244                ExplorerUtxo {
245                    outpoint,
246                    amount,
247                    is_spent: true,
248                    ..
249                } => spent.push((outpoint, amount)),
250            }
251        }
252    }
253
254    Ok(BoardingOutpoints {
255        spendable,
256        expired,
257        pending,
258        spent,
259    })
260}