diff --git a/lit-rust-sdk/Cargo.toml b/lit-rust-sdk/Cargo.toml index dcc70c0..680d84f 100644 --- a/lit-rust-sdk/Cargo.toml +++ b/lit-rust-sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lit-rust-sdk" -version = "0.3.0" +version = "0.4.0" edition = "2021" authors = ["Lit Protocol"] description = "Native Rust SDK for Lit Protocol - distributed key management and programmable signing" @@ -37,6 +37,7 @@ serde_bare = "0.5.0" alloy = { version = "1.0.24", features = ["full"] } eyre = "0.6.12" sha2 = "0.10" +ethabi = "18.0.0" [dev-dependencies] tokio-test = "0.4" diff --git a/lit-rust-sdk/src/client/decrypt.rs b/lit-rust-sdk/src/client/decrypt.rs index 6824b2c..0e7ca07 100644 --- a/lit-rust-sdk/src/client/decrypt.rs +++ b/lit-rust-sdk/src/client/decrypt.rs @@ -2,6 +2,7 @@ use crate::client::LitNodeClient; use crate::types::{ DecryptRequest, DecryptResponse, EncryptionSignRequest, EncryptionSignResponse, }; +use crate::utils; use alloy::providers::Provider as ProviderTrait; use eyre::{eyre, Result}; use sha2::{Digest, Sha256}; @@ -62,8 +63,7 @@ where .ok_or_else(|| eyre!("network_pub_key not set"))?; // Get identity parameter for decryption - let hash_of_conditions = self.get_hashed_access_control_conditions_from_decrypt(¶ms)?; - let hash_of_conditions_str = hex::encode(&hash_of_conditions); + let hash_of_conditions_str = self.hash_access_control_conditions(¶ms)?; let identity_param = self.get_identity_param_for_encryption( &hash_of_conditions_str, ¶ms.data_to_encrypt_hash, @@ -176,33 +176,48 @@ where Ok(decrypted) } - /// Hash the access control conditions from decrypt request - fn get_hashed_access_control_conditions_from_decrypt( - &self, - params: &DecryptRequest, - ) -> Result> { - // Serialize the conditions to JSON exactly like lit-node does - let conditions_json = if let Some(ref conditions) = params.unified_access_control_conditions + pub fn hash_access_control_conditions(&self, req: &DecryptRequest) -> Result { + // hash the access control condition and thing to decrypt + let mut hasher = Sha256::new(); + + // we need to check if we got passed an access control condition or an evm contract condition + if let Some(access_control_conditions) = &req.access_control_conditions { + let stringified_access_control_conditions = + serde_json::to_string(access_control_conditions)?; + debug!( + "stringified_access_control_conditions: {:?}", + stringified_access_control_conditions + ); + hasher.update(stringified_access_control_conditions.as_bytes()); + } else if let Some(evm_contract_conditions) = &req.evm_contract_conditions { + let stringified_access_control_conditions = + serde_json::to_string(evm_contract_conditions)?; + debug!( + "stringified_access_control_conditions: {:?}", + stringified_access_control_conditions + ); + hasher.update(stringified_access_control_conditions.as_bytes()); + } else if req.sol_rpc_conditions.is_some() { + return Err(eyre!("SolRpcConditions are not supported for decryption")); + } else if let Some(unified_access_control_conditions) = + &req.unified_access_control_conditions { - serde_json::to_string(conditions)? - } else if let Some(ref conditions) = params.access_control_conditions { - serde_json::to_string(conditions)? - } else if let Some(ref conditions) = params.evm_contract_conditions { - serde_json::to_string(conditions)? - } else if let Some(ref conditions) = params.sol_rpc_conditions { - serde_json::to_string(conditions)? + let stringified_access_control_conditions = + serde_json::to_string(unified_access_control_conditions)?; + debug!( + "stringified_access_control_conditions: {:?}", + stringified_access_control_conditions + ); + hasher.update(stringified_access_control_conditions.as_bytes()); } else { - return Err(eyre!("No access control conditions provided")); - }; + return Err(eyre!("Missing access control conditions")); + } + let hashed_access_control_conditions = utils::bytes_to_hex(hasher.finalize()); debug!( - "stringified_access_control_conditions: {:?}", - conditions_json + "hashed access control conditions: {:?}", + hashed_access_control_conditions ); - - // Hash the JSON string exactly like lit-node does - let mut hasher = Sha256::new(); - hasher.update(conditions_json.as_bytes()); - Ok(hasher.finalize().to_vec()) + Ok(hashed_access_control_conditions) } } diff --git a/lit-rust-sdk/src/lib.rs b/lit-rust-sdk/src/lib.rs index 1cac5e1..d551a76 100644 --- a/lit-rust-sdk/src/lib.rs +++ b/lit-rust-sdk/src/lib.rs @@ -54,6 +54,7 @@ pub mod bls; pub mod client; pub mod config; pub mod types; +pub mod utils; pub use client::LitNodeClient; pub use config::{LitNetwork, LitNodeClientConfig}; diff --git a/lit-rust-sdk/src/types.rs b/lit-rust-sdk/src/types.rs index 4b3542f..ce6b762 100644 --- a/lit-rust-sdk/src/types.rs +++ b/lit-rust-sdk/src/types.rs @@ -261,10 +261,10 @@ pub struct UnifiedAccessControlConditionItem { pub struct EvmContractCondition { pub contract_address: String, pub function_name: String, - pub function_params: Vec, - pub function_abi: serde_json::Value, + pub function_params: Vec, + pub function_abi: ethabi::Function, pub chain: String, - pub return_value_test: ReturnValueTest, + pub return_value_test: ReturnValueTestV2, } // For unified access control conditions, we need a version with conditionType @@ -274,7 +274,7 @@ pub struct UnifiedEvmContractConditionItem { pub condition_type: String, pub contract_address: String, pub function_name: String, - pub function_params: Vec, + pub function_params: Vec, pub function_abi: serde_json::Value, pub chain: String, pub return_value_test: ReturnValueTest, @@ -317,7 +317,14 @@ pub struct OperatorCondition { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReturnValueTest { pub comparator: String, - pub value: serde_json::Value, + pub value: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReturnValueTestV2 { + pub key: String, + pub comparator: String, + pub value: String, } // Encryption related types diff --git a/lit-rust-sdk/src/utils.rs b/lit-rust-sdk/src/utils.rs new file mode 100644 index 0000000..a0dee62 --- /dev/null +++ b/lit-rust-sdk/src/utils.rs @@ -0,0 +1,9 @@ +pub fn bytes_to_hex(vec: B) -> String +where + B: AsRef<[u8]>, +{ + vec.as_ref() + .iter() + .map(|b| format!("{b:02x}")) + .collect::() +} diff --git a/lit-rust-sdk/tests/decrypt_client_side.rs b/lit-rust-sdk/tests/decrypt_client_side.rs index 3d28bf5..ffb3b19 100644 --- a/lit-rust-sdk/tests/decrypt_client_side.rs +++ b/lit-rust-sdk/tests/decrypt_client_side.rs @@ -3,8 +3,9 @@ use lit_rust_sdk::{ types::{ AccessControlCondition, DecryptRequest, EncryptRequest, LitAbility, LitResourceAbilityRequest, LitResourceAbilityRequestResource, ReturnValueTest, + ReturnValueTestV2, }, - LitNetwork, LitNodeClient, LitNodeClientConfig, + EvmContractCondition, LitNetwork, LitNodeClient, LitNodeClientConfig, }; use std::time::Duration; @@ -62,7 +63,7 @@ async fn test_client_side_decryption_with_session_sigs() { parameters: vec![":userAddress".to_string(), "latest".to_string()], return_value_test: ReturnValueTest { comparator: ">=".to_string(), - value: serde_json::json!("0"), + value: "0".to_string(), }, }; @@ -154,3 +155,166 @@ async fn test_client_side_decryption_with_session_sigs() { println!("✅ Full encrypt/decrypt test with client-side decryption completed successfully!"); } + +#[tokio::test] +async fn test_client_side_decryption_with_session_sigs_and_evm_contract_conditions() { + // Initialize tracing for debugging (honors RUST_LOG) + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); + + // Load wallet from environment + let wallet = match load_wallet_from_env() { + Ok(w) => w, + Err(e) => { + println!( + "❌ Failed to load wallet from environment: {}. Make sure ETHEREUM_PRIVATE_KEY is set in .env", + e + ); + println!("Skipping test - required environment variables not set"); + return; + } + }; + + println!("🔑 Using wallet address: {}", wallet.address()); + + // Create client configuration + let config = LitNodeClientConfig { + lit_network: LitNetwork::DatilDev, + alert_when_unauthorized: true, + debug: true, + connect_timeout: Duration::from_secs(30), + check_node_attestation: false, + }; + + // Create and connect client + let mut client = LitNodeClient::new(config) + .await + .expect("Failed to create client"); + + match client.connect().await { + Ok(()) => { + println!("✅ Connected to Lit Network"); + } + Err(e) => { + panic!("❌ Failed to connect to Lit Network: {}", e); + } + } + + let usdc_address_on_eth = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + // Create EVM contract condition with simple whitelist check + let evm_contract_condition = EvmContractCondition { + contract_address: usdc_address_on_eth.to_string(), + function_name: "balanceOf".to_string(), + function_params: vec![":userAddress".to_string()], + function_abi: ethabi::Function { + inputs: vec![ethabi::Param { + internal_type: None, + name: "account".to_string(), + kind: ethabi::ParamType::Address, + }], + name: "balanceOf".to_string(), + outputs: vec![ethabi::Param { + internal_type: None, + name: "".to_string(), + kind: ethabi::ParamType::Uint(256), + }], + state_mutability: ethabi::StateMutability::View, + #[allow(deprecated)] // this is needed for compatibility with the version of ethabi running on the lit nodes which expects this param, and is an older version where this is not deprecated + constant: Some(false), + }, + chain: "ethereum".to_string(), + return_value_test: ReturnValueTestV2 { + key: "".to_string(), + comparator: ">=".to_string(), + value: "0".to_string(), + }, + }; + let evm_contract_conditions = vec![evm_contract_condition]; + + // Create test data to encrypt + let test_data = + b"Secret message that requires wallet ownership to decrypt using client-side decryption!"; + + // Create encrypt request using basic access control conditions + let encrypt_request = EncryptRequest { + data_to_encrypt: test_data.to_vec(), + access_control_conditions: None, + evm_contract_conditions: Some(evm_contract_conditions.clone()), + sol_rpc_conditions: None, + unified_access_control_conditions: None, + }; + + // Encrypt the data + println!("🔒 Encrypting data with access control conditions..."); + let encrypt_response = match client.encrypt(encrypt_request).await { + Ok(response) => { + println!("✅ Data encrypted successfully!"); + println!("📦 Ciphertext length: {} bytes", response.ciphertext.len()); + println!("🔗 Data hash: {}", response.data_to_encrypt_hash); + response + } + Err(e) => { + panic!("❌ Encryption failed: {}", e); + } + }; + + // Now let's prepare to decrypt by getting session signatures + let resource_ability_requests = vec![LitResourceAbilityRequest { + resource: LitResourceAbilityRequestResource { + resource: "*".to_string(), + resource_prefix: "lit-accesscontrolcondition".to_string(), + }, + ability: LitAbility::AccessControlConditionDecryption.to_string(), + }]; + + let expiration = (chrono::Utc::now() + chrono::Duration::minutes(10)).to_rfc3339(); + + println!("🔄 Getting session signatures for decryption..."); + let session_sigs = client + .get_local_session_sigs(&wallet, resource_ability_requests, &expiration) + .await + .expect("Failed to create local session signatures"); + + println!( + "✅ Got session signatures from {} nodes", + session_sigs.len() + ); + + // Now decrypt using client-side decryption + println!("🔓 Decrypting data using client-side decryption..."); + + let decrypt_request = DecryptRequest { + ciphertext: encrypt_response.ciphertext, + data_to_encrypt_hash: encrypt_response.data_to_encrypt_hash, + access_control_conditions: None, + evm_contract_conditions: Some(evm_contract_conditions), + sol_rpc_conditions: None, + unified_access_control_conditions: None, + chain: Some("ethereum".to_string()), + session_sigs, + }; + + match client.decrypt(decrypt_request).await { + Ok(response) => { + println!("✅ Client-side decryption successful!"); + + // Convert decrypted bytes to string + let decrypted_str = String::from_utf8_lossy(&response.decrypted_data); + println!("🔓 Decrypted content: {}", decrypted_str); + + // Verify it matches our original data + let original = String::from_utf8_lossy(test_data); + assert_eq!( + decrypted_str, original, + "Decrypted data should match original data" + ); + println!("✅ Decryption successful - data matches original!"); + } + Err(e) => { + panic!("❌ Failed to decrypt using client-side decryption: {}", e); + } + } + + println!("✅ Full encrypt/decrypt test with client-side decryption completed successfully!"); +} diff --git a/lit-rust-sdk/tests/encrypt_test.rs b/lit-rust-sdk/tests/encrypt_test.rs index 428fda1..8328a1d 100644 --- a/lit-rust-sdk/tests/encrypt_test.rs +++ b/lit-rust-sdk/tests/encrypt_test.rs @@ -63,7 +63,7 @@ async fn test_encrypt_and_decrypt_with_session_sigs() { parameters: vec![":userAddress".to_string(), "latest".to_string()], return_value_test: ReturnValueTest { comparator: ">=".to_string(), - value: serde_json::json!("0"), + value: "0".to_string(), }, };