lit_rust_sdk/client/
execute.rs

1use crate::types::{
2    ExecuteJsParams, ExecuteJsResponse, NodeShare, SessionSignature, SessionSignatures, SignedData,
3};
4use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
5use elliptic_curve::{scalar::IsHigh, subtle::ConditionallySelectable, PrimeField};
6use eyre::Result;
7use k256::{AffinePoint, ProjectivePoint, Scalar};
8use reqwest::Client;
9use std::collections::HashMap;
10use tracing::{debug, info, warn};
11
12impl<P: alloy::providers::Provider> super::LitNodeClient<P> {
13    pub async fn execute_js(&self, params: ExecuteJsParams) -> Result<ExecuteJsResponse> {
14        if !self.ready {
15            return Err(eyre::eyre!("Client not connected"));
16        }
17        if params.code.is_none() && params.ipfs_id.is_none() {
18            return Err(eyre::eyre!("Either code or ipfsId must be provided"));
19        }
20
21        let request_id = self.generate_request_id();
22        info!("Executing Lit Action with request ID: {}", request_id);
23
24        let node_urls = self.connected_nodes();
25        let min_responses = node_urls.len() * 2 / 3;
26        let http_client = &self.http_client;
27
28        let futures: Vec<_> = node_urls
29            .iter()
30            .map(|node_url| {
31                let node_url = node_url.clone();
32                let params = params.clone();
33                let request_id = request_id.clone();
34                async move {
35                    let result =
36                        Self::execute_js_node_request(http_client, &node_url, &params, &request_id)
37                            .await;
38                    (node_url, result)
39                }
40            })
41            .collect();
42
43        let results = futures::future::join_all(futures).await;
44
45        let mut node_responses = Vec::new();
46        for (node_url, result) in results {
47            match result {
48                Ok(response) => {
49                    info!("Got response from node: {}", node_url);
50                    node_responses.push(response);
51                }
52                Err(e) => {
53                    warn!("Failed to get response from node {}: {}", node_url, e);
54                }
55            }
56        }
57
58        if node_responses.len() < min_responses {
59            return Err(eyre::eyre!(format!(
60                "Not enough successful responses. Got {}, need {}",
61                node_responses.len(),
62                min_responses
63            )));
64        }
65
66        let most_common_response = self.find_most_common_response(&node_responses)?;
67        let has_signed_data = !most_common_response.signed_data.is_empty();
68        let has_claim_data = !most_common_response.claim_data.is_empty();
69
70        if most_common_response.success && !has_signed_data && !has_claim_data {
71            return Ok(ExecuteJsResponse {
72                claims: HashMap::new(),
73                signatures: None,
74                decryptions: vec![],
75                response: most_common_response.response,
76                logs: most_common_response.logs,
77            });
78        }
79
80        if !has_signed_data && !has_claim_data {
81            return Ok(ExecuteJsResponse {
82                claims: HashMap::new(),
83                signatures: None,
84                decryptions: vec![],
85                response: most_common_response.response,
86                logs: most_common_response.logs,
87            });
88        }
89
90        let combined_signatures = self.combine_ecdsa_signature_shares(&node_responses).await?;
91        Ok(ExecuteJsResponse {
92            claims: most_common_response.claim_data,
93            signatures: combined_signatures,
94            decryptions: vec![],
95            response: most_common_response.response,
96            logs: most_common_response.logs,
97        })
98    }
99
100    async fn execute_js_node_request(
101        http_client: &Client,
102        node_url: &str,
103        params: &ExecuteJsParams,
104        request_id: &str,
105    ) -> Result<NodeShare> {
106        let endpoint = format!("{}/web/execute", node_url);
107        let session_sig = Self::get_session_sig_by_url(&params.session_sigs, node_url)?;
108        let mut request_body = serde_json::json!({ "authSig": session_sig });
109        if let Some(code) = &params.code {
110            let encoded_code = BASE64.encode(code.as_bytes());
111            request_body["code"] = serde_json::Value::String(encoded_code);
112        }
113        if let Some(ipfs_id) = &params.ipfs_id {
114            request_body["ipfsId"] = serde_json::Value::String(ipfs_id.clone());
115        }
116        if let Some(auth_methods) = &params.auth_methods {
117            request_body["authMethods"] = serde_json::to_value(auth_methods)?;
118        }
119        if let Some(js_params) = &params.js_params {
120            request_body["jsParams"] = js_params.clone();
121        }
122        debug!("Sending execute request to {}: {}", endpoint, request_body);
123
124        let response = http_client
125            .post(&endpoint)
126            .header("X-Request-Id", request_id)
127            .header("Content-Type", "application/json")
128            .header("Accept", "application/json")
129            .json(&request_body)
130            .send()
131            .await?;
132
133        if !response.status().is_success() {
134            let status = response.status();
135            let body = response
136                .text()
137                .await
138                .unwrap_or_else(|_| "Unable to read body".to_string());
139            warn!("Execute JS failed with status {}: {}", status, body);
140            return Err(eyre::eyre!(format!("HTTP {} - {}", status, body)));
141        }
142
143        let response_body = response.text().await?;
144        info!("Execute JS response from {}: {}", node_url, response_body);
145        let node_response: NodeShare = serde_json::from_str(&response_body).map_err(|e| {
146            warn!("Failed to parse execute JS response: {}", e);
147            eyre::eyre!(e)
148        })?;
149        Ok(node_response)
150    }
151
152    fn find_most_common_response(&self, responses: &[NodeShare]) -> Result<NodeShare> {
153        if responses.is_empty() {
154            return Err(eyre::eyre!("No responses to find consensus from"));
155        }
156        for response in responses {
157            if response.success {
158                return Ok(response.clone());
159            }
160        }
161        Ok(responses[0].clone())
162    }
163
164    fn get_session_sig_by_url(
165        session_sigs: &SessionSignatures,
166        url: &str,
167    ) -> Result<SessionSignature> {
168        if session_sigs.is_empty() {
169            return Err(eyre::eyre!("You must pass in sessionSigs"));
170        }
171        let session_sig = session_sigs.get(url).ok_or_else(|| {
172            eyre::eyre!(format!(
173                "You passed sessionSigs but we could not find session sig for node {}",
174                url
175            ))
176        })?;
177        Ok(session_sig.clone())
178    }
179
180    async fn combine_ecdsa_signature_shares(
181        &self,
182        node_responses: &[NodeShare],
183    ) -> Result<Option<serde_json::Value>> {
184        let mut signatures_by_name: HashMap<String, Vec<SignedData>> = HashMap::new();
185        for response in node_responses {
186            if !response.success {
187                continue;
188            }
189            for (_key, signed_data) in &response.signed_data {
190                let sig_name = signed_data.sig_name.clone();
191                signatures_by_name
192                    .entry(sig_name)
193                    .or_default()
194                    .push(signed_data.clone());
195            }
196        }
197        if signatures_by_name.is_empty() {
198            return Ok(None);
199        }
200
201        let mut combined_signatures = HashMap::new();
202        for (sig_name, sig_shares) in signatures_by_name {
203            let threshold = self.connected_nodes().len() * 2 / 3;
204            if sig_shares.len() < threshold {
205                warn!(
206                    "Not enough signature shares for {}. Got {}, need {}",
207                    sig_name,
208                    sig_shares.len(),
209                    threshold
210                );
211                continue;
212            }
213            let first_share = &sig_shares[0];
214            if first_share.sig_type != "K256" {
215                warn!("Unsupported signature type: {}", first_share.sig_type);
216                continue;
217            }
218
219            let valid_shares: Vec<_> = sig_shares
220                .iter()
221                .filter(|share| share.data_signed != "fail" && !share.signature_share.is_empty())
222                .cloned()
223                .collect();
224            if valid_shares.len() < threshold {
225                warn!("Not enough valid signature shares for {}. Got {} valid shares (total {}), need {}", sig_name, valid_shares.len(), sig_shares.len(), threshold);
226                continue;
227            }
228            info!(
229                "Processing {} with {} valid shares out of {} total (threshold: {})",
230                sig_name,
231                valid_shares.len(),
232                sig_shares.len(),
233                threshold
234            );
235
236            let first_share = &valid_shares[0];
237            let mut parsed_shares = Vec::new();
238            let mut public_key = None;
239            let mut presignature_big_r = None;
240            let mut msg_hash = None;
241            for share in &valid_shares {
242                let sig_share: Result<Scalar> = serde_json::from_str(&share.signature_share)
243                    .map_err(|e| eyre::eyre!(format!("Failed to parse signature share: {}", e)));
244                if let Ok(sig_share) = sig_share {
245                    parsed_shares.push(sig_share);
246                    if public_key.is_none() {
247                        public_key =
248                            serde_json::from_str::<k256::AffinePoint>(&share.public_key).ok();
249                        presignature_big_r =
250                            serde_json::from_str::<k256::AffinePoint>(&share.big_r).ok();
251                        msg_hash = serde_json::from_str::<Scalar>(&share.data_signed).ok();
252                    }
253                }
254            }
255
256            if parsed_shares.len() >= threshold {
257                if let (Some(pub_key), Some(big_r), Some(hash)) =
258                    (public_key, presignature_big_r, msg_hash)
259                {
260                    match self.combine_signature_shares_k256(parsed_shares, big_r) {
261                        Ok((s, was_flipped)) => {
262                            if self.verify_signature(&pub_key, &hash, &big_r, &s) {
263                                info!(
264                                    "Successfully combined and verified signature for {}",
265                                    sig_name
266                                );
267                                let sig_json = self.convert_signature_to_response(
268                                    &big_r,
269                                    &s,
270                                    was_flipped,
271                                    &pub_key,
272                                    &hash,
273                                    first_share,
274                                )?;
275                                combined_signatures.insert(sig_name, sig_json);
276                            } else {
277                                warn!("Combined signature verification failed for {}", sig_name);
278                            }
279                        }
280                        Err(e) => {
281                            warn!(
282                                "Failed to combine signature shares for {}: {:?}",
283                                sig_name, e
284                            );
285                        }
286                    }
287                } else {
288                    warn!(
289                        "Missing required data to combine signatures for {}",
290                        sig_name
291                    );
292                }
293            }
294        }
295
296        if combined_signatures.is_empty() {
297            Ok(None)
298        } else {
299            Ok(Some(serde_json::to_value(combined_signatures).unwrap()))
300        }
301    }
302
303    fn combine_signature_shares_k256(
304        &self,
305        signature_shares: Vec<Scalar>,
306        _big_r: AffinePoint,
307    ) -> Result<(Scalar, bool)> {
308        if signature_shares.is_empty() {
309            return Err(eyre::eyre!("No signature shares provided"));
310        }
311        let mut s: Scalar = signature_shares.into_iter().sum();
312        let was_flipped = s.is_high().into();
313        s.conditional_assign(&(-s), s.is_high());
314        Ok((s, was_flipped))
315    }
316
317    fn verify_signature(
318        &self,
319        public_key: &AffinePoint,
320        msg_hash: &Scalar,
321        big_r: &AffinePoint,
322        s: &Scalar,
323    ) -> bool {
324        use elliptic_curve::ops::Reduce;
325        use k256::elliptic_curve::point::AffineCoordinates;
326        let r = <Scalar as Reduce<k256::U256>>::reduce_bytes(&big_r.x());
327        if r.is_zero().into() || s.is_zero().into() {
328            return false;
329        }
330        let s_inv = match Option::<Scalar>::from(s.invert()) {
331            Some(inv) => inv,
332            None => return false,
333        };
334        if msg_hash.is_zero().into() {
335            return false;
336        }
337        let public_key_proj = ProjectivePoint::from(*public_key);
338        let generator = ProjectivePoint::GENERATOR;
339        let reproduced = (generator * (*msg_hash * s_inv)) + (public_key_proj * (r * s_inv));
340        let reproduced_affine = reproduced.to_affine();
341        let reproduced_r = <Scalar as Reduce<k256::U256>>::reduce_bytes(&reproduced_affine.x());
342        reproduced_r == r
343    }
344
345    fn convert_signature_to_response(
346        &self,
347        big_r: &AffinePoint,
348        s: &Scalar,
349        was_flipped: bool,
350        _public_key: &AffinePoint,
351        _msg_hash: &Scalar,
352        first_share: &SignedData,
353    ) -> Result<serde_json::Value> {
354        use elliptic_curve::ops::Reduce;
355        use k256::elliptic_curve::point::AffineCoordinates;
356        let r = <Scalar as Reduce<k256::U256>>::reduce_bytes(&big_r.x());
357        let r_hex = hex::encode(r.to_repr());
358        let s_hex = hex::encode(s.to_repr());
359        let mut recid = if big_r.y_is_odd().into() { 1u8 } else { 0u8 };
360        if was_flipped {
361            recid = 1 - recid;
362        }
363        let signature_hex = format!("0x{}{}", r_hex, s_hex);
364
365        let public_key_clean = match serde_json::from_str::<String>(&first_share.public_key) {
366            Ok(pk) => pk.strip_prefix("0x").unwrap_or(&pk).to_string(),
367            Err(_) => first_share
368                .public_key
369                .strip_prefix("0x")
370                .unwrap_or(&first_share.public_key)
371                .to_string(),
372        };
373        let data_signed_clean = match serde_json::from_str::<String>(&first_share.data_signed) {
374            Ok(ds) => ds,
375            Err(_) => first_share.data_signed.clone(),
376        };
377        info!(
378            "Converted signature for {}: r={}, s={}, recid={}, verified=true",
379            first_share.sig_name,
380            &r_hex[..16],
381            &s_hex[..16],
382            recid
383        );
384        Ok(
385            serde_json::json!({ "r": r_hex, "s": s_hex, "recid": recid, "signature": signature_hex, "publicKey": public_key_clean, "dataSigned": data_signed_clean }),
386        )
387    }
388}