spark_rust/wallet/handlers/
create_tree.rs

1use std::sync::Arc;
2
3use bitcoin::{
4    absolute::LockTime, key::Secp256k1, transaction::Version, Address, OutPoint, ScriptBuf,
5    Transaction, TxIn, TxOut,
6};
7use parking_lot::RwLock;
8use spark_protos::{
9    common::SignatureIntent,
10    spark::{
11        FinalizeNodeSignaturesRequest, NodeSignatures, SigningJob, StartTreeCreationRequest,
12        TreeNode, Utxo,
13    },
14};
15
16use crate::{
17    error::{CryptoError, SparkSdkError, TransactionError},
18    signer::{default_signer::marshal_frost_commitments, traits::SparkSigner},
19    wallet::{
20        internal_handlers::traits::create_tree::CreateTreeInternalHandlers,
21        leaf_manager::SparkLeaf,
22        utils::{
23            bitcoin::{
24                bitcoin_tx_from_bytes, parse_public_key, serialize_bitcoin_transaction,
25                sighash_from_tx,
26            },
27            sequence::initial_sequence,
28        },
29    },
30    SparkSdk,
31};
32
33pub struct CreateTreeSdkResponse {
34    pub nodes: Vec<TreeNode>,
35}
36
37impl<S: SparkSigner + Send + Sync + Clone + 'static> SparkSdk<S> {
38    /// Creates a new tree of deposit addresses from either a deposit transaction or parent node.
39    ///
40    /// This function handles the creation of a new tree of deposit addresses by:
41    /// 1. Generating the deposit address tree structure based on the split level
42    /// 2. Finalizing the tree creation by signing all nodes and registering with the Spark network
43    ///
44    /// # Arguments
45    ///
46    /// * `deposit_transaction` - Optional Bitcoin transaction containing the deposit UTXO
47    /// * `parent_node` - Optional parent tree node to create child nodes from
48    /// * `vout` - The output index in the deposit transaction or parent node to use
49    /// * `split_level` - The number of levels to split the tree into (determines number of leaves)
50    /// * `parent_signing_public_key` - The public key of the parent node or deposit UTXO
51    ///
52    /// # Returns
53    ///
54    /// * `Ok(CreateTreeSdkResponse)` - Contains the finalized tree response including data for all created nodes, both branch and leaf nodes.
55    /// * `Err(SparkSdkError)` - If there was an error during tree creation
56    ///
57    /// # Errors
58    ///
59    /// Returns [`SparkSdkError`] if:
60    /// - Neither deposit transaction nor parent node is provided
61    /// - Failed to generate deposit addresses
62    /// - Failed to finalize tree creation with signatures
63    /// - Network errors when communicating with Spark operators
64    ///
65    /// # Example
66    ///
67    /// ```
68    /// # use spark_rust::SparkSdk;
69    /// # use bitcoin::{Transaction, transaction::Version, absolute::LockTime};
70    /// # async fn example(mut sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
71    /// // Assume we have a pre-existing deposit transaction
72    /// let deposit_tx = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, input: vec![], output: vec![] };
73    /// let split_level = 2; // Creates 4 leaf nodes
74    /// let parent_pubkey = vec![/* public key bytes */];
75    ///
76    /// let tree = sdk.create_tree(
77    ///     Some(deposit_tx),
78    ///     None,
79    ///     0,
80    ///     split_level,
81    ///     parent_pubkey
82    /// ).await?;
83    /// # Ok(())
84    /// # }
85    /// ```
86    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
87    async fn create_tree(
88        &self,
89        deposit_transaction: Option<Transaction>,
90        parent_node: Option<Arc<RwLock<TreeNode>>>,
91        vout: u32,
92        split_level: u32,
93        parent_signing_public_key: Vec<u8>,
94    ) -> Result<CreateTreeSdkResponse, SparkSdkError> {
95        // generate the tree
96        let time_start = std::time::Instant::now();
97        let deposit_address_response = self
98            .generate_deposit_address_for_tree(
99                deposit_transaction.clone(),
100                parent_node.clone(),
101                vout,
102                parent_signing_public_key.clone(),
103                split_level,
104            )
105            .await?;
106        let duration = time_start.elapsed();
107        #[cfg(feature = "telemetry")]
108        tracing::debug!(duration = ?duration, "generate_deposit_address_for_tree");
109
110        // finalize the tree
111        let time_start = std::time::Instant::now();
112        let finalize_tree_response = self
113            .finalize_tree_creation(
114                deposit_transaction,
115                parent_node,
116                vout,
117                deposit_address_response.tree,
118            )
119            .await?;
120        let duration = time_start.elapsed();
121        #[cfg(feature = "telemetry")]
122        tracing::debug!(duration = ?duration, "finalize_tree_creation");
123
124        let finalize_tree_response = finalize_tree_response.finalize_tree_response;
125        let nodes = finalize_tree_response.nodes;
126
127        Ok(CreateTreeSdkResponse { nodes })
128    }
129
130    pub(crate) async fn create_tree_root(
131        &self,
132        signing_pubkey_bytes: Vec<u8>,
133        verifying_pubkey_bytes: Vec<u8>,
134        deposit_tx_bytes: Vec<u8>,
135        vout: u32,
136    ) -> Result<CreateTreeSdkResponse, SparkSdkError> {
137        let deposit_tx = bitcoin_tx_from_bytes(&deposit_tx_bytes)?;
138
139        let mut root_tx = Transaction {
140            version: Version::TWO,
141            lock_time: LockTime::ZERO,
142            input: vec![],
143            output: vec![],
144        };
145
146        let output = deposit_tx
147            .output
148            .get(vout as usize)
149            .ok_or(SparkSdkError::from(TransactionError::InvalidDepositTx(
150                format!("Invalid deposit transaction: {}", vout),
151            )))?;
152
153        let script = output.script_pubkey.clone();
154        let amount = output.value;
155
156        let txid = deposit_tx.compute_txid();
157        root_tx.input.push(TxIn {
158            previous_output: OutPoint { txid, vout },
159            script_sig: ScriptBuf::new(),
160            sequence: Default::default(),
161            witness: Default::default(),
162        });
163
164        root_tx.output.push(TxOut {
165            script_pubkey: script,
166            value: amount,
167        });
168
169        // Get random signing commitment for root nonce
170        let root_nonce_commitment = self.signer.new_frost_signing_noncepair()?;
171
172        // Calculate sighash for root transaction
173        let root_tx_sighash = sighash_from_tx(&root_tx, 0, output)?;
174
175        // Create refund transaction
176        let mut refund_tx = Transaction {
177            version: Version::TWO,
178            lock_time: LockTime::ZERO,
179            input: vec![],
180            output: vec![],
181        };
182
183        let root_txid = root_tx.compute_txid();
184        refund_tx.input.push(TxIn {
185            previous_output: OutPoint {
186                txid: root_txid,
187                vout: 0,
188            },
189            script_sig: ScriptBuf::new(),
190            sequence: initial_sequence(),
191            witness: Default::default(),
192        });
193
194        // Create refund output
195        let secp = Secp256k1::new();
196        let signing_pubkey = parse_public_key(&signing_pubkey_bytes)?;
197        let refund_address = Address::p2tr(
198            &secp,
199            signing_pubkey.x_only_public_key().0,
200            None,
201            self.config.spark_config.network.to_bitcoin_network(),
202        );
203        let refund_script = refund_address.script_pubkey();
204
205        refund_tx.output.push(TxOut {
206            script_pubkey: refund_script,
207            value: amount,
208        });
209
210        // Get random signing commitment for refund nonce
211        let refund_nonce_commitment = self.signer.new_frost_signing_noncepair()?;
212
213        // Calculate sighash for refund transaction
214        let refund_tx_sighash = sighash_from_tx(&refund_tx, 0, &root_tx.output[0])?;
215
216        // Get spark client
217        let root_tx_bytes = serialize_bitcoin_transaction(&root_tx)?;
218        let refund_tx_bytes = serialize_bitcoin_transaction(&refund_tx)?;
219
220        // start tree creation
221        let request = StartTreeCreationRequest {
222            identity_public_key: self.get_spark_address()?.serialize().to_vec(),
223            on_chain_utxo: Some(Utxo {
224                vout,
225                raw_tx: deposit_tx_bytes,
226                network: self.config.spark_config.network.marshal_proto(),
227            }),
228            root_tx_signing_job: Some(SigningJob {
229                raw_tx: root_tx_bytes.clone(),
230                signing_public_key: signing_pubkey_bytes.clone(),
231                signing_nonce_commitment: Some(marshal_frost_commitments(&root_nonce_commitment)?),
232            }),
233            refund_tx_signing_job: Some(SigningJob {
234                raw_tx: refund_tx_bytes.clone(),
235                signing_public_key: signing_pubkey_bytes.clone(),
236                signing_nonce_commitment: Some(marshal_frost_commitments(
237                    &refund_nonce_commitment,
238                )?),
239            }),
240        };
241
242        let tree_creation_response = self
243            .config
244            .spark_config
245            .call_with_retry(
246                request,
247                |mut client, req| Box::pin(async move { client.start_tree_creation(req).await }),
248                None,
249            )
250            .await?;
251
252        let node_id = tree_creation_response
253            .root_node_signature_shares
254            .clone()
255            .ok_or(SparkSdkError::from(
256                CryptoError::MissingRootNodeSignatureShares,
257            ))?
258            .node_id
259            .clone();
260
261        let sigantures = self.signer.sign_root_creation(
262            signing_pubkey_bytes.clone(),
263            verifying_pubkey_bytes.clone(),
264            root_tx_bytes.clone(),
265            refund_tx_bytes.clone(),
266            root_tx_sighash.to_vec(),
267            refund_tx_sighash.to_vec(),
268            root_nonce_commitment,
269            refund_nonce_commitment,
270            tree_creation_response,
271        )?;
272
273        let request = FinalizeNodeSignaturesRequest {
274            intent: SignatureIntent::Creation as i32,
275            node_signatures: vec![NodeSignatures {
276                node_id,
277                node_tx_signature: sigantures[0].clone(),
278                refund_tx_signature: sigantures[1].clone(),
279            }],
280        };
281
282        let finalize_node_signatures_response = self
283            .config
284            .spark_config
285            .call_with_retry(
286                request,
287                |mut client, req| {
288                    Box::pin(async move { client.finalize_node_signatures(req).await })
289                },
290                None,
291            )
292            .await?;
293
294        let finalized_root_node = finalize_node_signatures_response.nodes[0].clone();
295
296        self.leaf_manager
297            .insert_leaves(vec![SparkLeaf::Bitcoin(finalized_root_node.clone())], false)?;
298
299        let response = CreateTreeSdkResponse {
300            nodes: vec![finalized_root_node],
301        };
302
303        Ok(response)
304    }
305}