1use crate::accs::{
2 canonicalize_unified_access_control_conditions, hash_unified_access_control_conditions,
3};
4use crate::auth::{
5 auth_config_from_delegation_auth_sig, create_siwe_message_with_resources,
6 generate_session_key_pair, pkp_eth_address_from_pubkey, validate_delegation_auth_sig,
7 AuthConfig, AuthContext, AuthData, AuthSig, CustomAuthParams, SessionKeyPair,
8};
9use crate::crypto::{
10 bls_encrypt, bls_verify_and_decrypt_with_signature_shares, combine_bls_signature_shares,
11};
12use crate::e2ee::{
13 wallet_decrypt, wallet_decrypt_with_any_key, wallet_encrypt, EncryptedPayload,
14 GenericEncryptedPayload,
15};
16use crate::error::LitSdkError;
17use crate::network::{Endpoint, NetworkConfig};
18use crate::session::{issue_session_sigs, issue_session_sigs_with_max_price};
19use crate::sev_snp::SevSnp;
20use crate::types::{
21 DecryptParams, DecryptResponse, EncryptParams, EncryptResponse, ExecuteJsResponse,
22 HandshakeRequestData, OrchestrateHandshakeResponse, RawHandshakeResponse,
23 ResolvedHandshakeResponse,
24};
25use base64ct::Encoding;
26use ethers::providers::{Http, Provider};
27use ethers::types::{Address, U256};
28use rand::RngCore;
29use reqwest::header::{HeaderMap, HeaderValue};
30use serde::{Deserialize, Serialize};
31use sha2::{Digest, Sha256, Sha384};
32use sha3::{Keccak256, Keccak384};
33use std::collections::{BTreeMap, HashMap, HashSet};
34use std::sync::Arc;
35use tokio::time::{timeout, Duration};
36
37ethers::contract::abigen!(
38 StakingContract,
39 r#"[{
40 "inputs":[{"internalType":"uint256","name":"realmId","type":"uint256"}],
41 "name":"getActiveUnkickedValidatorStructsAndCounts",
42 "outputs":[
43 {"components":[
44 {"internalType":"uint256","name":"epochLength","type":"uint256"},
45 {"internalType":"uint256","name":"number","type":"uint256"},
46 {"internalType":"uint256","name":"rewardEpochNumber","type":"uint256"},
47 {"internalType":"uint256","name":"nextRewardEpochNumber","type":"uint256"},
48 {"internalType":"uint256","name":"endTime","type":"uint256"},
49 {"internalType":"uint256","name":"retries","type":"uint256"},
50 {"internalType":"uint256","name":"timeout","type":"uint256"},
51 {"internalType":"uint256","name":"startTime","type":"uint256"},
52 {"internalType":"uint256","name":"lastAdvanceVoteTime","type":"uint256"}
53 ],"internalType":"struct LibStakingStorage.Epoch","name":"","type":"tuple"},
54 {"internalType":"uint256","name":"minNodeCount","type":"uint256"},
55 {"components":[
56 {"internalType":"uint32","name":"ip","type":"uint32"},
57 {"internalType":"uint128","name":"ipv6","type":"uint128"},
58 {"internalType":"uint32","name":"port","type":"uint32"},
59 {"internalType":"address","name":"nodeAddress","type":"address"},
60 {"internalType":"uint256","name":"reward","type":"uint256"},
61 {"internalType":"uint256","name":"senderPubKey","type":"uint256"},
62 {"internalType":"uint256","name":"receiverPubKey","type":"uint256"},
63 {"internalType":"uint256","name":"lastActiveEpoch","type":"uint256"},
64 {"internalType":"uint256","name":"commissionRate","type":"uint256"},
65 {"internalType":"uint256","name":"lastRewardEpoch","type":"uint256"},
66 {"internalType":"uint256","name":"lastRealmId","type":"uint256"},
67 {"internalType":"uint256","name":"delegatedStakeAmount","type":"uint256"},
68 {"internalType":"uint256","name":"delegatedStakeWeight","type":"uint256"},
69 {"internalType":"uint256","name":"lastRewardEpochClaimedFixedCostRewards","type":"uint256"},
70 {"internalType":"uint256","name":"lastRewardEpochClaimedCommission","type":"uint256"},
71 {"internalType":"address","name":"operatorAddress","type":"address"},
72 {"internalType":"uint256","name":"uniqueDelegatingStakerCount","type":"uint256"},
73 {"internalType":"bool","name":"registerAttestedWalletDisabled","type":"bool"}
74 ],"internalType":"struct LibStakingStorage.Validator[]","name":"","type":"tuple[]"}
75 ],
76 "stateMutability":"view",
77 "type":"function"
78 }]"#,
79);
80
81ethers::contract::abigen!(
82 PriceFeedContract,
83 r#"[{
84 "inputs":[
85 {"internalType":"uint256","name":"realmId","type":"uint256"},
86 {"internalType":"uint256[]","name":"productIds","type":"uint256[]"}
87 ],
88 "name":"getNodesForRequest",
89 "outputs":[
90 {"internalType":"uint256","name":"","type":"uint256"},
91 {"internalType":"uint256","name":"","type":"uint256"},
92 {"components":[
93 {"components":[
94 {"internalType":"uint32","name":"ip","type":"uint32"},
95 {"internalType":"uint128","name":"ipv6","type":"uint128"},
96 {"internalType":"uint32","name":"port","type":"uint32"},
97 {"internalType":"address","name":"nodeAddress","type":"address"},
98 {"internalType":"uint256","name":"reward","type":"uint256"},
99 {"internalType":"uint256","name":"senderPubKey","type":"uint256"},
100 {"internalType":"uint256","name":"receiverPubKey","type":"uint256"},
101 {"internalType":"uint256","name":"lastActiveEpoch","type":"uint256"},
102 {"internalType":"uint256","name":"commissionRate","type":"uint256"},
103 {"internalType":"uint256","name":"lastRewardEpoch","type":"uint256"},
104 {"internalType":"uint256","name":"lastRealmId","type":"uint256"},
105 {"internalType":"uint256","name":"delegatedStakeAmount","type":"uint256"},
106 {"internalType":"uint256","name":"delegatedStakeWeight","type":"uint256"},
107 {"internalType":"uint256","name":"lastRewardEpochClaimedFixedCostRewards","type":"uint256"},
108 {"internalType":"uint256","name":"lastRewardEpochClaimedCommission","type":"uint256"},
109 {"internalType":"address","name":"operatorAddress","type":"address"},
110 {"internalType":"uint256","name":"uniqueDelegatingStakerCount","type":"uint256"},
111 {"internalType":"bool","name":"registerAttestedWalletDisabled","type":"bool"}
112 ],"internalType":"struct LibStakingStorage.Validator","name":"validator","type":"tuple"},
113 {"internalType":"uint256[]","name":"prices","type":"uint256[]"}
114 ],"internalType":"struct LibPriceFeedStorage.NodeInfoAndPrices[]","name":"","type":"tuple[]"}
115 ],
116 "stateMutability":"view",
117 "type":"function"
118 }]"#,
119);
120
121const PRODUCT_ID_DECRYPTION: usize = 0;
122const PRODUCT_ID_SIGN: usize = 1;
123const PRODUCT_ID_LIT_ACTION: usize = 2;
124const PRODUCT_ID_SIGN_SESSION_KEY: usize = 3;
125
126fn int_to_ip(ip: u32) -> String {
127 let b = ip.to_be_bytes();
128 format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3])
129}
130
131fn price_feed_address_for(network: &str) -> Option<Address> {
132 match network {
133 "naga-dev" => Some("0xa997f8DE767d59ecb47A76B421E0C5a1764dD945".parse().ok()?),
134 "naga-test" => Some("0x556955025dD0981Bac684fbDEcE14cDa897d0837".parse().ok()?),
135 "naga-staging" => Some("0x651d3282E1F083036Bb136dBbe7df17aCC39A330".parse().ok()?),
136 "naga-proto" => Some("0xFF4ceEC38572fEd4a48f6D3DF2bed7ccadD115a6".parse().ok()?),
137 "naga" => Some("0x88F5535Fa6dA5C225a3C06489fE4e3405b87608C".parse().ok()?),
138 _ => None,
139 }
140}
141
142fn staking_address_for(network: &str) -> Option<Address> {
143 match network {
144 "naga-dev" => Some("0x544ac098670a266d3598B543aefBEbAb0A2C86C6".parse().ok()?),
145 "naga-test" => Some("0x9f3cE810695180C5f693a7cD2a0203A381fd57E1".parse().ok()?),
146 "naga-staging" => Some("0x9b8Ed3FD964Bc38dDc32CF637439e230CD50e3Dd".parse().ok()?),
147 "naga-proto" => Some("0x28759afC5989B961D0A8EB236C9074c4141Baea1".parse().ok()?),
148 "naga" => Some("0x8a861B3640c1ff058CCB109ba11CA3224d228159".parse().ok()?),
149 _ => None,
150 }
151}
152
153#[derive(Clone)]
154pub struct LitClient {
155 http: reqwest::Client,
156 config: NetworkConfig,
157 handshake: OrchestrateHandshakeResponse,
158 version: String,
159}
160
161#[derive(Clone, Copy, Debug, Default)]
162pub struct ExecuteJsOptions {
163 pub use_single_node: bool,
164 pub user_max_price_wei: Option<U256>,
165 pub response_strategy: ExecuteJsResponseStrategy,
166}
167
168#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
169pub enum ExecuteJsResponseStrategy {
170 #[default]
171 LeastCommon,
172 MostCommon,
173}
174
175#[derive(Clone, Debug)]
176struct JitKeyPair {
177 public_key: [u8; 32],
178 secret_key: [u8; 32],
179}
180
181#[derive(Clone, Debug)]
182struct NagaJitContext {
183 key_set: HashMap<String, JitKeyPair>,
184}
185
186pub async fn create_lit_client(config: NetworkConfig) -> Result<LitClient, LitSdkError> {
187 let mut config = config;
188 if config.bootstrap_urls.is_empty() {
189 config.bootstrap_urls = discover_bootstrap_urls(&config).await?;
190 }
191
192 let http = reqwest::Client::builder()
193 .user_agent("lit-sdk-rust/0.1.0")
194 .build()?;
195
196 let version = format!("8.0.0-{}", config.network);
199 let handshake = orchestrate_handshake(&http, &config, &version).await?;
200
201 Ok(LitClient {
202 http,
203 config,
204 handshake,
205 version,
206 })
207}
208
209async fn discover_bootstrap_urls(config: &NetworkConfig) -> Result<Vec<String>, LitSdkError> {
210 let rpc_url = config.rpc_url.as_deref().ok_or_else(|| {
211 LitSdkError::Config(
212 "rpc_url is required to auto-discover bootstrap_urls (or provide bootstrap_urls)"
213 .into(),
214 )
215 })?;
216 let staking_addr = staking_address_for(config.network).ok_or_else(|| {
217 LitSdkError::Config(format!(
218 "unknown Staking contract address for network {}",
219 config.network
220 ))
221 })?;
222
223 let provider = Arc::new(
224 Provider::<Http>::try_from(rpc_url).map_err(|e| LitSdkError::Config(e.to_string()))?,
225 );
226 let contract = StakingContract::new(staking_addr, provider);
227
228 let realm_id: U256 = 1u64.into();
229 let (_epoch, min_node_count, validators) = contract
230 .get_active_unkicked_validator_structs_and_counts(realm_id)
231 .call()
232 .await
233 .map_err(|e| LitSdkError::Network(e.to_string()))?;
234
235 let min = min_node_count.as_usize().max(1);
236 let mut urls: Vec<String> = validators
237 .iter()
238 .map(|v| format!("{}{}:{}", config.http_protocol, int_to_ip(v.ip), v.port))
239 .collect();
240 urls.sort();
241 urls.dedup();
242
243 if urls.len() < min {
244 return Err(LitSdkError::Network(format!(
245 "validator set below minNodeCount: min={min} got={}",
246 urls.len()
247 )));
248 }
249
250 Ok(urls)
251}
252
253impl LitClient {
254 pub fn network_name(&self) -> &'static str {
255 self.config.network
256 }
257
258 pub fn handshake_result(&self) -> &OrchestrateHandshakeResponse {
259 &self.handshake
260 }
261
262 fn is_node_payload_decryption_error(err: &LitSdkError) -> bool {
263 match err {
264 LitSdkError::Network(msg) => {
265 msg.contains("can't decrypt")
266 || msg.contains("encrypted payload decryption failed")
267 || msg.contains("E2EE decryption failed")
268 }
269 _ => false,
270 }
271 }
272
273 pub async fn encrypt(&self, params: EncryptParams) -> Result<EncryptResponse, LitSdkError> {
274 let subnet_pub_key = &self.handshake.core_node_config.subnet_pub_key;
275
276 let data_hash_hex = sha256_hex(¶ms.data_to_encrypt);
277
278 let conditions_hash_hex = if let Some(hex) = params.hashed_access_control_conditions_hex {
279 hex
280 } else if let Some(unified) = params.unified_access_control_conditions {
281 let hash_bytes = hash_unified_access_control_conditions(&unified)?;
282 hex::encode(hash_bytes)
283 } else {
284 return Err(LitSdkError::Accs(
285 "provide unified_access_control_conditions or hashed_access_control_conditions_hex"
286 .into(),
287 ));
288 };
289
290 let identity_param = format!(
291 "lit-accesscontrolcondition://{}/{}",
292 conditions_hash_hex, data_hash_hex
293 );
294 let identity_bytes = identity_param.as_bytes();
295
296 let ciphertext_b64 = bls_encrypt(subnet_pub_key, ¶ms.data_to_encrypt, identity_bytes)?;
297
298 Ok(EncryptResponse {
299 ciphertext_base64: ciphertext_b64,
300 data_to_encrypt_hash_hex: data_hash_hex,
301 metadata: params.metadata,
302 })
303 }
304
305 pub async fn decrypt(
306 &self,
307 params: DecryptParams,
308 auth_context: &AuthContext,
309 chain: &str,
310 ) -> Result<DecryptResponse, LitSdkError> {
311 let res = self
312 .decrypt_inner(params.clone(), auth_context, chain)
313 .await;
314 match res {
315 Ok(v) => Ok(v),
316 Err(e) if Self::is_node_payload_decryption_error(&e) => {
317 let refreshed = create_lit_client(self.config.clone()).await?;
318 refreshed.decrypt_inner(params, auth_context, chain).await
319 }
320 Err(e) => Err(e),
321 }
322 }
323
324 async fn decrypt_inner(
325 &self,
326 params: DecryptParams,
327 auth_context: &AuthContext,
328 chain: &str,
329 ) -> Result<DecryptResponse, LitSdkError> {
330 let jit = self.create_jit_context()?;
331 let threshold = self.handshake.threshold.max(1);
332 let node_urls = match self.select_priced_nodes(PRODUCT_ID_DECRYPTION).await {
333 Ok(urls) => urls,
334 Err(_) => self
335 .handshake
336 .connected_nodes
337 .iter()
338 .take(threshold)
339 .cloned()
340 .collect(),
341 };
342 if node_urls.len() < threshold {
343 return Err(LitSdkError::Network(format!(
344 "insufficient node urls for decrypt: got {}, need {}",
345 node_urls.len(),
346 threshold
347 )));
348 }
349
350 let session_sigs = issue_session_sigs(
351 &auth_context.session_key_pair,
352 &auth_context.auth_config,
353 &auth_context.delegation_auth_sig,
354 &node_urls,
355 )?;
356
357 let unified_canonical = params
358 .unified_access_control_conditions
359 .as_ref()
360 .map(canonicalize_unified_access_control_conditions)
361 .transpose()?;
362
363 let conditions_hash_hex = if let Some(hex) = params.hashed_access_control_conditions_hex {
364 hex
365 } else if let Some(unified) = unified_canonical.clone() {
366 let hash_bytes = hash_unified_access_control_conditions(&unified)?;
367 hex::encode(hash_bytes)
368 } else {
369 return Err(LitSdkError::Accs(
370 "provide unified_access_control_conditions or hashed_access_control_conditions_hex"
371 .into(),
372 ));
373 };
374
375 let identity_param = format!(
376 "lit-accesscontrolcondition://{}/{}",
377 conditions_hash_hex, params.data_to_encrypt_hash_hex
378 );
379
380 let request_id = random_hex(16);
381 let epoch = self.handshake.epoch;
382
383 let mut requests = vec![];
384 let http = self.http.clone();
385 let version = self.version.clone();
386 for (url, sig) in &session_sigs {
387 let request_data = serde_json::json!({
388 "ciphertext": params.ciphertext_base64,
389 "dataToEncryptHash": params.data_to_encrypt_hash_hex,
390 "authSig": sig,
391 "chain": chain,
392 "unifiedAccessControlConditions": unified_canonical.clone().unwrap_or_else(|| serde_json::json!([])),
393 });
394
395 let encrypted = wallet_encrypt(
396 &jit.key_set[url].secret_key,
397 &jit.key_set[url].public_key,
398 request_data.to_string().as_bytes(),
399 )?;
400
401 let full_url = compose_lit_url(url, &self.config.endpoints.encryption_sign);
402 let body = serde_json::to_value(&encrypted).unwrap();
403 let secret_key = jit.key_set[url].secret_key;
404 let http_cl = http.clone();
405 let version_cl = version.clone();
406 let request_id_cl = request_id.clone();
407 requests.push(async move {
408 send_encrypted_node_request(
409 &http_cl,
410 &full_url,
411 body,
412 &secret_key,
413 &request_id_cl,
414 &version_cl,
415 epoch,
416 )
417 .await
418 });
419 }
420
421 let results = futures::future::join_all(requests).await;
422 let batch = merge_encrypted_batch(results, threshold)?;
423
424 let decrypted_nodes: Vec<DecryptNodeResponse> =
425 decrypt_batch_response(&batch, &jit, |v| {
426 let data_val = v
427 .get("data")
428 .cloned()
429 .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
430 serde_json::from_value::<DecryptNodeResponse>(data_val)
431 .map_err(|e| LitSdkError::Network(e.to_string()))
432 })?;
433
434 let shares_json: Vec<String> = decrypted_nodes
435 .iter()
436 .map(|n| {
437 serde_json::to_string(&serde_json::json!({
438 "ProofOfPossession": {
439 "identifier": n.signature_share.proof_of_possession.identifier,
440 "value": n.signature_share.proof_of_possession.value,
441 }
442 }))
443 .unwrap()
444 })
445 .collect();
446
447 let plaintext = bls_verify_and_decrypt_with_signature_shares(
448 &self.handshake.core_node_config.subnet_pub_key,
449 identity_param.as_bytes(),
450 ¶ms.ciphertext_base64,
451 &shares_json,
452 )?;
453
454 Ok(DecryptResponse {
455 decrypted_data: plaintext,
456 metadata: None,
457 })
458 }
459
460 pub async fn execute_js(
461 &self,
462 code: Option<String>,
463 ipfs_id: Option<String>,
464 js_params: Option<serde_json::Value>,
465 auth_context: &AuthContext,
466 ) -> Result<ExecuteJsResponse, LitSdkError> {
467 self.execute_js_with_options(
468 code,
469 ipfs_id,
470 js_params,
471 auth_context,
472 ExecuteJsOptions::default(),
473 )
474 .await
475 }
476
477 pub async fn execute_js_with_options(
478 &self,
479 code: Option<String>,
480 ipfs_id: Option<String>,
481 js_params: Option<serde_json::Value>,
482 auth_context: &AuthContext,
483 options: ExecuteJsOptions,
484 ) -> Result<ExecuteJsResponse, LitSdkError> {
485 let res = self
486 .execute_js_with_options_inner(
487 code.clone(),
488 ipfs_id.clone(),
489 js_params.clone(),
490 auth_context,
491 options,
492 )
493 .await;
494 match res {
495 Ok(v) => Ok(v),
496 Err(e) if Self::is_node_payload_decryption_error(&e) => {
497 let refreshed = create_lit_client(self.config.clone()).await?;
498 refreshed
499 .execute_js_with_options_inner(code, ipfs_id, js_params, auth_context, options)
500 .await
501 }
502 Err(e) => Err(e),
503 }
504 }
505
506 async fn execute_js_with_options_inner(
507 &self,
508 code: Option<String>,
509 ipfs_id: Option<String>,
510 js_params: Option<serde_json::Value>,
511 auth_context: &AuthContext,
512 options: ExecuteJsOptions,
513 ) -> Result<ExecuteJsResponse, LitSdkError> {
514 let jit = self.create_jit_context()?;
515 let execute_threshold = if options.use_single_node {
516 1
517 } else {
518 self.handshake.threshold.max(1)
519 };
520
521 let mut node_urls = match self.select_priced_nodes(PRODUCT_ID_LIT_ACTION).await {
522 Ok(urls) => urls,
523 Err(_) => self.handshake.connected_nodes.clone(),
524 };
525 node_urls.truncate(execute_threshold);
526 if node_urls.len() < execute_threshold {
527 return Err(LitSdkError::Network(format!(
528 "insufficient node urls for executeJs: got {}, need {}",
529 node_urls.len(),
530 execute_threshold
531 )));
532 }
533
534 let per_node_max_price = options
535 .user_max_price_wei
536 .map(|p| p / ethers::types::U256::from(execute_threshold as u64));
537 let session_sigs = issue_session_sigs_with_max_price(
538 &auth_context.session_key_pair,
539 &auth_context.auth_config,
540 &auth_context.delegation_auth_sig,
541 &node_urls,
542 per_node_max_price,
543 )?;
544
545 let node_set = node_set_from_urls(&node_urls);
546 let request_id = random_hex(16);
547 let epoch = self.handshake.epoch;
548
549 let mut requests = vec![];
550 let http = self.http.clone();
551 let version = self.version.clone();
552 for (url, sig) in &session_sigs {
553 let mut request_data = serde_json::json!({
554 "authSig": sig,
555 "nodeSet": node_set.clone(),
556 });
557 if let Some(code_str) = &code {
558 request_data["code"] =
559 serde_json::json!(base64ct::Base64::encode_string(code_str.as_bytes()));
560 }
561 if let Some(id) = &ipfs_id {
562 request_data["ipfsId"] = serde_json::json!(id);
563 }
564 if let Some(params) = &js_params {
565 request_data["jsParams"] = serde_json::json!({ "jsParams": params.clone() });
566 }
567
568 let encrypted = wallet_encrypt(
569 &jit.key_set[url].secret_key,
570 &jit.key_set[url].public_key,
571 request_data.to_string().as_bytes(),
572 )?;
573
574 let full_url = compose_lit_url(url, &self.config.endpoints.execute_js);
575 let body = serde_json::to_value(&encrypted).unwrap();
576 let secret_key = jit.key_set[url].secret_key;
577 let http_cl = http.clone();
578 let version_cl = version.clone();
579 let request_id_cl = request_id.clone();
580 requests.push(async move {
581 send_encrypted_node_request(
582 &http_cl,
583 &full_url,
584 body,
585 &secret_key,
586 &request_id_cl,
587 &version_cl,
588 epoch,
589 )
590 .await
591 });
592 }
593
594 let results = futures::future::join_all(requests).await;
595 let batch = merge_encrypted_batch(results, execute_threshold)?;
596
597 let node_values: Vec<ExecuteJsNodeValue> = decrypt_batch_response(&batch, &jit, |v| {
598 let data = v
599 .get("data")
600 .cloned()
601 .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
602 serde_json::from_value::<ExecuteJsNodeValue>(data)
603 .map_err(|e| LitSdkError::Network(e.to_string()))
604 })?;
605
606 let successful: Vec<&ExecuteJsNodeValue> =
607 node_values.iter().filter(|v| v.success).collect();
608 if successful.len() < execute_threshold {
609 return Err(LitSdkError::Network(format!(
610 "insufficient successful executeJs responses: got {}, need {}",
611 successful.len(),
612 execute_threshold
613 )));
614 }
615
616 let mut freq: Vec<(&str, usize)> = vec![];
617 for v in &successful {
618 let resp = v.response.as_str();
619 if let Some((_, count)) = freq.iter_mut().find(|(r, _)| *r == resp) {
620 *count += 1;
621 } else {
622 freq.push((resp, 1));
623 }
624 }
625
626 let mut least_common = freq[0].0;
627 let mut least_common_count = freq[0].1;
628 let mut most_common = freq[0].0;
629 let mut most_common_count = freq[0].1;
630 for (resp, count) in &freq {
631 if *count < least_common_count {
632 least_common = *resp;
633 least_common_count = *count;
634 }
635 if *count > most_common_count {
636 most_common = *resp;
637 most_common_count = *count;
638 }
639 }
640
641 let selected_response = match options.response_strategy {
642 ExecuteJsResponseStrategy::LeastCommon => least_common,
643 ExecuteJsResponseStrategy::MostCommon => most_common,
644 };
645
646 let logs = successful
647 .iter()
648 .find(|v| v.response == selected_response)
649 .map(|v| v.logs.clone())
650 .unwrap_or_default();
651
652 let mut signatures: HashMap<String, serde_json::Value> = HashMap::new();
653 let mut signature_keys: HashSet<String> = HashSet::new();
654 for v in &successful {
655 for k in v.signed_data.keys() {
656 signature_keys.insert(k.clone());
657 }
658 }
659
660 for sig_name in signature_keys {
661 let signature_threshold = self.handshake.threshold.max(1);
662 let mut shares: Vec<String> = vec![];
663 for v in &successful {
664 let Some(entry) = v.signed_data.get(&sig_name) else {
665 continue;
666 };
667 let Some(share_val) = entry.get("signatureShare") else {
668 continue;
669 };
670 if let Some(s) = share_val.as_str() {
671 shares.push(s.to_string());
672 } else {
673 shares.push(
674 serde_json::to_string(share_val)
675 .map_err(|e| LitSdkError::Network(e.to_string()))?,
676 );
677 }
678 }
679
680 if shares.is_empty() {
681 continue;
682 }
683 if shares.len() < signature_threshold {
684 return Err(LitSdkError::Network(format!(
685 "insufficient signature shares for {sig_name}: got {}, need {}",
686 shares.len(),
687 signature_threshold
688 )));
689 }
690
691 let combined = crate::crypto::combine_and_verify(shares)?;
692 let combined_value: serde_json::Value =
693 serde_json::from_str(&combined).map_err(|e| LitSdkError::Crypto(e.to_string()))?;
694 signatures.insert(sig_name, combined_value);
695 }
696
697 let response_val: serde_json::Value = serde_json::from_str(selected_response)
698 .unwrap_or_else(|_| serde_json::Value::String(selected_response.to_string()));
699
700 Ok(ExecuteJsResponse {
701 success: true,
702 signatures,
703 response: response_val,
704 logs,
705 })
706 }
707
708 pub async fn create_pkp_auth_context(
709 &self,
710 pkp_public_key: &str,
711 auth_data: AuthData,
712 auth_config: AuthConfig,
713 session_key_pair: Option<SessionKeyPair>,
714 delegation_auth_sig: Option<AuthSig>,
715 user_max_price_wei: Option<U256>,
716 ) -> Result<AuthContext, LitSdkError> {
717 let has_session_key_pair = session_key_pair.is_some();
718 let has_delegation_auth_sig = delegation_auth_sig.is_some();
719 if has_session_key_pair != has_delegation_auth_sig {
720 return Err(LitSdkError::Config(
721 "Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided".into(),
722 ));
723 }
724
725 let session_key_pair = session_key_pair.unwrap_or_else(generate_session_key_pair);
726 let delegation_auth_sig = match delegation_auth_sig {
727 Some(sig) => sig,
728 None => {
729 self.sign_session_key_for_pkp(
730 pkp_public_key,
731 &auth_data,
732 &auth_config,
733 &session_key_pair,
734 user_max_price_wei,
735 )
736 .await?
737 }
738 };
739
740 validate_delegation_auth_sig(&delegation_auth_sig, &session_key_pair.public_key)?;
741
742 Ok(AuthContext {
743 session_key_pair,
744 auth_config,
745 delegation_auth_sig,
746 })
747 }
748
749 pub async fn create_custom_auth_context(
750 &self,
751 pkp_public_key: &str,
752 auth_config: AuthConfig,
753 custom_auth_params: CustomAuthParams,
754 session_key_pair: Option<SessionKeyPair>,
755 delegation_auth_sig: Option<AuthSig>,
756 user_max_price_wei: Option<U256>,
757 ) -> Result<AuthContext, LitSdkError> {
758 let has_session_key_pair = session_key_pair.is_some();
759 let has_delegation_auth_sig = delegation_auth_sig.is_some();
760 if has_session_key_pair != has_delegation_auth_sig {
761 return Err(LitSdkError::Config(
762 "Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided".into(),
763 ));
764 }
765
766 let session_key_pair = session_key_pair.unwrap_or_else(generate_session_key_pair);
767 let delegation_auth_sig = match delegation_auth_sig {
768 Some(sig) => sig,
769 None => {
770 self.sign_custom_session_key_for_pkp(
771 pkp_public_key,
772 &auth_config,
773 &custom_auth_params,
774 &session_key_pair,
775 user_max_price_wei,
776 )
777 .await?
778 }
779 };
780
781 validate_delegation_auth_sig(&delegation_auth_sig, &session_key_pair.public_key)?;
782
783 Ok(AuthContext {
784 session_key_pair,
785 auth_config,
786 delegation_auth_sig,
787 })
788 }
789
790 pub fn create_pkp_auth_context_from_pre_generated(
791 &self,
792 session_key_pair: SessionKeyPair,
793 delegation_auth_sig: AuthSig,
794 ) -> Result<AuthContext, LitSdkError> {
795 validate_delegation_auth_sig(&delegation_auth_sig, &session_key_pair.public_key)?;
796 let auth_config = auth_config_from_delegation_auth_sig(&delegation_auth_sig)?;
797
798 Ok(AuthContext {
799 session_key_pair,
800 auth_config,
801 delegation_auth_sig,
802 })
803 }
804
805 async fn sign_session_key_for_pkp(
806 &self,
807 pkp_public_key: &str,
808 auth_data: &AuthData,
809 auth_config: &AuthConfig,
810 session_key_pair: &SessionKeyPair,
811 user_max_price_wei: Option<U256>,
812 ) -> Result<AuthSig, LitSdkError> {
813 let res = self
814 .sign_session_key_for_pkp_inner(
815 pkp_public_key,
816 auth_data,
817 auth_config,
818 session_key_pair,
819 user_max_price_wei,
820 )
821 .await;
822 match res {
823 Ok(v) => Ok(v),
824 Err(e) if Self::is_node_payload_decryption_error(&e) => {
825 let refreshed = create_lit_client(self.config.clone()).await?;
826 refreshed
827 .sign_session_key_for_pkp_inner(
828 pkp_public_key,
829 auth_data,
830 auth_config,
831 session_key_pair,
832 user_max_price_wei,
833 )
834 .await
835 }
836 Err(e) => Err(e),
837 }
838 }
839
840 async fn sign_session_key_for_pkp_inner(
841 &self,
842 pkp_public_key: &str,
843 auth_data: &AuthData,
844 auth_config: &AuthConfig,
845 session_key_pair: &SessionKeyPair,
846 user_max_price_wei: Option<U256>,
847 ) -> Result<AuthSig, LitSdkError> {
848 let jit = self.create_jit_context()?;
849 let threshold = self.handshake.threshold.max(1);
850
851 let selected_urls = match self.select_priced_nodes(PRODUCT_ID_SIGN_SESSION_KEY).await {
852 Ok(urls) => urls,
853 Err(_) => self
854 .handshake
855 .connected_nodes
856 .iter()
857 .take(threshold)
858 .cloned()
859 .collect(),
860 };
861 if selected_urls.len() < threshold {
862 return Err(LitSdkError::Network(format!(
863 "insufficient node urls for signSessionKey: got {}, need {}",
864 selected_urls.len(),
865 threshold
866 )));
867 }
868
869 let node_set = node_set_from_urls(&selected_urls);
870 let request_id = random_hex(16);
871 let epoch = self.handshake.epoch;
872
873 let pkp_eth_address = pkp_eth_address_from_pubkey(pkp_public_key)?;
874
875 let mut pkp_auth_config = auth_config.clone();
876 let base_stmt = "Lit Protocol PKP session signature";
877 let extra = pkp_auth_config.statement.trim();
878 pkp_auth_config.statement = if extra.is_empty() {
879 base_stmt.to_string()
880 } else {
881 format!("{base_stmt} {extra}")
882 };
883 if pkp_auth_config.domain.trim().is_empty() {
884 pkp_auth_config.domain = "localhost".into();
885 }
886
887 let nonce = self.handshake.core_node_config.latest_blockhash.clone();
888 let session_public_key_hex = session_key_pair
889 .public_key
890 .strip_prefix("lit:session:")
891 .unwrap_or(&session_key_pair.public_key);
892 let session_key_uri = format!("lit:session:{session_public_key_hex}");
893
894 let siwe_message = create_siwe_message_with_resources(
895 &pkp_eth_address,
896 session_public_key_hex,
897 &pkp_auth_config,
898 &nonce,
899 )?;
900
901 #[derive(Debug, Clone, Serialize)]
902 #[serde(rename_all = "camelCase")]
903 struct AuthMethodForRequest {
904 auth_method_type: u32,
905 access_token: String,
906 }
907
908 let auth_method = AuthMethodForRequest {
909 auth_method_type: auth_data.auth_method_type,
910 access_token: auth_data.access_token.clone(),
911 };
912
913 let max_price = user_max_price_wei.unwrap_or_else(|| U256::from(u128::MAX));
914
915 let mut requests = vec![];
916 let http = self.http.clone();
917 let version = self.version.clone();
918 for url in &selected_urls {
919 let request_data = serde_json::json!({
920 "sessionKey": session_key_uri.clone(),
921 "authMethods": [auth_method.clone()],
922 "pkpPublicKey": pkp_public_key,
923 "siweMessage": siwe_message.clone(),
924 "curveType": "BLS",
925 "epoch": epoch,
926 "nodeSet": node_set.clone(),
927 "maxPrice": max_price.to_string(),
928 });
929
930 let encrypted = wallet_encrypt(
931 &jit.key_set[url].secret_key,
932 &jit.key_set[url].public_key,
933 request_data.to_string().as_bytes(),
934 )?;
935
936 let full_url = compose_lit_url(url, &self.config.endpoints.sign_session_key);
937 let body = serde_json::to_value(&encrypted).unwrap();
938 let secret_key = jit.key_set[url].secret_key;
939 let http_cl = http.clone();
940 let version_cl = version.clone();
941 let request_id_cl = request_id.clone();
942 requests.push(async move {
943 send_encrypted_node_request(
944 &http_cl,
945 &full_url,
946 body,
947 &secret_key,
948 &request_id_cl,
949 &version_cl,
950 epoch,
951 )
952 .await
953 });
954 }
955
956 let results = futures::future::join_all(requests).await;
957 let batch = merge_encrypted_batch(results, threshold)?;
958
959 #[derive(Debug, Deserialize)]
960 #[serde(rename_all = "camelCase")]
961 struct SignSessionKeyNodeData {
962 #[serde(rename = "signatureShare")]
963 signature_share: SignatureShareWrapper,
964 #[serde(rename = "siweMessage")]
965 siwe_message: String,
966 }
967
968 let node_values: Vec<SignSessionKeyNodeData> = decrypt_batch_response(&batch, &jit, |v| {
969 let data = v
970 .get("data")
971 .cloned()
972 .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
973 serde_json::from_value::<SignSessionKeyNodeData>(data)
974 .map_err(|e| LitSdkError::Network(e.to_string()))
975 })?;
976
977 let shares_json: Vec<String> = node_values
978 .iter()
979 .map(|n| {
980 serde_json::to_string(&serde_json::json!({
981 "ProofOfPossession": {
982 "identifier": n.signature_share.proof_of_possession.identifier,
983 "value": n.signature_share.proof_of_possession.value,
984 }
985 }))
986 .unwrap()
987 })
988 .collect();
989 if shares_json.len() < threshold {
990 return Err(LitSdkError::Network(format!(
991 "insufficient signature shares for signSessionKey: got {}, need {}",
992 shares_json.len(),
993 threshold
994 )));
995 }
996
997 let combined = combine_bls_signature_shares(&shares_json)?;
998 if combined.len() != 192 {
999 return Err(LitSdkError::Crypto(format!(
1000 "combined BLS signature must be 192 hex chars; got {}",
1001 combined.len()
1002 )));
1003 }
1004
1005 let most_common_siwe =
1006 most_common_value(node_values.iter().map(|v| v.siwe_message.clone()).collect())
1007 .unwrap_or_else(|| siwe_message.clone());
1008
1009 let sig_json = serde_json::to_string(&serde_json::json!({
1010 "ProofOfPossession": combined,
1011 }))
1012 .map_err(|e| LitSdkError::Crypto(e.to_string()))?;
1013
1014 Ok(AuthSig {
1015 sig: sig_json,
1016 derived_via: "lit.bls".into(),
1017 signed_message: most_common_siwe,
1018 address: pkp_eth_address,
1019 algo: Some("LIT_BLS".into()),
1020 })
1021 }
1022
1023 async fn sign_custom_session_key_for_pkp(
1024 &self,
1025 pkp_public_key: &str,
1026 auth_config: &AuthConfig,
1027 custom_auth_params: &CustomAuthParams,
1028 session_key_pair: &SessionKeyPair,
1029 user_max_price_wei: Option<U256>,
1030 ) -> Result<AuthSig, LitSdkError> {
1031 if custom_auth_params.lit_action_ipfs_id.is_none()
1032 && custom_auth_params.lit_action_code.is_none()
1033 {
1034 return Err(LitSdkError::Config(
1035 "custom auth requires lit_action_ipfs_id or lit_action_code".into(),
1036 ));
1037 }
1038
1039 let res = self
1040 .sign_custom_session_key_for_pkp_inner(
1041 pkp_public_key,
1042 auth_config,
1043 custom_auth_params,
1044 session_key_pair,
1045 user_max_price_wei,
1046 )
1047 .await;
1048 match res {
1049 Ok(v) => Ok(v),
1050 Err(e) if Self::is_node_payload_decryption_error(&e) => {
1051 let refreshed = create_lit_client(self.config.clone()).await?;
1052 refreshed
1053 .sign_custom_session_key_for_pkp_inner(
1054 pkp_public_key,
1055 auth_config,
1056 custom_auth_params,
1057 session_key_pair,
1058 user_max_price_wei,
1059 )
1060 .await
1061 }
1062 Err(e) => Err(e),
1063 }
1064 }
1065
1066 async fn sign_custom_session_key_for_pkp_inner(
1067 &self,
1068 pkp_public_key: &str,
1069 auth_config: &AuthConfig,
1070 custom_auth_params: &CustomAuthParams,
1071 session_key_pair: &SessionKeyPair,
1072 user_max_price_wei: Option<U256>,
1073 ) -> Result<AuthSig, LitSdkError> {
1074 let jit = self.create_jit_context()?;
1075 let threshold = self.handshake.threshold.max(1);
1076
1077 let selected_urls = match self.select_priced_nodes(PRODUCT_ID_LIT_ACTION).await {
1078 Ok(urls) => urls,
1079 Err(_) => self
1080 .handshake
1081 .connected_nodes
1082 .iter()
1083 .take(threshold)
1084 .cloned()
1085 .collect(),
1086 };
1087 if selected_urls.len() < threshold {
1088 return Err(LitSdkError::Network(format!(
1089 "insufficient node urls for signCustomSessionKey: got {}, need {}",
1090 selected_urls.len(),
1091 threshold
1092 )));
1093 }
1094
1095 let node_set = node_set_from_urls(&selected_urls);
1096 let request_id = random_hex(16);
1097 let epoch = self.handshake.epoch;
1098
1099 let pkp_eth_address = pkp_eth_address_from_pubkey(pkp_public_key)?;
1100
1101 let mut pkp_auth_config = auth_config.clone();
1102 let base_stmt = "Lit Protocol PKP session signature";
1103 let extra = pkp_auth_config.statement.trim();
1104 pkp_auth_config.statement = if extra.is_empty() {
1105 base_stmt.to_string()
1106 } else {
1107 format!("{base_stmt} {extra}")
1108 };
1109 if pkp_auth_config.domain.trim().is_empty() {
1110 pkp_auth_config.domain = "localhost".into();
1111 }
1112
1113 let nonce = self.handshake.core_node_config.latest_blockhash.clone();
1114 let session_public_key_hex = session_key_pair
1115 .public_key
1116 .strip_prefix("lit:session:")
1117 .unwrap_or(&session_key_pair.public_key);
1118 let session_key_uri = format!("lit:session:{session_public_key_hex}");
1119
1120 let siwe_message = create_siwe_message_with_resources(
1121 &pkp_eth_address,
1122 session_public_key_hex,
1123 &pkp_auth_config,
1124 &nonce,
1125 )?;
1126
1127 let max_price = user_max_price_wei.unwrap_or_else(|| U256::from(u128::MAX));
1128
1129 let mut requests = vec![];
1130 let http = self.http.clone();
1131 let version = self.version.clone();
1132 for url in &selected_urls {
1133 let mut request_data = serde_json::json!({
1134 "sessionKey": session_key_uri.clone(),
1135 "authMethods": [],
1136 "pkpPublicKey": pkp_public_key,
1137 "siweMessage": siwe_message.clone(),
1138 "curveType": "BLS",
1139 "epoch": epoch,
1140 "nodeSet": node_set.clone(),
1141 "maxPrice": max_price.to_string(),
1142 });
1143
1144 if let Some(ipfs_id) = &custom_auth_params.lit_action_ipfs_id {
1145 if let Some(obj) = request_data.as_object_mut() {
1146 obj.insert(
1147 "litActionIpfsId".into(),
1148 serde_json::Value::String(ipfs_id.clone()),
1149 );
1150 }
1151 }
1152 if let Some(code) = &custom_auth_params.lit_action_code {
1153 if let Some(obj) = request_data.as_object_mut() {
1154 obj.insert(
1155 "litActionCode".into(),
1156 serde_json::Value::String(code.clone()),
1157 );
1158 }
1159 }
1160 if let Some(js_params) = &custom_auth_params.js_params {
1161 if let Some(obj) = request_data.as_object_mut() {
1162 obj.insert(
1163 "jsParams".into(),
1164 serde_json::json!({ "jsParams": js_params.clone() }),
1165 );
1166 }
1167 }
1168
1169 let encrypted = wallet_encrypt(
1170 &jit.key_set[url].secret_key,
1171 &jit.key_set[url].public_key,
1172 request_data.to_string().as_bytes(),
1173 )?;
1174
1175 let full_url = compose_lit_url(url, &self.config.endpoints.sign_session_key);
1176 let body = serde_json::to_value(&encrypted).unwrap();
1177 let secret_key = jit.key_set[url].secret_key;
1178 let http_cl = http.clone();
1179 let version_cl = version.clone();
1180 let request_id_cl = request_id.clone();
1181 requests.push(async move {
1182 send_encrypted_node_request(
1183 &http_cl,
1184 &full_url,
1185 body,
1186 &secret_key,
1187 &request_id_cl,
1188 &version_cl,
1189 epoch,
1190 )
1191 .await
1192 });
1193 }
1194
1195 let results = futures::future::join_all(requests).await;
1196 let batch = merge_encrypted_batch(results, threshold)?;
1197
1198 #[derive(Debug, Deserialize)]
1199 #[serde(rename_all = "camelCase")]
1200 struct SignSessionKeyNodeData {
1201 #[serde(rename = "signatureShare")]
1202 signature_share: SignatureShareWrapper,
1203 #[serde(rename = "siweMessage")]
1204 siwe_message: String,
1205 }
1206
1207 let node_values: Vec<SignSessionKeyNodeData> = decrypt_batch_response(&batch, &jit, |v| {
1208 let data = v
1209 .get("data")
1210 .cloned()
1211 .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
1212 serde_json::from_value::<SignSessionKeyNodeData>(data)
1213 .map_err(|e| LitSdkError::Network(e.to_string()))
1214 })?;
1215
1216 let shares_json: Vec<String> = node_values
1217 .iter()
1218 .map(|n| {
1219 serde_json::to_string(&serde_json::json!({
1220 "ProofOfPossession": {
1221 "identifier": n.signature_share.proof_of_possession.identifier,
1222 "value": n.signature_share.proof_of_possession.value,
1223 }
1224 }))
1225 .unwrap()
1226 })
1227 .collect();
1228 if shares_json.len() < threshold {
1229 return Err(LitSdkError::Network(format!(
1230 "insufficient signature shares for signCustomSessionKey: got {}, need {}",
1231 shares_json.len(),
1232 threshold
1233 )));
1234 }
1235
1236 let combined = combine_bls_signature_shares(&shares_json)?;
1237 if combined.len() != 192 {
1238 return Err(LitSdkError::Crypto(format!(
1239 "combined BLS signature must be 192 hex chars; got {}",
1240 combined.len()
1241 )));
1242 }
1243
1244 let most_common_siwe =
1245 most_common_value(node_values.iter().map(|v| v.siwe_message.clone()).collect())
1246 .unwrap_or_else(|| siwe_message.clone());
1247
1248 let sig_json = serde_json::to_string(&serde_json::json!({
1249 "ProofOfPossession": combined,
1250 }))
1251 .map_err(|e| LitSdkError::Crypto(e.to_string()))?;
1252
1253 Ok(AuthSig {
1254 sig: sig_json,
1255 derived_via: "lit.bls".into(),
1256 signed_message: most_common_siwe,
1257 address: pkp_eth_address,
1258 algo: Some("LIT_BLS".into()),
1259 })
1260 }
1261
1262 pub async fn pkp_sign_ethereum(
1263 &self,
1264 pkp_pubkey: &str,
1265 to_sign: &[u8],
1266 auth_context: &AuthContext,
1267 user_max_price_wei: Option<ethers::types::U256>,
1268 ) -> Result<serde_json::Value, LitSdkError> {
1269 self.pkp_sign_ethereum_with_options(
1270 pkp_pubkey,
1271 to_sign,
1272 auth_context,
1273 user_max_price_wei,
1274 false,
1275 )
1276 .await
1277 }
1278
1279 pub async fn pkp_sign_ethereum_with_options(
1280 &self,
1281 pkp_pubkey: &str,
1282 to_sign: &[u8],
1283 auth_context: &AuthContext,
1284 user_max_price_wei: Option<ethers::types::U256>,
1285 bypass_auto_hashing: bool,
1286 ) -> Result<serde_json::Value, LitSdkError> {
1287 self.pkp_sign_raw_with_options(
1288 "ethereum",
1289 "EcdsaK256Sha256",
1290 pkp_pubkey,
1291 to_sign,
1292 auth_context,
1293 user_max_price_wei,
1294 bypass_auto_hashing,
1295 )
1296 .await
1297 }
1298
1299 pub async fn pkp_sign_raw(
1300 &self,
1301 chain: &str,
1302 signing_scheme: &str,
1303 pkp_pubkey: &str,
1304 to_sign: &[u8],
1305 auth_context: &AuthContext,
1306 user_max_price_wei: Option<ethers::types::U256>,
1307 ) -> Result<serde_json::Value, LitSdkError> {
1308 self.pkp_sign_raw_with_options(
1309 chain,
1310 signing_scheme,
1311 pkp_pubkey,
1312 to_sign,
1313 auth_context,
1314 user_max_price_wei,
1315 false,
1316 )
1317 .await
1318 }
1319
1320 #[allow(clippy::too_many_arguments)]
1321 pub async fn pkp_sign_raw_with_options(
1322 &self,
1323 chain: &str,
1324 signing_scheme: &str,
1325 pkp_pubkey: &str,
1326 to_sign: &[u8],
1327 auth_context: &AuthContext,
1328 user_max_price_wei: Option<ethers::types::U256>,
1329 bypass_auto_hashing: bool,
1330 ) -> Result<serde_json::Value, LitSdkError> {
1331 let res = self
1332 .pkp_sign_raw_with_options_inner(
1333 chain,
1334 signing_scheme,
1335 pkp_pubkey,
1336 to_sign,
1337 auth_context,
1338 user_max_price_wei,
1339 bypass_auto_hashing,
1340 )
1341 .await;
1342 match res {
1343 Ok(v) => Ok(v),
1344 Err(e) if Self::is_node_payload_decryption_error(&e) => {
1345 let refreshed = create_lit_client(self.config.clone()).await?;
1346 refreshed
1347 .pkp_sign_raw_with_options_inner(
1348 chain,
1349 signing_scheme,
1350 pkp_pubkey,
1351 to_sign,
1352 auth_context,
1353 user_max_price_wei,
1354 bypass_auto_hashing,
1355 )
1356 .await
1357 }
1358 Err(e) => Err(e),
1359 }
1360 }
1361
1362 #[allow(clippy::too_many_arguments)]
1363 async fn pkp_sign_raw_with_options_inner(
1364 &self,
1365 chain: &str,
1366 signing_scheme: &str,
1367 pkp_pubkey: &str,
1368 to_sign: &[u8],
1369 auth_context: &AuthContext,
1370 user_max_price_wei: Option<ethers::types::U256>,
1371 bypass_auto_hashing: bool,
1372 ) -> Result<serde_json::Value, LitSdkError> {
1373 let to_sign_data =
1374 pkp_sign_message_bytes(chain, signing_scheme, to_sign, bypass_auto_hashing)?;
1375
1376 let jit = self.create_jit_context()?;
1377
1378 let threshold = self.handshake.threshold.max(1);
1379 let selected_urls = match self.select_priced_nodes(PRODUCT_ID_SIGN).await {
1380 Ok(urls) => urls,
1381 Err(_) => self
1382 .handshake
1383 .connected_nodes
1384 .iter()
1385 .take(threshold)
1386 .cloned()
1387 .collect(),
1388 };
1389 if selected_urls.len() < threshold {
1390 return Err(LitSdkError::Network(format!(
1391 "insufficient node urls for pkpSign: got {}, need {}",
1392 selected_urls.len(),
1393 threshold
1394 )));
1395 }
1396
1397 let per_node_max_price =
1398 user_max_price_wei.map(|p| p / ethers::types::U256::from(threshold as u64));
1399
1400 let session_sigs = issue_session_sigs_with_max_price(
1401 &auth_context.session_key_pair,
1402 &auth_context.auth_config,
1403 &auth_context.delegation_auth_sig,
1404 &selected_urls,
1405 per_node_max_price,
1406 )?;
1407
1408 let node_set = node_set_from_urls(&selected_urls);
1409 let request_id = random_hex(16);
1410 let epoch = self.handshake.epoch;
1411
1412 let mut requests = vec![];
1413 let http = self.http.clone();
1414 let version = self.version.clone();
1415 for (url, sig) in &session_sigs {
1416 let to_sign_vec = to_sign_data.clone();
1417 let request_data = serde_json::json!({
1418 "toSign": to_sign_vec,
1419 "signingScheme": signing_scheme,
1420 "pubkey": pkp_pubkey,
1421 "authSig": sig,
1422 "nodeSet": node_set.clone(),
1423 "epoch": epoch,
1424 "authMethods": [],
1425 });
1426
1427 let encrypted = wallet_encrypt(
1428 &jit.key_set[url].secret_key,
1429 &jit.key_set[url].public_key,
1430 request_data.to_string().as_bytes(),
1431 )?;
1432
1433 let full_url = compose_lit_url(url, &self.config.endpoints.pkp_sign);
1434 let body = serde_json::to_value(&encrypted).unwrap();
1435 let secret_key = jit.key_set[url].secret_key;
1436 let http_cl = http.clone();
1437 let version_cl = version.clone();
1438 let request_id_cl = request_id.clone();
1439 requests.push(async move {
1440 send_encrypted_node_request(
1441 &http_cl,
1442 &full_url,
1443 body,
1444 &secret_key,
1445 &request_id_cl,
1446 &version_cl,
1447 epoch,
1448 )
1449 .await
1450 });
1451 }
1452
1453 let results = futures::future::join_all(requests).await;
1454 let batch = merge_encrypted_batch(results, threshold)?;
1455
1456 let node_values: Vec<serde_json::Value> = decrypt_batch_response(&batch, &jit, |v| {
1457 v.get("data")
1458 .cloned()
1459 .ok_or_else(|| LitSdkError::Network("missing data field".into()))
1460 })?;
1461
1462 let combiner_shares: Vec<String> = node_values
1463 .iter()
1464 .filter(|v| v.get("success").and_then(|s| s.as_bool()) == Some(true))
1465 .filter_map(|v| v.get("signatureShare"))
1466 .filter_map(|s| serde_json::to_string(s).ok())
1467 .collect();
1468 if combiner_shares.len() < threshold {
1469 return Err(LitSdkError::Network(format!(
1470 "insufficient signature shares: got {}, need {}",
1471 combiner_shares.len(),
1472 threshold
1473 )));
1474 }
1475
1476 let combined = crate::crypto::combine_and_verify(combiner_shares)?;
1477
1478 let combined_value: serde_json::Value =
1479 serde_json::from_str(&combined).map_err(|e| LitSdkError::Crypto(e.to_string()))?;
1480
1481 Ok(combined_value)
1482 }
1483
1484 async fn select_priced_nodes(&self, product_id: usize) -> Result<Vec<String>, LitSdkError> {
1485 let rpc_url =
1486 self.config.rpc_url.as_deref().ok_or_else(|| {
1487 LitSdkError::Config("rpc_url is required for priced requests".into())
1488 })?;
1489 let price_feed_addr = price_feed_address_for(self.config.network).ok_or_else(|| {
1490 LitSdkError::Config(format!(
1491 "unknown PriceFeed contract address for network {}",
1492 self.config.network
1493 ))
1494 })?;
1495
1496 let provider = Arc::new(
1497 Provider::<Http>::try_from(rpc_url).map_err(|e| LitSdkError::Config(e.to_string()))?,
1498 );
1499 let contract = PriceFeedContract::new(price_feed_addr, provider);
1500
1501 let realm_id: U256 = 1u64.into();
1502 let product_ids: Vec<U256> = vec![0u64.into(), 1u64.into(), 2u64.into(), 3u64.into()];
1503 let (_epoch_id, min_node_count, nodes_and_prices) = contract
1504 .get_nodes_for_request(realm_id, product_ids)
1505 .call()
1506 .await
1507 .map_err(|e| LitSdkError::Network(e.to_string()))?;
1508
1509 let required = self.handshake.threshold.max(1);
1510 let contract_min = (min_node_count.as_u64() as usize).max(1);
1511 if required < contract_min {
1512 return Err(LitSdkError::Config(format!(
1513 "minimum_threshold ({required}) is below chain minNodeCount ({contract_min})"
1514 )));
1515 }
1516 let connected: HashSet<&str> = self
1517 .handshake
1518 .connected_nodes
1519 .iter()
1520 .map(|s| s.as_str())
1521 .collect();
1522
1523 let mut candidates: Vec<(String, U256)> = nodes_and_prices
1524 .into_iter()
1525 .filter_map(|node| {
1526 let price = *node.prices.get(product_id)?;
1527 let url = format!(
1528 "{}{}:{}",
1529 self.config.http_protocol,
1530 int_to_ip(node.validator.ip),
1531 node.validator.port
1532 );
1533 connected.contains(url.as_str()).then_some((url, price))
1534 })
1535 .collect();
1536
1537 candidates.sort_by(|a, b| a.1.cmp(&b.1));
1538 let selected: Vec<String> = candidates
1539 .into_iter()
1540 .take(required)
1541 .map(|(url, _)| url)
1542 .collect();
1543
1544 if selected.len() < required {
1545 return Err(LitSdkError::Network(format!(
1546 "price feed returned only {} usable nodes, need {}",
1547 selected.len(),
1548 required
1549 )));
1550 }
1551
1552 Ok(selected)
1553 }
1554
1555 fn create_jit_context(&self) -> Result<NagaJitContext, LitSdkError> {
1556 let mut key_set = HashMap::new();
1557 for url in self.handshake.server_keys.keys() {
1558 let server_key = &self.handshake.server_keys[url];
1559 let pk_hex = server_key.node_identity_key.trim_start_matches("0x");
1560 let pk_bytes = hex::decode(pk_hex).map_err(|e| {
1561 LitSdkError::Config(format!("invalid node identity key for {url}: {e}"))
1562 })?;
1563 let mut pk32 = [0u8; 32];
1564 if pk_bytes.len() != 32 {
1565 return Err(LitSdkError::Config(
1566 "node identity key must be 32 bytes".into(),
1567 ));
1568 }
1569 pk32.copy_from_slice(&pk_bytes);
1570
1571 let mut sk32 = [0u8; 32];
1572 rand::thread_rng().fill_bytes(&mut sk32);
1573
1574 key_set.insert(
1575 url.clone(),
1576 JitKeyPair {
1577 public_key: pk32,
1578 secret_key: sk32,
1579 },
1580 );
1581 }
1582
1583 Ok(NagaJitContext { key_set })
1584 }
1585}
1586
1587fn compose_lit_url(base: &str, endpoint: &Endpoint) -> String {
1588 format!("{}{}{}", base, endpoint.path, endpoint.version)
1589}
1590
1591async fn send_node_request<T: serde::de::DeserializeOwned>(
1592 http: &reqwest::Client,
1593 full_url: &str,
1594 mut body: serde_json::Value,
1595 request_id: &str,
1596 version: &str,
1597 epoch: u64,
1598) -> Result<T, LitSdkError> {
1599 let mut headers = HeaderMap::new();
1600 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
1601 headers.insert("Accept", HeaderValue::from_static("application/json"));
1602 headers.insert("X-Lit-SDK-Version", HeaderValue::from_str(version).unwrap());
1603 headers.insert("X-Lit-SDK-Type", HeaderValue::from_static("Typescript"));
1604 headers.insert("X-Request-Id", HeaderValue::from_str(request_id).unwrap());
1605
1606 if let Some(obj) = body.as_object_mut() {
1607 obj.insert("epoch".into(), serde_json::json!(epoch));
1608 }
1609
1610 let res = http
1611 .post(full_url)
1612 .headers(headers)
1613 .json(&body)
1614 .send()
1615 .await?;
1616 let status = res.status();
1617
1618 if !status.is_success() {
1619 let text = res.text().await.unwrap_or_default();
1620 return Err(LitSdkError::Network(format!(
1621 "node request failed {}: {}",
1622 status, text
1623 )));
1624 }
1625
1626 let value: serde_json::Value = res.json().await?;
1627 match serde_json::from_value::<T>(value.clone()) {
1628 Ok(parsed) => Ok(parsed),
1629 Err(err) => {
1630 if let Some(data) = value.get("data").cloned() {
1631 serde_json::from_value::<T>(data).map_err(|e| LitSdkError::Network(e.to_string()))
1632 } else {
1633 Err(LitSdkError::Network(format!(
1634 "unexpected response shape: {err}; body={value}"
1635 )))
1636 }
1637 }
1638 }
1639}
1640
1641async fn send_encrypted_node_request(
1647 http: &reqwest::Client,
1648 full_url: &str,
1649 mut body: serde_json::Value,
1650 jit_secret_key: &[u8; 32],
1651 request_id: &str,
1652 version: &str,
1653 epoch: u64,
1654) -> Result<GenericEncryptedPayload, LitSdkError> {
1655 let mut headers = HeaderMap::new();
1656 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
1657 headers.insert("Accept", HeaderValue::from_static("application/json"));
1658 headers.insert("X-Lit-SDK-Version", HeaderValue::from_str(version).unwrap());
1659 headers.insert("X-Lit-SDK-Type", HeaderValue::from_static("Typescript"));
1660 headers.insert("X-Request-Id", HeaderValue::from_str(request_id).unwrap());
1661
1662 if let Some(obj) = body.as_object_mut() {
1663 obj.insert("epoch".into(), serde_json::json!(epoch));
1664 }
1665
1666 let res = http
1667 .post(full_url)
1668 .headers(headers)
1669 .json(&body)
1670 .send()
1671 .await?;
1672 let status = res.status();
1673 let text = res.text().await.unwrap_or_default();
1674
1675 let value: serde_json::Value =
1676 serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!({ "raw": text }));
1677
1678 if status.is_success() {
1679 if let Ok(parsed) = serde_json::from_value::<GenericEncryptedPayload>(value.clone()) {
1680 return Ok(parsed);
1681 }
1682 if let Some(data) = value.get("data").cloned() {
1683 if let Ok(parsed) = serde_json::from_value::<GenericEncryptedPayload>(data.clone()) {
1684 return Ok(parsed);
1685 }
1686 if let Ok(enc) = serde_json::from_value::<EncryptedPayload>(data) {
1688 return Ok(GenericEncryptedPayload {
1689 success: true,
1690 values: vec![enc],
1691 error: None,
1692 });
1693 }
1694 }
1695 if let Ok(enc) = serde_json::from_value::<EncryptedPayload>(value.clone()) {
1697 return Ok(GenericEncryptedPayload {
1698 success: true,
1699 values: vec![enc],
1700 error: None,
1701 });
1702 }
1703 return Err(LitSdkError::Network(format!(
1704 "unexpected response shape; body={value}"
1705 )));
1706 }
1707
1708 if let Ok(encrypted_err) = serde_json::from_value::<EncryptedPayload>(value.clone()) {
1710 if let Ok(bytes) = wallet_decrypt(jit_secret_key, &encrypted_err) {
1711 if let Ok(decrypted_text) = String::from_utf8(bytes) {
1712 return Err(LitSdkError::Network(format!(
1713 "node returned encrypted error: {decrypted_text}"
1714 )));
1715 }
1716 }
1717 }
1718
1719 Err(LitSdkError::Network(format!(
1720 "node request failed {}: {}",
1721 status, text
1722 )))
1723}
1724
1725fn merge_encrypted_batch(
1726 results: Vec<Result<GenericEncryptedPayload, LitSdkError>>,
1727 minimum_successes: usize,
1728) -> Result<GenericEncryptedPayload, LitSdkError> {
1729 let mut values = vec![];
1730 let mut errors = vec![];
1731 let mut success_count = 0usize;
1732 for r in results {
1733 match r {
1734 Ok(batch) if batch.success => {
1735 success_count += 1;
1736 values.extend(batch.values)
1737 }
1738 Ok(batch) => {
1739 let err = batch
1740 .error
1741 .clone()
1742 .unwrap_or_else(|| serde_json::json!(batch));
1743 errors.push(err);
1744 }
1745 Err(e) => errors.push(serde_json::json!({ "error": e.to_string() })),
1746 }
1747 }
1748
1749 if values.is_empty() || success_count < minimum_successes {
1750 return Err(LitSdkError::Network(format!(
1751 "insufficient successful encrypted responses: got {success_count}, need {minimum_successes}; errors={}",
1752 serde_json::to_string(&errors).unwrap_or_default()
1753 )));
1754 }
1755
1756 Ok(GenericEncryptedPayload {
1757 success: true,
1758 values,
1759 error: None,
1760 })
1761}
1762
1763fn decrypt_batch_response<T, F>(
1764 encrypted_result: &GenericEncryptedPayload,
1765 jit: &NagaJitContext,
1766 extract: F,
1767) -> Result<Vec<T>, LitSdkError>
1768where
1769 F: Fn(serde_json::Value) -> Result<T, LitSdkError>,
1770{
1771 if !encrypted_result.success {
1772 return Err(LitSdkError::Network("batch decrypt failed".into()));
1773 }
1774
1775 let secret_keys: Vec<[u8; 32]> = jit.key_set.values().map(|kp| kp.secret_key).collect();
1777
1778 let mut out = vec![];
1779 for encrypted in &encrypted_result.values {
1780 let decrypted_bytes = wallet_decrypt_with_any_key(&secret_keys, encrypted)?;
1781 let decrypted_text =
1782 String::from_utf8(decrypted_bytes).map_err(|e| LitSdkError::Network(e.to_string()))?;
1783 let json_val: serde_json::Value = serde_json::from_str(&decrypted_text)
1784 .map_err(|e| LitSdkError::Network(e.to_string()))?;
1785
1786 out.push(extract(json_val)?);
1787 }
1788
1789 Ok(out)
1790}
1791
1792#[derive(Debug, Deserialize)]
1793#[serde(rename_all = "camelCase")]
1794struct DecryptNodeResponse {
1795 #[serde(rename = "signatureShare")]
1796 signature_share: SignatureShareWrapper,
1797 #[serde(rename = "shareId")]
1798 #[allow(dead_code)]
1799 share_id: String,
1800}
1801
1802#[derive(Debug, Deserialize)]
1803#[serde(rename_all = "camelCase")]
1804struct SignatureShareWrapper {
1805 #[serde(rename = "ProofOfPossession")]
1806 proof_of_possession: ProofOfPossession,
1807}
1808
1809#[derive(Debug, Deserialize)]
1810#[serde(rename_all = "camelCase")]
1811struct ProofOfPossession {
1812 identifier: String,
1813 value: String,
1814}
1815
1816#[derive(Debug, Deserialize)]
1817#[serde(rename_all = "camelCase")]
1818struct ExecuteJsNodeValue {
1819 success: bool,
1820 #[serde(default)]
1821 #[allow(dead_code)]
1822 claim_data: HashMap<String, serde_json::Value>,
1823 #[serde(default)]
1824 #[allow(dead_code)]
1825 decrypted_data: serde_json::Value,
1826 #[serde(default)]
1827 logs: String,
1828 #[serde(default)]
1829 response: String,
1830 #[serde(default)]
1831 signed_data: HashMap<String, serde_json::Value>,
1832}
1833
1834#[derive(Debug, Clone, Serialize)]
1835#[serde(rename_all = "camelCase")]
1836struct NodeSetEntry {
1837 socket_address: String,
1838 value: u64,
1839}
1840
1841fn pkp_sign_message_bytes(
1842 chain: &str,
1843 signing_scheme: &str,
1844 to_sign: &[u8],
1845 bypass_auto_hashing: bool,
1846) -> Result<Vec<u8>, LitSdkError> {
1847 if bypass_auto_hashing {
1848 return Ok(to_sign.to_vec());
1849 }
1850
1851 match signing_scheme {
1852 "EcdsaK256Sha256" | "EcdsaP256Sha256" => match chain {
1853 "ethereum" => Ok(Keccak256::digest(to_sign).to_vec()),
1854 "bitcoin" | "cosmos" => Ok(Sha256::digest(to_sign).to_vec()),
1855 other => Err(LitSdkError::Config(format!(
1856 "chain \"{other}\" does not support ECDSA signing with Lit yet"
1857 ))),
1858 },
1859 "EcdsaP384Sha384" => match chain {
1860 "ethereum" => Ok(Keccak384::digest(to_sign).to_vec()),
1861 "bitcoin" | "cosmos" => Ok(Sha384::digest(to_sign).to_vec()),
1862 other => Err(LitSdkError::Config(format!(
1863 "chain \"{other}\" does not support ECDSA signing with Lit yet"
1864 ))),
1865 },
1866 _ => Ok(to_sign.to_vec()),
1867 }
1868}
1869
1870fn node_set_from_urls(urls: &[String]) -> Vec<NodeSetEntry> {
1871 urls.iter()
1872 .map(|url| NodeSetEntry {
1873 socket_address: url.replace("http://", "").replace("https://", ""),
1874 value: 1,
1875 })
1876 .collect()
1877}
1878
1879async fn orchestrate_handshake(
1880 http: &reqwest::Client,
1881 config: &NetworkConfig,
1882 version: &str,
1883) -> Result<OrchestrateHandshakeResponse, LitSdkError> {
1884 let request_id = random_hex(16);
1885 let timeout_dur = Duration::from_millis(config.abort_timeout_ms);
1886
1887 let fut = async {
1888 let mut server_keys: HashMap<String, RawHandshakeResponse> = HashMap::new();
1889
1890 let mut tasks = vec![];
1891 for url in &config.bootstrap_urls {
1892 let full_url = compose_lit_url(url, &config.endpoints.handshake);
1893 let challenge = random_hex(32);
1894 let body = serde_json::to_value(HandshakeRequestData {
1895 client_public_key: "test".into(),
1896 challenge: challenge.clone(),
1897 epoch: Some(0),
1898 })
1899 .unwrap();
1900
1901 let url_clone = url.clone();
1902 let request_id_clone = request_id.clone();
1903 let challenge_clone = challenge.clone();
1904 tasks.push(async move {
1905 let res: Result<RawHandshakeResponse, LitSdkError> =
1906 send_node_request(http, &full_url, body, &request_id_clone, version, 0).await;
1907 (url_clone, challenge_clone, res)
1908 });
1909 }
1910
1911 let results = futures::future::join_all(tasks).await;
1912
1913 let mut successful_urls = vec![];
1914 let mut failed_urls = vec![];
1915 let mut errors: Vec<String> = vec![];
1916
1917 for (url, challenge, res) in results {
1918 match res {
1919 Ok(keys) => {
1920 if config.required_attestation {
1921 let Some(attestation) = keys.attestation.as_ref() else {
1922 errors.push(format!("{url}: missing attestation"));
1923 failed_urls.push(url);
1924 continue;
1925 };
1926 if let Err(e) =
1927 verify_sev_snp_attestation(http, attestation, &challenge).await
1928 {
1929 errors.push(format!("{url}: {e}"));
1930 failed_urls.push(url);
1931 continue;
1932 }
1933 }
1934 let pk_hex = keys.node_identity_key.trim_start_matches("0x");
1935 match hex::decode(pk_hex) {
1936 Ok(bytes) if bytes.len() == 32 => {}
1937 _ => {
1938 errors.push(format!("{url}: invalid nodeIdentityKey"));
1939 failed_urls.push(url);
1940 continue;
1941 }
1942 }
1943 successful_urls.push(url.clone());
1944 server_keys.insert(url, keys);
1945 }
1946 Err(e) => {
1947 errors.push(format!("{url}: {e}"));
1948 failed_urls.push(url);
1949 }
1950 }
1951 }
1952
1953 let minimum_required = config.minimum_threshold.max(1);
1954
1955 if successful_urls.len() < minimum_required {
1956 return Err(LitSdkError::Handshake(format!(
1957 "insufficient successful handshakes: got {}, need {}; errors={}",
1958 successful_urls.len(),
1959 minimum_required,
1960 errors.join("; ")
1961 )));
1962 }
1963
1964 let core = resolve_handshake_response(&server_keys, &request_id)?;
1965 let epoch = most_common_u64(server_keys.values().map(|k| k.epoch).collect()).unwrap_or(0);
1966
1967 Ok(OrchestrateHandshakeResponse {
1968 server_keys,
1969 connected_nodes: successful_urls,
1970 core_node_config: core,
1971 threshold: minimum_required,
1972 epoch,
1973 })
1974 };
1975
1976 timeout(timeout_dur, fut).await.map_err(|_| {
1977 LitSdkError::Handshake(format!(
1978 "handshake timed out after {}ms",
1979 config.abort_timeout_ms
1980 ))
1981 })?
1982}
1983
1984async fn verify_sev_snp_attestation(
1985 http: &reqwest::Client,
1986 attestation: &serde_json::Value,
1987 challenge_hex: &str,
1988) -> Result<(), LitSdkError> {
1989 let att_obj = attestation
1990 .as_object()
1991 .ok_or_else(|| LitSdkError::Handshake("invalid attestation: expected object".into()))?;
1992
1993 let typ = att_obj
1994 .get("type")
1995 .and_then(|v| v.as_str())
1996 .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing type".into()))?;
1997
1998 if typ != "AMD_SEV_SNP" {
1999 return Err(LitSdkError::Handshake(format!(
2000 "unsupported attestation type {typ}"
2001 )));
2002 }
2003
2004 let challenge_bytes = hex::decode(challenge_hex)
2005 .map_err(|e| LitSdkError::Handshake(format!("invalid attestation challenge hex: {e}")))?;
2006 if challenge_bytes.len() != 32 {
2007 return Err(LitSdkError::Handshake(format!(
2008 "attestation challenge must be 32 bytes; got {}",
2009 challenge_bytes.len()
2010 )));
2011 }
2012
2013 let noonce_val = att_obj
2014 .get("noonce")
2015 .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing noonce".into()))?;
2016 let noonce = parse_attestation_bytes(noonce_val, "noonce")?;
2017 if noonce != challenge_bytes {
2018 return Err(LitSdkError::Handshake(
2019 "attestation noonce does not match challenge".into(),
2020 ));
2021 }
2022
2023 let report_val = att_obj
2024 .get("report")
2025 .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing report".into()))?;
2026 let report_bytes = parse_attestation_bytes(report_val, "report")?;
2027
2028 let mut data_map: BTreeMap<String, Vec<u8>> = BTreeMap::new();
2029 let data_obj = att_obj
2030 .get("data")
2031 .and_then(|v| v.as_object())
2032 .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing data".into()))?;
2033 for (k, v) in data_obj {
2034 data_map.insert(k.clone(), parse_attestation_bytes(v, &format!("data.{k}"))?);
2035 }
2036
2037 let mut signatures: Vec<Vec<u8>> = vec![];
2038 if let Some(sig_arr) = att_obj.get("signatures").and_then(|v| v.as_array()) {
2039 for (idx, v) in sig_arr.iter().enumerate() {
2040 signatures.push(parse_attestation_bytes(v, &format!("signatures[{idx}]"))?);
2041 }
2042 }
2043
2044 use lit_sdk::sev::certs::snp::Certificate;
2045 use lit_sdk::sev::firmware::guest::AttestationReport;
2046 use lit_sdk::sev::parser::ByteParser;
2047
2048 let report = AttestationReport::from_bytes(&report_bytes).map_err(|e| {
2049 LitSdkError::Handshake(format!("invalid SEV-SNP attestation report bytes: {e}"))
2050 })?;
2051
2052 let vcek_url = SevSnp::vcek_url(&report);
2053 let res = http.get(&vcek_url).send().await?;
2054 let status = res.status();
2055 let cert_bytes = res.bytes().await?;
2056 if !status.is_success() {
2057 return Err(LitSdkError::Handshake(format!(
2058 "failed to fetch VCEK certificate ({status}) from {vcek_url}"
2059 )));
2060 }
2061
2062 let cert = Certificate::from_der(cert_bytes.as_ref())
2063 .or_else(|_| Certificate::from_pem(cert_bytes.as_ref()))
2064 .map_err(|e| LitSdkError::Handshake(format!("invalid VCEK certificate: {e}")))?;
2065
2066 SevSnp::verify(&report, &data_map, &signatures, &challenge_bytes, &cert)?;
2067 Ok(())
2068}
2069
2070fn parse_attestation_bytes(v: &serde_json::Value, field: &str) -> Result<Vec<u8>, LitSdkError> {
2071 match v {
2072 serde_json::Value::String(s) => base64ct::Base64::decode_vec(s)
2073 .map_err(|e| LitSdkError::Handshake(format!("invalid base64 for {field}: {e}"))),
2074 serde_json::Value::Array(arr) => {
2075 let mut out = Vec::with_capacity(arr.len());
2076 for (idx, entry) in arr.iter().enumerate() {
2077 let n = entry.as_u64().ok_or_else(|| {
2078 LitSdkError::Handshake(format!(
2079 "invalid byte value for {field}[{idx}]: expected number"
2080 ))
2081 })?;
2082 if n > 255 {
2083 return Err(LitSdkError::Handshake(format!(
2084 "invalid byte value for {field}[{idx}]: {n}"
2085 )));
2086 }
2087 out.push(n as u8);
2088 }
2089 Ok(out)
2090 }
2091 serde_json::Value::Null => Err(LitSdkError::Handshake(format!("invalid {field}: null"))),
2092 other => Err(LitSdkError::Handshake(format!(
2093 "invalid {field}: expected base64 string or byte array, got {other}"
2094 ))),
2095 }
2096}
2097
2098fn resolve_handshake_response(
2099 server_keys: &HashMap<String, RawHandshakeResponse>,
2100 request_id: &str,
2101) -> Result<ResolvedHandshakeResponse, LitSdkError> {
2102 let latest_blockhash = most_common_value(
2103 server_keys
2104 .values()
2105 .map(|k| k.latest_blockhash.clone())
2106 .collect(),
2107 )
2108 .ok_or_else(|| {
2109 LitSdkError::Handshake(format!(
2110 "latestBlockhash unavailable for request {}",
2111 request_id
2112 ))
2113 })?;
2114
2115 let subnet_pub_key = most_common_value(
2116 server_keys
2117 .values()
2118 .map(|k| k.subnet_public_key.clone())
2119 .collect(),
2120 )
2121 .unwrap_or_default();
2122
2123 let network_pub_key = most_common_value(
2124 server_keys
2125 .values()
2126 .map(|k| k.network_public_key.clone())
2127 .collect(),
2128 )
2129 .unwrap_or_default();
2130
2131 let network_pub_key_set = most_common_value(
2132 server_keys
2133 .values()
2134 .map(|k| k.network_public_key_set.clone())
2135 .collect(),
2136 )
2137 .unwrap_or_default();
2138
2139 let hd_root_pubkeys = most_common_value_vec(
2140 server_keys
2141 .values()
2142 .map(|k| k.hd_root_pubkeys.clone())
2143 .collect(),
2144 )
2145 .unwrap_or_default();
2146
2147 Ok(ResolvedHandshakeResponse {
2148 subnet_pub_key,
2149 network_pub_key,
2150 network_pub_key_set,
2151 hd_root_pubkeys,
2152 latest_blockhash,
2153 })
2154}
2155
2156fn most_common_value(values: Vec<String>) -> Option<String> {
2157 let mut counts: HashMap<String, usize> = HashMap::new();
2158 for v in values {
2159 *counts.entry(v).or_insert(0) += 1;
2160 }
2161 counts.into_iter().max_by_key(|(_, c)| *c).map(|(v, _)| v)
2162}
2163
2164fn most_common_value_vec(values: Vec<Vec<String>>) -> Option<Vec<String>> {
2165 let mut counts: HashMap<String, usize> = HashMap::new();
2166 for v in values {
2167 let key = serde_json::to_string(&v).ok()?;
2168 *counts.entry(key).or_insert(0) += 1;
2169 }
2170 counts
2171 .into_iter()
2172 .max_by_key(|(_, c)| *c)
2173 .and_then(|(k, _)| serde_json::from_str(&k).ok())
2174}
2175
2176fn most_common_u64(values: Vec<u64>) -> Option<u64> {
2177 let mut counts: HashMap<u64, usize> = HashMap::new();
2178 for v in values {
2179 *counts.entry(v).or_insert(0) += 1;
2180 }
2181 counts.into_iter().max_by_key(|(_, c)| *c).map(|(v, _)| v)
2182}
2183
2184fn sha256_hex(bytes: &[u8]) -> String {
2185 let mut hasher = Sha256::new();
2186 hasher.update(bytes);
2187 hex::encode(hasher.finalize())
2188}
2189
2190fn random_hex(num_bytes: usize) -> String {
2191 let mut b = vec![0u8; num_bytes];
2192 rand::thread_rng().fill_bytes(&mut b);
2193 hex::encode(b)
2194}