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