1291 lines
45 KiB
Rust
1291 lines
45 KiB
Rust
use anyhow::{Error, Result};
|
|
use rs_merkle::{algorithms::Sha256, MerkleTree};
|
|
use serde::ser::SerializeStruct;
|
|
use std::collections::btree_map::Keys;
|
|
use std::collections::{BTreeMap, HashSet};
|
|
use std::fmt;
|
|
use std::hash::{Hash as StdHash, Hasher};
|
|
use std::io::Write;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use sp_client::{
|
|
bitcoin::{hashes::Hash, secp256k1::PublicKey, OutPoint},
|
|
silentpayments::SilentPaymentAddress,
|
|
};
|
|
use tsify::Tsify;
|
|
|
|
use crate::hash::AnkPcdHash;
|
|
use crate::serialization::OutPointMemberMap;
|
|
use crate::ROLESLABEL;
|
|
use crate::{
|
|
serialization::hex_array_btree,
|
|
signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof},
|
|
};
|
|
|
|
pub const PCD_VERSION: u8 = 1;
|
|
pub(crate) const ZSTD_COMPRESSION_LEVEL: i32 = zstd::DEFAULT_COMPRESSION_LEVEL;
|
|
|
|
pub trait PcdSerializable {
|
|
fn serialize_to_pcd(&self) -> Result<Vec<u8>>;
|
|
fn deserialize_from_pcd(data: &[u8]) -> Result<Self>
|
|
where
|
|
Self: Sized;
|
|
}
|
|
|
|
impl PcdSerializable for serde_json::Value {
|
|
fn serialize_to_pcd(&self) -> Result<Vec<u8>> {
|
|
let mut compressed = Vec::new();
|
|
let mut encoder = zstd::stream::Encoder::new(&mut compressed, ZSTD_COMPRESSION_LEVEL)?;
|
|
|
|
encoder.write_all(&[PCD_VERSION])?;
|
|
encoder.write_all(&[DataType::Json as u8])?;
|
|
serde_json::to_writer(&mut encoder, self)?;
|
|
encoder.finish()?;
|
|
|
|
Ok(compressed)
|
|
}
|
|
|
|
fn deserialize_from_pcd(data: &[u8]) -> Result<Self> {
|
|
let mut decompressed = Vec::new();
|
|
zstd::stream::copy_decode(data, &mut decompressed)?;
|
|
|
|
if decompressed.len() < 3 {
|
|
return Err(Error::msg("Invalid data: too short"));
|
|
}
|
|
|
|
let version = decompressed[0];
|
|
let data_type = DataType::try_from(decompressed[1])?;
|
|
|
|
match (version, data_type) {
|
|
(PCD_VERSION, DataType::Json) => {
|
|
let json_bytes = &decompressed[2..];
|
|
let json_string = String::from_utf8(json_bytes.to_vec())?;
|
|
Ok(serde_json::from_str(&json_string)?)
|
|
}
|
|
_ => Err(Error::msg("Invalid version or data type")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PcdSerializable for FileBlob {
|
|
fn serialize_to_pcd(&self) -> Result<Vec<u8>> {
|
|
let mut compressed = Vec::new();
|
|
let mut encoder = zstd::stream::Encoder::new(&mut compressed, ZSTD_COMPRESSION_LEVEL)?;
|
|
|
|
encoder.write_all(&[PCD_VERSION])?;
|
|
encoder.write_all(&[DataType::FileBlob as u8])?;
|
|
|
|
let type_len = self.r#type.as_bytes().len() as u8;
|
|
encoder.write_all(&[type_len])?;
|
|
encoder.write_all(self.r#type.as_bytes())?;
|
|
encoder.write_all(&self.data)?;
|
|
|
|
encoder.finish()?;
|
|
Ok(compressed)
|
|
}
|
|
|
|
fn deserialize_from_pcd(data: &[u8]) -> Result<Self> {
|
|
let mut decompressed = Vec::new();
|
|
zstd::stream::copy_decode(data, &mut decompressed)?;
|
|
|
|
if decompressed.len() < 4 {
|
|
return Err(Error::msg("Invalid data: too short"));
|
|
}
|
|
|
|
let version = decompressed[0];
|
|
let data_type = DataType::try_from(decompressed[1])?;
|
|
|
|
match (version, data_type) {
|
|
(PCD_VERSION, DataType::FileBlob) => {
|
|
let type_len = decompressed[2] as usize;
|
|
let type_str = String::from_utf8(decompressed[3..3 + type_len].to_vec())?;
|
|
let data = decompressed[3 + type_len..].to_vec();
|
|
|
|
Ok(FileBlob {
|
|
r#type: type_str,
|
|
data,
|
|
})
|
|
}
|
|
_ => Err(Error::msg("Invalid version or data type")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(u8)]
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum DataType {
|
|
FileBlob = 0,
|
|
Json = 1,
|
|
}
|
|
|
|
impl TryFrom<u8> for DataType {
|
|
type Error = Error;
|
|
|
|
fn try_from(value: u8) -> Result<Self> {
|
|
match value {
|
|
0 => Ok(DataType::FileBlob),
|
|
1 => Ok(DataType::Json),
|
|
_ => return Err(Error::msg(format!("Unknown data type: {}", value))),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct FileBlob {
|
|
pub r#type: String,
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
#[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>) -> Self {
|
|
let unique_addresses: HashSet<_> = sp_addresses.into_iter().collect();
|
|
|
|
let res: Vec<String> = unique_addresses
|
|
.iter()
|
|
.map(|a| Into::<String>::into(*a))
|
|
.collect();
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Tsify)]
|
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
|
pub struct Pcd(BTreeMap<String, Vec<u8>>);
|
|
|
|
impl IntoIterator for Pcd {
|
|
type Item = (String, Vec<u8>);
|
|
type IntoIter = std::collections::btree_map::IntoIter<String, Vec<u8>>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.0.into_iter()
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Value> for Pcd {
|
|
type Error = Error;
|
|
|
|
fn try_from(value: Value) -> Result<Self> {
|
|
let as_object = value
|
|
.as_object()
|
|
.ok_or_else(|| Error::msg("Pcd must be an object"))?;
|
|
let map: Result<BTreeMap<String, Vec<u8>>> = as_object
|
|
.into_iter()
|
|
.map(|(key, value)| {
|
|
// Use the trait method instead of manual serialization
|
|
let compressed = value.serialize_to_pcd()?;
|
|
Ok((key.clone(), compressed))
|
|
})
|
|
.collect();
|
|
|
|
Ok(Pcd(map?))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<BTreeMap<String, FileBlob>> for Pcd {
|
|
type Error = Error;
|
|
|
|
fn try_from(file_blob_map: BTreeMap<String, FileBlob>) -> Result<Self> {
|
|
let map: Result<BTreeMap<String, Vec<u8>>> = file_blob_map
|
|
.into_iter()
|
|
.map(|(key, value)| {
|
|
// Use the trait method instead of manual serialization
|
|
let compressed = value.serialize_to_pcd()?;
|
|
Ok((key, compressed))
|
|
})
|
|
.collect();
|
|
|
|
Ok(Pcd(map?))
|
|
}
|
|
}
|
|
|
|
impl Pcd {
|
|
pub fn new(map: BTreeMap<String, Vec<u8>>) -> Self {
|
|
Self(map)
|
|
}
|
|
|
|
pub fn get(&self, key: &str) -> Option<&Vec<u8>> {
|
|
self.0.get(key)
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.0.len()
|
|
}
|
|
|
|
pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, Vec<u8>> {
|
|
self.0.iter()
|
|
}
|
|
|
|
pub fn iter_mut(&mut self) -> std::collections::btree_map::IterMut<'_, String, Vec<u8>> {
|
|
self.0.iter_mut()
|
|
}
|
|
|
|
pub fn insert(&mut self, key: String, value: Vec<u8>) -> Option<Vec<u8>> {
|
|
self.0.insert(key, value)
|
|
}
|
|
|
|
// Helper methods for deserialization using the trait
|
|
pub fn get_as_json(&self, key: &str) -> Result<serde_json::Value> {
|
|
if let Some(data) = self.get(key) {
|
|
serde_json::Value::deserialize_from_pcd(data)
|
|
} else {
|
|
Err(Error::msg("Key not found"))
|
|
}
|
|
}
|
|
|
|
pub fn get_as_file_blob(&self, key: &str) -> Result<FileBlob> {
|
|
if let Some(data) = self.get(key) {
|
|
FileBlob::deserialize_from_pcd(data)
|
|
} else {
|
|
Err(Error::msg("Key not found"))
|
|
}
|
|
}
|
|
|
|
pub fn get_as<T: PcdSerializable>(&self, key: &str) -> Result<T> {
|
|
if let Some(data) = self.get(key) {
|
|
T::deserialize_from_pcd(data)
|
|
} else {
|
|
Err(Error::msg("Key not found"))
|
|
}
|
|
}
|
|
|
|
pub fn insert_serializable<T: PcdSerializable>(
|
|
&mut self,
|
|
key: String,
|
|
value: &T,
|
|
) -> Result<Option<Vec<u8>>> {
|
|
let compressed = value.serialize_to_pcd()?;
|
|
Ok(self.insert(key, compressed))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)]
|
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
|
pub struct PcdCommitments(
|
|
#[serde(with = "hex_array_btree")]
|
|
#[tsify(type = "Record<string, string>")]
|
|
BTreeMap<String, [u8; 32]>,
|
|
);
|
|
|
|
impl PcdCommitments {
|
|
/// Creates a new commitments map with both permissioned and public data, + roles
|
|
pub fn new(commited_in: &OutPoint, attributes: &Pcd, roles: &Roles) -> Result<Self> {
|
|
let mut field2hash: BTreeMap<String, [u8; 32]> = BTreeMap::new();
|
|
for (field, value) in attributes.iter() {
|
|
let tagged_hash =
|
|
AnkPcdHash::from_pcd_value(value.as_slice(), field.as_bytes(), commited_in);
|
|
field2hash.insert(field.to_owned(), tagged_hash.to_byte_array());
|
|
}
|
|
|
|
if roles.len() > 0 {
|
|
let roles_label = String::from(ROLESLABEL);
|
|
let roles_hash = AnkPcdHash::from_pcd_value(
|
|
roles.to_bytes()?.as_slice(),
|
|
roles_label.as_bytes(),
|
|
commited_in,
|
|
);
|
|
|
|
field2hash.insert(roles_label, roles_hash.to_byte_array());
|
|
} // We should probably return an error if roles are empty
|
|
|
|
Ok(Self(field2hash))
|
|
}
|
|
|
|
pub fn new_empty() -> Self {
|
|
Self(BTreeMap::new())
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
|
|
pub fn update_with_value(
|
|
&mut self,
|
|
outpoint: &OutPoint,
|
|
field: &str,
|
|
new_value: &[u8],
|
|
) -> Result<()> {
|
|
if let Some(old_hash) = self.get_mut(field) {
|
|
// We hash the new_value
|
|
let tagged_hash = AnkPcdHash::from_pcd_value(new_value, field.as_bytes(), outpoint);
|
|
*old_hash = tagged_hash.to_byte_array();
|
|
Ok(())
|
|
} else {
|
|
Err(Error::msg("Field not found"))
|
|
}
|
|
}
|
|
|
|
pub fn contains_key(&self, key: &str) -> bool {
|
|
self.0.contains_key(key)
|
|
}
|
|
|
|
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) -> Keys<String, [u8; 32]> {
|
|
self.0.keys()
|
|
}
|
|
|
|
/// 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"));
|
|
} else if self.quorum <= 0.0
|
|
|| self.quorum > 1.0
|
|
|| self.quorum.is_sign_negative()
|
|
|| self.quorum.is_nan()
|
|
{
|
|
// Just to be sure
|
|
return Err(Error::msg("This rule is read only"));
|
|
}
|
|
|
|
let required_members = (members.len() as f32 * self.quorum).ceil() as usize;
|
|
let validating_members = members
|
|
.iter()
|
|
.filter(|member| {
|
|
if member.sp_addresses.is_empty() {
|
|
return false;
|
|
}; // This can happen when a member in the rule wasn't found in the network
|
|
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<OutPoint>, // We use the pairing process id so we don't have to update the role if the user add a device
|
|
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],
|
|
members_list: &OutPointMemberMap,
|
|
) -> Result<()> {
|
|
let empty_member = Member::new(vec![]);
|
|
if diff.iter().all(|field| {
|
|
self.validation_rules.iter().any(|rule| {
|
|
let members: Vec<&Member> = self
|
|
.members
|
|
.iter()
|
|
.map(|outpoint| {
|
|
if let Some(member) = members_list.0.get(outpoint) {
|
|
member
|
|
} else {
|
|
&empty_member
|
|
}
|
|
})
|
|
.collect();
|
|
rule.is_satisfied(field, new_state_merkle_root, proofs, &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()
|
|
}
|
|
}
|
|
|
|
#[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 is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
|
|
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
|
Ok(serde_json::to_vec(self)?)
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.0.len()
|
|
}
|
|
|
|
pub fn get(&self, key: &str) -> Option<&RoleDefinition> {
|
|
self.0.get(key)
|
|
}
|
|
|
|
pub fn get_mut(&mut self, key: &str) -> Option<&mut RoleDefinition> {
|
|
self.0.get_mut(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 serde_json::json;
|
|
use sp_client::{
|
|
bitcoin::{secp256k1::SecretKey, Network},
|
|
SpClient, SpendKey,
|
|
};
|
|
use std::{collections::HashMap, str::FromStr};
|
|
|
|
use super::*;
|
|
use crate::{
|
|
pcd::Member,
|
|
signature::{AnkHash, Proof},
|
|
};
|
|
|
|
fn create_alice_wallet() -> SpClient {
|
|
SpClient::new(
|
|
SecretKey::from_str("a67fb6bf5639efd0aeb19c1c584dd658bceda87660ef1088d4a29d2e77846973")
|
|
.unwrap(),
|
|
SpendKey::Secret(
|
|
SecretKey::from_str(
|
|
"a1e4e7947accf33567e716c9f4d186f26398660e36cf6d2e711af64b3518e65c",
|
|
)
|
|
.unwrap(),
|
|
),
|
|
Network::Signet,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn create_bob_wallet() -> SpClient {
|
|
SpClient::new(
|
|
SecretKey::from_str("4d9f62b2340de3f0bafd671b78b19edcfded918c4106baefd34512f12f520e9b")
|
|
.unwrap(),
|
|
SpendKey::Secret(
|
|
SecretKey::from_str(
|
|
"dafb99602721577997a6fe3da54f86fd113b1b58f0c9a04783d486f87083a32e",
|
|
)
|
|
.unwrap(),
|
|
),
|
|
Network::Signet,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn get_members_map(addresses: [String; 2]) -> HashMap<OutPoint, Member> {
|
|
let alice_address = &addresses[0];
|
|
let bob_address = &addresses[1];
|
|
HashMap::from([
|
|
(
|
|
OutPoint::from_str(
|
|
"b2f105a9df436d16b99e46453b15a0ffc584d136ceda35c0baea28e7e3ade8be:0",
|
|
)
|
|
.unwrap(),
|
|
Member::new(vec![
|
|
SilentPaymentAddress::try_from(alice_address.as_str()).unwrap()
|
|
]),
|
|
),
|
|
(
|
|
OutPoint::from_str(
|
|
"3cb9e3bf8ec72625c0347a665ab383fda9213d4544ff114ac800a9837b585897:0",
|
|
)
|
|
.unwrap(),
|
|
Member::new(vec![
|
|
SilentPaymentAddress::try_from(bob_address.as_str()).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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new(); // roles are not necessary here, we can leave it empty
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_spend_key().try_into().unwrap();
|
|
let bob_spend_key: SecretKey = bob_wallet.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_list = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
|
|
let members: Vec<&Member> = members_list.values().collect();
|
|
|
|
// 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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_spend_key().try_into().unwrap();
|
|
let bob_spend_key: SecretKey = bob_wallet.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_list = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
|
|
let members: Vec<&Member> = members_list.values().collect();
|
|
|
|
// 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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_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_list = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
|
|
let members: Vec<&Member> = members_list.values().collect();
|
|
|
|
// 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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_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_list = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
|
|
let members: Vec<&Member> = members_list.values().collect();
|
|
|
|
// 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_receiving_address(),
|
|
)
|
|
.unwrap()]);
|
|
let clear_state_value = json!({"field1": "value1", "field2": "value2"});
|
|
let pcd: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &Roles::new(roles))
|
|
.unwrap();
|
|
let new_state_merkle_root = commitments.create_merkle_tree().unwrap().root().unwrap();
|
|
|
|
let alice_spend_key: SecretKey = alice_wallet.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 = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
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.keys().map(|k| *k).collect(),
|
|
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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_spend_key().try_into().unwrap();
|
|
let bob_spend_key: SecretKey = bob_wallet.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,
|
|
&OutPointMemberMap(members)
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_rule_satisfied() {
|
|
let alice_wallet = create_alice_wallet();
|
|
let bob_wallet = create_bob_wallet();
|
|
|
|
let members_list = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
|
|
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_list.keys().cloned().collect(),
|
|
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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_spend_key().try_into().unwrap();
|
|
let bob_spend_key: SecretKey = bob_wallet.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,
|
|
&OutPointMemberMap(members_list)
|
|
)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_partial_modification_satisfied() {
|
|
let alice_wallet = create_alice_wallet();
|
|
let bob_wallet = create_bob_wallet();
|
|
|
|
let members = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
|
|
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.keys().cloned().collect(),
|
|
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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_spend_key().try_into().unwrap();
|
|
let bob_spend_key: SecretKey = bob_wallet.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,
|
|
&OutPointMemberMap(members)
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_partial_modification_not_satisfied() {
|
|
let alice_wallet = create_alice_wallet();
|
|
let bob_wallet = create_bob_wallet();
|
|
|
|
let members = get_members_map([
|
|
alice_wallet.get_receiving_address().to_string(),
|
|
bob_wallet.get_receiving_address().to_string(),
|
|
]);
|
|
|
|
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.keys().cloned().collect(),
|
|
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: Pcd = clear_state_value.try_into().unwrap();
|
|
let public_data = BTreeMap::new();
|
|
let roles = BTreeMap::new();
|
|
let attributes = BTreeMap::from_iter(pcd.into_iter().chain(public_data));
|
|
let commitments =
|
|
PcdCommitments::new(&OutPoint::null(), &Pcd::new(attributes), &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_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,
|
|
&OutPointMemberMap(members)
|
|
)
|
|
.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);
|
|
}
|
|
}
|