use anyhow::{Error, Result}; use rs_merkle::{algorithms::Sha256, MerkleTree}; use serde::ser::SerializeStruct; use std::collections::btree_map::Keys; use std::collections::{BTreeMap, HashSet}; use std::fmt; use std::hash::{Hash as StdHash, Hasher}; use std::io::Write; use serde::{Deserialize, Serialize}; use serde_json::Value; use sp_client::{ bitcoin::{hashes::Hash, secp256k1::PublicKey, OutPoint}, silentpayments::SilentPaymentAddress, }; use tsify::Tsify; use crate::hash::AnkPcdHash; use crate::serialization::OutPointMemberMap; use crate::ROLESLABEL; use crate::{ serialization::hex_array_btree, signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}, }; pub const PCD_VERSION: u8 = 1; pub(crate) const ZSTD_COMPRESSION_LEVEL: i32 = zstd::DEFAULT_COMPRESSION_LEVEL; pub trait PcdSerializable { fn serialize_to_pcd(&self) -> Result>; fn deserialize_from_pcd(data: &[u8]) -> Result where Self: Sized; } impl PcdSerializable for serde_json::Value { fn serialize_to_pcd(&self) -> Result> { let mut compressed = Vec::new(); let mut encoder = zstd::stream::Encoder::new(&mut compressed, ZSTD_COMPRESSION_LEVEL)?; encoder.write_all(&[PCD_VERSION])?; encoder.write_all(&[DataType::Json as u8])?; serde_json::to_writer(&mut encoder, self)?; encoder.finish()?; Ok(compressed) } fn deserialize_from_pcd(data: &[u8]) -> Result { let mut decompressed = Vec::new(); zstd::stream::copy_decode(data, &mut decompressed)?; if decompressed.len() < 3 { return Err(Error::msg("Invalid data: too short")); } let version = decompressed[0]; let data_type = DataType::try_from(decompressed[1])?; match (version, data_type) { (PCD_VERSION, DataType::Json) => { let json_bytes = &decompressed[2..]; let json_string = String::from_utf8(json_bytes.to_vec())?; Ok(serde_json::from_str(&json_string)?) } _ => Err(Error::msg("Invalid version or data type")), } } } impl PcdSerializable for FileBlob { fn serialize_to_pcd(&self) -> Result> { let mut compressed = Vec::new(); let mut encoder = zstd::stream::Encoder::new(&mut compressed, ZSTD_COMPRESSION_LEVEL)?; encoder.write_all(&[PCD_VERSION])?; encoder.write_all(&[DataType::FileBlob as u8])?; let type_len = self.r#type.as_bytes().len() as u8; encoder.write_all(&[type_len])?; encoder.write_all(self.r#type.as_bytes())?; encoder.write_all(&self.data)?; encoder.finish()?; Ok(compressed) } fn deserialize_from_pcd(data: &[u8]) -> Result { let mut decompressed = Vec::new(); zstd::stream::copy_decode(data, &mut decompressed)?; if decompressed.len() < 4 { return Err(Error::msg("Invalid data: too short")); } let version = decompressed[0]; let data_type = DataType::try_from(decompressed[1])?; match (version, data_type) { (PCD_VERSION, DataType::FileBlob) => { let type_len = decompressed[2] as usize; let type_str = String::from_utf8(decompressed[3..3 + type_len].to_vec())?; let data = decompressed[3 + type_len..].to_vec(); Ok(FileBlob { r#type: type_str, data, }) } _ => Err(Error::msg("Invalid version or data type")), } } } #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq)] pub enum DataType { FileBlob = 0, Json = 1, } impl TryFrom for DataType { type Error = Error; fn try_from(value: u8) -> Result { match value { 0 => Ok(DataType::FileBlob), 1 => Ok(DataType::Json), _ => return Err(Error::msg(format!("Unknown data type: {}", value))), } } } #[derive(Serialize, Deserialize)] pub struct FileBlob { pub r#type: String, pub data: Vec, } #[derive(Debug, Default, Clone, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Member { sp_addresses: Vec, } impl fmt::Display for Member { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.sp_addresses.join(",")) } } impl Serialize for Member { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { // Deduplicate and sort sp_addresses let set: HashSet<_> = self.sp_addresses.iter().collect(); let mut unique_items: Vec<_> = set.into_iter().collect(); unique_items.sort_unstable(); // Serialize as an object with the sp_addresses key let mut state = serializer.serialize_struct("Member", 1)?; state.serialize_field("sp_addresses", &unique_items)?; state.end() } } 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) -> Self { let unique_addresses: HashSet<_> = sp_addresses.into_iter().collect(); let res: Vec = unique_addresses .iter() .map(|a| Into::::into(*a)) .collect(); 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() } } #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Pcd(BTreeMap>); impl IntoIterator for Pcd { type Item = (String, Vec); type IntoIter = std::collections::btree_map::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl TryFrom for Pcd { type Error = Error; fn try_from(value: Value) -> Result { let as_object = value .as_object() .ok_or_else(|| Error::msg("Pcd must be an object"))?; let map: Result>> = as_object .into_iter() .map(|(key, value)| { // Use the trait method instead of manual serialization let compressed = value.serialize_to_pcd()?; Ok((key.clone(), compressed)) }) .collect(); Ok(Pcd(map?)) } } impl TryFrom> for Pcd { type Error = Error; fn try_from(file_blob_map: BTreeMap) -> Result { let map: Result>> = file_blob_map .into_iter() .map(|(key, value)| { // Use the trait method instead of manual serialization let compressed = value.serialize_to_pcd()?; Ok((key, compressed)) }) .collect(); Ok(Pcd(map?)) } } impl Pcd { pub fn new(map: BTreeMap>) -> Self { Self(map) } pub fn get(&self, key: &str) -> Option<&Vec> { self.0.get(key) } pub fn len(&self) -> usize { self.0.len() } pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, Vec> { self.0.iter() } pub fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, Vec> { self.0.iter_mut() } pub fn insert(&mut self, key: String, value: Vec) -> Option> { self.0.insert(key, value) } // Helper methods for deserialization using the trait pub fn get_as_json(&self, key: &str) -> Result { if let Some(data) = self.get(key) { serde_json::Value::deserialize_from_pcd(data) } else { Err(Error::msg("Key not found")) } } pub fn get_as_file_blob(&self, key: &str) -> Result { if let Some(data) = self.get(key) { FileBlob::deserialize_from_pcd(data) } else { Err(Error::msg("Key not found")) } } pub fn get_as(&self, key: &str) -> Result { if let Some(data) = self.get(key) { T::deserialize_from_pcd(data) } else { Err(Error::msg("Key not found")) } } pub fn insert_serializable( &mut self, key: String, value: &T, ) -> Result>> { let compressed = value.serialize_to_pcd()?; Ok(self.insert(key, compressed)) } } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct PcdCommitments( #[serde(with = "hex_array_btree")] #[tsify(type = "Record")] BTreeMap, ); impl PcdCommitments { /// Creates a new commitments map with both permissioned and public data, + roles pub fn new(commited_in: &OutPoint, attributes: &Pcd, roles: &Roles) -> Result { let mut field2hash: BTreeMap = BTreeMap::new(); for (field, value) in attributes.iter() { let tagged_hash = AnkPcdHash::from_pcd_value(value.as_slice(), field.as_bytes(), commited_in); field2hash.insert(field.to_owned(), tagged_hash.to_byte_array()); } if roles.len() > 0 { let roles_label = String::from(ROLESLABEL); let roles_hash = AnkPcdHash::from_pcd_value( roles.to_bytes()?.as_slice(), roles_label.as_bytes(), commited_in, ); field2hash.insert(roles_label, roles_hash.to_byte_array()); } // We should probably return an error if roles are empty Ok(Self(field2hash)) } pub fn new_empty() -> Self { Self(BTreeMap::new()) } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn update_with_value( &mut self, outpoint: &OutPoint, field: &str, new_value: &[u8], ) -> Result<()> { if let Some(old_hash) = self.get_mut(field) { // We hash the new_value let tagged_hash = AnkPcdHash::from_pcd_value(new_value, field.as_bytes(), outpoint); *old_hash = tagged_hash.to_byte_array(); Ok(()) } else { Err(Error::msg("Field not found")) } } pub fn contains_key(&self, key: &str) -> bool { self.0.contains_key(key) } pub fn get(&self, field: &str) -> Option<&[u8; 32]> { self.0.get(field) } pub fn get_mut(&mut self, field: &str) -> Option<&mut [u8; 32]> { self.0.get_mut(field) } pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, [u8; 32]> { self.0.iter() } pub fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, [u8; 32]> { self.0.iter_mut() } pub fn keys(&self) -> Keys { self.0.keys() } /// Since BTreeMap keys order is deterministic, we can guarantee a consistent merkle tree pub fn create_merkle_tree(&self) -> Result> { let leaves: Vec<[u8; 32]> = self.0.values().map(|hash| *hash).collect(); let merkle_tree = MerkleTree::::from_leaves(leaves.as_slice()); Ok(merkle_tree) } pub fn find_index_of(&self, field: &str) -> Option { self.iter().position(|(key, _)| key.as_str() == field) } } #[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")); } else if self.quorum <= 0.0 || self.quorum > 1.0 || self.quorum.is_sign_negative() || self.quorum.is_nan() { // Just to be sure return Err(Error::msg("This rule is read only")); } let required_members = (members.len() as f32 * self.quorum).ceil() as usize; let validating_members = members .iter() .filter(|member| { if member.sp_addresses.is_empty() { return false; }; // This can happen when a member in the rule wasn't found in the network 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, // We use the pairing process id so we don't have to update the role if the user add a device pub validation_rules: Vec, pub storages: Vec, } impl RoleDefinition { pub fn is_satisfied( &self, diff: Vec, new_state_merkle_root: [u8; 32], proofs: &[Proof], members_list: &OutPointMemberMap, ) -> Result<()> { let empty_member = Member::new(vec![]); if diff.iter().all(|field| { self.validation_rules.iter().any(|rule| { let members: Vec<&Member> = self .members .iter() .map(|outpoint| { if let Some(member) = members_list.0.get(outpoint) { member } else { &empty_member } }) .collect(); rule.is_satisfied(field, new_state_merkle_root, proofs, &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() } } #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Roles(BTreeMap); impl IntoIterator for Roles { type Item = (String, RoleDefinition); type IntoIter = std::collections::btree_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl Roles { pub fn new(roles: BTreeMap) -> Self { Roles(roles) } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn to_bytes(&self) -> Result> { Ok(serde_json::to_vec(self)?) } pub fn len(&self) -> usize { self.0.len() } pub fn get(&self, key: &str) -> Option<&RoleDefinition> { self.0.get(key) } pub fn get_mut(&mut self, key: &str) -> Option<&mut RoleDefinition> { self.0.get_mut(key) } pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, RoleDefinition> { self.0.iter() } pub fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, RoleDefinition> { self.0.iter_mut() } } #[cfg(test)] mod tests { use serde_json::json; use sp_client::{ bitcoin::{secp256k1::SecretKey, Network}, SpClient, SpendKey, }; use std::{collections::HashMap, str::FromStr}; use super::*; use crate::{ pcd::Member, signature::{AnkHash, Proof}, }; fn create_alice_wallet() -> SpClient { SpClient::new( SecretKey::from_str("a67fb6bf5639efd0aeb19c1c584dd658bceda87660ef1088d4a29d2e77846973") .unwrap(), SpendKey::Secret( SecretKey::from_str( "a1e4e7947accf33567e716c9f4d186f26398660e36cf6d2e711af64b3518e65c", ) .unwrap(), ), Network::Signet, ) .unwrap() } fn create_bob_wallet() -> SpClient { SpClient::new( SecretKey::from_str("4d9f62b2340de3f0bafd671b78b19edcfded918c4106baefd34512f12f520e9b") .unwrap(), SpendKey::Secret( SecretKey::from_str( "dafb99602721577997a6fe3da54f86fd113b1b58f0c9a04783d486f87083a32e", ) .unwrap(), ), Network::Signet, ) .unwrap() } fn get_members_map(addresses: [String; 2]) -> HashMap { let alice_address = &addresses[0]; let bob_address = &addresses[1]; HashMap::from([ ( OutPoint::from_str( "b2f105a9df436d16b99e46453b15a0ffc584d136ceda35c0baea28e7e3ade8be:0", ) .unwrap(), Member::new(vec![ SilentPaymentAddress::try_from(alice_address.as_str()).unwrap() ]), ), ( OutPoint::from_str( "3cb9e3bf8ec72625c0347a665ab383fda9213d4544ff114ac800a9837b585897:0", ) .unwrap(), Member::new(vec![ SilentPaymentAddress::try_from(bob_address.as_str()).unwrap() ]), ), ]) } #[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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); // roles are not necessary here, we can leave it empty let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = commitments.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_spend_key().try_into().unwrap(); let bob_spend_key: SecretKey = bob_wallet.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_list = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); let members: Vec<&Member> = members_list.values().collect(); // 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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = commitments.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_spend_key().try_into().unwrap(); let bob_spend_key: SecretKey = bob_wallet.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_list = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); let members: Vec<&Member> = members_list.values().collect(); // 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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = commitments.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_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_list = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); let members: Vec<&Member> = members_list.values().collect(); // 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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = commitments.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_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_list = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); let members: Vec<&Member> = members_list.values().collect(); // 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_receiving_address(), ) .unwrap()]); let clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap(); let alice_spend_key: SecretKey = alice_wallet.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 = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); 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.keys().map(|k| *k).collect(), 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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = commitments.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_spend_key().try_into().unwrap(); let bob_spend_key: SecretKey = bob_wallet.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, &OutPointMemberMap(members) ) .is_ok()); } #[test] fn test_no_rule_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let members_list = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); 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_list.keys().cloned().collect(), 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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = 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_spend_key().try_into().unwrap(); let bob_spend_key: SecretKey = bob_wallet.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, &OutPointMemberMap(members_list) ) .is_err()); } #[test] fn test_partial_modification_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let members = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); 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.keys().cloned().collect(), 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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = 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_spend_key().try_into().unwrap(); let bob_spend_key: SecretKey = bob_wallet.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, &OutPointMemberMap(members) ) .is_ok()); } #[test] fn test_partial_modification_not_satisfied() { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let members = get_members_map([ alice_wallet.get_receiving_address().to_string(), bob_wallet.get_receiving_address().to_string(), ]); 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.keys().cloned().collect(), 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 clear_state_value = json!({"field1": "value1", "field2": "value2"}); let pcd: Pcd = clear_state_value.try_into().unwrap(); let public_data = BTreeMap::new(); let roles = BTreeMap::new(); let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data)); let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles)) .unwrap(); let new_state_merkle_root = 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_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, &OutPointMemberMap(members) ) .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); } }