sdk_common/src/process.rs

885 lines
33 KiB
Rust

use std::{
collections::{BTreeMap, HashMap, HashSet},
sync::{Mutex, MutexGuard, OnceLock},
};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sp_client::bitcoin::{hex::{DisplayHex, FromHex}, OutPoint, Transaction};
use tsify::Tsify;
use crate::{
pcd::{Member, Pcd, RoleDefinition},
signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof},
MutexExt,
};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct ProcessState {
pub commited_in: OutPoint,
#[tsify(type = "Record<string, string>")]
pub pcd_commitment: Value, // If we can't modify a field, we just copy the previous value
pub state_id: String, // the root of the tree created with all the commitments. Serves as an unique id for a state too
#[tsify(type = "Record<string, string>")]
pub encrypted_pcd: Value, // Some fields may be clear, if the owner of the process decides so
#[tsify(type = "Record<string, string>")]
pub keys: Map<String, Value>, // We may not always have all the keys
pub validation_tokens: Vec<Proof>, // Signature of the hash of the encrypted pcd tagged with some decision like "yes" or "no"
#[tsify(type = "Record<string, string>")]
pub public_data: BTreeMap<String, String>, // long descriptions that can be used for the ihm
#[tsify(type = "Record<string, RoleDefinition>")]
pub roles: BTreeMap<String, RoleDefinition>,
}
impl ProcessState {
pub fn new(commited_in: OutPoint, clear_state: Map<String, Value>, public_data: &BTreeMap<String, String>, roles: BTreeMap<String, RoleDefinition>) -> anyhow::Result<Self> {
let mut keys = Map::new();
let mut encrypted = Map::new();
let clear_pcd = Value::Object(clear_state);
let sorted_pcd = Value::Object(clear_pcd.to_sorted_key_values()?);
let pcd_commitment = Value::Object(sorted_pcd.hash_all_fields(commited_in)?);
let merkle_root = pcd_commitment.create_merkle_tree()?.root().ok_or(anyhow::Error::msg("Invalid merkle tree"))?.to_lower_hex_string();
let keys_to_encrypt: Vec<String> = pcd_commitment.as_object().unwrap().keys().map(|k| k.clone()).collect();
sorted_pcd.encrypt_fields(&keys_to_encrypt, &mut keys, &mut encrypted)?;
let res = Self {
commited_in,
pcd_commitment,
state_id: merkle_root,
encrypted_pcd: Value::Object(encrypted),
keys,
validation_tokens: vec![],
public_data: public_data.clone(),
roles,
};
Ok(res)
}
pub fn update_value(&mut self, key: &str, new_value: Value) -> anyhow::Result<()> {
// First decrypt values
let mut clear_pcd = self.decrypt_pcd()?;
if let Some(value) = clear_pcd.get_mut(key) {
// We can only update a value we can decrypt
if let None = self.keys.get(key) {
return Err(anyhow::Error::msg("Trying to update a value we can't access"));
}
// We replace the clear value by the new_value
*value = new_value;
} else {
return Err(anyhow::Error::msg(format!("{} doesn't exist", key)))
}
// Update the commitment
self.pcd_commitment = Value::Object(Value::Object(clear_pcd.clone()).hash_all_fields(self.commited_in)?);
// Todo for now we rehash everything, which is a bit wasteful but fine for a proto
// Update state_id
self.state_id = self.pcd_commitment.create_merkle_tree()?.root().unwrap().to_lower_hex_string();
// Update the encrypted value
Value::Object(clear_pcd).encrypt_fields(&[key.to_string()], &mut self.keys, self.encrypted_pcd.as_object_mut().unwrap())?;
Ok(())
}
/// Return a decrypted version of the pcd in this state
/// 3 outputs possible for each field:
/// 1) We have the key and we return the decrypted value
/// 2) We don't have the key, we return the commitment
/// 3) the field is unencrypted, we leave it as sit is
pub fn decrypt_pcd(&self) -> anyhow::Result<Map<String, Value>> {
let mut fields2plain = Map::new();
let fields2commit = self.pcd_commitment.to_value_object()?;
self.encrypted_pcd.decrypt_all(self.commited_in, &fields2commit, &self.keys, &mut fields2plain)?;
Ok(fields2plain)
}
pub fn get_message_hash(&self, approval: bool) -> anyhow::Result<AnkHash> {
let merkle_root = <Value as Pcd>::create_merkle_tree(&self.pcd_commitment)?.root().unwrap();
if approval {
Ok(AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(merkle_root)))
} else {
Ok(AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(merkle_root)))
}
}
fn list_modified_fields(&self, previous_state: Option<&ProcessState>) -> Vec<String> {
let new_state = &self.pcd_commitment;
// Ensure the new state is a JSON object
let new_state_commitments = new_state
.as_object()
.expect("New state should be a JSON object");
if let Some(prev_state) = previous_state {
// Previous state exists; compute differences
let previous_state_commitments = prev_state
.pcd_commitment
.as_object()
.expect("Previous state should be a JSON object");
// Compute modified fields by comparing with previous state
new_state_commitments
.iter()
.filter_map(|(key, value)| {
let previous_value = previous_state_commitments.get(key);
if previous_value.is_none() || value != previous_value.unwrap() {
Some(key.clone())
} else {
None
}
})
.collect()
} else {
// No previous state; all fields are considered modified
new_state_commitments.keys().cloned().collect()
}
}
pub fn is_valid(&self, previous_state: Option<&ProcessState>) -> anyhow::Result<()> {
if self.validation_tokens.is_empty() {
return Err(anyhow::anyhow!(
"Can't validate a state with no proofs attached"
));
}
// Compute modified fields
let modified_fields = self.list_modified_fields(previous_state);
if modified_fields.is_empty() {
return Err(anyhow::anyhow!("State is identical to the previous state"));
}
// Check if each modified field satisfies at least one applicable rule across all roles
let all_fields_validated = modified_fields.iter().all(|field| {
// Collect applicable rules from all roles for the current field
let applicable_roles: Vec<RoleDefinition> = self.roles
.iter()
.filter_map(|(_, role_def)| {
let mut filtered_role_def = role_def.clone();
let rules = filtered_role_def.get_applicable_rules(field);
filtered_role_def.validation_rules =
rules.into_iter().map(|r| r.clone()).collect();
if filtered_role_def.validation_rules.is_empty() {
None
} else {
Some(filtered_role_def)
}
})
.collect();
if applicable_roles.is_empty() {
return false; // No rules apply to this field, consider it invalid
}
let mut merkle_root = [0u8; 32];
merkle_root.copy_from_slice(&Vec::from_hex(&self.state_id).unwrap());
applicable_roles.into_iter().any(|role_def| {
role_def.validation_rules.iter().any(|rule| {
rule.is_satisfied(
field,
merkle_root,
&self.validation_tokens,
&role_def.members,
).is_ok()
})
})
});
if all_fields_validated {
Ok(())
} else {
Err(anyhow::anyhow!("Not enough valid proofs"))
}
}
pub fn is_empty(&self) -> bool {
self.encrypted_pcd == Value::Null ||
self.pcd_commitment == Value::Null
}
pub fn get_fields_to_validate_for_member(&self, member: &Member) -> anyhow::Result<Vec<String>> {
let mut res: HashSet<String> = HashSet::new();
// Are we in that role?
for (_, role_def) in &self.roles {
if !role_def.members.contains(member) {
continue;
} else {
// what are the fields we can modify?
for rule in &role_def.validation_rules {
if rule.allows_modification() {
res.extend(rule.fields.iter().map(|f| f.clone()));
}
}
}
}
Ok(res.into_iter().collect())
}
}
/// A process is basically a succession of states
/// The latest state MUST be an empty state with only the commited_in field set at the last unspent outpoint
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct Process {
states: Vec<ProcessState>,
}
impl Process {
pub fn new(
commited_in: OutPoint
) -> Self {
let empty_state = ProcessState {
commited_in,
..Default::default()
};
Self {
states: vec![empty_state],
}
}
pub fn get_process_id(&self) -> anyhow::Result<OutPoint> {
Ok(self.states.get(0).ok_or(anyhow::Error::msg("Empty state list"))?.commited_in)
}
pub fn get_process_tip(&self) -> anyhow::Result<OutPoint> {
Ok(self.states.last().ok_or(anyhow::Error::msg("Empty state list"))?.commited_in)
}
pub fn get_last_unspent_outpoint(&self) -> anyhow::Result<OutPoint> {
if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); }
let last_state = self.states.last().unwrap();
Ok(last_state.commited_in)
}
pub fn update_states_tip(&mut self, new_commitment: OutPoint) -> anyhow::Result<()> {
if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); }
let last_value = self.states.last().unwrap();
if !last_value.is_empty() {
return Err(anyhow::Error::msg("Last value should be empty"));
}
if last_value.commited_in == new_commitment {
return Err(anyhow::Error::msg("new_commitment is the same than existing tip"));
}
// Before updating we make sure that we only have one concurrent state
let concurrent_states = self.get_latest_concurrent_states()?;
if concurrent_states.len() != 2 {
return Err(anyhow::Error::msg("We must have exactly one state for the current tip"));
}
// Replace the existing tip
let new_tip = ProcessState {
commited_in: new_commitment,
..Default::default()
};
let _ = self.states.pop().unwrap();
self.states.push(new_tip);
Ok(())
}
/// We want to insert a new state that would be commited by the last UTXO
/// The new state *must* have the same commited_in than the last empty one
/// We want to always keep an empty state with only the latest unspent commited_in value at the last position
pub fn insert_concurrent_state(&mut self, new_state: ProcessState) -> anyhow::Result<()> {
if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); }
let last_value = self.states.last().unwrap();
if !last_value.is_empty() {
return Err(anyhow::Error::msg("Last value should be empty"));
}
if last_value.commited_in != new_state.commited_in {
return Err(anyhow::Error::msg("A concurrent state must have the same commited in than the tip of the states"));
}
let empty_state = self.states.pop().unwrap();
self.states.push(new_state);
// We always keep an empty state at the end
self.states.push(empty_state);
Ok(())
}
pub fn get_state_at(&self, index: usize) -> Option<&ProcessState> {
self.states.get(index)
}
pub fn get_state_at_mut(&mut self, index: usize) -> Option<&mut ProcessState> {
self.states.get_mut(index)
}
pub fn get_latest_state(&self) -> Option<&ProcessState> {
self.states.last()
}
pub fn get_latest_state_mut(&mut self) -> Option<&mut ProcessState> {
self.states.last_mut()
}
/// If `state` is a concurrent state, parent is the last commited one
/// Otherwise it's just the state commited before
/// If it's the initial state we return `None`
pub fn get_parent_state(&self, target_commited_in: &OutPoint) -> Option<&ProcessState> {
let tip = self.get_process_tip().ok()?; // Use `?` for cleaner error handling.
// If the target is the current tip, return the last committed state.
if tip == *target_commited_in {
return self.get_latest_commited_state();
}
// Iterate over the states to find the parent.
let mut parent_state = None;
for state in &self.states {
// Check if the current state's `commited_in` matches the target.
if state.commited_in == *target_commited_in {
return parent_state; // Return the parent state if found.
}
// Update the parent_state to the current state.
parent_state = Some(state);
}
// Return `None` if no matching state is found.
None
}
pub fn get_state_for_id(&self, state_id: &str) -> anyhow::Result<&ProcessState> {
if self.get_number_of_states() == 0 {
// This should never happen, but we better get rid of it now
return Err(anyhow::Error::msg("process is empty".to_owned()));
}
for p in &self.states {
if state_id == p.state_id.as_str() {
return Ok(p);
}
}
return Err(anyhow::Error::msg("No state for this merkle root"));
}
pub fn get_state_for_id_mut(&mut self, state_id: &str) -> anyhow::Result<&mut ProcessState> {
if self.get_number_of_states() == 0 {
// This should never happen, but we better get rid of it now
return Err(anyhow::Error::msg("process is empty".to_owned()));
}
for p in &mut self.states {
if state_id == p.state_id.as_str() {
return Ok(p);
}
}
return Err(anyhow::Error::msg("No state for this merkle root"));
}
/// This is useful when multiple unvalidated states are pending waiting for enough validations
/// It returns the latest state and all the previous states that have the same commited_in
/// It means that all the returned states are unvalidated and except for the one that get validated they will be pruned
pub fn get_latest_concurrent_states(&self) -> anyhow::Result<Vec<&ProcessState>> {
if self.get_number_of_states() == 0 {
// This should never happen, but we better get rid of it now
return Err(anyhow::Error::msg("process is empty".to_owned()));
}
let mut states = vec![];
let mut previous_commited_in = OutPoint::null();
// We iterate backwards until we find a state that has a different commited_in
for state in self.states.iter().rev() {
if previous_commited_in == OutPoint::null() {
previous_commited_in = state.commited_in;
} else if previous_commited_in != state.commited_in {
break;
}
states.push(state);
}
Ok(states)
}
pub fn get_latest_concurrent_states_mut(&mut self) -> anyhow::Result<Vec<&mut ProcessState>> {
if self.get_number_of_states() == 0 {
// This should never happen, but we better get rid of it now
return Err(anyhow::Error::msg("process is empty".to_owned()));
}
let mut states = vec![];
let mut previous_commited_in = OutPoint::null();
// We iterate backwards until we find a state that has a different commited_in
for state in self.states.iter_mut().rev() {
if previous_commited_in == OutPoint::null() {
previous_commited_in = state.commited_in;
} else if previous_commited_in != state.commited_in {
break;
}
states.push(state);
}
Ok(states)
}
pub fn remove_all_concurrent_states(&mut self) -> anyhow::Result<Vec<ProcessState>> {
if self.get_number_of_states() == 0 {
// This should never happen, but we better get rid of it now
return Err(anyhow::Error::msg("process is empty".to_owned()));
}
let empty_state = self.states.pop().unwrap();
let last_commitment_outpoint = empty_state.commited_in;
if let Some(split_index) = self.states.iter().position(|state| state.commited_in == last_commitment_outpoint) {
let removed = self.states.split_off(split_index);
// We make sure we always have an empty state at the end
self.states.push(empty_state);
Ok(removed)
} else {
// This could happen if we weren't aware there was pending concurrent updates
// We push the empty state back and return an empty vec
self.states.push(empty_state);
Ok(vec![])
}
}
pub fn get_latest_commited_state(&self) -> Option<&ProcessState> {
if self.states.is_empty() {
return None;
}
let last_state = self.states.last().unwrap();
debug_assert!(last_state.is_empty()); // Last state must always be empty
// We look for the last commited in before all the pending states
let latest_outpoint = last_state.commited_in;
return self
.states
.iter()
.rev()
.find(|s| s.commited_in != latest_outpoint);
}
pub fn get_state_index(&self, state: &ProcessState) -> Option<usize> {
// Get the commited_in value of the provided state
let target_commited_in = state.commited_in;
// Find the index of the first state with the same commited_in value
self.states
.iter()
.position(|s| s.commited_in == target_commited_in)
}
pub fn get_number_of_states(&self) -> usize {
self.states.len()
}
}
pub static CACHEDPROCESSES: OnceLock<Mutex<HashMap<OutPoint, Process>>> = OnceLock::new();
pub fn lock_processes() -> Result<MutexGuard<'static, HashMap<OutPoint, Process>>, anyhow::Error> {
CACHEDPROCESSES
.get_or_init(|| Mutex::new(HashMap::new()))
.lock_anyhow()
}
pub fn check_tx_for_process_updates(tx: &Transaction) -> anyhow::Result<OutPoint> {
let mut processes = lock_processes()?;
for (outpoint, process) in &mut *processes {
let process_tip = if let Ok(tip) = process.get_process_tip() { tip } else { continue };
for input in &tx.input {
if process_tip == input.previous_output {
log::debug!("Find a match for process tip {}", process_tip);
// This transaction commits a new state
let last_output = &tx.output.get(tx.output.len()-1).unwrap().script_pubkey;
let state_id: String;
if last_output.is_op_return() {
if last_output.as_bytes().len() != 34 {
return Err(anyhow::Error::msg("commited data is not 32B long"));
}
state_id = last_output.as_bytes()[2..].to_lower_hex_string();
} else {
return Err(anyhow::Error::msg("last output must be op_return"));
}
// Check if we know about the commited state
let new_state: ProcessState;
if let Ok(commited_state) = process.get_state_for_id(&state_id) {
new_state = commited_state.clone();
} else {
new_state = ProcessState {
commited_in: process_tip,
state_id,
..Default::default()
};
}
// We update the process in place
process.remove_all_concurrent_states()?;
process.insert_concurrent_state(new_state)?;
process.update_states_tip(OutPoint::new(tx.txid(), 0))?;
return Ok(*outpoint)
}
}
}
Err(anyhow::Error::msg("Transaction doesn't spend any known commitment"))
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use serde_json::json;
use sp_client::{
bitcoin::{secp256k1::SecretKey, Network}, silentpayments::utils::SilentPaymentAddress, spclient::{SpClient, SpWallet, SpendKey}
};
use crate::pcd::{Member, ValidationRule};
use super::*;
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()
}
fn create_carol_wallet() -> SpWallet {
SpWallet::new(
SpClient::new(
"default".to_owned(),
SecretKey::from_str(
"e4a5906eaa1a7ab24d5fc8d9b600d47f79caa6511c056c111677b7a33e62c5e9",
)
.unwrap(),
SpendKey::Secret(
SecretKey::from_str(
"e4c282e14668af1435e39df78403a7b406a791e3c6e666295496a6a865ade162",
)
.unwrap(),
),
None,
Network::Signet,
)
.unwrap(),
None,
vec![],
)
.unwrap()
}
fn dummy_process_state() -> ProcessState {
let alice_wallet = create_alice_wallet();
let bob_wallet = create_bob_wallet();
let carol_wallet = create_carol_wallet();
let alice_address =
SilentPaymentAddress::try_from(alice_wallet.get_client().get_receiving_address())
.unwrap();
let bob_address =
SilentPaymentAddress::try_from(bob_wallet.get_client().get_receiving_address())
.unwrap();
let carol_address =
SilentPaymentAddress::try_from(carol_wallet.get_client().get_receiving_address())
.unwrap();
let alice_bob = Member::new(vec![alice_address, bob_address]).unwrap();
let carol = Member::new(vec![carol_address]).unwrap();
let validation_rule1 =
ValidationRule::new(1.0, vec!["field1".to_owned()], 0.5).unwrap();
let validation_rule2 = ValidationRule::new(1.0, vec!["field2".to_owned()], 0.5).unwrap();
let role_def1 = RoleDefinition {
members: vec![alice_bob],
validation_rules: vec![validation_rule1],
storages: vec![]
};
let role_def2 = RoleDefinition {
members: vec![carol],
validation_rules: vec![validation_rule2],
storages: vec![]
};
let roles: BTreeMap<String, RoleDefinition> = BTreeMap::from([
("role1".to_owned(), role_def1),
("role2".to_owned(), role_def2)
]);
let clear_pcd = json!({
"field1": "value1",
"field2": "value2",
});
let outpoint = OutPoint::null();
ProcessState::new(outpoint, clear_pcd.as_object().unwrap().clone(), &BTreeMap::new(), roles).unwrap()
}
#[test]
fn test_error_no_proofs() {
let state = dummy_process_state();
let result = state.is_valid(None);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Can't validate a state with no proofs attached"
);
}
#[test]
fn test_error_identical_previous_state() {
let mut state = dummy_process_state();
// We sign with a random key
let signing_key =
SecretKey::from_str("39b2a765dc93e02da04a0e9300224b4f99fa7b83cfae49036dff58613fd3277c")
.unwrap();
let message_hash = state.get_message_hash(true).unwrap();
state.validation_tokens.push(Proof::new(message_hash, signing_key));
let result = state.is_valid(Some(&state));
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"State is identical to the previous state"
);
}
#[test]
/// We provide a proof signed with a key that is not the spending key for either alice or bob
fn test_error_invalid_proof() {
let mut state = dummy_process_state();
// We sign with a random key
let signing_key =
SecretKey::from_str("39b2a765dc93e02da04a0e9300224b4f99fa7b83cfae49036dff58613fd3277c")
.unwrap();
let message_hash = state.get_message_hash(true).unwrap();
state.validation_tokens.push(Proof::new(message_hash, signing_key));
let result = state.is_valid(None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Not enough valid proofs");
}
#[test]
/// Carol signs alone for an init state
fn test_error_not_enough_signatures() {
let mut state = dummy_process_state();
// We sign with Carol key
let carol_key: SecretKey = create_carol_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let message_hash = state.get_message_hash(true).unwrap();
state.validation_tokens.push(Proof::new(message_hash, carol_key));
let result = state.is_valid(None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Not enough valid proofs");
}
#[test]
/// Alice signs alone for her fields in an init state
fn test_valid_just_enough_signatures() {
let mut state = dummy_process_state();
// We sign with Alice and Carol keys
let alice_key: SecretKey = create_alice_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let carol_key: SecretKey = create_carol_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let message_hash = state.get_message_hash(true).unwrap();
state.validation_tokens.push(Proof::new(message_hash, alice_key));
state.validation_tokens.push(Proof::new(message_hash, carol_key));
let result = state.is_valid(None);
assert!(result.is_ok());
}
#[test]
/// everyone signs for everything
fn test_valid_all_signatures() {
let mut state = dummy_process_state();
let alice_key: SecretKey = create_alice_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let bob_key: SecretKey = create_bob_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let carol_key: SecretKey = create_carol_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let message_hash = state.get_message_hash(true).unwrap();
state.validation_tokens.push(Proof::new(message_hash, alice_key));
state.validation_tokens.push(Proof::new(message_hash, bob_key));
state.validation_tokens.push(Proof::new(message_hash, carol_key));
let result = state.is_valid(None);
assert!(result.is_ok());
}
#[test]
/// Carol refuses change for her part
fn test_error_carol_votes_no() {
let mut state = dummy_process_state();
let alice_key: SecretKey = create_alice_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let bob_key: SecretKey = create_bob_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let carol_key: SecretKey = create_carol_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let message_hash_yes = state.get_message_hash(true).unwrap();
let message_hash_no = state.get_message_hash(false).unwrap();
state.validation_tokens.push(Proof::new(message_hash_yes, alice_key));
state.validation_tokens.push(Proof::new(message_hash_yes, bob_key));
state.validation_tokens.push(Proof::new(message_hash_no, carol_key));
let result = state.is_valid(None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Not enough valid proofs");
}
#[test]
/// Bob refuses change for his part, but Alice is enough to reach quorum
fn test_valid_bob_votes_no() {
let mut state = dummy_process_state();
let alice_key: SecretKey = create_alice_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let bob_key: SecretKey = create_bob_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let carol_key: SecretKey = create_carol_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let message_hash_yes = state.get_message_hash(true).unwrap();
let message_hash_no = state.get_message_hash(false).unwrap();
state.validation_tokens.push(Proof::new(message_hash_yes, alice_key));
state.validation_tokens.push(Proof::new(message_hash_no, bob_key));
state.validation_tokens.push(Proof::new(message_hash_yes, carol_key));
let result = state.is_valid(None);
assert!(result.is_ok());
}
#[test]
/// Everyone signs, and we have a previous state
fn test_valid_everyone_signs_with_prev_state() {
let state = dummy_process_state();
let mut new_state = state.clone();
let key_to_modify = state.encrypted_pcd.as_object().unwrap().keys().next().unwrap();
new_state.update_value(key_to_modify.as_str(), Value::String("new_value1".to_string())).unwrap();
let alice_key: SecretKey = create_alice_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let bob_key: SecretKey = create_bob_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let carol_key: SecretKey = create_carol_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let message_hash = new_state.get_message_hash(true).unwrap();
new_state.validation_tokens.push(Proof::new(message_hash, alice_key));
new_state.validation_tokens.push(Proof::new(message_hash, bob_key));
new_state.validation_tokens.push(Proof::new(message_hash, carol_key));
let result = new_state.is_valid(Some(&state));
assert!(result.is_ok());
}
#[test]
/// Only Carol signs, but she doesn't have modification rights on modified field
fn test_error_not_right_signatures_with_prev_state() {
let state = dummy_process_state();
let mut new_state = state.clone();
let key_to_modify = state.encrypted_pcd.as_object().unwrap().keys().next().unwrap();
new_state.update_value(key_to_modify.as_str(), Value::String("new_value1".to_string())).unwrap();
let carol_key: SecretKey = create_carol_wallet()
.get_client()
.get_spend_key()
.try_into()
.unwrap();
let message_hash = new_state.get_message_hash(true).unwrap();
new_state.validation_tokens.push(Proof::new(message_hash, carol_key));
let result = new_state.is_valid(Some(&state));
assert!(result.is_err());
}
}