From 21dccde051080bf26ce8302f46e29d35f4ef1876 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 8 Oct 2024 11:02:59 +0200 Subject: [PATCH] ProcessState validation tests + bug fixes --- src/process.rs | 414 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 394 insertions(+), 20 deletions(-) diff --git a/src/process.rs b/src/process.rs index 662d1c1..6d111f2 100644 --- a/src/process.rs +++ b/src/process.rs @@ -9,7 +9,7 @@ use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress}; use crate::{ crypto::AnkSharedSecretHash, - pcd::{AnkPcdHash, Pcd, RoleDefinition, ValidationRule}, + pcd::{AnkPcdHash, Pcd, RoleDefinition}, prd::Prd, signature::Proof, MutexExt, @@ -57,12 +57,11 @@ impl ProcessState { } } - pub fn is_valid( - &self, - previous_state: Option<&ProcessState>, - ) -> anyhow::Result<()> { + pub fn is_valid(&self, previous_state: Option<&ProcessState>) -> anyhow::Result<()> { if self.validation_tokens.is_empty() { - return Err(anyhow::anyhow!("Can't validate a state with no proofs attached")); + return Err(anyhow::anyhow!( + "Can't validate a state with no proofs attached" + )); } // Compute modified fields @@ -97,35 +96,35 @@ impl ProcessState { // Check if each modified field satisfies at least one applicable rule across all roles let all_fields_validated = modified_fields.iter().all(|field| { - println!("validating field: {}", field); // Collect applicable rules from all roles for the current field - let applicable_roles: Vec = roles2rules.iter() - .map(|(_, role_def)| { + let applicable_roles: Vec = roles2rules + .iter() + .filter_map(|(_, role_def)| { let mut filtered_role_def = role_def.clone(); let rules = filtered_role_def.get_applicable_rules(field); - filtered_role_def.validation_rules = rules.into_iter().map(|r| r.clone()).collect(); - filtered_role_def - }) + filtered_role_def.validation_rules = + rules.into_iter().map(|r| r.clone()).collect(); + if filtered_role_def.validation_rules.is_empty() { + None + } else { + Some(filtered_role_def) + } + }) .collect(); if applicable_roles.is_empty() { return false; // No rules apply to this field, consider it invalid } - println!("applicable_roles: {:?}", applicable_roles); - - // Check if any applicable rule is satisfied applicable_roles.into_iter().any(|role_def| { - let res = false; - for rule in role_def.validation_rules { + role_def.validation_rules.iter().any(|rule| { rule.is_satisfied( field, new_state_hash.clone(), &self.validation_tokens, &role_def.members, - ); - } - res + ) + }) }) }); @@ -318,3 +317,378 @@ pub fn lock_processes() -> Result .get_or_init(|| Mutex::new(HashMap::new())) .lock_anyhow() } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use serde_json::json; + use sp_client::{ + bitcoin::{secp256k1::SecretKey, Network}, + spclient::{SpClient, SpWallet, SpendKey}, + }; + + use crate::pcd::{Member, ValidationRule}; + use crate::signature::{AnkValidationNoHash, AnkValidationYesHash}; + + use super::*; + + 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() + } + + fn create_carol_wallet() -> SpWallet { + SpWallet::new( + SpClient::new( + "default".to_owned(), + SecretKey::from_str( + "e4a5906eaa1a7ab24d5fc8d9b600d47f79caa6511c056c111677b7a33e62c5e9", + ) + .unwrap(), + SpendKey::Secret( + SecretKey::from_str( + "e4c282e14668af1435e39df78403a7b406a791e3c6e666295496a6a865ade162", + ) + .unwrap(), + ), + None, + Network::Signet, + ) + .unwrap(), + None, + vec![], + ) + .unwrap() + } + + fn dummy_process_state() -> ProcessState { + let alice_wallet = create_alice_wallet(); + let bob_wallet = create_bob_wallet(); + let carol_wallet = create_carol_wallet(); + + let alice_address = + SilentPaymentAddress::try_from(alice_wallet.get_client().get_receiving_address()) + .unwrap(); + let bob_address = + SilentPaymentAddress::try_from(bob_wallet.get_client().get_receiving_address()) + .unwrap(); + let carol_address = + SilentPaymentAddress::try_from(carol_wallet.get_client().get_receiving_address()) + .unwrap(); + + let alice_bob = Member::new(vec![alice_address, bob_address]).unwrap(); + let carol = Member::new(vec![carol_address]).unwrap(); + + let validation_rule1 = + ValidationRule::new(1.0, vec!["field1".to_owned(), "roles".to_owned()], 0.5).unwrap(); + let validation_rule2 = ValidationRule::new(1.0, vec!["field2".to_owned()], 0.5).unwrap(); + + let encrypted_pcd = json!({ + "field1": "value1", + "field2": "value2", + "roles": { + "role1": { + "members": [alice_bob], + "validation_rules": [validation_rule1] + }, + "role2": { + "members": [carol], + "validation_rules": [validation_rule2] + } + } + }); + + let mut fields2keys = Map::new(); + let mut fields2cipher = Map::new(); + // let field_to_encrypt: Vec = encrypted_pcd.as_object().unwrap().keys().map(|k| k.clone()).collect(); + let field_to_encrypt = vec!["field1".to_string(), "field2".to_string()]; + + encrypted_pcd + .encrypt_fields(&field_to_encrypt, &mut fields2keys, &mut fields2cipher) + .unwrap(); + + ProcessState { + commited_in: OutPoint::null(), + encrypted_pcd, + keys: Map::new(), + validation_tokens: vec![], + } + } + + fn add_validation_token(state: &mut ProcessState, signing_key: SecretKey, accept: bool) { + let pcd_hash = AnkPcdHash::from_value(&state.encrypted_pcd); + if accept { + let validation_hash = AnkValidationYesHash::from_commitment(pcd_hash); + let proof = Proof::new( + crate::signature::AnkHash::ValidationYes(validation_hash), + signing_key, + ); + state.validation_tokens.push(proof); + } else { + let validation_hash = AnkValidationNoHash::from_commitment(pcd_hash); + let proof = Proof::new( + crate::signature::AnkHash::ValidationNo(validation_hash), + signing_key, + ); + state.validation_tokens.push(proof); + } + } + + #[test] + fn test_error_no_proofs() { + let state = dummy_process_state(); + let result = state.is_valid(None); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Can't validate a state with no proofs attached" + ); + } + + #[test] + fn test_error_identical_previous_state() { + let mut state = dummy_process_state(); + // We sign with a random key + let signing_key = + SecretKey::from_str("39b2a765dc93e02da04a0e9300224b4f99fa7b83cfae49036dff58613fd3277c") + .unwrap(); + add_validation_token(&mut state, signing_key, true); + let result = state.is_valid(Some(&state)); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "State is identical to the previous state" + ); + } + + #[test] + /// We provide a proof signed with a key that is not the spending key for either alice or bob + fn test_error_invalid_proof() { + let mut state = dummy_process_state(); + // We sign with a random key + let signing_key = + SecretKey::from_str("39b2a765dc93e02da04a0e9300224b4f99fa7b83cfae49036dff58613fd3277c") + .unwrap(); + add_validation_token(&mut state, signing_key, true); + let result = state.is_valid(None); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Not enough valid proofs"); + } + + #[test] + /// Carol signs alone for an init state + fn test_error_not_enough_signatures() { + let mut state = dummy_process_state(); + // We sign with Carol key + let carol_key: SecretKey = create_carol_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + add_validation_token(&mut state, carol_key, true); + let result = state.is_valid(None); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Not enough valid proofs"); + } + + #[test] + /// Alice signs alone for her fields in an init state + fn test_valid_just_enough_signatures() { + let mut state = dummy_process_state(); + // We sign with Alice and Carol keys + let alice_key: SecretKey = create_alice_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let carol_key: SecretKey = create_carol_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + add_validation_token(&mut state, alice_key, true); + add_validation_token(&mut state, carol_key, true); + let result = state.is_valid(None); + assert!(result.is_ok()); + } + + #[test] + /// everyone signs for everything + fn test_valid_all_signatures() { + let mut state = dummy_process_state(); + let alice_key: SecretKey = create_alice_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let bob_key: SecretKey = create_bob_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let carol_key: SecretKey = create_carol_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + add_validation_token(&mut state, alice_key, true); + add_validation_token(&mut state, bob_key, true); + add_validation_token(&mut state, carol_key, true); + let result = state.is_valid(None); + assert!(result.is_ok()); + } + + #[test] + /// Carol refuses change for her part + fn test_error_carol_votes_no() { + let mut state = dummy_process_state(); + let alice_key: SecretKey = create_alice_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let bob_key: SecretKey = create_bob_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let carol_key: SecretKey = create_carol_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + add_validation_token(&mut state, alice_key, true); + add_validation_token(&mut state, bob_key, true); + add_validation_token(&mut state, carol_key, false); + let result = state.is_valid(None); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Not enough valid proofs"); + } + + #[test] + /// Bob refuses change for his part, but Alice is enough to reach quorum + fn test_valid_bob_votes_no() { + let mut state = dummy_process_state(); + let alice_key: SecretKey = create_alice_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let bob_key: SecretKey = create_bob_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let carol_key: SecretKey = create_carol_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + add_validation_token(&mut state, alice_key, true); + add_validation_token(&mut state, bob_key, false); + add_validation_token(&mut state, carol_key, true); + let result = state.is_valid(None); + assert!(result.is_ok()); + } + + #[test] + /// Everyone signs, and we have a previous state + fn test_valid_everyone_signs_with_prev_state() { + let state = dummy_process_state(); + let mut new_state = state.clone(); + if let Value::Object(ref mut map) = new_state.encrypted_pcd { + // Modify the field + map.insert("field1".to_string(), Value::String("new_value1".to_owned())); + } else { + // Handle the case where encrypted_pcd is not an object + panic!("encrypted_pcd is not a JSON object."); + } + let alice_key: SecretKey = create_alice_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let bob_key: SecretKey = create_bob_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + let carol_key: SecretKey = create_carol_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + add_validation_token(&mut new_state, alice_key, true); + add_validation_token(&mut new_state, bob_key, true); + add_validation_token(&mut new_state, carol_key, true); + let result = new_state.is_valid(Some(&state)); + assert!(result.is_ok()); + } + + #[test] + /// Only Carol signs, but she doesn't have modification rights on modified field + fn test_error_not_right_signatures_with_prev_state() { + let state = dummy_process_state(); + let mut new_state = state.clone(); + if let Value::Object(ref mut map) = new_state.encrypted_pcd { + // Modify the field + map.insert("field1".to_string(), Value::String("new_value1".to_owned())); + } else { + // Handle the case where encrypted_pcd is not an object + panic!("encrypted_pcd is not a JSON object."); + } + let carol_key: SecretKey = create_carol_wallet() + .get_client() + .get_spend_key() + .try_into() + .unwrap(); + add_validation_token(&mut new_state, carol_key, true); + let result = new_state.is_valid(Some(&state)); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Not enough valid proofs"); + } +}