diff --git a/src/crypto.rs b/src/crypto.rs index 7fecd13..02b6be9 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,5 +1,8 @@ use anyhow::{Error, Result}; -use sp_client::silentpayments::{bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine}, secp256k1::PublicKey}; +use sp_client::silentpayments::{ + bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine}, + secp256k1::PublicKey, +}; use aes_gcm::aead::{Aead, Payload}; pub use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; @@ -29,7 +32,8 @@ pub fn encrypt_with_key(key: &[u8; 32], plaintext: &[u8]) -> Result> { msg: plaintext, aad: AAD, }; - let ciphertext = encryption_eng.encrypt(&nonce, payload) + let ciphertext = encryption_eng + .encrypt(&nonce, payload) .map_err(|e| anyhow::anyhow!(e))?; let mut res: Vec = Vec::with_capacity(nonce.len() + ciphertext.len()); @@ -46,7 +50,8 @@ pub fn decrypt_with_key(key: &[u8; 32], ciphertext: &[u8]) -> Result> { msg: &ciphertext[12..], aad: AAD, }; - let plaintext = decryption_eng.decrypt(nonce.into(), payload) + let plaintext = decryption_eng + .decrypt(nonce.into(), payload) .map_err(|e| anyhow::anyhow!(e))?; Ok(plaintext) diff --git a/src/device.rs b/src/device.rs index 66b3632..d7c0648 100644 --- a/src/device.rs +++ b/src/device.rs @@ -2,7 +2,10 @@ use serde::{Deserialize, Serialize}; use tsify::Tsify; use wasm_bindgen::prelude::*; -use sp_client::{bitcoin::{hashes::Hash, OutPoint, Txid}, spclient::SpWallet}; +use sp_client::{ + bitcoin::{hashes::Hash, OutPoint, Txid}, + spclient::SpWallet, +}; use crate::pcd::Member; @@ -65,6 +68,11 @@ impl Device { pub fn get_other_addresses(&self) -> Vec { let our_address = self.get_wallet().get_client().get_receiving_address(); - self.to_member().unwrap().get_addresses().into_iter().filter(|a| *a != our_address).collect() + self.to_member() + .unwrap() + .get_addresses() + .into_iter() + .filter(|a| *a != our_address) + .collect() } } diff --git a/src/lib.rs b/src/lib.rs index 4e56d23..e77e8b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,9 @@ -use std::sync::{Mutex, MutexGuard}; use std::fmt::Debug; +use std::sync::{Mutex, MutexGuard}; -pub use sp_client; -pub use log; pub use aes_gcm; +pub use log; +pub use sp_client; pub mod crypto; pub mod device; @@ -12,8 +12,8 @@ pub mod network; pub mod pcd; pub mod prd; pub mod process; -pub mod silentpayments; pub mod signature; +pub mod silentpayments; pub const MAX_PRD_PAYLOAD_SIZE: usize = u16::MAX as usize; // 64KiB sounds reasonable for now diff --git a/src/network.rs b/src/network.rs index c7c2b54..3724e9d 100644 --- a/src/network.rs +++ b/src/network.rs @@ -58,7 +58,7 @@ impl AnkFlag { match self { Self::NewTx => "NewTx", Self::Faucet => "Faucet", - Self::Cipher => "Cipher", + Self::Cipher => "Cipher", Self::Commit => "Commit", Self::Unknown => "Unknown", } @@ -82,7 +82,11 @@ impl CommitMessage { /// Create a new commitment message for the first transaction of the chain /// init_tx must be the hex string of the transaction /// validation_tokens must be empty - pub fn new_first_commitment(transaction: Transaction, encrypted_pcd: Map, keys: Map) -> Self { + pub fn new_first_commitment( + transaction: Transaction, + encrypted_pcd: Map, + keys: Map, + ) -> Self { Self { init_tx: serialize(&transaction).to_lower_hex_string(), encrypted_pcd, @@ -95,7 +99,11 @@ impl CommitMessage { /// Create a new commitment message for an update transaction /// init_tx must be the hex string of the txid of the first commitment transaction /// validation_tokens must be empty - pub fn new_update_commitment(init_tx: OutPoint, encrypted_pcd: Map, keys: Map) -> Self { + pub fn new_update_commitment( + init_tx: OutPoint, + encrypted_pcd: Map, + keys: Map, + ) -> Self { Self { init_tx: init_tx.to_string(), encrypted_pcd, @@ -197,8 +205,8 @@ pub struct CachedMessage { pub id: u32, pub status: CachedMessageStatus, pub transaction: Option, - pub commitment: Option, // content of the op_return - pub sender: Option, // Never None when message sent + pub commitment: Option, // content of the op_return + pub sender: Option, // Never None when message sent pub shared_secrets: Vec, // Max 2 secrets in case we send to both address of the recipient pub cipher: Vec, // Max 2 ciphers in case we send to both address of the recipient pub timestamp: u64, @@ -242,7 +250,7 @@ impl CachedMessage { }; match engine.decrypt(&nonce.into(), payload) { Ok(plain) => return Ok(plain), - Err(_) => continue + Err(_) => continue, } } @@ -267,7 +275,7 @@ impl CachedMessage { }; match engine.decrypt(&nonce.into(), payload) { Ok(plain) => return Ok(plain), - Err(_) => continue + Err(_) => continue, } } diff --git a/src/pcd.rs b/src/pcd.rs index db7fb94..ad34021 100644 --- a/src/pcd.rs +++ b/src/pcd.rs @@ -1,26 +1,37 @@ +use anyhow::{Error, Result}; use std::{collections::HashSet, str::FromStr}; -use anyhow::{Result, Error}; -use aes_gcm::{aead::{Aead, Payload}, AeadCore, Aes256Gcm, KeyInit}; +use aes_gcm::{ + aead::{Aead, Payload}, + AeadCore, Aes256Gcm, KeyInit, +}; use log::debug; use rand::thread_rng; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use sp_client::{bitcoin::{hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, XOnlyPublicKey}, silentpayments::utils::SilentPaymentAddress}; +use sp_client::{ + bitcoin::{ + hashes::{sha256t_hash_newtype, Hash, HashEngine}, + hex::{DisplayHex, FromHex}, + XOnlyPublicKey, + }, + silentpayments::utils::SilentPaymentAddress, +}; use tsify::Tsify; -use crate::{crypto::AAD, signature::{AnkValidationNoHash, AnkValidationYesHash, Proof}}; +use crate::{ + crypto::AAD, + signature::{AnkValidationNoHash, AnkValidationYesHash, Proof}, +}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Member { - sp_addresses: Vec + sp_addresses: Vec, } impl Member { - pub fn new( - sp_addresses: Vec, - ) -> Result { + pub fn new(sp_addresses: Vec) -> Result { if sp_addresses.is_empty() { return Err(Error::msg("empty address set")); } @@ -32,13 +43,12 @@ impl Member { } } - let res: Vec = sp_addresses.iter() + let res: Vec = sp_addresses + .iter() .map(|a| Into::::into(*a)) .collect(); - Ok(Self { - sp_addresses: res - }) + Ok(Self { sp_addresses: res }) } pub fn get_addresses(&self) -> Vec { @@ -80,15 +90,24 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { AnkPcdHash::from_value(&self.to_value()) } - fn encrypt_fields(&self, fields2keys: &mut Map, fields2cipher: &mut Map) -> Result<()> { + fn encrypt_fields( + &self, + 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 as_map = as_value + .as_object() + .ok_or_else(|| Error::msg("Expected object"))?; let mut rng = thread_rng(); for (field, value) in as_map { let aes_key = Aes256Gcm::generate_key(&mut rng); let nonce = Aes256Gcm::generate_nonce(&mut rng); - fields2keys.insert(field.to_owned(), Value::String(aes_key.to_lower_hex_string())); + fields2keys.insert( + field.to_owned(), + Value::String(aes_key.to_lower_hex_string()), + ); let encrypt_eng = Aes256Gcm::new(&aes_key); let value_string = value.to_string(); @@ -96,7 +115,8 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { msg: value_string.as_bytes(), aad: AAD, }; - let cipher = encrypt_eng.encrypt(&nonce, payload) + 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()); @@ -109,21 +129,32 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { Ok(()) } - fn decrypt_fields(&self, fields2keys: &Map, fields2plain: &mut Map) -> Result<()> { + fn decrypt_fields( + &self, + fields2keys: &Map, + fields2plain: &mut Map, + ) -> Result<()> { let value = self.to_value(); let map = value.as_object().unwrap(); for (field, encrypted_value) in map.iter() { if let Some(aes_key) = fields2keys.get(field) { - let key_buf = Vec::from_hex(&aes_key.to_string().trim_matches('\"'))?; let decrypt_eng = Aes256Gcm::new(key_buf.as_slice().into()); - - let raw_cipher = Vec::from_hex(&encrypted_value.as_str().ok_or_else(|| Error::msg("Expected string"))?.trim_matches('\"'))?; + + let raw_cipher = Vec::from_hex( + &encrypted_value + .as_str() + .ok_or_else(|| Error::msg("Expected string"))? + .trim_matches('\"'), + )?; if raw_cipher.len() < 28 { - return Err(Error::msg(format!("Invalid ciphertext length for field {}", field))); + return Err(Error::msg(format!( + "Invalid ciphertext length for field {}", + field + ))); } let payload = Payload { @@ -131,7 +162,8 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { aad: AAD, }; - let plain = decrypt_eng.decrypt(raw_cipher[..12].into(), payload) + let plain = decrypt_eng + .decrypt(raw_cipher[..12].into(), payload) .map_err(|_| Error::msg(format!("Failed to decrypt field {}", field)))?; let decrypted_value: String = String::from_utf8(plain)?; @@ -151,10 +183,10 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { impl Pcd<'_> for Value {} -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[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 + 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? } @@ -166,7 +198,9 @@ impl ValidationRule { } 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")); + return Err(Error::msg( + "min_signatures_member must be 0.0 < min_signatures_member <= 1.0", + )); } if fields.is_empty() { @@ -182,28 +216,41 @@ impl ValidationRule { Ok(res) } - pub fn is_satisfied(&self, field: &str, new_state_hash: AnkPcdHash, proofs: &[&Proof], members: &[Member]) -> bool { + pub fn is_satisfied( + &self, + field: &str, + new_state_hash: AnkPcdHash, + proofs: &[Proof], + members: &[Member], + ) -> bool { // Check if this rule applies to the field if !self.fields.contains(&field.to_string()) || members.is_empty() { return false; } let required_members = (members.len() as f32 * self.quorum).ceil() as usize; - let validating_members = members.iter() + let validating_members = members + .iter() .filter(|member| { - let member_proofs: Vec<&Proof> = proofs.iter() + let member_proofs: Vec<&Proof> = proofs + .iter() .filter(|p| member.key_is_part_of_member(&p.get_key())) - .cloned() .collect(); - - self.satisfy_min_sig_member(member, new_state_hash, &member_proofs).is_ok() + + self.satisfy_min_sig_member(member, new_state_hash, &member_proofs) + .is_ok() }) .count(); validating_members >= required_members } - pub fn satisfy_min_sig_member(&self, member: &Member, new_state_hash: AnkPcdHash, proofs: &[&Proof]) -> Result<()> { + pub fn satisfy_min_sig_member( + &self, + member: &Member, + new_state_hash: AnkPcdHash, + proofs: &[&Proof], + ) -> Result<()> { if proofs.len() == 0 { return Err(Error::msg("Can't validate with 0 proof")); } @@ -260,9 +307,16 @@ pub struct RoleDefinition { } impl RoleDefinition { - pub fn is_satisfied(&self, new_state: &Value, previous_state: &Value, proofs: &[&Proof]) -> bool { + pub fn is_satisfied( + &self, + new_state: &Value, + previous_state: &Value, + proofs: &[Proof], + ) -> bool { // compute the modified fields - let modified_fields: Vec = new_state.as_object().unwrap() + let modified_fields: Vec = new_state + .as_object() + .unwrap() .iter() .filter_map(|(key, value)| { let previous_value = previous_state.as_object().unwrap().get(key); @@ -278,12 +332,15 @@ impl RoleDefinition { // check that for each field we can satisfy at least one rule modified_fields.iter().all(|field| { - self.validation_rules.iter().any(|rule| rule.is_satisfied(field, new_state_hash, proofs, &self.members)) + self.validation_rules + .iter() + .any(|rule| rule.is_satisfied(field, new_state_hash, proofs, &self.members)) }) } pub fn get_applicable_rules(&self, field: &str) -> Vec<&ValidationRule> { - self.validation_rules.iter() + self.validation_rules + .iter() .filter(|rule| rule.fields.contains(&field.to_string())) .collect() } @@ -341,11 +398,14 @@ fn compare_arrays(array1: &Vec, array2: &Vec) -> bool { #[cfg(test)] mod tests { use serde_json::json; - use sp_client::{bitcoin::{secp256k1::SecretKey, Network}, spclient::{SpClient, SpWallet, SpendKey}}; + use sp_client::{ + bitcoin::{secp256k1::SecretKey, Network}, + spclient::{SpClient, SpWallet, SpendKey}, + }; use super::*; use crate::{ - pcd::{Member, AnkPcdHash}, + pcd::{AnkPcdHash, Member}, signature::{AnkHash, Proof}, }; @@ -353,28 +413,48 @@ mod tests { SpWallet::new( SpClient::new( "default".to_owned(), - SecretKey::from_str("a67fb6bf5639efd0aeb19c1c584dd658bceda87660ef1088d4a29d2e77846973").unwrap(), - SpendKey::Secret(SecretKey::from_str("a1e4e7947accf33567e716c9f4d186f26398660e36cf6d2e711af64b3518e65c").unwrap()), + SecretKey::from_str( + "a67fb6bf5639efd0aeb19c1c584dd658bceda87660ef1088d4a29d2e77846973", + ) + .unwrap(), + SpendKey::Secret( + SecretKey::from_str( + "a1e4e7947accf33567e716c9f4d186f26398660e36cf6d2e711af64b3518e65c", + ) + .unwrap(), + ), None, - Network::Signet - ).unwrap(), + Network::Signet, + ) + .unwrap(), None, - vec![] - ).unwrap() + 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()), + SecretKey::from_str( + "4d9f62b2340de3f0bafd671b78b19edcfded918c4106baefd34512f12f520e9b", + ) + .unwrap(), + SpendKey::Secret( + SecretKey::from_str( + "dafb99602721577997a6fe3da54f86fd113b1b58f0c9a04783d486f87083a32e", + ) + .unwrap(), + ), None, - Network::Signet - ).unwrap(), + Network::Signet, + ) + .unwrap(), None, - vec![] - ).unwrap() + vec![], + ) + .unwrap() } #[test] @@ -423,25 +503,52 @@ mod tests { let validation_hash1 = AnkValidationYesHash::from_commitment(new_state_hash); let validation_hash2 = AnkValidationNoHash::from_commitment(new_state_hash); - let alice_spend_key: SecretKey = alice_wallet.get_client().get_spend_key().try_into().unwrap(); + let alice_spend_key: SecretKey = alice_wallet + .get_client() + .get_spend_key() + .try_into() + .unwrap(); let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationNo(validation_hash2), alice_spend_key); let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash1), bob_spend_key); let members = vec![ - Member::new(vec![SilentPaymentAddress::try_from(alice_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), - Member::new(vec![SilentPaymentAddress::try_from(bob_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + alice_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + bob_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), ]; // We test that the rule is satisfied with only bob proof - let result = validation_rule.is_satisfied(fields[0].as_str(), new_state_hash, &vec![&bob_proof], &members); + let result = validation_rule.is_satisfied( + fields[0].as_str(), + new_state_hash, + &vec![bob_proof], + &members, + ); assert!(result); // 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_hash, &vec![&alice_proof], &members); + let result = validation_rule.is_satisfied( + fields[0].as_str(), + new_state_hash, + &vec![alice_proof], + &members, + ); assert!(!result); // 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_hash, &vec![&alice_proof, &bob_proof], &members); + let result = validation_rule.is_satisfied( + fields[0].as_str(), + new_state_hash, + &vec![alice_proof, bob_proof], + &members, + ); assert!(result); } @@ -459,28 +566,39 @@ mod tests { let validation_hash1 = AnkValidationYesHash::from_commitment(new_state_hash); let validation_hash2 = AnkValidationNoHash::from_commitment(new_state_hash); - let alice_spend_key: SecretKey = alice_wallet.get_client().get_spend_key().try_into().unwrap(); + let alice_spend_key: SecretKey = alice_wallet + .get_client() + .get_spend_key() + .try_into() + .unwrap(); let bob_spend_key: SecretKey = bob_wallet.get_client().get_spend_key().try_into().unwrap(); let alice_proof = Proof::new(AnkHash::ValidationNo(validation_hash2), alice_spend_key); let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash1), bob_spend_key); - let proofs = vec![ - &alice_proof, - &bob_proof - ]; + let proofs = vec![alice_proof, bob_proof]; let members = vec![ - Member::new(vec![SilentPaymentAddress::try_from(alice_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), - Member::new(vec![SilentPaymentAddress::try_from(bob_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + alice_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + bob_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), ]; // Test with empty members list - let result = validation_rule.is_satisfied(fields[0].as_str(), new_state_hash, &proofs, &vec![]); + let result = + validation_rule.is_satisfied(fields[0].as_str(), new_state_hash, &proofs, &vec![]); assert!(!result); // Test with no matching field - let result = validation_rule.is_satisfied("nonexistent_field", new_state_hash, &proofs, &members); + let result = + validation_rule.is_satisfied("nonexistent_field", new_state_hash, &proofs, &members); assert!(!result); } @@ -497,24 +615,34 @@ mod tests { let validation_hash = AnkValidationYesHash::from_commitment(new_state_hash); - let alice_spend_key: SecretKey = alice_wallet.get_client().get_spend_key().try_into().unwrap(); + let alice_spend_key: SecretKey = alice_wallet + .get_client() + .get_spend_key() + .try_into() + .unwrap(); // Both proofs are signed by Alice let alice_proof_1 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let alice_proof_2 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); - let proofs = vec![ - &alice_proof_1, - &alice_proof_2 - ]; + let proofs = vec![alice_proof_1, alice_proof_2]; let members = vec![ - Member::new(vec![SilentPaymentAddress::try_from(alice_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), - Member::new(vec![SilentPaymentAddress::try_from(bob_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + alice_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + bob_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), ]; // Test case where both proofs are signed by Alice, but both Alice and Bob are passed as members - let result = validation_rule.is_satisfied(fields[0].as_str(), new_state_hash, &proofs, &members); + let result = + validation_rule.is_satisfied(fields[0].as_str(), new_state_hash, &proofs, &members); assert!(!result); } @@ -531,24 +659,34 @@ mod tests { let validation_hash = AnkValidationYesHash::from_commitment(new_state_hash); - let alice_spend_key: SecretKey = alice_wallet.get_client().get_spend_key().try_into().unwrap(); + let alice_spend_key: SecretKey = alice_wallet + .get_client() + .get_spend_key() + .try_into() + .unwrap(); // Both proofs are signed by Alice let alice_proof_1 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); let alice_proof_2 = Proof::new(AnkHash::ValidationYes(validation_hash), alice_spend_key); - let proofs = vec![ - &alice_proof_1, - &alice_proof_2 - ]; + let proofs = vec![alice_proof_1, alice_proof_2]; let members = vec![ - Member::new(vec![SilentPaymentAddress::try_from(alice_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), - Member::new(vec![SilentPaymentAddress::try_from(bob_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + alice_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), + Member::new(vec![SilentPaymentAddress::try_from( + bob_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(), ]; // Test case where quorum is 0.5, but Alice provides two proofs. This should fail since the quorum requires different members. - let result = validation_rule.is_satisfied(fields[0].as_str(), new_state_hash, &proofs, &members); + let result = + validation_rule.is_satisfied(fields[0].as_str(), new_state_hash, &proofs, &members); assert!(!result); } @@ -559,12 +697,23 @@ mod tests { let alice_wallet = create_alice_wallet(); - let member = Member::new(vec![SilentPaymentAddress::try_from(alice_wallet.get_client().get_receiving_address()).unwrap()]).unwrap(); + let member = Member::new(vec![SilentPaymentAddress::try_from( + alice_wallet.get_client().get_receiving_address(), + ) + .unwrap()]) + .unwrap(); let pcd = json!({"field1": "value1"}); let new_state_hash = AnkPcdHash::from_value(&pcd); - let alice_spend_key: SecretKey = alice_wallet.get_client().get_spend_key().try_into().unwrap(); + let alice_spend_key: SecretKey = alice_wallet + .get_client() + .get_spend_key() + .try_into() + .unwrap(); - let proof = Proof::new(AnkHash::ValidationYes(AnkValidationYesHash::from_commitment(new_state_hash)), alice_spend_key); + let proof = Proof::new( + AnkHash::ValidationYes(AnkValidationYesHash::from_commitment(new_state_hash)), + alice_spend_key, + ); let proofs = vec![&proof]; let result = validation_rule.satisfy_min_sig_member(&member, new_state_hash, &proofs); diff --git a/src/prd.rs b/src/prd.rs index f5bc7fc..e72d1f1 100644 --- a/src/prd.rs +++ b/src/prd.rs @@ -1,16 +1,16 @@ use std::collections::HashSet; use std::str::FromStr; -use anyhow::{Result, Error}; -use serde::{Serialize, Deserialize}; +use anyhow::{Error, Result}; +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, XOnlyPublicKey}; +use sp_client::bitcoin::{OutPoint, Psbt, XOnlyPublicKey}; use sp_client::silentpayments::utils::SilentPaymentAddress; use sp_client::spclient::SpWallet; -use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; use tsify::Tsify; use crate::pcd::{AnkPcdHash, Member, Pcd}; @@ -24,8 +24,8 @@ pub enum PrdType { None, Message, Update, // Update an existing process - List, // request a list of items - Response, + 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 } @@ -61,7 +61,7 @@ pub struct Prd { pub sender: String, pub keys: Map, // key is a key in pcd, value is the key to decrypt it pub validation_tokens: Vec, - pub payload: String, // Payload depends on the actual type + pub payload: String, // Payload depends on the actual type pub proof: Option, // This must be None up to the creation of the network message } @@ -70,7 +70,7 @@ impl Prd { root_commitment: OutPoint, sender: String, // Should take Member as argument encrypted_pcd: Map, - keys: Map + keys: Map, ) -> Self { Self { prd_type: PrdType::Update, @@ -83,6 +83,18 @@ impl Prd { } } + 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(), + proof: None, + } + } + pub fn new_response( root_commitment: OutPoint, sender: String, @@ -116,13 +128,14 @@ impl Prd { } } - fn _extract_from_message(plain: &[u8], commitment: Option<&AnkPrdHash>) -> 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")); + return Err(anyhow::Error::msg( + "Received prd is not what was commited in the transaction", + )); } } // check that the proof is consistent @@ -132,7 +145,12 @@ impl Prd { let addresses = sender.get_addresses(); let mut spend_keys: Vec = vec![]; for address in addresses { - spend_keys.push(::try_from(address)?.get_spend_key().x_only_public_key().0); + spend_keys.push( + ::try_from(address)? + .get_spend_key() + .x_only_public_key() + .0, + ); } // The key in proof must be one of the sender keys let proof_key = proof.get_key(); @@ -146,7 +164,7 @@ impl Prd { if !known_key { return Err(anyhow::Error::msg("Proof signed with an unknown key")); } - proof.verify()?; + proof.verify()?; } // check that the commitment outpoint is valid, just in case OutPoint::from_str(&prd.root_commitment)?; @@ -157,13 +175,17 @@ impl Prd { Self::_extract_from_message(plain, None) } - pub fn extract_from_message_with_commitment(plain: &[u8], commitment: &AnkPrdHash) -> Result { + pub fn extract_from_message_with_commitment( + plain: &[u8], + commitment: &AnkPrdHash, + ) -> Result { Self::_extract_from_message(plain, Some(commitment)) } pub fn filter_keys(&mut self, to_keep: HashSet) { let current_keys = self.keys.clone(); - let filtered_keys: Map = current_keys.into_iter() + let filtered_keys: Map = current_keys + .into_iter() .filter(|(field, _)| to_keep.contains(field)) .collect(); self.keys = filtered_keys; @@ -178,9 +200,12 @@ impl Prd { to_commit.proof = None; 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(); + to_commit.payload = Value::from_str(&to_commit.payload) + .unwrap() + .tagged_hash() + .to_string(); } - + AnkPrdHash::from_value(&to_commit.to_value()) } @@ -189,10 +214,11 @@ impl Prd { let spend_sk: SecretKey = sp_wallet.get_client().get_spend_key().try_into()?; let to_sign = self.clone(); // we sign the whole prd, incl the keys, for each recipient - let message_hash = AnkHash::Message(AnkMessageHash::from_message(to_sign.to_string().as_bytes())); + let message_hash = + AnkHash::Message(AnkMessageHash::from_message(to_sign.to_string().as_bytes())); let proof = Proof::new(message_hash, spend_sk); - + let mut res = self.clone(); res.proof = Some(proof); diff --git a/src/process.rs b/src/process.rs index 653c116..9d66c57 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,10 +1,19 @@ -use std::{collections::HashMap, sync::{Mutex, MutexGuard, OnceLock}}; +use std::{ + collections::HashMap, + sync::{Mutex, MutexGuard, OnceLock}, +}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress}; -use crate::{crypto::AnkSharedSecretHash, pcd::{AnkPcdHash, Pcd, RoleDefinition, ValidationRule}, prd::Prd, signature::Proof, MutexExt}; +use crate::{ + crypto::AnkSharedSecretHash, + pcd::{AnkPcdHash, Pcd, RoleDefinition, ValidationRule}, + prd::Prd, + signature::Proof, + MutexExt, +}; #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct ProcessState { @@ -18,19 +27,21 @@ impl ProcessState { /// A state is valid if the attached validation_tokens satisfy the updated conditions defined in the encrypted_pcd pub fn is_valid(&self, previous_state: Option<&ProcessState>) -> Result { // Determine modified fields - let modified_fields: Vec = if let Some(previous_state) = previous_state - { - let res: Vec = self.encrypted_pcd.as_object().unwrap() - .iter() - .filter_map(|(key, value)| { - let previous_value = previous_state.encrypted_pcd.get(key); - if previous_value.is_none() || value != previous_value.unwrap() { - Some(key.clone()) - } else { - None - } - }) - .collect(); + let modified_fields: Vec = if let Some(previous_state) = previous_state { + let res: Vec = self + .encrypted_pcd + .as_object() + .unwrap() + .iter() + .filter_map(|(key, value)| { + let previous_value = previous_state.encrypted_pcd.get(key); + if previous_value.is_none() || value != previous_value.unwrap() { + Some(key.clone()) + } else { + None + } + }) + .collect(); if res.is_empty() { return Err(anyhow::anyhow!("State is identical to the previous state")); @@ -38,7 +49,9 @@ impl ProcessState { res } else { - self.encrypted_pcd.as_object().unwrap() + self.encrypted_pcd + .as_object() + .unwrap() .keys() .cloned() .collect() @@ -46,7 +59,8 @@ impl ProcessState { // Extract roles and their definitions let mut fields2plains = Map::new(); - self.encrypted_pcd.decrypt_fields(&self.keys, &mut fields2plains)?; + self.encrypted_pcd + .decrypt_fields(&self.keys, &mut fields2plains)?; let mut roles2rules: HashMap = HashMap::new(); if let Some(roles) = fields2plains.get("roles") { @@ -67,9 +81,11 @@ 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| { - let applicable_rules: Vec<(&RoleDefinition, &ValidationRule)> = roles2rules.values() + let applicable_rules: Vec<(&RoleDefinition, &ValidationRule)> = roles2rules + .values() .flat_map(|role_def| { - role_def.get_applicable_rules(field) + role_def + .get_applicable_rules(field) .into_iter() .map(move |rule| (role_def, rule)) }) @@ -79,10 +95,13 @@ impl ProcessState { return false; // No rules apply to this field, consider it invalid } - let validation_tokens: Vec<&Proof> = self.validation_tokens.iter().collect(); - applicable_rules.into_iter().any(|(role_def, rule)| { - rule.is_satisfied(field, AnkPcdHash::from_value(&self.encrypted_pcd), &validation_tokens, &role_def.members) + rule.is_satisfied( + field, + AnkPcdHash::from_value(&self.encrypted_pcd), + &self.validation_tokens, + &role_def.members, + ) }) }); @@ -100,24 +119,42 @@ pub struct Process { } impl Process { - pub fn new(states: Vec, shared_secrets: HashMap, impending_requests: Vec) -> Self { + pub fn new( + states: Vec, + shared_secrets: HashMap, + impending_requests: Vec, + ) -> Self { Self { states, - shared_secrets: shared_secrets.into_iter().map(|(k, v)| (k.to_string(), v)).collect(), + shared_secrets: shared_secrets + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), impending_requests, } } - pub fn insert_shared_secret(&mut self, address: SilentPaymentAddress, secret: AnkSharedSecretHash) { + pub fn insert_shared_secret( + &mut self, + address: SilentPaymentAddress, + secret: AnkSharedSecretHash, + ) { self.shared_secrets.insert(address.to_string(), secret); } - pub fn get_shared_secret_for_address(&self, address: &SilentPaymentAddress) -> Option { + pub fn get_shared_secret_for_address( + &self, + address: &SilentPaymentAddress, + ) -> Option { self.shared_secrets.get(&address.to_string()).cloned() } pub fn get_all_secrets(&self) -> HashMap { - self.shared_secrets.clone().into_iter().map(|(k, v)| (SilentPaymentAddress::try_from(k.as_str()).unwrap(), v)).collect() + self.shared_secrets + .clone() + .into_iter() + .map(|(k, v)| (SilentPaymentAddress::try_from(k.as_str()).unwrap(), v)) + .collect() } pub fn insert_state(&mut self, state: ProcessState) { @@ -142,12 +179,15 @@ impl Process { pub fn get_previous_state(&self, current_state: &ProcessState) -> Option<&ProcessState> { // Find the index of the current state - let current_index = self.states.iter().position(|state| state == current_state)?; + let current_index = self + .states + .iter() + .position(|state| state == current_state)?; // Check if there is a previous state if current_index > 0 { // Create a new Process with the previous state - let previous_state = self.get_state_at(current_index-1).unwrap(); + let previous_state = self.get_state_at(current_index - 1).unwrap(); Some(&previous_state) } else { None // No previous state exists @@ -207,7 +247,11 @@ impl Process { let latest_state = self.get_latest_state().unwrap(); let latest_outpoint = latest_state.commited_in; - let pos = self.states.iter().position(|s| s.commited_in == latest_outpoint).unwrap(); + let pos = self + .states + .iter() + .position(|s| s.commited_in == latest_outpoint) + .unwrap(); self.states.split_off(pos) } @@ -229,7 +273,9 @@ impl Process { 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) + self.states + .iter() + .position(|s| s.commited_in == target_commited_in) } pub fn get_number_of_states(&self) -> usize { diff --git a/src/signature.rs b/src/signature.rs index 67deb99..c0db384 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -1,10 +1,10 @@ use anyhow::Result; use rand::{thread_rng, RngCore}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; +use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; use sp_client::bitcoin::key::Secp256k1; use sp_client::bitcoin::secp256k1::schnorr::Signature; use sp_client::bitcoin::secp256k1::{Keypair, Message, SecretKey, XOnlyPublicKey}; -use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; use crate::pcd::AnkPcdHash; @@ -68,9 +68,9 @@ impl AnkHash { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Proof { - signature: Signature, + signature: Signature, message: AnkHash, - key: XOnlyPublicKey + key: XOnlyPublicKey, } impl Proof { @@ -83,12 +83,16 @@ impl Proof { thread_rng().fill_bytes(&mut aux_rand); - let sig = secp.sign_schnorr_with_aux_rand(&Message::from_digest(message_hash.to_byte_array()), &keypair, &aux_rand); + let sig = secp.sign_schnorr_with_aux_rand( + &Message::from_digest(message_hash.to_byte_array()), + &keypair, + &aux_rand, + ); Self { signature: sig, message: message_hash, - key: keypair.x_only_public_key().0 + key: keypair.x_only_public_key().0, } } @@ -102,7 +106,11 @@ impl Proof { pub fn verify(&self) -> Result<()> { let secp = Secp256k1::verification_only(); - secp.verify_schnorr(&self.signature, &Message::from_digest(self.message.to_byte_array()), &self.key)?; + secp.verify_schnorr( + &self.signature, + &Message::from_digest(self.message.to_byte_array()), + &self.key, + )?; Ok(()) } @@ -110,6 +118,4 @@ impl Proof { pub fn to_string(&self) -> String { serde_json::to_string(self).unwrap() } - } -