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