use anyhow::{Error, Result}; use rs_merkle::{algorithms::Sha256, MerkleTree}; use std::collections::{HashMap, HashSet}; use std::hash::{Hash as StdHash, Hasher}; use aes_gcm::{ aead::{Aead, Payload}, AeadCore, Aes256Gcm, KeyInit, }; use rand::thread_rng; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use sp_client::{ bitcoin::{ consensus::serialize, hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, secp256k1::PublicKey, OutPoint }, silentpayments::utils::SilentPaymentAddress, }; use tsify::Tsify; use crate::{ crypto::AAD, signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}, }; #[derive(Debug, Default, Clone, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Member { sp_addresses: Vec, } impl PartialEq for Member { fn eq(&self, other: &Self) -> bool { let self_set: HashSet<_> = self.sp_addresses.iter().collect(); let other_set: HashSet<_> = other.sp_addresses.iter().collect(); self_set == other_set } } impl Eq for Member {} impl StdHash for Member { fn hash(&self, state: &mut H) { // Convert to a set to ensure order independence let set: HashSet<_> = self.sp_addresses.iter().collect(); let mut unique_items: Vec<_> = set.into_iter().collect(); unique_items.sort_unstable(); // Sort to ensure consistent hashing for item in unique_items { item.hash(state); } } } impl Member { pub fn new(sp_addresses: Vec) -> Result { if sp_addresses.is_empty() { return Err(Error::msg("empty address set")); } let mut seen = HashSet::new(); for s in sp_addresses.iter() { if !seen.insert(s.clone()) { return Err(Error::msg("Duplicate addresses found")); } } let res: Vec = sp_addresses .iter() .map(|a| Into::::into(*a)) .collect(); Ok(Self { sp_addresses: res }) } pub fn get_addresses(&self) -> Vec { self.sp_addresses.clone() } pub fn key_is_part_of_member(&self, key: &PublicKey) -> bool { self.sp_addresses.iter().any(|a| { let addr = SilentPaymentAddress::try_from(a.as_str()).unwrap(); addr.get_spend_key() == *key }) } pub fn get_address_for_key(&self, key: &PublicKey) -> Option { self.sp_addresses.iter().find(|a| { let addr = SilentPaymentAddress::try_from(a.as_str()).unwrap(); addr.get_spend_key() == *key }) .cloned() } } sha256t_hash_newtype! { pub struct AnkPcdTag = hash_str("4nk/Pcd"); #[hash_newtype(forward)] pub struct AnkPcdHash(_); } impl AnkPcdHash { pub fn from_value(value: &Value) -> Self { let mut eng = AnkPcdHash::engine(); eng.input(value.to_string().as_bytes()); AnkPcdHash::from_engine(eng) } /// Adding the root_commitment guarantee that the same clear value across different processes wont' produce the same hash pub fn from_value_with_outpoint(value: &[u8], outpoint: &[u8]) -> Self { let mut eng = AnkPcdHash::engine(); eng.input(outpoint); eng.input(value); AnkPcdHash::from_engine(eng) } } pub trait Pcd<'a>: Serialize + Deserialize<'a> { fn new_from_string(str: &str) -> Result { let value: Value = serde_json::from_str(str)?; match value { Value::Object(_) => Ok(value), _ => Err(Error::msg("Not a Pcd: not a valid JSON object")) } } fn to_sorted_key_values(&self) -> Result> { let map = self.to_value_object()?; let mut sorted_key_values: Vec<(String, Value)> = map .into_iter() .map(|(key, value)| { let sorted_value = match value { Value::Object(obj) => { // Recursively sort nested objects let mut sorted_nested: Vec<(String, Value)> = obj.into_iter().collect(); sorted_nested.sort_by_key(|(k, _)| k.clone()); Value::Object(sorted_nested.into_iter().collect()) } _ => value, // Keep other values unchanged }; (key, sorted_value) }) .collect(); // Sort top-level keys sorted_key_values.sort_by_key(|(key, _)| key.clone()); Ok(sorted_key_values.into_iter().collect()) } /// Create a hashed commitments for all values in the pcd /// We need the commited_in outpoint to prevent same data producing the same hashes between different processes or differents states of the same process fn hash_all_fields(&self, commited_in: OutPoint) -> Result> { let outpoint = serialize(&commited_in); // To prevent fields with identical data having identical commitments, we order the map in alphabetical order and append a counter to the value let sorted_key_values = self.to_sorted_key_values()?; let mut field2hash = Map::with_capacity(sorted_key_values.len()); // this could be optimised since there's a midstate we're reusing for (i, (field, value)) in sorted_key_values.into_iter().enumerate() { let mut value_bin = serde_json::to_string(&value)?.into_bytes(); value_bin.push(i.try_into()?); let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &outpoint); field2hash.insert(field, Value::String(tagged_hash.to_string())); } Ok(field2hash) } /// We run this on the result of `hash_all_fields` fn create_merkle_tree(&self) -> Result> { let map = self.to_value_object()?; let leaves: Result> = map .iter() .map(|(_, value)| { let mut res = [0u8; 32]; if !value.is_string() { return Err(Error::msg("value is not a string")); } let vec = Vec::from_hex(value.as_str().unwrap())?; if vec.len() != 32 { return Err(Error::msg("value must be 32B length")); } res.copy_from_slice(&vec); Ok(res) }) .collect(); let mut leaves = leaves?; leaves.sort_unstable(); let merkle_tree = MerkleTree::::from_leaves(&leaves); Ok(merkle_tree) } fn encrypt_fields( &self, fields_to_encrypt: &[String], fields2keys: &mut Map, fields2cipher: &mut Map, ) -> Result<()> { let map = self.to_value_object()?; let mut rng = thread_rng(); for (field, value) in map { if fields_to_encrypt.contains(&field) { if let None = fields2keys.get(&field) { let aes_key = Aes256Gcm::generate_key(&mut rng); fields2keys.insert( field.to_owned(), Value::String(aes_key.to_lower_hex_string()), ); } let nonce = Aes256Gcm::generate_nonce(&mut rng); let aes_key_value = fields2keys.get(&field).expect("We should have a key"); let aes_key_str: String = serde_json::from_value(aes_key_value.clone())?; let aes_key = Vec::from_hex(&aes_key_str)?; let encrypt_eng = Aes256Gcm::new(aes_key.as_slice().into()); let value_string = serde_json::to_string(&value)?; let payload = Payload { msg: value_string.as_bytes(), aad: AAD, }; let cipher = encrypt_eng.encrypt(&nonce, payload).map_err(|e| { Error::msg(format!("Encryption failed for field {}: {}", field, e)) })?; let mut res = Vec::with_capacity(nonce.len() + cipher.len()); res.extend_from_slice(&nonce); res.extend_from_slice(&cipher); fields2cipher.insert(field.to_owned(), Value::String(res.to_lower_hex_string())); } else { if let None = fields2cipher.get(&field) { fields2cipher.insert(field.to_owned(), value.clone()); } // if we already have something in the encrypted map, we leave it as it is } } Ok(()) } fn decrypt_all( &self, commited_in: OutPoint, fields2commit: &Map, fields2keys: &Map, fields2plain: &mut Map, ) -> Result<()> { let sorted_key_values = self.to_sorted_key_values()?; for (i, (field, encrypted_value)) in sorted_key_values.iter().enumerate() { if let Some(aes_key) = fields2keys.get(field) { let key_buf = Vec::from_hex(&aes_key.to_string().trim_matches('\"'))?; let decrypt_eng = Aes256Gcm::new(key_buf.as_slice().into()); let raw_cipher = Vec::from_hex( &encrypted_value .as_str() .ok_or_else(|| Error::msg("Expected string"))? .trim_matches('\"'), )?; if raw_cipher.len() < 28 { return Err(Error::msg(format!( "Invalid ciphertext length for field {}", field ))); } let payload = Payload { msg: &raw_cipher[12..], aad: AAD, }; let plain = decrypt_eng .decrypt(raw_cipher[..12].into(), payload) .map_err(|_| Error::msg(format!("Failed to decrypt field {}", field)))?; let decrypted_value: String = String::from_utf8(plain)?; fields2plain.insert(field.to_owned(), serde_json::from_str(&decrypted_value)?); } else if let Some(commitment) = fields2commit.get(field) { // We should always have a commitment // We check if the hashed value is the commitment let mut value_bin = encrypted_value.to_string().into_bytes(); value_bin.push(i.try_into()?); let hashed_value = AnkPcdHash::from_value_with_outpoint(&value_bin, &serialize(&commited_in)); if commitment.as_str().unwrap() != &hashed_value.to_string() { // The value is encrypted, and we don't have the key // We put the commitment instead of the encrypted value fields2plain.insert(field.to_owned(), commitment.clone()); } // else it means the value is simply unencrypted, we leave it as it is } else { return Err(Error::msg(format!("Missing commitment for field {}", field))); } } Ok(()) } fn to_value_object(&self) -> Result> { let value = serde_json::to_value(self)?; match value { Value::Object(map) => Ok(map), _ => Err(Error::msg("not a valid json object")) } } fn extract_roles(&self) -> Result> { let obj = self.to_value_object()?; let parse_roles_map = |m: &Map| { let mut res: HashMap = HashMap::new(); for (name, role_def) in m { res.insert(name.clone(), serde_json::from_value(role_def.clone())?); } , Error>>::Ok(res) }; if let Some(roles) = obj.get("roles") { match roles { Value::Object(m) => { parse_roles_map(m) }, Value::String(s) => { let m: Map = serde_json::from_str(&s)?; parse_roles_map(&m) } _ => Err(Error::msg("\"roles\" is not an object")) } } else { Err(Error::msg("No \"roles\" key in this pcd")) } } fn is_hex_string(&self, length: Option) -> Result<()> { let value = serde_json::to_value(self)?; match value { Value::String(s) => { let vec = Vec::from_hex(&s)?; if let Some(len) = length { let got_length = vec.len(); if got_length != len { return Err(Error::msg(format!("Wrong length: expected {}, got {}", len, got_length))); } } Ok(()) } _ => Err(Error::msg("Not a string Value")) } } } impl Pcd<'_> for Value {} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct ValidationRule { quorum: f32, // Must be >= 0.0, <= 1.0, 0.0 means reading right pub fields: Vec, // Which fields are concerned by this rule min_sig_member: f32, // Must be >= 0.0, <= 1.0, does each member need to sign with all it's devices? } impl ValidationRule { pub fn new(quorum: f32, fields: Vec, min_sig_member: f32) -> Result { if quorum < 0.0 || quorum > 1.0 { return Err(Error::msg("quorum must be 0.0 < quorum <= 1.0")); } if min_sig_member < 0.0 || min_sig_member > 1.0 { return Err(Error::msg( "min_signatures_member must be 0.0 < min_signatures_member <= 1.0", )); } if fields.is_empty() { return Err(Error::msg("Fields can't be empty")); } let res = Self { quorum, fields, min_sig_member, }; Ok(res) } pub fn allows_modification(&self) -> bool { self.quorum > 0.0 && self.min_sig_member > 0.0 } pub fn is_satisfied( &self, field: &str, merkle_root: [u8; 32], proofs: &[Proof], members: &[Member], ) -> Result<()> { // Check if this rule applies to the field if !self.fields.contains(&field.to_string()) { return Err(Error::msg("Field isn't part of this rule")); } else if members.is_empty() { return Err(Error::msg("Members list is empty")); } let required_members = (members.len() as f32 * self.quorum).ceil() as usize; let validating_members = members .iter() .filter(|member| { let member_proofs: Vec<&Proof> = proofs .iter() .filter(|p| member.key_is_part_of_member(&p.get_key())) .collect(); self.satisfy_min_sig_member(member, merkle_root, &member_proofs) .is_ok() }) .count(); if validating_members >= required_members { Ok(()) } else { Err(Error::msg("Not enough members to validate"))} } pub fn satisfy_min_sig_member( &self, member: &Member, merkle_root: [u8; 32], proofs: &[&Proof], ) -> Result<()> { if proofs.len() == 0 { return Err(Error::msg("Can't validate with 0 proof")); } let registered_devices = member.get_addresses().len(); if proofs.len() > registered_devices { // We can't have more proofs than registered devices for one member return Err(Error::msg("More proofs than requirefor member")); } let required_sigs = (registered_devices as f32 * self.min_sig_member).ceil() as usize; // println!("required_sigs {} and proofs.len() {}", required_sigs, proofs.len()); if proofs.len() < required_sigs { // Even if all proof are valid yes, we don't reach the quota return Err(Error::msg("Not enough provided proofs to reach quota")); } let mut yes_votes: Vec = Vec::new(); let mut no_votes: Vec = Vec::new(); let yes = AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(merkle_root)); let no = AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(merkle_root)); // Validate proofs here for proof in proofs { if !proof.verify().is_ok() { return Err(Error::msg("Invalid proof")); } let signed_message = proof.get_message(); if signed_message == yes.to_byte_array() { yes_votes.push(**proof); } else if signed_message == no.to_byte_array() { no_votes.push(**proof); } else { return Err(Error::msg("We don't know what this proof signs for")); } } if yes_votes.len() >= required_sigs { Ok(()) } else { Err(Error::msg("Not enough yes votes")) } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct RoleDefinition { pub members: Vec, pub validation_rules: Vec, pub storages: Vec, } impl RoleDefinition { pub fn is_satisfied( &self, diff: Vec, new_state_merkle_root: [u8; 32], proofs: &[Proof], ) -> Result<()> { if diff.iter().all(|field| { self.validation_rules .iter() .any(|rule| rule.is_satisfied(field, new_state_merkle_root, proofs, &self.members).is_ok()) }) { Ok(()) } else { Err(Error::msg("Failed to validate all rules")) } } pub fn get_applicable_rules(&self, field: &str) -> Vec<&ValidationRule> { self.validation_rules .iter() .filter(|rule| rule.fields.contains(&field.to_string())) .collect() } pub fn is_member_validation_needed(&self, member: Member, modified_fields: Vec) -> bool { if !self.members.iter().any(|m| *m == member) { return false; } for field in modified_fields { if !self.get_applicable_rules(&field).is_empty() { return true; } } false } } #[cfg(test)] mod tests { use std::str::FromStr; use serde_json::json; use sp_client::{ bitcoin::{secp256k1::SecretKey, Network}, spclient::{SpClient, SpWallet, SpendKey}, }; use super::*; use crate::{ pcd::Member, signature::{AnkHash, Proof}, }; fn create_alice_wallet() -> SpWallet { SpWallet::new( SpClient::new( "default".to_owned(), SecretKey::from_str( "a67fb6bf5639efd0aeb19c1c584dd658bceda87660ef1088d4a29d2e77846973", ) .unwrap(), SpendKey::Secret( SecretKey::from_str( "a1e4e7947accf33567e716c9f4d186f26398660e36cf6d2e711af64b3518e65c", ) .unwrap(), ), None, Network::Signet, ) .unwrap(), None, vec![], ) .unwrap() } fn create_bob_wallet() -> SpWallet { SpWallet::new( SpClient::new( "default".to_owned(), SecretKey::from_str( "4d9f62b2340de3f0bafd671b78b19edcfded918c4106baefd34512f12f520e9b", ) .unwrap(), SpendKey::Secret( SecretKey::from_str( "dafb99602721577997a6fe3da54f86fd113b1b58f0c9a04783d486f87083a32e", ) .unwrap(), ), None, Network::Signet, ) .unwrap(), None, vec![], ) .unwrap() } #[test] fn test_sort_map() { let pcd = json!({ "z": 1, "b": 2, "a": 3 }); let expected = json!({ "a": 3, "b": 2, "z": 1 }); let sorted_map = pcd.to_sorted_key_values().unwrap(); assert_eq!(Value::Object(sorted_map), expected); } #[test] fn test_sort_empty_map() { let empty_map = json!({}); let expected = json!({}); let actual_sorted_map = empty_map.to_sorted_key_values().expect("Failed to sort keys"); assert_eq!( Value::Object(actual_sorted_map), expected, "Sorting failed for an empty map" ); } #[test] fn test_sort_already_sorted_map() { let sorted_map = json!({ "a": 1, "b": 2, "c": 3 }); let expected = sorted_map.clone(); // Expected result is the same let actual_sorted_map = sorted_map.to_sorted_key_values().expect("Failed to sort keys"); assert_eq!( Value::Object(actual_sorted_map), expected, "Sorting failed for an already sorted map" ); } #[test] fn test_sort_mixed_value_map() { let mixed_map = json!({ "z": [1, 2, 3], "b": { "nested": true }, "a": 42 }); let expected = json!({ "a": 42, "b": { "nested": true }, "z": [1, 2, 3] }); let actual_sorted_map = mixed_map.to_sorted_key_values().expect("Failed to sort keys"); assert_eq!( Value::Object(actual_sorted_map), expected, "Sorting failed for a map with mixed value types" ); } #[test] fn test_validation_rule_new() { // Valid input let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5); assert!(validation_rule.is_ok()); let rule = validation_rule.unwrap(); assert_eq!(rule.quorum, 0.5); assert_eq!(rule.fields, fields); assert_eq!(rule.min_sig_member, 0.5); // Invalid quorum (< 0.0) let validation_rule = ValidationRule::new(-0.1, fields.clone(), 0.5); assert!(validation_rule.is_err()); // Invalid quorum (> 1.0) let validation_rule = ValidationRule::new(1.1, fields.clone(), 0.5); assert!(validation_rule.is_err()); // Invalid min_sig_member (< 0.0) let validation_rule = ValidationRule::new(0.5, fields.clone(), -0.1); assert!(validation_rule.is_err()); // Invalid min_sig_member (> 1.0) let validation_rule = ValidationRule::new(0.5, fields.clone(), 1.1); assert!(validation_rule.is_err()); // Empty fields let validation_rule = ValidationRule::new(0.5, vec![], 0.5); assert!(validation_rule.is_err()); } #[test] fn test_is_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5).unwrap(); let pcd = json!({"field1": "value1", "field2": "value2"}); let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(commitment).create_merkle_tree().unwrap().root().unwrap(); let validation_hash1 = AnkValidationYesHash::from_merkle_root(new_state_merkle_root); let validation_hash2 = AnkValidationNoHash::from_merkle_root(new_state_merkle_root); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationNo(validation_hash2), alice_spend_key); let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash1), bob_spend_key); let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; // We test that the rule is satisfied with only bob proof let result = validation_rule.is_satisfied( fields[0].as_str(), new_state_merkle_root, &vec![bob_proof], &members, ); assert!(result.is_ok()); // Since Alice voted no, rule shouldn't be satisfied only with her proof let result = validation_rule.is_satisfied( fields[0].as_str(), new_state_merkle_root, &vec![alice_proof], &members, ); assert!(result.is_err()); // Since the quorum is 0.5, Bob yes should be enough to satisfy even with Alice's no let result = validation_rule.is_satisfied( fields[0].as_str(), new_state_merkle_root, &vec![alice_proof, bob_proof], &members, ); assert!(result.is_ok()); } #[test] fn test_is_satisfied_error_cases() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5).unwrap(); let pcd = json!({"field1": "value1", "field2": "value2"}); let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(commitment).create_merkle_tree().unwrap().root().unwrap(); let validation_hash_yes = AnkValidationYesHash::from_merkle_root(new_state_merkle_root); let validation_hash_no = AnkValidationNoHash::from_merkle_root(new_state_merkle_root); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationNo(validation_hash_no), alice_spend_key); let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash_yes), bob_spend_key); let proofs = vec![alice_proof, bob_proof]; let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; // Test with empty members list let result = validation_rule.is_satisfied(fields[0].as_str(), new_state_merkle_root, &proofs, &vec![]); assert!(result.is_err()); // Test with no matching field let result = validation_rule.is_satisfied("nonexistent_field", new_state_merkle_root, &proofs, &members); assert!(result.is_err()); } #[test] fn test_is_satisfied_error_with_alice_providing_proofs_for_bob() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule = ValidationRule::new(1.0, fields.clone(), 0.5).unwrap(); let pcd = json!({"field1": "value1", "field2": "value2"}); let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(commitment).create_merkle_tree().unwrap().root().unwrap(); let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); // Both proofs are signed by Alice let alice_proof_1 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let alice_proof_2 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let proofs = vec![alice_proof_1, alice_proof_2]; let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; // Test case where both proofs are signed by Alice, but both Alice and Bob are passed as members let result = validation_rule.is_satisfied(fields[0].as_str(), new_state_merkle_root, &proofs, &members); assert!(result.is_err()); } #[test] fn test_is_satisfied_error_quorum_half_with_alice_providing_two_proofs() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5).unwrap(); let pcd = json!({"field1": "value1", "field2": "value2"}); let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(commitment).create_merkle_tree().unwrap().root().unwrap(); let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); // Both proofs are signed by Alice let alice_proof_1 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let alice_proof_2 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let proofs = vec![alice_proof_1, alice_proof_2]; let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; // Test case where quorum is 0.5, but Alice provides two proofs. This should fail since the quorum requires different members. let result = validation_rule.is_satisfied(fields[0].as_str(), new_state_merkle_root, &proofs, &members); assert!(result.is_err()); } #[test] fn test_satisfy_min_sig_member() { let fields = vec!["field1".to_string()]; let validation_rule = ValidationRule::new(0.5, fields, 0.5).unwrap(); let alice_wallet = create_alice_wallet(); let member = Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(); let pcd = json!({"field1": "value1"}); let commitments = pcd.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(commitments).create_merkle_tree().unwrap().root().unwrap(); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); let proof = Proof::new( AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(new_state_merkle_root)), alice_spend_key, ); let proofs = vec![&proof]; let result = validation_rule.satisfy_min_sig_member(&member, new_state_merkle_root, &proofs); assert!(result.is_ok()); // Example check - make more meaningful assertions based on real Proof and Member implementations } #[test] fn test_all_rules_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap(); let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap(); let rules = vec![validation_rule1, validation_rule2]; // 2 rules, to modify each field, all members must agree let role_def = RoleDefinition { members: members.clone(), validation_rules: rules.clone(), storages: vec![], }; let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" }); let new_state = json!({ "field1": "new_value1", "field2": "new_value2" }); let new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(new_state_commitments).create_merkle_tree().unwrap().root().unwrap(); let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root); // let validation_hash = AnkValidationNoHash::from_commitment(new_state_hash); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash), bob_spend_key); let proofs = vec![alice_proof, bob_proof]; let modified_fields: Vec = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect(); assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_ok()); } #[test] fn test_no_rule_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap(); let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap(); let rules = vec![validation_rule1, validation_rule2]; // 2 rules, to modify each field, all members must agree let role_def = RoleDefinition { members: members.clone(), validation_rules: rules.clone(), storages: vec![], }; let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" }); let new_state = json!({ "field1": "new_value1", "field2": "new_value2" }); let new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(new_state_commitments).create_merkle_tree().unwrap().root().unwrap(); // let validation_hash1 = AnkValidationYesHash::from_commitment(new_state_hash); let validation_hash = AnkValidationNoHash::from_merkle_root(new_state_merkle_root); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationNo(validation_hash), alice_spend_key); let bob_proof = Proof::new(AnkHash::ValidationNo(validation_hash), bob_spend_key); let proofs = vec![alice_proof, bob_proof]; let modified_fields: Vec = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect(); assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_err()); } #[test] fn test_partial_modification_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap(); let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap(); let rules = vec![validation_rule1, validation_rule2]; // 2 rules, to modify each field, all members must agree let role_def = RoleDefinition { members: members.clone(), validation_rules: rules.clone(), storages: vec![], }; let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" }); let new_state = json!({ "field1": "old_value1", "field2": "new_value2" }); let new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(new_state_commitments).create_merkle_tree().unwrap().root().unwrap(); let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root); // let validation_hash = AnkValidationNoHash::from_merkle_root(new_state_merkle_root); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash), bob_spend_key); let proofs = vec![alice_proof, bob_proof]; let modified_fields: Vec = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect(); assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_ok()); } #[test] fn test_partial_modification_not_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let members = vec![ Member::new(vec![SilentPaymentAddress::try_from( alice_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), Member::new(vec![SilentPaymentAddress::try_from( bob_wallet.get_client().get_receiving_address(), ) .unwrap()]) .unwrap(), ]; let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap(); let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap(); let rules = vec![validation_rule1, validation_rule2]; // 2 rules, to modify each field, all members must agree let role_def = RoleDefinition { members: members.clone(), validation_rules: rules.clone(), storages: vec![], }; let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" }); let new_state = json!({ "field1": "old_value1", "field2": "new_value2" }); let new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap(); let new_state_merkle_root = Value::Object(new_state_commitments).create_merkle_tree().unwrap().root().unwrap(); let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root); // let validation_hash = AnkValidationNoHash::from_merkle_root(new_state_merkle_root); let alice_spend_key: SecretKey = alice_wallet .get_client() .get_spend_key() .try_into() .unwrap(); // let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); // let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash), bob_spend_key); let proofs = vec![alice_proof]; let modified_fields: Vec = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect(); assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_err()); } #[test] fn test_get_applicable_rules() { let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap(); let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap(); let rules = vec![validation_rule1.clone(), validation_rule2]; let role_def = RoleDefinition { members: vec![], validation_rules: rules, storages: vec![], }; let applicable_rules = role_def.get_applicable_rules("field1"); assert_eq!(applicable_rules.len(), 1); assert_eq!(*applicable_rules[0], validation_rule1); } #[test] fn test_get_applicable_rules_no_rules() { let fields = vec!["field1".to_string(), "field2".to_string()]; let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap(); let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap(); let rules = vec![validation_rule1.clone(), validation_rule2]; let role_def = RoleDefinition { members: vec![], validation_rules: rules, storages: vec![], }; let applicable_rules = role_def.get_applicable_rules("nonexistent_field"); assert_eq!(applicable_rules.len(), 0); } }