sdk_common/src/pcd.rs

1112 lines
40 KiB
Rust

use anyhow::{Error, Result};
use rs_merkle::{algorithms::Sha256, MerkleTree};
use serde::de::{DeserializeOwned, Error as DeError};
use serde::ser::SerializeStruct;
use wasm_bindgen::JsValue;
use std::collections::{BTreeMap, HashSet};
use std::hash::{Hash as StdHash, Hasher};
use std::fmt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sp_client::{
bitcoin::{
consensus::serialize, hashes::{sha256t_hash_newtype, Hash, HashEngine}, secp256k1::PublicKey, OutPoint
},
silentpayments::utils::SilentPaymentAddress,
};
use tsify::Tsify;
use crate::{
signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof},
serialization::hex_array_btree
};
#[derive(Debug, Default, Clone, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Member {
sp_addresses: Vec<String>,
}
impl fmt::Display for Member {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.sp_addresses.join(","))
}
}
impl Serialize for Member {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// Deduplicate and sort sp_addresses
let set: HashSet<_> = self.sp_addresses.iter().collect();
let mut unique_items: Vec<_> = set.into_iter().collect();
unique_items.sort_unstable();
// Serialize as an object with the sp_addresses key
let mut state = serializer.serialize_struct("Member", 1)?;
state.serialize_field("sp_addresses", &unique_items)?;
state.end()
}
}
impl PartialEq for Member {
fn eq(&self, other: &Self) -> bool {
let self_set: HashSet<_> = self.sp_addresses.iter().collect();
let other_set: HashSet<_> = other.sp_addresses.iter().collect();
self_set == other_set
}
}
impl Eq for Member {}
impl StdHash for Member {
fn hash<H: Hasher>(&self, state: &mut H) {
// Convert to a set to ensure order independence
let set: HashSet<_> = self.sp_addresses.iter().collect();
let mut unique_items: Vec<_> = set.into_iter().collect();
unique_items.sort_unstable(); // Sort to ensure consistent hashing
for item in unique_items {
item.hash(state);
}
}
}
impl Member {
pub fn new(sp_addresses: Vec<SilentPaymentAddress>) -> Result<Self> {
if sp_addresses.is_empty() {
return Err(Error::msg("empty address set"));
}
let unique_addresses: HashSet<_> = sp_addresses.into_iter().collect();
let res: Vec<String> = unique_addresses
.iter()
.map(|a| Into::<String>::into(*a))
.collect();
Ok(Self { sp_addresses: res })
}
pub fn get_addresses(&self) -> Vec<String> {
self.sp_addresses.clone()
}
pub fn key_is_part_of_member(&self, key: &PublicKey) -> bool {
self.sp_addresses.iter().any(|a| {
let addr = SilentPaymentAddress::try_from(a.as_str()).unwrap();
addr.get_spend_key() == *key
})
}
pub fn get_address_for_key(&self, key: &PublicKey) -> Option<String> {
self.sp_addresses.iter().find(|a| {
let addr = SilentPaymentAddress::try_from(a.as_str()).unwrap();
addr.get_spend_key() == *key
})
.cloned()
}
}
sha256t_hash_newtype! {
pub struct AnkPcdTag = hash_str("4nk/Pcd");
#[hash_newtype(forward)]
pub struct AnkPcdHash(_);
}
impl AnkPcdHash {
pub fn from_value(value: &Value) -> Self {
let mut eng = AnkPcdHash::engine();
eng.input(value.to_string().as_bytes());
AnkPcdHash::from_engine(eng)
}
/// 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: &[u8], outpoint: &[u8]) -> Self {
let mut eng = AnkPcdHash::engine();
eng.input(outpoint);
eng.input(value);
AnkPcdHash::from_engine(eng)
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Tsify)]
#[tsify(into_wasm_abi)]
pub struct Pcd(BTreeMap<String, Value>);
impl IntoIterator for Pcd {
type Item = (String, Value);
type IntoIter = std::collections::btree_map::IntoIter<String, Value>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Pcd {
pub fn new(map: BTreeMap<String, Value>) -> Self {
Self(map)
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.0.get(key)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, Value> {
self.0.iter()
}
pub fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, Value> {
self.0.iter_mut()
}
pub fn insert(&mut self, key: String, value: Value) -> Option<Value> {
self.0.insert(key, value)
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct PcdCommitments(#[serde(with = "hex_array_btree")] BTreeMap<String, [u8; 32]>);
impl Tsify for PcdCommitments {
type JsType = JsValue;
const DECL: &'static str = "Record<string, string>";
fn from_js<T: Into<wasm_bindgen::JsValue>>(js: T) -> serde_json::Result<Self>
where
Self: DeserializeOwned, {
serde_wasm_bindgen::from_value(js.into()).map_err(|e| serde_json::Error::custom(e.to_string()))
}
fn into_js(&self) -> serde_json::Result<Self::JsType>
where
Self: Serialize, {
serde_wasm_bindgen::to_value(self)
.map_err(|e| serde_json::Error::custom(e.to_string()))
}
}
impl PcdCommitments {
/// Creates a new commitments map with both permissioned and public data, + roles
pub fn new(commited_in: &OutPoint, clear_state: &Pcd, public_data: &Pcd, roles: &Roles) -> Result<Self> {
let serialized_outpoint = serialize(commited_in);
let mut field2hash: BTreeMap<String, [u8; 32]> = BTreeMap::new();
for (field, value) in clear_state.iter() {
let mut value_bin = serde_json::to_string(&value)?.into_bytes();
value_bin.extend_from_slice(field.as_bytes());
let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &serialized_outpoint);
field2hash.insert(field.to_owned(), tagged_hash.to_byte_array());
}
for (field, value) in public_data.iter() {
let mut value_bin = serde_json::to_string(&value)?.into_bytes();
value_bin.extend_from_slice(field.as_bytes());
let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &serialized_outpoint);
field2hash.insert(field.to_owned(), tagged_hash.to_byte_array());
}
let serialized_roles = roles.to_bytes()?;
let roles_hash = AnkPcdHash::from_value_with_outpoint(&serialized_roles, &serialized_outpoint);
field2hash.insert("roles".to_owned(), roles_hash.to_byte_array());
Ok(Self(field2hash))
}
pub fn new_empty() -> Self {
Self(BTreeMap::new())
}
pub fn update_with_value(&mut self, outpoint: &OutPoint, field: &str, new_value: &Value) -> Result<()> {
let serialized_outpoint = serialize(outpoint);
if let Some(old_hash) = self.get_mut(field) {
// We hash the new_value
let mut value_bin = serde_json::to_string(new_value)?.into_bytes();
value_bin.extend_from_slice(field.as_bytes());
let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &serialized_outpoint);
*old_hash = tagged_hash.to_byte_array();
}
Ok(())
}
pub fn get(&self, field: &str) -> Option<&[u8; 32]> {
self.0.get(field)
}
pub fn get_mut(&mut self, field: &str) -> Option<&mut [u8; 32]> {
self.0.get_mut(field)
}
pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, [u8; 32]> {
self.0.iter()
}
pub fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, [u8; 32]> {
self.0.iter_mut()
}
pub fn keys(&self) -> Vec<String> {
self.0.keys().map(|k| k.to_owned()).collect()
}
/// Since BTreeMap keys order is deterministic, we can guarantee a consistent merkle tree
pub fn create_merkle_tree(&self) -> Result<MerkleTree<Sha256>> {
let leaves: Vec<[u8; 32]> = self.0
.values()
.map(|hash| *hash)
.collect();
let merkle_tree = MerkleTree::<Sha256>::from_leaves(leaves.as_slice());
Ok(merkle_tree)
}
pub fn find_index_of(&self, field: &str) -> Option<usize> {
self.iter().position(|(key, _)| key.as_str() == field)
}
}
#[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
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?
}
impl ValidationRule {
pub fn new(quorum: f32, fields: Vec<String>, min_sig_member: f32) -> Result<Self> {
if quorum < 0.0 || quorum > 1.0 {
return Err(Error::msg("quorum must be 0.0 < quorum <= 1.0"));
}
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",
));
}
if fields.is_empty() {
return Err(Error::msg("Fields can't be empty"));
}
let res = Self {
quorum,
fields,
min_sig_member,
};
Ok(res)
}
pub fn allows_modification(&self) -> bool {
self.quorum > 0.0 && self.min_sig_member > 0.0
}
pub fn is_satisfied(
&self,
field: &str,
merkle_root: [u8; 32],
proofs: &[Proof],
members: &[Member],
) -> Result<()> {
// Check if this rule applies to the field
if !self.fields.contains(&field.to_string()) {
return Err(Error::msg("Field isn't part of this rule"));
} else if members.is_empty() {
return Err(Error::msg("Members list is empty"));
}
let required_members = (members.len() as f32 * self.quorum).ceil() as usize;
let validating_members = members
.iter()
.filter(|member| {
let member_proofs: Vec<&Proof> = proofs
.iter()
.filter(|p| member.key_is_part_of_member(&p.get_key()))
.collect();
self.satisfy_min_sig_member(member, merkle_root, &member_proofs)
.is_ok()
})
.count();
if validating_members >= required_members { Ok(()) } else { Err(Error::msg("Not enough members to validate"))}
}
pub fn satisfy_min_sig_member(
&self,
member: &Member,
merkle_root: [u8; 32],
proofs: &[&Proof],
) -> Result<()> {
if proofs.len() == 0 {
return Err(Error::msg("Can't validate with 0 proof"));
}
let registered_devices = member.get_addresses().len();
if proofs.len() > registered_devices {
// We can't have more proofs than registered devices for one member
return Err(Error::msg("More proofs than requirefor member"));
}
let required_sigs = (registered_devices as f32 * self.min_sig_member).ceil() as usize;
// println!("required_sigs {} and proofs.len() {}", required_sigs, proofs.len());
if proofs.len() < required_sigs {
// Even if all proof are valid yes, we don't reach the quota
return Err(Error::msg("Not enough provided proofs to reach quota"));
}
let mut yes_votes: Vec<Proof> = Vec::new();
let mut no_votes: Vec<Proof> = Vec::new();
let yes = AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(merkle_root));
let no = AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(merkle_root));
// Validate proofs here
for proof in proofs {
if !proof.verify().is_ok() {
return Err(Error::msg("Invalid proof"));
}
let signed_message = proof.get_message();
if signed_message == yes.to_byte_array() {
yes_votes.push(**proof);
} else if signed_message == no.to_byte_array() {
no_votes.push(**proof);
} else {
return Err(Error::msg("We don't know what this proof signs for"));
}
}
if yes_votes.len() >= required_sigs {
Ok(())
} else {
Err(Error::msg("Not enough yes votes"))
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct RoleDefinition {
pub members: Vec<Member>,
pub validation_rules: Vec<ValidationRule>,
pub storages: Vec<String>,
}
impl RoleDefinition {
pub fn is_satisfied(
&self,
diff: Vec<String>,
new_state_merkle_root: [u8; 32],
proofs: &[Proof],
) -> Result<()> {
if diff.iter().all(|field| {
self.validation_rules
.iter()
.any(|rule| rule.is_satisfied(field, new_state_merkle_root, proofs, &self.members).is_ok())
})
{
Ok(())
} else {
Err(Error::msg("Failed to validate all rules"))
}
}
pub fn get_applicable_rules(&self, field: &str) -> Vec<&ValidationRule> {
self.validation_rules
.iter()
.filter(|rule| rule.fields.contains(&field.to_string()))
.collect()
}
pub fn is_member_validation_needed(&self, member: Member, modified_fields: Vec<String>) -> bool {
if !self.members.iter().any(|m| *m == member) {
return false;
}
for field in modified_fields {
if !self.get_applicable_rules(&field).is_empty() {
return true;
}
}
false
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Roles(BTreeMap<String, RoleDefinition>);
impl IntoIterator for Roles {
type Item = (String, RoleDefinition);
type IntoIter = std::collections::btree_map::IntoIter<String, RoleDefinition>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Roles {
pub fn new(roles: BTreeMap<String, RoleDefinition>) -> Self {
Roles(roles)
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
serde_json::to_vec(&self.0).map_err(|e| Error::msg(e.to_string()))
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn get(&self, key: &str) -> Option<&RoleDefinition> {
self.0.get(key)
}
pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, RoleDefinition> {
self.0.iter()
}
pub fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, RoleDefinition> {
self.0.iter_mut()
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use serde_json::json;
use sp_client::{
bitcoin::{secp256k1::SecretKey, Network},
spclient::{SpClient, SpWallet, SpendKey},
};
use super::*;
use crate::{
pcd::Member,
signature::{AnkHash, Proof},
};
fn create_alice_wallet() -> SpWallet {
SpWallet::new(
SpClient::new(
"default".to_owned(),
SecretKey::from_str(
"a67fb6bf5639efd0aeb19c1c584dd658bceda87660ef1088d4a29d2e77846973",
)
.unwrap(),
SpendKey::Secret(
SecretKey::from_str(
"a1e4e7947accf33567e716c9f4d186f26398660e36cf6d2e711af64b3518e65c",
)
.unwrap(),
),
None,
Network::Signet,
)
.unwrap(),
None,
vec![],
)
.unwrap()
}
fn create_bob_wallet() -> SpWallet {
SpWallet::new(
SpClient::new(
"default".to_owned(),
SecretKey::from_str(
"4d9f62b2340de3f0bafd671b78b19edcfded918c4106baefd34512f12f520e9b",
)
.unwrap(),
SpendKey::Secret(
SecretKey::from_str(
"dafb99602721577997a6fe3da54f86fd113b1b58f0c9a04783d486f87083a32e",
)
.unwrap(),
),
None,
Network::Signet,
)
.unwrap(),
None,
vec![],
)
.unwrap()
}
#[test]
fn test_validation_rule_new() {
// Valid input
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5);
assert!(validation_rule.is_ok());
let rule = validation_rule.unwrap();
assert_eq!(rule.quorum, 0.5);
assert_eq!(rule.fields, fields);
assert_eq!(rule.min_sig_member, 0.5);
// Invalid quorum (< 0.0)
let validation_rule = ValidationRule::new(-0.1, fields.clone(), 0.5);
assert!(validation_rule.is_err());
// Invalid quorum (> 1.0)
let validation_rule = ValidationRule::new(1.1, fields.clone(), 0.5);
assert!(validation_rule.is_err());
// Invalid min_sig_member (< 0.0)
let validation_rule = ValidationRule::new(0.5, fields.clone(), -0.1);
assert!(validation_rule.is_err());
// Invalid min_sig_member (> 1.0)
let validation_rule = ValidationRule::new(0.5, fields.clone(), 1.1);
assert!(validation_rule.is_err());
// Empty fields
let validation_rule = ValidationRule::new(0.5, vec![], 0.5);
assert!(validation_rule.is_err());
}
#[test]
fn test_is_satisfied() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5).unwrap();
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let validation_hash1 = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
let validation_hash2 = AnkValidationNoHash::from_merkle_root(new_state_merkle_root);
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(),
];
// We test that the rule is satisfied with only bob proof
let result = validation_rule.is_satisfied(
fields[0].as_str(),
new_state_merkle_root,
&vec![bob_proof],
&members,
);
assert!(result.is_ok());
// 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_merkle_root,
&vec![alice_proof],
&members,
);
assert!(result.is_err());
// 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_merkle_root,
&vec![alice_proof, bob_proof],
&members,
);
assert!(result.is_ok());
}
#[test]
fn test_is_satisfied_error_cases() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5).unwrap();
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let validation_hash_yes = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
let validation_hash_no = AnkValidationNoHash::from_merkle_root(new_state_merkle_root);
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_hash_no), alice_spend_key);
let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash_yes), bob_spend_key);
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(),
];
// Test with empty members list
let result =
validation_rule.is_satisfied(fields[0].as_str(), new_state_merkle_root, &proofs, &vec![]);
assert!(result.is_err());
// Test with no matching field
let result =
validation_rule.is_satisfied("nonexistent_field", new_state_merkle_root, &proofs, &members);
assert!(result.is_err());
}
#[test]
fn test_is_satisfied_error_with_alice_providing_proofs_for_bob() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule = ValidationRule::new(1.0, fields.clone(), 0.5).unwrap();
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
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 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(),
];
// 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_merkle_root, &proofs, &members);
assert!(result.is_err());
}
#[test]
fn test_is_satisfied_error_quorum_half_with_alice_providing_two_proofs() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule = ValidationRule::new(0.5, fields.clone(), 0.5).unwrap();
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
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 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(),
];
// 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_merkle_root, &proofs, &members);
assert!(result.is_err());
}
#[test]
fn test_satisfy_min_sig_member() {
let fields = vec!["field1".to_string()];
let validation_rule = ValidationRule::new(0.5, fields, 0.5).unwrap();
let alice_wallet = create_alice_wallet();
let member = Member::new(vec![SilentPaymentAddress::try_from(
alice_wallet.get_client().get_receiving_address(),
)
.unwrap()])
.unwrap();
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let alice_spend_key: SecretKey = alice_wallet
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let proof = Proof::new(
AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(new_state_merkle_root)),
alice_spend_key,
);
let proofs = vec![&proof];
let result = validation_rule.satisfy_min_sig_member(&member, new_state_merkle_root, &proofs);
assert!(result.is_ok()); // Example check - make more meaningful assertions based on real Proof and Member implementations
}
#[test]
fn test_all_rules_satisfied() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
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(),
];
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap();
let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap();
let rules = vec![validation_rule1, validation_rule2];
// 2 rules, to modify each field, all members must agree
let role_def = RoleDefinition {
members: members.clone(),
validation_rules: rules.clone(),
storages: vec![],
};
let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" });
let new_state = json!({ "field1": "new_value1", "field2": "new_value2" });
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
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::ValidationYes(validation_hash), alice_spend_key);
let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash), bob_spend_key);
let proofs = vec![alice_proof, bob_proof];
let modified_fields: Vec<String> = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect();
assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_ok());
}
#[test]
fn test_no_rule_satisfied() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
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(),
];
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap();
let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap();
let rules = vec![validation_rule1, validation_rule2];
// 2 rules, to modify each field, all members must agree
let role_def = RoleDefinition {
members: members.clone(),
validation_rules: rules.clone(),
storages: vec![],
};
let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" });
let new_state = json!({ "field1": "new_value1", "field2": "new_value2" });
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
// let validation_hash1 = AnkValidationYesHash::from_commitment(new_state_hash);
let validation_hash = AnkValidationNoHash::from_merkle_root(new_state_merkle_root);
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_hash), alice_spend_key);
let bob_proof = Proof::new(AnkHash::ValidationNo(validation_hash), bob_spend_key);
let proofs = vec![alice_proof, bob_proof];
let modified_fields: Vec<String> = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect();
assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_err());
}
#[test]
fn test_partial_modification_satisfied() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
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(),
];
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap();
let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap();
let rules = vec![validation_rule1, validation_rule2];
// 2 rules, to modify each field, all members must agree
let role_def = RoleDefinition {
members: members.clone(),
validation_rules: rules.clone(),
storages: vec![],
};
let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" });
let new_state = json!({ "field1": "old_value1", "field2": "new_value2" });
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
// let validation_hash = AnkValidationNoHash::from_merkle_root(new_state_merkle_root);
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::ValidationYes(validation_hash), alice_spend_key);
let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash), bob_spend_key);
let proofs = vec![alice_proof, bob_proof];
let modified_fields: Vec<String> = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect();
assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_ok());
}
#[test]
fn test_partial_modification_not_satisfied() {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
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(),
];
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap();
let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap();
let rules = vec![validation_rule1, validation_rule2];
// 2 rules, to modify each field, all members must agree
let role_def = RoleDefinition {
members: members.clone(),
validation_rules: rules.clone(),
storages: vec![],
};
let previous_state = json!({ "field1": "old_value1", "field2": "old_value2" });
let new_state = json!({ "field1": "old_value1", "field2": "new_value2" });
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
let pcd: BTreeMap<String, Value> = serde_json::from_value(clear_state_value).unwrap();
let public_data = BTreeMap::new();
let roles = BTreeMap::new();
let commitments = PcdCommitments::new(&OutPoint::null(), &Pcd::new(pcd), &Pcd::new(public_data), &Roles::new(roles)).unwrap();
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
// let validation_hash = AnkValidationNoHash::from_merkle_root(new_state_merkle_root);
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::ValidationYes(validation_hash), alice_spend_key);
// let bob_proof = Proof::new(AnkHash::ValidationYes(validation_hash), bob_spend_key);
let proofs = vec![alice_proof];
let modified_fields: Vec<String> = new_state.as_object().unwrap().iter().map(|(key, _)| key.clone()).collect();
assert!(role_def.is_satisfied(modified_fields, new_state_merkle_root, &proofs).is_err());
}
#[test]
fn test_get_applicable_rules() {
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap();
let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap();
let rules = vec![validation_rule1.clone(), validation_rule2];
let role_def = RoleDefinition {
members: vec![],
validation_rules: rules,
storages: vec![],
};
let applicable_rules = role_def.get_applicable_rules("field1");
assert_eq!(applicable_rules.len(), 1);
assert_eq!(*applicable_rules[0], validation_rule1);
}
#[test]
fn test_get_applicable_rules_no_rules() {
let fields = vec!["field1".to_string(), "field2".to_string()];
let validation_rule1 = ValidationRule::new(1.0, vec![fields[0].clone()], 0.5).unwrap();
let validation_rule2 = ValidationRule::new(1.0, vec![fields[1].clone()], 0.5).unwrap();
let rules = vec![validation_rule1.clone(), validation_rule2];
let role_def = RoleDefinition {
members: vec![],
validation_rules: rules,
storages: vec![],
};
let applicable_rules = role_def.get_applicable_rules("nonexistent_field");
assert_eq!(applicable_rules.len(), 0);
}
}