use anyhow::Result; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::{Mutex, MutexGuard, OnceLock}; use std::str::FromStr; use serde::{Deserialize, Serialize}; use sp_client::{ bitcoin::{OutPoint, Transaction}, silentpayments::SilentPaymentAddress, }; use tsify::Tsify; use crate::{ pcd::{Member, Pcd, PcdCommitments, RoleDefinition, Roles, ValidationRule}, serialization::{deserialize_hex, hex_array_btree, serialize_hex, OutPointMemberMap}, signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}, MutexExt, SpecialRoles, APOPHIS, PAIREDADDRESSES, PAIRING }; #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct ProcessState { pub commited_in: OutPoint, #[tsify(type = "Record")] pub pcd_commitment: PcdCommitments, #[serde(serialize_with = "serialize_hex", deserialize_with = "deserialize_hex")] #[tsify(type = "string")] pub state_id: [u8; 32], // the root of the tree created with all the commitments + public_data + roles. Serves as an unique id for a state too #[serde(with = "hex_array_btree")] #[tsify(type = "Record")] pub keys: BTreeMap, // We may not always have all the keys pub validation_tokens: Vec, // Signature of the hash of pcd_commitment tagged with some decision like "yes" or "no" pub public_data: Pcd, #[tsify(type = "Record")] pub roles: Roles, } impl ProcessState { pub fn new(commited_in: OutPoint, private_data: Pcd, public_data: Pcd, roles: Roles) -> anyhow::Result { // TODO check that we don't have duplicated field names in private and public data, nor a "roles" field name let all_attributes = Pcd::new(private_data.clone().into_iter().chain(public_data.clone()).collect()); let pcd_commitment = PcdCommitments::new(&commited_in, &all_attributes, &roles)?; let merkle_root = pcd_commitment.create_merkle_tree()?.root().ok_or(anyhow::Error::msg("Invalid merkle tree"))?; let res = Self { commited_in, pcd_commitment, state_id: merkle_root, keys: BTreeMap::new(), validation_tokens: vec![], public_data, roles, }; Ok(res) } pub fn update_value(&mut self, key: &str, new_value: &[u8]) -> anyhow::Result<()> { // Update the commitment let mut updated_commitments = self.pcd_commitment.clone(); updated_commitments.update_with_value(&self.commited_in, key, new_value)?; // Update merkle tree let merkle_tree = updated_commitments.create_merkle_tree()?; // Update state_id self.state_id = merkle_tree.root().ok_or_else(|| anyhow::Error::msg("Invalid merkle tree"))?; // Everything is ok, we can update the state self.pcd_commitment = updated_commitments; Ok(()) } pub fn get_message_hash(&self, approval: bool) -> anyhow::Result { if approval { Ok(AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(self.state_id))) } else { Ok(AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(self.state_id))) } } fn handle_demiurge(&self, demiurge_role: &RoleDefinition) -> anyhow::Result { if demiurge_role.members.is_empty() { return Err(anyhow::Error::msg("Invalid demiurge role: members can't be empty")); } // validation_rules is empty if !demiurge_role.validation_rules.is_empty() { return Err(anyhow::Error::msg("Invalid demiurge role: validation_rules must be empty")); } // if demiurge_role.storages.is_empty() { // return Err(anyhow::Error::msg("Invalid demiurge role: storages can't be empty")); // } let all_keys: Vec = self.pcd_commitment.keys().map(|k| k.clone()).collect(); // define the rule let validation_rule = ValidationRule::new( 1.0, all_keys.clone(), 1.0 )?; let role = RoleDefinition { members: demiurge_role.members.clone(), storages: demiurge_role.storages.clone(), validation_rules: vec![validation_rule] }; Ok(role) } /// This is a simplified and streamlined validation for obliteration state fn handle_obliteration(&self, apophis: &RoleDefinition, members_list: &OutPointMemberMap) -> anyhow::Result<()> { // Apophis should have only one rule if apophis.validation_rules.len() != 1 { return Err(anyhow::Error::msg("Should have only one rule")); }; let obliteration_rule = apophis.validation_rules.get(0).unwrap(); let empty_field = ""; // This rule should have only one empty string as field if obliteration_rule.fields.len() != 1 { return Err(anyhow::Error::msg("Should have only one field")); }; if obliteration_rule.fields.get(0).unwrap() != empty_field { return Err(anyhow::Error::msg("Field should be empty")); }; apophis.is_satisfied(vec![empty_field.to_owned()], [0u8; 32], &self.validation_tokens, &members_list) } fn handle_pairing(&self, pairing_role: RoleDefinition, previous_addresses: Vec) -> anyhow::Result<()> { // members must be empty if !pairing_role.members.is_empty() { return Err(anyhow::Error::msg("Invalid pairing role: members list must be empty")); } // pairing_role must have one rule that modifies pairedAddresses let paired_addresses_rule = pairing_role.get_applicable_rules(PAIREDADDRESSES); if paired_addresses_rule.len() != 1 { return Err(anyhow::anyhow!("Invalid pairing role: there must one and only one rule for \"pairedAddresses\"")); } // TODO check that it matches what we have in the commitment here or somewhere else? let updated_addresses_json = self.public_data.get_as_json(PAIREDADDRESSES)?; let updated_addresses_vec: Vec = serde_json::from_value(updated_addresses_json)?; let updated_member = Member::new(updated_addresses_vec); let previous_member = Member::new(previous_addresses); let members = if previous_member.get_addresses().is_empty() { vec![&updated_member] } else { vec![&previous_member] }; paired_addresses_rule.get(0).unwrap().is_satisfied(PAIREDADDRESSES, self.state_id, &self.validation_tokens, members.as_slice()) } pub fn is_valid(&self, previous_state: Option<&ProcessState>, members_list: &OutPointMemberMap) -> anyhow::Result<()> { if self.validation_tokens.is_empty() { return Err(anyhow::anyhow!( "Can't validate a state with no proofs attached" )); } // We first handle obliteration if self.state_id == [0u8; 32] { if let Some(prev_state) = previous_state { if let Some(apophis) = prev_state.roles.get(APOPHIS) { return self.handle_obliteration(apophis, members_list); } } return Err(anyhow::anyhow!("Can't find apophis")); // then pairing } else if let Some(pairing_role) = self.roles.get(PAIRING) { if self.pcd_commitment.contains_key(PAIREDADDRESSES) { if let Some(prev_state) = previous_state { let prev_paired_addresses_json = prev_state.public_data.get_as_json(PAIREDADDRESSES)?; let paired_addresses: Vec = serde_json::from_value(prev_paired_addresses_json)?; return self.handle_pairing(pairing_role.clone(), paired_addresses); } else { // We are in a creation return self.handle_pairing(pairing_role.clone(), vec![]); } } // If we don't update pairedAddresses, we don't need to bother about pairing } // Check if each modified field satisfies at least one applicable rule across all roles let all_fields_validated = !self.pcd_commitment.is_empty() && self.pcd_commitment.keys().all(|field| { // Collect applicable rules from all roles for the current field let applicable_roles: Vec = self.roles .iter() .filter_map(|(role_name, role_def)| { if let Ok(special_role) = SpecialRoles::from_str(&role_name) { match special_role { // We allow for a special case with a role that works only for initial state // That's optional though SpecialRoles::Demiurge => { // If we're not in initial state just ignore it if previous_state.is_some() { return None; } else { // We try to validate with demiurge match self.handle_demiurge(role_def) { Ok(role) => return Some(role), Err(e) => { log::error!("{}", e.to_string()); return None; } } } // Otherwise we just continue normal validation } // We already handled other special roles _ => return None } } 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(); 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 } applicable_roles.into_iter().any(|role_def| { role_def.validation_rules.iter().any(|rule| { let members: Vec<&Member> = role_def.members.iter() .filter_map(|outpoint| { members_list.0.get(outpoint) }) .collect(); rule.is_satisfied( field, self.state_id, &self.validation_tokens, members.as_slice(), ).is_ok() }) }) }); if all_fields_validated { Ok(()) } else { Err(anyhow::anyhow!("Not enough valid proofs")) } } pub fn is_empty(&self) -> bool { self.state_id == [0u8; 32] } pub fn get_fields_to_validate_for_member(&self, member: &OutPoint) -> anyhow::Result> { let mut res: HashSet = HashSet::new(); // Are we in that role? for (_, role_def) in self.roles.iter() { if !role_def.members.contains(member) { continue; } else { // what are the fields we can modify? for rule in &role_def.validation_rules { if rule.allows_modification() { res.extend(rule.fields.iter().map(|f| f.clone())); } } } } Ok(res.into_iter().collect()) } } /// A process is basically a succession of states /// The latest state MUST be an empty state with only the commited_in field set at the last unspent outpoint /// Commiting this last empty state in a transaction is called obliterating a process, basically terminating it #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Process { states: Vec, } impl Process { pub fn new( commited_in: OutPoint ) -> Self { let empty_state = ProcessState { commited_in, ..Default::default() }; Self { states: vec![empty_state], } } pub fn get_process_id(&self) -> anyhow::Result { Ok(self.states.get(0).ok_or(anyhow::Error::msg("Empty state list"))?.commited_in) } pub fn get_process_tip(&self) -> anyhow::Result { Ok(self.states.last().ok_or(anyhow::Error::msg("Empty state list"))?.commited_in) } pub fn get_last_unspent_outpoint(&self) -> anyhow::Result { if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); } let last_state = self.states.last().unwrap(); Ok(last_state.commited_in) } pub fn update_states_tip(&mut self, new_commitment: OutPoint) -> anyhow::Result<()> { if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); } let last_value = self.states.last().unwrap(); if !last_value.is_empty() { return Err(anyhow::Error::msg("Last value should be empty")); } if last_value.commited_in == new_commitment { return Err(anyhow::Error::msg("new_commitment is the same than existing tip")); } // Before updating we make sure that we only have one concurrent state let concurrent_states = self.get_latest_concurrent_states()?; if concurrent_states.len() != 2 { return Err(anyhow::Error::msg("We must have exactly one state for the current tip")); } // Replace the existing tip let new_tip = ProcessState { commited_in: new_commitment, ..Default::default() }; let _ = self.states.pop().unwrap(); self.states.push(new_tip); Ok(()) } /// We want to insert a new state that would be commited by the last UTXO /// The new state *must* have the same commited_in than the last empty one /// We want to always keep an empty state with only the latest unspent commited_in value at the last position pub fn insert_concurrent_state(&mut self, new_state: ProcessState) -> anyhow::Result<()> { if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); } let last_value = self.states.last().unwrap(); if !last_value.is_empty() { return Err(anyhow::Error::msg("Last value should be empty")); } if last_value.commited_in != new_state.commited_in { return Err(anyhow::Error::msg("A concurrent state must have the same commited in than the tip of the states")); } let empty_state = self.states.pop().unwrap(); self.states.push(new_state); // We always keep an empty state at the end self.states.push(empty_state); Ok(()) } pub fn get_state_at(&self, index: usize) -> Option<&ProcessState> { self.states.get(index) } pub fn get_state_at_mut(&mut self, index: usize) -> Option<&mut ProcessState> { self.states.get_mut(index) } pub fn get_latest_state(&self) -> Option<&ProcessState> { self.states.last() } pub fn get_latest_state_mut(&mut self) -> Option<&mut ProcessState> { self.states.last_mut() } /// If `state` is a concurrent state, parent is the last commited one /// Otherwise it's just the state commited before /// If it's the initial state we return `None` pub fn get_parent_state(&self, target_commited_in: &OutPoint) -> Option<&ProcessState> { let tip = self.get_process_tip().ok()?; // Use `?` for cleaner error handling. // If the target is the current tip, return the last committed state. if tip == *target_commited_in { return self.get_latest_commited_state(); } // Iterate over the states to find the parent. let mut parent_state = None; for state in &self.states { // Check if the current state's `commited_in` matches the target. if state.commited_in == *target_commited_in { return parent_state; // Return the parent state if found. } // Update the parent_state to the current state. parent_state = Some(state); } // Return `None` if no matching state is found. None } pub fn get_state_for_id(&self, state_id: &[u8; 32]) -> anyhow::Result<&ProcessState> { if self.get_number_of_states() == 0 { // This should never happen, but we better get rid of it now return Err(anyhow::Error::msg("process is empty".to_owned())); } for p in &self.states { if *state_id == p.state_id { return Ok(p); } } return Err(anyhow::Error::msg("No state for this merkle root")); } pub fn get_state_for_id_mut(&mut self, state_id: &[u8; 32]) -> anyhow::Result<&mut ProcessState> { if self.get_number_of_states() == 0 { // This should never happen, but we better get rid of it now return Err(anyhow::Error::msg("process is empty".to_owned())); } for p in &mut self.states { if *state_id == p.state_id { return Ok(p); } } return Err(anyhow::Error::msg("No state for this merkle root")); } /// This is useful when multiple unvalidated states are pending waiting for enough validations /// It returns the latest state and all the previous states that have the same commited_in /// It means that all the returned states are unvalidated and except for the one that get validated they will be pruned pub fn get_latest_concurrent_states(&self) -> anyhow::Result> { if self.get_number_of_states() == 0 { // This should never happen, but we better get rid of it now return Err(anyhow::Error::msg("process is empty".to_owned())); } let mut states = vec![]; let mut previous_commited_in = OutPoint::null(); // We iterate backwards until we find a state that has a different commited_in for state in self.states.iter().rev() { if previous_commited_in == OutPoint::null() { previous_commited_in = state.commited_in; } else if previous_commited_in != state.commited_in { break; } states.push(state); } Ok(states) } pub fn get_latest_concurrent_states_mut(&mut self) -> anyhow::Result> { if self.get_number_of_states() == 0 { // This should never happen, but we better get rid of it now return Err(anyhow::Error::msg("process is empty".to_owned())); } let mut states = vec![]; let mut previous_commited_in = OutPoint::null(); // We iterate backwards until we find a state that has a different commited_in for state in self.states.iter_mut().rev() { if previous_commited_in == OutPoint::null() { previous_commited_in = state.commited_in; } else if previous_commited_in != state.commited_in { break; } states.push(state); } Ok(states) } pub fn remove_all_concurrent_states(&mut self) -> anyhow::Result> { if self.get_number_of_states() == 0 { // This should never happen, but we better get rid of it now return Err(anyhow::Error::msg("process is empty".to_owned())); } let empty_state = self.states.pop().unwrap(); let last_commitment_outpoint = empty_state.commited_in; if let Some(split_index) = self.states.iter().position(|state| state.commited_in == last_commitment_outpoint) { let removed = self.states.split_off(split_index); // We make sure we always have an empty state at the end self.states.push(empty_state); Ok(removed) } else { // This could happen if we weren't aware there was pending concurrent updates // We push the empty state back and return an empty vec self.states.push(empty_state); Ok(vec![]) } } pub fn get_latest_commited_state(&self) -> Option<&ProcessState> { if self.states.is_empty() { return None; } let last_state = self.states.last().unwrap(); debug_assert!(last_state.is_empty()); // Last state must always be empty // We look for the last commited in before all the pending states let latest_outpoint = last_state.commited_in; return self .states .iter() .rev() .find(|s| s.commited_in != latest_outpoint); } pub fn get_state_index(&self, state: &ProcessState) -> Option { // Get the commited_in value of the provided state let target_commited_in = state.commited_in; // Find the index of the first state with the same commited_in value self.states .iter() .position(|s| s.commited_in == target_commited_in) } pub fn get_number_of_states(&self) -> usize { self.states.len() } } pub static CACHEDPROCESSES: OnceLock>> = OnceLock::new(); pub fn lock_processes() -> Result>, anyhow::Error> { CACHEDPROCESSES .get_or_init(|| Mutex::new(HashMap::new())) .lock_anyhow() } pub fn check_tx_for_process_updates(tx: &Transaction) -> anyhow::Result { let mut processes = lock_processes()?; for (outpoint, process) in &mut *processes { let process_tip = if let Ok(tip) = process.get_process_tip() { tip } else { continue }; for input in &tx.input { if process_tip == input.previous_output { log::debug!("Found a match for process tip {}", process_tip); // This transaction commits a new state // Look for the op_return let op_return_outputs: Vec<_> = tx.output.iter().filter(|o| o.script_pubkey.is_op_return()).collect(); if op_return_outputs.len() != 1 { return Err(anyhow::Error::msg("Transaction must contain exactly one op_return output")); } let mut state_id = [0u8; 32]; let data = &op_return_outputs.into_iter().next().unwrap().script_pubkey.as_bytes()[2..]; if data.len() != 32 { return Err(anyhow::Error::msg("commited data is not 32B long")); } state_id.clone_from_slice(data); // Check if we know about the commited state let new_state: ProcessState; if let Ok(commited_state) = process.get_state_for_id(&state_id) { new_state = commited_state.clone(); } else { new_state = ProcessState { commited_in: process_tip, state_id, ..Default::default() }; } // We update the process in place process.remove_all_concurrent_states()?; process.insert_concurrent_state(new_state)?; process.update_states_tip(OutPoint::new(tx.txid(), 0))?; return Ok(*outpoint) } } } Err(anyhow::Error::msg("Transaction doesn't spend any known commitment")) } #[cfg(test)] mod tests { use std::str::FromStr; use serde_json::{json, Value}; use sp_client::{ bitcoin::{secp256k1::SecretKey, Network, Txid}, silentpayments::SilentPaymentAddress, SpClient, SpendKey }; use crate::pcd::{Member, ValidationRule, ZSTD_COMPRESSION_LEVEL}; use super::*; const ALICE_BOB_PAIRING: &str = "fcd21fcf92c8ddd74bb2726138bee9951946ca03b10c1297accd67da159df82b:0"; const CAROL_PAIRING: &str = "a4f3d2d5ca7af258e6a2c1cfe85b85d4e3f3d1387417fd64012d3c7bfb95a9e9:0"; const DAVE_PAIRING: &str = "bd21f6acdd0e026e8c02298a51ec40dfaced34d95aec685f407ab5ac91b5f775:0"; 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 create_carol_wallet() -> SpClient { SpClient::new( SecretKey::from_str( "e4a5906eaa1a7ab24d5fc8d9b600d47f79caa6511c056c111677b7a33e62c5e9", ) .unwrap(), SpendKey::Secret( SecretKey::from_str( "e4c282e14668af1435e39df78403a7b406a791e3c6e666295496a6a865ade162", ) .unwrap(), ), Network::Signet, ) .unwrap() } fn create_dave_wallet() -> SpClient { SpClient::new( SecretKey::from_str( "261d5f9ae4d2b0d8b17ed0c52bd2be7dbce14d9ac1f0f1d4904d3ca7df03766d", ) .unwrap(), SpendKey::Secret( SecretKey::from_str( "8441e2adbb39736f384617fafc61e0d894bf3a5c2b69801fd4476bdcce04fb59", ) .unwrap(), ), Network::Signet, ) .unwrap() } fn get_members_map() -> HashMap { let alice_wallet = create_alice_wallet(); let bob_wallet = create_bob_wallet(); let carol_wallet = create_carol_wallet(); let dave_wallet = create_dave_wallet(); let alice_address = SilentPaymentAddress::try_from(alice_wallet.get_receiving_address()) .unwrap(); let bob_address = SilentPaymentAddress::try_from(bob_wallet.get_receiving_address()) .unwrap(); let carol_address = SilentPaymentAddress::try_from(carol_wallet.get_receiving_address()) .unwrap(); let dave_address = SilentPaymentAddress::try_from(dave_wallet.get_receiving_address()) .unwrap(); let members_map: HashMap = HashMap::from([ (OutPoint::from_str(ALICE_BOB_PAIRING).unwrap(), Member::new(vec![alice_address, bob_address])), (OutPoint::from_str(CAROL_PAIRING).unwrap(), Member::new(vec![carol_address])), (OutPoint::from_str(DAVE_PAIRING).unwrap(), Member::new(vec![dave_address])), ]); members_map } fn dummy_process_state() -> ProcessState { let validation_rule1 = ValidationRule::new(1.0, vec!["field1".to_owned()], 0.5).unwrap(); let validation_rule2 = ValidationRule::new(1.0, vec!["field2".to_owned()], 0.5).unwrap(); let validation_rule3 = ValidationRule::new(1.0, vec!["roles".to_owned()], 0.5).unwrap(); let validation_rule4 = ValidationRule::new(1.0, vec!["public1".to_owned(), "public2".to_owned()], 0.5).unwrap(); let apophis_rule = ValidationRule::new(1.0, vec!["".to_owned()], 1.0).unwrap(); let role_demiurge = RoleDefinition { members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()], validation_rules: vec![], storages: vec![] }; let role_def1 = RoleDefinition { members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()], validation_rules: vec![validation_rule1], storages: vec![] }; let role_def2 = RoleDefinition { members: vec![OutPoint::from_str(CAROL_PAIRING).unwrap()], validation_rules: vec![validation_rule2], storages: vec![] }; let role_def_roles = RoleDefinition { members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()], validation_rules: vec![validation_rule3], storages: vec![] }; let role_def_public_data = RoleDefinition { members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()], validation_rules: vec![validation_rule4], storages: vec![] }; let role_def_apophis = RoleDefinition { members: vec![OutPoint::from_str(DAVE_PAIRING).unwrap()], validation_rules: vec![apophis_rule], storages: vec![] }; let roles: BTreeMap = BTreeMap::from([ ("demiurge".to_owned(), role_demiurge), ("role1".to_owned(), role_def1), ("role2".to_owned(), role_def2), ("role_roles".to_owned(), role_def_roles), ("role_public_data".to_owned(), role_def_public_data), ("apophis".to_owned(), role_def_apophis) ]); let private_data: Pcd = json!({ "field1": "value1", "field2": "value2", }).try_into().unwrap(); let outpoint = OutPoint::null(); let public_data: Pcd = json!({ "public1": "public1", "public2": "public2", }).try_into().unwrap(); ProcessState::new(outpoint, private_data, public_data, Roles::new(roles)).unwrap() } fn create_pairing_process_one() -> ProcessState { let carol_wallet = create_carol_wallet(); let carol_address = carol_wallet.get_receiving_address(); let pairing_rule = ValidationRule::new(1.0, vec![PAIREDADDRESSES.to_owned()], 1.0).unwrap(); let pairing_role_def = RoleDefinition { members: vec![], validation_rules: vec![pairing_rule], storages: vec![] }; let outpoint = OutPoint::from_str("8425e9749957fd8ca83460c21718be4017692fd3ae61cbb0f0d401e7a5ddce6a:0").unwrap(); let private_data: Pcd = json!({"description": "pairing"}).try_into().unwrap(); let paired_addresses: Pcd = json!({PAIREDADDRESSES: [carol_address]}).try_into().unwrap(); ProcessState::new(outpoint, private_data, paired_addresses, Roles::new(BTreeMap::from([(PAIRING.to_owned(), pairing_role_def)]))).unwrap() } fn create_pairing_process_two() -> ProcessState { let carol_wallet = create_carol_wallet(); let carol_address = carol_wallet.get_receiving_address(); let dave_wallet = create_dave_wallet(); let dave_address = dave_wallet.get_receiving_address(); let pairing_rule = ValidationRule::new(1.0, vec![PAIREDADDRESSES.to_owned()], 1.0).unwrap(); let pairing_role_def = RoleDefinition { members: vec![], validation_rules: vec![pairing_rule], storages: vec![] }; let outpoint = OutPoint::from_str("8425e9749957fd8ca83460c21718be4017692fd3ae61cbb0f0d401e7a5ddce6a:0").unwrap(); let private_data: Pcd = json!({"description": "pairing"}).try_into().unwrap(); let paired_addresses: Pcd = json!({PAIREDADDRESSES: [carol_address, dave_address]}).try_into().unwrap(); ProcessState::new(outpoint, private_data, paired_addresses, Roles::new(BTreeMap::from([(PAIRING.to_owned(), pairing_role_def)]))).unwrap() } #[test] fn test_error_no_proofs() { let state = dummy_process_state(); let members_map = get_members_map(); let result = state.is_valid(None, &OutPointMemberMap(members_map)); assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), "Can't validate a state with no proofs attached" ); } #[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(); let message_hash = state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, signing_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); 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_spend_key() .try_into() .unwrap(); let message_hash = state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); 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_spend_key() .try_into() .unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, alice_key)); state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); assert!(result.is_ok()); } #[test] /// AliceBob is the demiurge fn test_valid_demiurge() { let mut state = dummy_process_state(); // We sign with Alice and Carol keys let alice_key: SecretKey = create_alice_wallet() .get_spend_key() .try_into() .unwrap(); let bob_key: SecretKey = create_bob_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, alice_key)); state.validation_tokens.push(Proof::new(message_hash, bob_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); assert!(result.is_ok()); } #[test] /// Carol tries to bypass demiurge role fn test_error_demiurge() { let mut state = dummy_process_state(); // We sign with Alice and Carol keys let carol_key: SecretKey = create_alice_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); assert!(result.is_err()); } #[test] /// Carol tries to bypass demiurge role fn test_error_demiurge_not_init_state() { let mut state = dummy_process_state(); // We sign with Alice and Carol keys let carol_key: SecretKey = create_alice_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = state.is_valid(Some(&dummy_process_state()), &OutPointMemberMap(get_members_map())); assert!(result.is_err()); } #[test] fn test_valid_pairing() { let mut pairing_first_state = create_pairing_process_one(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = pairing_first_state.get_message_hash(true).unwrap(); pairing_first_state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = pairing_first_state.is_valid(None, &OutPointMemberMap(get_members_map())); assert!(result.is_ok()); } #[test] fn test_error_pairing_wrong_proof() { let mut pairing_first_state = create_pairing_process_one(); let alice_key: SecretKey = create_alice_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = pairing_first_state.get_message_hash(true).unwrap(); pairing_first_state.validation_tokens.push(Proof::new(message_hash, alice_key)); let result = pairing_first_state.is_valid(None, &OutPointMemberMap(get_members_map())); assert!(result.is_err()); } #[test] fn test_valid_pairing_add_device() { let pairing_state = create_pairing_process_one(); let mut paired_addresses: Vec = serde_json::from_value(pairing_state.public_data.get_as_json(PAIREDADDRESSES).unwrap()).unwrap(); let mut pairing_process = Process::new(pairing_state.commited_in); pairing_process.insert_concurrent_state(pairing_state).unwrap(); let new_commitment = OutPoint::from_str("7f1a6d8923d6ee58a075c0e99e25472bb22a3eea221739281c2beaf829f03f27:0").unwrap(); pairing_process.update_states_tip(new_commitment).unwrap(); let members_list = &OutPointMemberMap(HashMap::from( [ ( pairing_process.get_process_id().unwrap(), Member::new( paired_addresses.clone() )) ] )); // Add Dave address let dave_address = create_dave_wallet().get_receiving_address(); paired_addresses.push(dave_address); let roles = &pairing_process.get_latest_commited_state().unwrap().roles; let mut add_device_state = ProcessState::new(new_commitment, Pcd::new(BTreeMap::new()), json!({PAIREDADDRESSES: paired_addresses}).try_into().unwrap(), roles.clone()).unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = add_device_state.get_message_hash(true).unwrap(); add_device_state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = add_device_state.is_valid(pairing_process.get_latest_commited_state(), members_list); assert!(result.is_ok()); } #[test] fn test_valid_pairing_rm_device() { let pairing_state = create_pairing_process_two(); let paired_addresses: Vec = serde_json::from_value(pairing_state.public_data.get_as_json(PAIREDADDRESSES).unwrap()).unwrap(); let mut pairing_process = Process::new(pairing_state.commited_in); pairing_process.insert_concurrent_state(pairing_state).unwrap(); let new_commitment = OutPoint::from_str("7f1a6d8923d6ee58a075c0e99e25472bb22a3eea221739281c2beaf829f03f27:0").unwrap(); pairing_process.update_states_tip(new_commitment).unwrap(); let members_list = &OutPointMemberMap(HashMap::from( [ ( pairing_process.get_process_id().unwrap(), Member::new( paired_addresses.clone() )) ] )); // Remove Dave address let dave_address = create_dave_wallet().get_receiving_address(); let filtered_paired_addresses: Vec = paired_addresses.into_iter().filter_map(|a| { if a != dave_address { Some(a) } else { None } }) .collect(); let roles = &pairing_process.get_latest_commited_state().unwrap().roles; let mut rm_device_state = ProcessState::new( new_commitment, Pcd::new(BTreeMap::new()), json!({PAIREDADDRESSES: filtered_paired_addresses}).try_into().unwrap(), roles.clone() ).unwrap(); // We need both devices to agree to remove one let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let dave_key: SecretKey = create_dave_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = rm_device_state.get_message_hash(true).unwrap(); rm_device_state.validation_tokens.push(Proof::new(message_hash, carol_key)); rm_device_state.validation_tokens.push(Proof::new(message_hash, dave_key)); let result = rm_device_state.is_valid(pairing_process.get_latest_commited_state(), members_list); assert!(result.is_ok()); } /// We check that Dave can't add himself to the pairing #[test] fn test_error_pairing_add_device_wrong_signature() { let pairing_state = create_pairing_process_one(); let mut paired_addresses: Vec = serde_json::from_value(pairing_state.public_data.get_as_json(PAIREDADDRESSES).unwrap()).unwrap(); let mut pairing_process = Process::new(pairing_state.commited_in); pairing_process.insert_concurrent_state(pairing_state).unwrap(); let new_commitment = OutPoint::from_str("7f1a6d8923d6ee58a075c0e99e25472bb22a3eea221739281c2beaf829f03f27:0").unwrap(); pairing_process.update_states_tip(new_commitment).unwrap(); let members_list = &OutPointMemberMap(HashMap::from( [ ( pairing_process.get_process_id().unwrap(), Member::new( paired_addresses.clone() )) ] )); // Add Dave address let dave_address = create_dave_wallet().get_receiving_address(); paired_addresses.push(dave_address); let roles = &pairing_process.get_latest_commited_state().unwrap().roles; let mut add_device_state = ProcessState::new(new_commitment, Pcd::new(BTreeMap::new()), json!({PAIREDADDRESSES: paired_addresses}).try_into().unwrap(), roles.clone()).unwrap(); let dave_key: SecretKey = create_dave_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = add_device_state.get_message_hash(true).unwrap(); add_device_state.validation_tokens.push(Proof::new(message_hash, dave_key)); let result = add_device_state.is_valid(pairing_process.get_latest_commited_state(), members_list); assert!(result.is_err()); } #[test] /// everyone signs for everything fn test_valid_all_signatures() { let mut state = dummy_process_state(); let alice_key: SecretKey = create_alice_wallet() .get_spend_key() .try_into() .unwrap(); let bob_key: SecretKey = create_bob_wallet() .get_spend_key() .try_into() .unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, alice_key)); state.validation_tokens.push(Proof::new(message_hash, bob_key)); state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); assert!(result.is_ok()); } #[test] /// Carol tries to obliterate the process but she's not apophis fn test_error_invalid_obliteration() { let mut state = dummy_process_state(); let mut process = Process::new(state.commited_in); process.insert_concurrent_state(state.clone()).unwrap(); // We simulate a first commitment process.update_states_tip( OutPoint::new( Txid::from_str( "cbeb4455f8d11848809bacd59bfd570243dbe7c4e9a340fa949aae3020fdb127" ).unwrap() , 0 ) ).unwrap(); // Now we take the last empty state and try to invalidate it let empty_state = process.get_state_for_id(&[0u8; 32]).unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = empty_state.get_message_hash(true).unwrap(); state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); assert!(result.is_err()); } #[test] /// Dave signs to obliterate the process fn test_valid_obliteration() { let state = dummy_process_state(); let mut process = Process::new(state.commited_in); process.insert_concurrent_state(state.clone()).unwrap(); // We simulate a first commitment process.update_states_tip( OutPoint::new( Txid::from_str( "cbeb4455f8d11848809bacd59bfd570243dbe7c4e9a340fa949aae3020fdb127" ).unwrap() , 0 ) ).unwrap(); // Now we take the last empty state and try to commit it to invalidate the whole process let empty_state = process.get_state_for_id_mut(&[0u8; 32]).unwrap(); let dave_key: SecretKey = create_dave_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = empty_state.get_message_hash(true).unwrap(); empty_state.validation_tokens.push(Proof::new(message_hash, dave_key)); let result = empty_state.is_valid(Some(&state), &OutPointMemberMap(get_members_map())); 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_spend_key() .try_into() .unwrap(); let bob_key: SecretKey = create_bob_wallet() .get_spend_key() .try_into() .unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash_yes = state.get_message_hash(true).unwrap(); let message_hash_no = state.get_message_hash(false).unwrap(); state.validation_tokens.push(Proof::new(message_hash_yes, alice_key)); state.validation_tokens.push(Proof::new(message_hash_yes, bob_key)); state.validation_tokens.push(Proof::new(message_hash_no, carol_key)); let result = state.is_valid(Some(&dummy_process_state()), &OutPointMemberMap(get_members_map())); 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_spend_key() .try_into() .unwrap(); let bob_key: SecretKey = create_bob_wallet() .get_spend_key() .try_into() .unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash_yes = state.get_message_hash(true).unwrap(); let message_hash_no = state.get_message_hash(false).unwrap(); state.validation_tokens.push(Proof::new(message_hash_yes, alice_key)); state.validation_tokens.push(Proof::new(message_hash_no, bob_key)); state.validation_tokens.push(Proof::new(message_hash_yes, carol_key)); let result = state.is_valid(None, &OutPointMemberMap(get_members_map())); 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(); let key_to_modify = state.pcd_commitment.keys().into_iter().next().unwrap(); let mut serialized_value = Vec::new(); serde_json::to_writer(&mut serialized_value, &Value::String("new_value1".to_owned())).unwrap(); let updated_value = zstd::encode_all(serialized_value.as_slice(), ZSTD_COMPRESSION_LEVEL).unwrap(); new_state.update_value(key_to_modify.as_str(), updated_value.as_slice()).unwrap(); let alice_key: SecretKey = create_alice_wallet() .get_spend_key() .try_into() .unwrap(); let bob_key: SecretKey = create_bob_wallet() .get_spend_key() .try_into() .unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = new_state.get_message_hash(true).unwrap(); new_state.validation_tokens.push(Proof::new(message_hash, alice_key)); new_state.validation_tokens.push(Proof::new(message_hash, bob_key)); new_state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = new_state.is_valid(Some(&state), &OutPointMemberMap(get_members_map())); 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(); let key_to_modify = state.pcd_commitment.keys().into_iter().next().unwrap(); let mut serialized_value = Vec::new(); serde_json::to_writer(&mut serialized_value, &Value::String("new_value1".to_owned())).unwrap(); let updated_value = zstd::encode_all(serialized_value.as_slice(), ZSTD_COMPRESSION_LEVEL).unwrap(); new_state.update_value(key_to_modify.as_str(), updated_value.as_slice()).unwrap(); let carol_key: SecretKey = create_carol_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = new_state.get_message_hash(true).unwrap(); new_state.validation_tokens.push(Proof::new(message_hash, carol_key)); let result = new_state.is_valid(Some(&state), &OutPointMemberMap(get_members_map())); assert!(result.is_err()); } #[test] fn test_valid_add_someone_role() { let mut state = dummy_process_state(); if let Some(role) = state.roles.get_mut(APOPHIS) { role.members = vec![]; } let mut process = Process::new(state.commited_in); process.insert_concurrent_state(state).unwrap(); process.update_states_tip( OutPoint::new( Txid::from_str( "cbeb4455f8d11848809bacd59bfd570243dbe7c4e9a340fa949aae3020fdb127" ).unwrap() , 0 ) ).unwrap(); // We now try to add dave into apophis role let mut new_roles = process.get_latest_commited_state().unwrap().roles.clone(); if let Some(role) = new_roles.get_mut(APOPHIS) { role.members = vec![OutPoint::from_str(DAVE_PAIRING).unwrap()]; } let mut new_state = ProcessState::new(process.get_process_tip().unwrap(), Pcd::new(BTreeMap::new()), Pcd::new(BTreeMap::new()), new_roles).unwrap(); // Alice and Bob validate let alice_key: SecretKey = create_alice_wallet() .get_spend_key() .try_into() .unwrap(); let bob_key: SecretKey = create_bob_wallet() .get_spend_key() .try_into() .unwrap(); let message_hash = new_state.get_message_hash(true).unwrap(); new_state.validation_tokens.push(Proof::new(message_hash, alice_key)); new_state.validation_tokens.push(Proof::new(message_hash, bob_key)); let result = new_state.is_valid(process.get_parent_state(&new_state.commited_in), &OutPointMemberMap(get_members_map())); assert!(result.is_ok()); } }