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, ¶ms, &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(¶ms.session_sigs, node_url)?;
108 let mut request_body = serde_json::json!({ "authSig": session_sig });
109 if let Some(code) = ¶ms.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) = ¶ms.ipfs_id {
114 request_body["ipfsId"] = serde_json::Value::String(ipfs_id.clone());
115 }
116 if let Some(auth_methods) = ¶ms.auth_methods {
117 request_body["authMethods"] = serde_json::to_value(auth_methods)?;
118 }
119 if let Some(js_params) = ¶ms.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}