diff --git a/src/network.rs b/src/network.rs index 3724e9d..c79619e 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use aes_gcm::aead::{Aead, Payload}; use aes_gcm::{Aes256Gcm, KeyInit}; use anyhow::{Error, Result}; @@ -12,7 +14,7 @@ use tsify::Tsify; use crate::crypto::AAD; use crate::error::AnkError; -use crate::pcd::Member; +use crate::pcd::{Member, RoleDefinition}; use crate::signature::Proof; #[derive(Debug, Serialize, Deserialize, Tsify)] @@ -72,8 +74,8 @@ impl AnkFlag { #[tsify(into_wasm_abi, from_wasm_abi)] pub struct CommitMessage { pub init_tx: String, // Can be tx or txid of the first transaction of the chain, which is maybe not ideal - pub encrypted_pcd: Map, - pub keys: Map, + pub pcd_commitment: Value, // map of field <=> hash of the clear value + pub roles: HashMap, // Can be hashed and compared with the value above pub validation_tokens: Vec, pub error: Option, } @@ -84,13 +86,13 @@ impl CommitMessage { /// validation_tokens must be empty pub fn new_first_commitment( transaction: Transaction, - encrypted_pcd: Map, - keys: Map, + pcd_commitment: Value, + roles: HashMap, ) -> Self { Self { init_tx: serialize(&transaction).to_lower_hex_string(), - encrypted_pcd, - keys, + pcd_commitment, + roles, validation_tokens: vec![], error: None, } @@ -101,13 +103,13 @@ impl CommitMessage { /// validation_tokens must be empty pub fn new_update_commitment( init_tx: OutPoint, - encrypted_pcd: Map, - keys: Map, + pcd_commitment: Value, + roles: HashMap, ) -> Self { Self { init_tx: init_tx.to_string(), - encrypted_pcd, - keys, + pcd_commitment, + roles, validation_tokens: vec![], error: None, } diff --git a/src/pcd.rs b/src/pcd.rs index 4c8cbe3..4cc6bda 100644 --- a/src/pcd.rs +++ b/src/pcd.rs @@ -1,5 +1,6 @@ use anyhow::{Error, Result}; -use std::{collections::HashSet, str::FromStr}; +use rs_merkle::{algorithms::Sha256, Hasher, MerkleTree}; +use std::{collections::{HashMap, HashSet}, str::FromStr}; use aes_gcm::{ aead::{Aead, Payload}, @@ -11,9 +12,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use sp_client::{ bitcoin::{ - hashes::{sha256t_hash_newtype, Hash, HashEngine}, - hex::{DisplayHex, FromHex}, - XOnlyPublicKey, + consensus::serialize, hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, secp256k1::PublicKey, OutPoint, XOnlyPublicKey }, silentpayments::utils::SilentPaymentAddress, }; @@ -85,17 +84,57 @@ impl AnkPcdHash { AnkPcdHash::from_engine(eng) } - pub fn from_map(map: &Map) -> Self { - let value = Value::Object(map.clone()); + /// 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: &Value, outpoint: &[u8]) -> Self { let mut eng = AnkPcdHash::engine(); + eng.input(outpoint); eng.input(value.to_string().as_bytes()); AnkPcdHash::from_engine(eng) } } pub trait Pcd<'a>: Serialize + Deserialize<'a> { - fn tagged_hash(&self) -> AnkPcdHash { - AnkPcdHash::from_value(&self.to_value()) + fn 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 hash_fields(&self, root_commitment: OutPoint) -> Result> { + let map = self.to_value_object()?; + + let outpoint = serialize(&root_commitment); + + let mut field2hash = Map::with_capacity(map.len()); + // this could be optimised since there's a midstate we're reusing + for (field, value) in map { + let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value, &outpoint); + field2hash.insert(field, Value::String(tagged_hash.to_string())); + } + + Ok(field2hash) + } + + /// We need to run `hash_fields` before + /// This will just take all the hash value and produces a merkle tree + fn create_merkle_tree(fields2hash: &Value) -> Result> { + let leaves: Vec<[u8; 32]> = fields2hash + .as_object() + .unwrap() + .iter() + .map(|(_, value)| { + let mut res = [0u8; 32]; + res.copy_from_slice(&Vec::from_hex(value.as_str().unwrap()).unwrap()); + res + }) + .collect(); + + let merkle_tree = MerkleTree::::from_leaves(&leaves); + + Ok(merkle_tree) } fn encrypt_fields( @@ -104,14 +143,11 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { fields2keys: &mut Map, fields2cipher: &mut Map, ) -> Result<()> { - let as_value = self.to_value(); - let as_map = as_value - .as_object() - .ok_or_else(|| Error::msg("Expected object"))?; + let map = self.to_value_object()?; let mut rng = thread_rng(); - for (field, value) in as_map { - if fields_to_encrypt.contains(field) { + for (field, value) in map { + if fields_to_encrypt.contains(&field) { let aes_key = Aes256Gcm::generate_key(&mut rng); let nonce = Aes256Gcm::generate_nonce(&mut rng); fields2keys.insert( @@ -125,9 +161,9 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { 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 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); @@ -147,8 +183,7 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { fields2keys: &Map, fields2plain: &mut Map, ) -> Result<()> { - let value = self.to_value(); - let map = value.as_object().unwrap(); + let map = self.to_value_object()?; for (field, encrypted_value) in map.iter() { if let Some(aes_key) = fields2keys.get(field) { @@ -190,8 +225,41 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { Ok(()) } - fn to_value(&self) -> Value { - Value::from_str(&serde_json::to_string(&self).unwrap()).unwrap() + fn to_value_object(&self) -> Result> { + let value = serde_json::to_value(self)?; + + match value { + Value::Object(map) => Ok(map), + _ => Err(Error::msg("self is 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")) + } } } @@ -313,7 +381,7 @@ impl ValidationRule { } } -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct RoleDefinition { pub members: Vec, diff --git a/src/prd.rs b/src/prd.rs index 87c631d..f83e8e0 100644 --- a/src/prd.rs +++ b/src/prd.rs @@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; use sp_client::bitcoin::hex::FromHex; -use sp_client::bitcoin::secp256k1::SecretKey; -use sp_client::bitcoin::{OutPoint, Psbt, XOnlyPublicKey}; +use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; +use sp_client::bitcoin::{OutPoint, Psbt}; use sp_client::silentpayments::utils::SilentPaymentAddress; use sp_client::spclient::SpWallet; use tsify::Tsify; @@ -26,9 +26,9 @@ pub enum PrdType { Message, Update, // Update an existing process List, // request a list of items - Response, - Confirm, - TxProposal, // Send a psbt asking for recipient signature, used for login not sure about other use cases + Response, // Validate (or disagree) with a prd update + Confirm, // Confirm we received an update + TxProposal, // Send a psbt asking for recipient signature } sha256t_hash_newtype! { @@ -61,8 +61,9 @@ pub struct Prd { pub root_commitment: String, pub sender: String, pub keys: Map, // key is a key in pcd, value is the key to decrypt it + pub pcd_commitments: Value, pub validation_tokens: Vec, - pub payload: String, // Payload depends on the actual type + pub payload: String, // Additional information depending on the type pub proof: Option, // This must be None up to the creation of the network message } @@ -76,6 +77,7 @@ impl Prd { Self { prd_type: PrdType::Connect, root_commitment: String::default(), + pcd_commitments: Value::Null, sender: serde_json::to_string(&sender).unwrap(), validation_tokens, keys: Map::new(), @@ -87,8 +89,9 @@ impl Prd { pub fn new_update( root_commitment: OutPoint, sender: String, // Should take Member as argument - encrypted_pcd: Map, + encrypted_values_merkle_root: String, keys: Map, + pcd_commitments: Value, ) -> Self { Self { prd_type: PrdType::Update, @@ -96,19 +99,8 @@ impl Prd { sender, validation_tokens: vec![], keys, - payload: Value::Object(encrypted_pcd).to_string(), - proof: None, - } - } - - pub fn new_tx_proposal(root_commitment: OutPoint, sender: Member, psbt: Psbt) -> Self { - Self { - prd_type: PrdType::TxProposal, - root_commitment: root_commitment.to_string(), - sender: serde_json::to_string(&sender).unwrap(), - validation_tokens: vec![], - keys: Map::new(), - payload: serde_json::to_string(&psbt).unwrap(), + pcd_commitments, + payload: encrypted_values_merkle_root, proof: None, } } @@ -117,7 +109,7 @@ impl Prd { root_commitment: OutPoint, sender: String, validation_tokens: Vec, - pcd_commitment: AnkPcdHash, + pcd_commitments: Value, ) -> Self { Self { prd_type: PrdType::Response, @@ -125,7 +117,8 @@ impl Prd { sender, validation_tokens: validation_tokens, keys: Map::new(), - payload: pcd_commitment.to_string(), + pcd_commitments, + payload: String::default(), proof: None, } } @@ -133,29 +126,22 @@ impl Prd { pub fn new_confirm( root_commitment: OutPoint, sender: Member, - pcd_commitment: AnkPcdHash, + pcd_commitments: Value, ) -> Self { Self { prd_type: PrdType::Confirm, root_commitment: root_commitment.to_string(), + pcd_commitments, sender: serde_json::to_string(&sender).unwrap(), validation_tokens: vec![], keys: Map::new(), - payload: pcd_commitment.to_string(), + payload: String::default(), proof: None, } } - fn _extract_from_message(plain: &[u8], local_address: SilentPaymentAddress, commitment: Option<&AnkPrdHash>) -> Result { + pub fn extract_from_message(plain: &[u8], local_address: SilentPaymentAddress) -> Result { let prd: Prd = serde_json::from_slice(plain)?; - if let Some(commitment) = commitment { - // check that the hash of the prd is consistent with what's commited in the op_return - if prd.create_commitment() != *commitment { - return Err(anyhow::Error::msg( - "Received prd is not what was commited in the transaction", - )); - } - } // check that the proof is consistent if let Some(proof) = prd.proof { @@ -190,23 +176,9 @@ impl Prd { } else { log::warn!("No proof for prd with root_commitment {}", prd.root_commitment); } - // check that the commitment outpoint is valid, just in case - OutPoint::from_str(&prd.root_commitment)?; Ok(prd) } - pub fn extract_from_message(plain: &[u8], local_address: SilentPaymentAddress) -> Result { - Self::_extract_from_message(plain, local_address, None) - } - - pub fn extract_from_message_with_commitment( - plain: &[u8], - local_address: SilentPaymentAddress, - commitment: &AnkPrdHash, - ) -> Result { - Self::_extract_from_message(plain, local_address, Some(commitment)) - } - pub fn filter_keys(&mut self, to_keep: HashSet) { let current_keys = self.keys.clone(); let filtered_keys: Map = current_keys @@ -216,25 +188,6 @@ impl Prd { self.keys = filtered_keys; } - /// We commit to everything except the keys and the proof - /// Because 1) we need one commitment to common data for all recipients of the transaction - /// 2) we already commit to the keys in the sender proof anyway - pub fn create_commitment(&self) -> AnkPrdHash { - let mut to_commit = self.clone(); - to_commit.keys = Map::new(); - to_commit.proof = None; - to_commit.validation_tokens = vec![]; - - if to_commit.payload.len() != 64 && Vec::from_hex(&to_commit.payload).is_err() { - to_commit.payload = Value::from_str(&to_commit.payload) - .unwrap() - .tagged_hash() - .to_string(); - } - - AnkPrdHash::from_value(&to_commit.to_value()) - } - /// Generate the signed proof and serialize to send over the network pub fn to_network_msg(&self, sp_wallet: &SpWallet) -> Result { let spend_sk: SecretKey = sp_wallet.get_client().get_spend_key().try_into()?; @@ -255,6 +208,6 @@ impl Prd { } pub fn to_value(&self) -> Value { - Value::from_str(&self.to_string()).unwrap() + serde_json::to_value(self).unwrap() } } diff --git a/src/process.rs b/src/process.rs index 60487f1..ac83ac6 100644 --- a/src/process.rs +++ b/src/process.rs @@ -5,12 +5,11 @@ use std::{ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress}; +use sp_client::bitcoin::OutPoint; use crate::{ - crypto::AnkSharedSecretHash, pcd::{AnkPcdHash, Pcd, RoleDefinition}, - prd::Prd, + prd::{Prd, PrdType}, signature::Proof, MutexExt, }; @@ -18,12 +17,20 @@ use crate::{ #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct ProcessState { pub commited_in: OutPoint, + pub pcd_commitment: Value, // If we can't modify a field, we just copy the previous value pub encrypted_pcd: Value, // Some fields may be clear, if the owner of the process decides so pub keys: Map, // We may not always have all the keys pub validation_tokens: Vec, // Signature of the hash of the encrypted pcd tagged with some decision like "yes" or "no" } impl ProcessState { + pub fn decrypt_pcd(&self) -> Value { + // TODO add real error management + let mut fields2plain = Map::new(); + let _ = self.encrypted_pcd.decrypt_fields(&self.keys, &mut fields2plain); + Value::Object(fields2plain) + } + fn compute_modified_fields(&self, previous_state: Option<&ProcessState>) -> Vec { let new_state = &self.encrypted_pcd; @@ -134,10 +141,16 @@ impl ProcessState { Err(anyhow::anyhow!("Not enough valid proofs")) } } + + pub fn is_empty(&self) -> bool { + self.encrypted_pcd == Value::Null || + self.pcd_commitment == Value::Null + } } /// A process is basically a succession of states /// If a process has nothing to do with us, impending_requests will be empty +/// The latest state MUST be an empty state with only the commited_in field set at the last unspent outpoint #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct Process { states: Vec, @@ -155,8 +168,38 @@ impl Process { } } - pub fn insert_state(&mut self, state: ProcessState) { - self.states.push(state); + 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) + } + + /// We want to always keep an empty state with only the latest unspent commited_in value at the last position + pub fn insert_state(&mut self, prd_update: &Prd) -> anyhow::Result<()> { + if prd_update.prd_type != PrdType::Update { return Err(anyhow::Error::msg("Only update prd allowed")) } + if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); } + let new_encrypted_pcd: Value = serde_json::from_str(&prd_update.payload).unwrap(); + let last_index = self.states.len() - 1; + let last_value = self.states.get(last_index).unwrap(); + if last_value.encrypted_pcd != Value::Null || + last_value.keys != Map::new() || + last_value.pcd_commitment != Value::Null || + last_value.validation_tokens != vec![] + { + return Err(anyhow::Error::msg("Last state is not empty")); + } + let empty_state = self.states.remove(last_index); + let new_state = ProcessState { + commited_in: empty_state.commited_in, + pcd_commitment: prd_update.pcd_commitments.clone(), + encrypted_pcd: new_encrypted_pcd, + keys: prd_update.keys.clone(), + validation_tokens: vec![] + }; + 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> { @@ -195,63 +238,79 @@ impl Process { /// 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) -> Vec<&ProcessState> { - let mut states = vec![]; - let latest_state = self.get_latest_state(); - - if latest_state.is_none() { - return states; + 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 latest_state_outpoint = latest_state.unwrap().commited_in; - + 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 state.commited_in != latest_state_outpoint { + log::debug!("state: {:#?}", state); + if previous_commited_in == OutPoint::null() { + previous_commited_in = state.commited_in; + } else if previous_commited_in != state.commited_in { break; } states.push(state); } - states + Ok(states) } - pub fn get_latest_concurrent_states_mut(&mut self) -> Vec<&mut ProcessState> { - let mut states = vec![]; - let latest_state = self.get_latest_state(); - - if latest_state.is_none() { - return 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 latest_state_outpoint = latest_state.unwrap().commited_in; - + 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 state.commited_in != latest_state_outpoint { + if previous_commited_in == OutPoint::null() { + previous_commited_in = state.commited_in; + } else if previous_commited_in != state.commited_in { break; } states.push(state); } - states + Ok(states) } - pub fn remove_latest_concurrent_states(&mut self) -> Vec { - if self.states.is_empty() { - return vec![]; + pub fn remove_latest_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 latest_state = self.get_latest_state().unwrap(); - let latest_outpoint = latest_state.commited_in; + let mut previous_commited_in = OutPoint::null(); + // Iterate backwards, find the reverse position, and adjust to forward position + let reverse_position = self.states.iter().rev().position(|state| { + if previous_commited_in == OutPoint::null() { + previous_commited_in = state.commited_in; + false // Continue iterating + } else if previous_commited_in != state.commited_in { + true // Stop when a different commited_in is found + } else { + false // Continue iterating + } + }); - let pos = self - .states - .iter() - .position(|s| s.commited_in == latest_outpoint) - .unwrap(); + let forward_position = reverse_position.map(|pos| self.states.len() - pos - 1); - self.states.split_off(pos) + match forward_position { + Some(pos) => Ok(self.states.split_off(pos)), + None => { + // We take everything out + let res = self.states.to_vec(); + self.states = vec![]; + Ok(res) + } + } } pub fn get_latest_commited_state(&self) -> Option<&ProcessState> { @@ -259,30 +318,18 @@ impl Process { return None; } - let latest_state = self.get_latest_state().unwrap(); + let last_state = self.states.last().unwrap(); - // a commited outpoint with an index of u32::MAX is a pending state - if latest_state.commited_in.vout != u32::MAX { - // This state is commited, there's no pending state - return Some(latest_state); - } + debug_assert!(last_state.is_empty()); // Last state must always be empty - // We look for the last state before all the pending states - let latest_outpoint = latest_state.commited_in; + // We look for the last commited in before all the pending states + let latest_outpoint = last_state.commited_in; - let pos = self + return self .states .iter() - .position(|s| s.commited_in == latest_outpoint) - .unwrap(); - - if pos == 0 { - // There's no commited states, we just return None - return None; - } else { - // The state just before is last commited state - return self.get_state_at(pos-1); - } + .rev() + .find(|s| s.commited_in != latest_outpoint); } pub fn insert_impending_request(&mut self, request: Prd) { @@ -297,6 +344,12 @@ impl Process { self.impending_requests.iter_mut().collect() } + pub fn prune_impending_requests(&mut self) { + self.impending_requests = self.impending_requests.clone().into_iter() + .filter(|r| r.prd_type != PrdType::None) + .collect(); + } + 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; @@ -444,6 +497,10 @@ mod tests { } }); + let outpoint = OutPoint::null(); + + let pcd_commitment = encrypted_pcd.hash_fields(outpoint).unwrap(); + 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(); @@ -454,7 +511,8 @@ mod tests { .unwrap(); ProcessState { - commited_in: OutPoint::null(), + commited_in: outpoint, + pcd_commitment: Value::Object(pcd_commitment), encrypted_pcd, keys: Map::new(), validation_tokens: vec![],