This commit is contained in:
Sosthene 2024-10-06 10:40:29 +02:00 committed by Nicolas Cantu
parent 9e1c9b12cf
commit 8e42596184
8 changed files with 406 additions and 158 deletions

View File

@ -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<Vec<u8>> {
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<u8> = Vec::with_capacity(nonce.len() + ciphertext.len());
@ -46,7 +50,8 @@ pub fn decrypt_with_key(key: &[u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>> {
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)

View File

@ -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<String> {
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()
}
}

View File

@ -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

View File

@ -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<String, Value>, keys: Map<String, Value>) -> Self {
pub fn new_first_commitment(
transaction: Transaction,
encrypted_pcd: Map<String, Value>,
keys: Map<String, Value>,
) -> 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<String, Value>, keys: Map<String, Value>) -> Self {
pub fn new_update_commitment(
init_tx: OutPoint,
encrypted_pcd: Map<String, Value>,
keys: Map<String, Value>,
) -> 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<String>,
pub commitment: Option<String>, // content of the op_return
pub sender: Option<Member>, // Never None when message sent
pub commitment: Option<String>, // content of the op_return
pub sender: Option<Member>, // Never None when message sent
pub shared_secrets: Vec<String>, // Max 2 secrets in case we send to both address of the recipient
pub cipher: Vec<String>, // 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,
}
}

View File

@ -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<String>
sp_addresses: Vec<String>,
}
impl Member {
pub fn new(
sp_addresses: Vec<SilentPaymentAddress>,
) -> Result<Self> {
pub fn new(sp_addresses: Vec<SilentPaymentAddress>) -> Result<Self> {
if sp_addresses.is_empty() {
return Err(Error::msg("empty address set"));
}
@ -32,13 +43,12 @@ impl Member {
}
}
let res: Vec<String> = sp_addresses.iter()
let res: Vec<String> = sp_addresses
.iter()
.map(|a| Into::<String>::into(*a))
.collect();
Ok(Self {
sp_addresses: res
})
Ok(Self { sp_addresses: res })
}
pub fn get_addresses(&self) -> Vec<String> {
@ -80,15 +90,24 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> {
AnkPcdHash::from_value(&self.to_value())
}
fn encrypt_fields(&self, fields2keys: &mut Map<String, Value>, fields2cipher: &mut Map<String, Value>) -> Result<()> {
fn encrypt_fields(
&self,
fields2keys: &mut Map<String, Value>,
fields2cipher: &mut Map<String, Value>,
) -> 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<String, Value>, fields2plain: &mut Map<String, Value>) -> Result<()> {
fn decrypt_fields(
&self,
fields2keys: &Map<String, Value>,
fields2plain: &mut Map<String, Value>,
) -> 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<String>, // 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<String> = new_state.as_object().unwrap()
let modified_fields: Vec<String> = 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<Value>, array2: &Vec<Value>) -> 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);

View File

@ -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<String, Value>, // key is a key in pcd, value is the key to decrypt it
pub validation_tokens: Vec<Proof>,
pub payload: String, // Payload depends on the actual type
pub payload: String, // Payload depends on the actual type
pub proof: Option<Proof>, // 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<String, Value>,
keys: Map<String, Value>
keys: Map<String, Value>,
) -> 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<Self> {
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<XOnlyPublicKey> = vec![];
for address in addresses {
spend_keys.push(<SilentPaymentAddress>::try_from(address)?.get_spend_key().x_only_public_key().0);
spend_keys.push(
<SilentPaymentAddress>::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<Self> {
pub fn extract_from_message_with_commitment(
plain: &[u8],
commitment: &AnkPrdHash,
) -> Result<Self> {
Self::_extract_from_message(plain, Some(commitment))
}
pub fn filter_keys(&mut self, to_keep: HashSet<String>) {
let current_keys = self.keys.clone();
let filtered_keys: Map<String, Value> = current_keys.into_iter()
let filtered_keys: Map<String, Value> = 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);

View File

@ -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<bool, anyhow::Error> {
// Determine modified fields
let modified_fields: Vec<String> = if let Some(previous_state) = previous_state
{
let res: Vec<String> = 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<String> = if let Some(previous_state) = previous_state {
let res: Vec<String> = 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<String, RoleDefinition> = 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<ProcessState>, shared_secrets: HashMap<SilentPaymentAddress, AnkSharedSecretHash>, impending_requests: Vec<Prd>) -> Self {
pub fn new(
states: Vec<ProcessState>,
shared_secrets: HashMap<SilentPaymentAddress, AnkSharedSecretHash>,
impending_requests: Vec<Prd>,
) -> 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<AnkSharedSecretHash> {
pub fn get_shared_secret_for_address(
&self,
address: &SilentPaymentAddress,
) -> Option<AnkSharedSecretHash> {
self.shared_secrets.get(&address.to_string()).cloned()
}
pub fn get_all_secrets(&self) -> HashMap<SilentPaymentAddress, AnkSharedSecretHash> {
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 {

View File

@ -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()
}
}