1080 lines
39 KiB
Rust
1080 lines
39 KiB
Rust
use anyhow::{Error, Result};
|
|
use rs_merkle::{algorithms::Sha256, MerkleTree};
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
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::{
|
|
consensus::serialize, hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, secp256k1::PublicKey, OutPoint
|
|
},
|
|
silentpayments::utils::SilentPaymentAddress,
|
|
};
|
|
use tsify::Tsify;
|
|
|
|
use crate::{
|
|
crypto::AAD,
|
|
signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof},
|
|
};
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify)]
|
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
|
pub struct Member {
|
|
sp_addresses: Vec<String>,
|
|
}
|
|
|
|
impl Member {
|
|
pub fn new(sp_addresses: Vec<SilentPaymentAddress>) -> Result<Self> {
|
|
if sp_addresses.is_empty() {
|
|
return Err(Error::msg("empty address set"));
|
|
}
|
|
|
|
let mut seen = HashSet::new();
|
|
for s in sp_addresses.iter() {
|
|
if !seen.insert(s.clone()) {
|
|
return Err(Error::msg("Duplicate addresses found"));
|
|
}
|
|
}
|
|
|
|
let res: Vec<String> = sp_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: &Value, outpoint: &[u8]) -> Self {
|
|
let mut eng = AnkPcdHash::engine();
|
|
eng.input(outpoint);
|
|
eng.input(value.to_string().as_bytes());
|
|
AnkPcdHash::from_engine(eng)
|
|
}
|
|
}
|
|
|
|
pub trait Pcd<'a>: Serialize + Deserialize<'a> {
|
|
fn new_from_string(str: &str) -> Result<Value> {
|
|
let value: Value = serde_json::from_str(str)?;
|
|
|
|
match value {
|
|
Value::Object(_) => Ok(value),
|
|
_ => Err(Error::msg("Not a Pcd: not a valid JSON object"))
|
|
}
|
|
}
|
|
|
|
fn hash_all_fields(&self, commited_in: OutPoint) -> Result<Map<String, Value>> {
|
|
let map = self.to_value_object()?;
|
|
|
|
let outpoint = serialize(&commited_in);
|
|
|
|
let mut field2hash = Map::with_capacity(map.len());
|
|
// this could be optimised since there's a midstate we're reusing
|
|
for (field, value) in map {
|
|
let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value, &outpoint);
|
|
field2hash.insert(field, Value::String(tagged_hash.to_string()));
|
|
}
|
|
|
|
Ok(field2hash)
|
|
}
|
|
|
|
/// We run this on the result of `hash_all_fields`
|
|
fn create_merkle_tree(&self) -> Result<MerkleTree<Sha256>> {
|
|
let map = self.to_value_object()?;
|
|
let leaves: Result<Vec<[u8; 32]>> = map
|
|
.iter()
|
|
.map(|(_, value)| {
|
|
let mut res = [0u8; 32];
|
|
if !value.is_string() {
|
|
return Err(Error::msg("value is not a string"));
|
|
}
|
|
let vec = Vec::from_hex(value.as_str().unwrap())?;
|
|
if vec.len() != 32 {
|
|
return Err(Error::msg("value must be 32B length"));
|
|
}
|
|
res.copy_from_slice(&vec);
|
|
Ok(res)
|
|
})
|
|
.collect();
|
|
|
|
let mut leaves = leaves?;
|
|
leaves.sort_unstable();
|
|
|
|
let merkle_tree = MerkleTree::<Sha256>::from_leaves(&leaves);
|
|
|
|
Ok(merkle_tree)
|
|
}
|
|
|
|
fn encrypt_fields(
|
|
&self,
|
|
fields_to_encrypt: &[String],
|
|
fields2keys: &mut Map<String, Value>,
|
|
fields2cipher: &mut Map<String, Value>,
|
|
) -> Result<()> {
|
|
let map = self.to_value_object()?;
|
|
let mut rng = thread_rng();
|
|
|
|
for (field, value) in map {
|
|
if fields_to_encrypt.contains(&field) {
|
|
if let None = fields2keys.get(&field) {
|
|
let aes_key = Aes256Gcm::generate_key(&mut rng);
|
|
fields2keys.insert(
|
|
field.to_owned(),
|
|
Value::String(aes_key.to_lower_hex_string()),
|
|
);
|
|
}
|
|
|
|
let nonce = Aes256Gcm::generate_nonce(&mut rng);
|
|
let aes_key_value = fields2keys.get(&field).expect("We should have a key");
|
|
|
|
let aes_key_str: String = serde_json::from_value(aes_key_value.clone())?;
|
|
|
|
let aes_key = Vec::from_hex(&aes_key_str)?;
|
|
|
|
let encrypt_eng = Aes256Gcm::new(aes_key.as_slice().into());
|
|
let value_string = serde_json::to_string(&value)?;
|
|
let payload = Payload {
|
|
msg: value_string.as_bytes(),
|
|
aad: AAD,
|
|
};
|
|
let cipher = encrypt_eng.encrypt(&nonce, payload).map_err(|e| {
|
|
Error::msg(format!("Encryption failed for field {}: {}", field, e))
|
|
})?;
|
|
|
|
let mut res = Vec::with_capacity(nonce.len() + cipher.len());
|
|
res.extend_from_slice(&nonce);
|
|
res.extend_from_slice(&cipher);
|
|
|
|
fields2cipher.insert(field.to_owned(), Value::String(res.to_lower_hex_string()));
|
|
} else {
|
|
if let None = fields2cipher.get(&field) {
|
|
fields2cipher.insert(field.to_owned(), value.clone());
|
|
}
|
|
// if we already have something in the encrypted map, we leave it as it is
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn decrypt_all(
|
|
&self,
|
|
commited_in: OutPoint,
|
|
fields2commit: &Map<String, Value>,
|
|
fields2keys: &Map<String, Value>,
|
|
fields2plain: &mut Map<String, Value>,
|
|
) -> Result<()> {
|
|
let map = self.to_value_object()?;
|
|
|
|
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('\"'),
|
|
)?;
|
|
|
|
if raw_cipher.len() < 28 {
|
|
return Err(Error::msg(format!(
|
|
"Invalid ciphertext length for field {}",
|
|
field
|
|
)));
|
|
}
|
|
|
|
let payload = Payload {
|
|
msg: &raw_cipher[12..],
|
|
aad: AAD,
|
|
};
|
|
|
|
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)?;
|
|
|
|
fields2plain.insert(field.to_owned(), serde_json::from_str(&decrypted_value)?);
|
|
} else if let Some(commitment) = fields2commit.get(field) { // We should always have a commitment
|
|
// We check if the hashed value is the commitment
|
|
let hashed_value = AnkPcdHash::from_value_with_outpoint(encrypted_value, &serialize(&commited_in));
|
|
if commitment.as_str().unwrap() != &hashed_value.to_string() {
|
|
// The value is encrypted, and we don't have the key
|
|
// We put the commitment instead of the encrypted value
|
|
fields2plain.insert(field.to_owned(), commitment.clone());
|
|
} // else it means the value is simply unencrypted, we leave it as it is
|
|
} else {
|
|
return Err(Error::msg(format!("Missing commitment for field {}", field)));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn to_value_object(&self) -> Result<Map<String, Value>> {
|
|
let value = serde_json::to_value(self)?;
|
|
|
|
match value {
|
|
Value::Object(map) => Ok(map),
|
|
_ => Err(Error::msg("self is not a valid json object"))
|
|
}
|
|
}
|
|
|
|
fn extract_roles(&self) -> Result<HashMap<String, RoleDefinition>> {
|
|
let obj = self.to_value_object()?;
|
|
|
|
let parse_roles_map = |m: &Map<String, Value>| {
|
|
let mut res: HashMap<String, RoleDefinition> = HashMap::new();
|
|
for (name, role_def) in m {
|
|
res.insert(name.clone(), serde_json::from_value(role_def.clone())?);
|
|
}
|
|
<Result<HashMap<String, RoleDefinition>, Error>>::Ok(res)
|
|
};
|
|
|
|
if let Some(roles) = obj.get("roles") {
|
|
match roles {
|
|
Value::Object(m) => {
|
|
parse_roles_map(m)
|
|
},
|
|
Value::String(s) => {
|
|
let m: Map<String, Value> = serde_json::from_str(&s)?;
|
|
|
|
parse_roles_map(&m)
|
|
}
|
|
_ => Err(Error::msg("\"roles\" is not an object"))
|
|
}
|
|
} else {
|
|
Err(Error::msg("No \"roles\" key in this pcd"))
|
|
}
|
|
}
|
|
|
|
fn is_hex_string(&self, length: Option<usize>) -> Result<()> {
|
|
let value = serde_json::to_value(self)?;
|
|
|
|
match value {
|
|
Value::String(s) => {
|
|
let vec = Vec::from_hex(&s)?;
|
|
if let Some(len) = length {
|
|
let got_length = vec.len();
|
|
if got_length != len {
|
|
return Err(Error::msg(format!("Wrong length: expected {}, got {}", len, got_length)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => Err(Error::msg("Not a string Value"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Pcd<'_> for Value {}
|
|
|
|
#[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
|
|
}
|
|
}
|
|
|
|
#[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 pcd = json!({"field1": "value1", "field2": "value2"});
|
|
let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(commitment).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 pcd = json!({"field1": "value1", "field2": "value2"});
|
|
|
|
let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(commitment).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 pcd = json!({"field1": "value1", "field2": "value2"});
|
|
let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(commitment).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 pcd = json!({"field1": "value1", "field2": "value2"});
|
|
let commitment = pcd.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(commitment).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 pcd = json!({"field1": "value1"});
|
|
let commitments = pcd.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(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 new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(new_state_commitments).create_merkle_tree().unwrap().root().unwrap();
|
|
|
|
let validation_hash = AnkValidationYesHash::from_merkle_root(new_state_merkle_root);
|
|
// let validation_hash = AnkValidationNoHash::from_commitment(new_state_hash);
|
|
|
|
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 new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(new_state_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 new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(new_state_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 new_state_commitments = new_state.hash_all_fields(OutPoint::null()).unwrap();
|
|
let new_state_merkle_root = Value::Object(new_state_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);
|
|
}
|
|
}
|