1295 lines
53 KiB
Rust
1295 lines
53 KiB
Rust
use anyhow::Result;
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
use std::sync::{Mutex, MutexGuard, OnceLock};
|
|
use std::str::FromStr;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use sp_client::{
|
|
bitcoin::{OutPoint, Transaction},
|
|
silentpayments::SilentPaymentAddress,
|
|
};
|
|
use tsify::Tsify;
|
|
|
|
use crate::{
|
|
pcd::{Member, Pcd, PcdCommitments, RoleDefinition, Roles, ValidationRule}, serialization::{deserialize_hex, hex_array_btree, serialize_hex, OutPointMemberMap}, signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}, MutexExt, SpecialRoles, APOPHIS, PAIREDADDRESSES, PAIRING
|
|
};
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Tsify)]
|
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
|
pub struct ProcessState {
|
|
pub commited_in: OutPoint,
|
|
#[tsify(type = "Record<string, string>")]
|
|
pub pcd_commitment: PcdCommitments,
|
|
#[serde(serialize_with = "serialize_hex", deserialize_with = "deserialize_hex")]
|
|
#[tsify(type = "string")]
|
|
pub state_id: [u8; 32], // the root of the tree created with all the commitments + public_data + roles. Serves as an unique id for a state too
|
|
#[serde(with = "hex_array_btree")]
|
|
#[tsify(type = "Record<string, string>")]
|
|
pub keys: BTreeMap<String, [u8; 32]>, // We may not always have all the keys
|
|
pub validation_tokens: Vec<Proof>, // Signature of the hash of pcd_commitment tagged with some decision like "yes" or "no"
|
|
pub public_data: Pcd,
|
|
#[tsify(type = "Record<string, RoleDefinition>")]
|
|
pub roles: Roles,
|
|
}
|
|
|
|
impl ProcessState {
|
|
pub fn new(commited_in: OutPoint, private_data: Pcd, public_data: Pcd, roles: Roles) -> anyhow::Result<Self> {
|
|
// TODO check that we don't have duplicated field names in private and public data, nor a "roles" field name
|
|
let all_attributes = Pcd::new(private_data.clone().into_iter().chain(public_data.clone()).collect());
|
|
let pcd_commitment = PcdCommitments::new(&commited_in, &all_attributes, &roles)?;
|
|
|
|
let merkle_root = pcd_commitment.create_merkle_tree()?.root().ok_or(anyhow::Error::msg("Invalid merkle tree"))?;
|
|
|
|
let res = Self {
|
|
commited_in,
|
|
pcd_commitment,
|
|
state_id: merkle_root,
|
|
keys: BTreeMap::new(),
|
|
validation_tokens: vec![],
|
|
public_data,
|
|
roles,
|
|
};
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
pub fn update_value(&mut self, key: &str, new_value: &[u8]) -> anyhow::Result<()> {
|
|
// Update the commitment
|
|
let mut updated_commitments = self.pcd_commitment.clone();
|
|
updated_commitments.update_with_value(&self.commited_in, key, new_value)?;
|
|
|
|
// Update merkle tree
|
|
let merkle_tree = updated_commitments.create_merkle_tree()?;
|
|
|
|
// Update state_id
|
|
self.state_id = merkle_tree.root().ok_or_else(|| anyhow::Error::msg("Invalid merkle tree"))?;
|
|
|
|
// Everything is ok, we can update the state
|
|
self.pcd_commitment = updated_commitments;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_message_hash(&self, approval: bool) -> anyhow::Result<AnkHash> {
|
|
if approval {
|
|
Ok(AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(self.state_id)))
|
|
} else {
|
|
Ok(AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(self.state_id)))
|
|
}
|
|
}
|
|
|
|
fn handle_demiurge(&self, demiurge_role: &RoleDefinition) -> anyhow::Result<RoleDefinition> {
|
|
if demiurge_role.members.is_empty() {
|
|
return Err(anyhow::Error::msg("Invalid demiurge role: members can't be empty"));
|
|
}
|
|
// validation_rules is empty
|
|
if !demiurge_role.validation_rules.is_empty() {
|
|
return Err(anyhow::Error::msg("Invalid demiurge role: validation_rules must be empty"));
|
|
}
|
|
|
|
// if demiurge_role.storages.is_empty() {
|
|
// return Err(anyhow::Error::msg("Invalid demiurge role: storages can't be empty"));
|
|
// }
|
|
|
|
let all_keys: Vec<String> = self.pcd_commitment.keys().map(|k| k.clone()).collect();
|
|
|
|
// define the rule
|
|
let validation_rule = ValidationRule::new(
|
|
1.0,
|
|
all_keys.clone(),
|
|
1.0
|
|
)?;
|
|
let role = RoleDefinition {
|
|
members: demiurge_role.members.clone(),
|
|
storages: demiurge_role.storages.clone(),
|
|
validation_rules: vec![validation_rule]
|
|
};
|
|
Ok(role)
|
|
}
|
|
|
|
/// This is a simplified and streamlined validation for obliteration state
|
|
fn handle_obliteration(&self, apophis: &RoleDefinition, members_list: &OutPointMemberMap) -> anyhow::Result<()> {
|
|
// Apophis should have only one rule
|
|
if apophis.validation_rules.len() != 1 { return Err(anyhow::Error::msg("Should have only one rule")); };
|
|
let obliteration_rule = apophis.validation_rules.get(0).unwrap();
|
|
|
|
let empty_field = "";
|
|
// This rule should have only one empty string as field
|
|
if obliteration_rule.fields.len() != 1 { return Err(anyhow::Error::msg("Should have only one field")); };
|
|
if obliteration_rule.fields.get(0).unwrap() != empty_field { return Err(anyhow::Error::msg("Field should be empty")); };
|
|
|
|
apophis.is_satisfied(vec![empty_field.to_owned()], [0u8; 32], &self.validation_tokens, &members_list)
|
|
}
|
|
|
|
fn handle_pairing(&self, pairing_role: RoleDefinition, previous_addresses: Vec<SilentPaymentAddress>) -> anyhow::Result<()> {
|
|
// members must be empty
|
|
if !pairing_role.members.is_empty() { return Err(anyhow::Error::msg("Invalid pairing role: members list must be empty")); }
|
|
// pairing_role must have one rule that modifies pairedAddresses
|
|
let paired_addresses_rule = pairing_role.get_applicable_rules(PAIREDADDRESSES);
|
|
if paired_addresses_rule.len() != 1 {
|
|
return Err(anyhow::anyhow!("Invalid pairing role: there must one and only one rule for \"pairedAddresses\""));
|
|
}
|
|
|
|
// TODO check that it matches what we have in the commitment here or somewhere else?
|
|
|
|
let updated_addresses_json = self.public_data.get_as_json(PAIREDADDRESSES)?;
|
|
let updated_addresses_vec: Vec<SilentPaymentAddress> = serde_json::from_value(updated_addresses_json)?;
|
|
let updated_member = Member::new(updated_addresses_vec);
|
|
let previous_member = Member::new(previous_addresses);
|
|
|
|
let members = if previous_member.get_addresses().is_empty() {
|
|
vec![&updated_member]
|
|
} else {
|
|
vec![&previous_member]
|
|
};
|
|
|
|
paired_addresses_rule.get(0).unwrap().is_satisfied(PAIREDADDRESSES, self.state_id, &self.validation_tokens, members.as_slice())
|
|
}
|
|
|
|
pub fn is_valid(&self, previous_state: Option<&ProcessState>, members_list: &OutPointMemberMap) -> anyhow::Result<()> {
|
|
if self.validation_tokens.is_empty() {
|
|
return Err(anyhow::anyhow!(
|
|
"Can't validate a state with no proofs attached"
|
|
));
|
|
}
|
|
|
|
// We first handle obliteration
|
|
if self.state_id == [0u8; 32] {
|
|
if let Some(prev_state) = previous_state {
|
|
if let Some(apophis) = prev_state.roles.get(APOPHIS) {
|
|
return self.handle_obliteration(apophis, members_list);
|
|
}
|
|
}
|
|
return Err(anyhow::anyhow!("Can't find apophis"));
|
|
// then pairing
|
|
} else if let Some(pairing_role) = self.roles.get(PAIRING) {
|
|
if self.pcd_commitment.contains_key(PAIREDADDRESSES) {
|
|
if let Some(prev_state) = previous_state {
|
|
let prev_paired_addresses_json = prev_state.public_data.get_as_json(PAIREDADDRESSES)?;
|
|
let paired_addresses: Vec<SilentPaymentAddress> = serde_json::from_value(prev_paired_addresses_json)?;
|
|
return self.handle_pairing(pairing_role.clone(), paired_addresses);
|
|
} else {
|
|
// We are in a creation
|
|
return self.handle_pairing(pairing_role.clone(), vec![]);
|
|
}
|
|
}
|
|
// If we don't update pairedAddresses, we don't need to bother about pairing
|
|
}
|
|
|
|
// Check if each modified field satisfies at least one applicable rule across all roles
|
|
let all_fields_validated = !self.pcd_commitment.is_empty() && self.pcd_commitment.keys().all(|field| {
|
|
// Collect applicable rules from all roles for the current field
|
|
let applicable_roles: Vec<RoleDefinition> = self.roles
|
|
.iter()
|
|
.filter_map(|(role_name, role_def)| {
|
|
if let Ok(special_role) = SpecialRoles::from_str(&role_name) {
|
|
match special_role {
|
|
// We allow for a special case with a role that works only for initial state
|
|
// That's optional though
|
|
SpecialRoles::Demiurge => {
|
|
// If we're not in initial state just ignore it
|
|
if previous_state.is_some() {
|
|
return None;
|
|
} else {
|
|
// We try to validate with demiurge
|
|
match self.handle_demiurge(role_def) {
|
|
Ok(role) => return Some(role),
|
|
Err(e) => {
|
|
log::error!("{}", e.to_string());
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
// Otherwise we just continue normal validation
|
|
}
|
|
// We already handled other special roles
|
|
_ => return None
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
applicable_roles.into_iter().any(|role_def| {
|
|
role_def.validation_rules.iter().any(|rule| {
|
|
let members: Vec<&Member> = role_def.members.iter()
|
|
.filter_map(|outpoint| {
|
|
members_list.0.get(outpoint)
|
|
})
|
|
.collect();
|
|
rule.is_satisfied(
|
|
field,
|
|
self.state_id,
|
|
&self.validation_tokens,
|
|
members.as_slice(),
|
|
).is_ok()
|
|
})
|
|
})
|
|
});
|
|
|
|
if all_fields_validated {
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("Not enough valid proofs"))
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.state_id == [0u8; 32]
|
|
}
|
|
|
|
pub fn get_fields_to_validate_for_member(&self, member: &OutPoint) -> anyhow::Result<Vec<String>> {
|
|
let mut res: HashSet<String> = HashSet::new();
|
|
|
|
// Are we in that role?
|
|
for (_, role_def) in self.roles.iter() {
|
|
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
|
|
/// Commiting this last empty state in a transaction is called obliterating a process, basically terminating it
|
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)]
|
|
#[tsify(into_wasm_abi, from_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: &[u8; 32]) -> 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 {
|
|
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: &[u8; 32]) -> 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 {
|
|
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!("Found a match for process tip {}", process_tip);
|
|
// This transaction commits a new state
|
|
// Look for the op_return
|
|
let op_return_outputs: Vec<_> = tx.output.iter().filter(|o| o.script_pubkey.is_op_return()).collect();
|
|
if op_return_outputs.len() != 1 {
|
|
return Err(anyhow::Error::msg("Transaction must contain exactly one op_return output"));
|
|
}
|
|
let mut state_id = [0u8; 32];
|
|
let data = &op_return_outputs.into_iter().next().unwrap().script_pubkey.as_bytes()[2..];
|
|
if data.len() != 32 {
|
|
return Err(anyhow::Error::msg("commited data is not 32B long"));
|
|
}
|
|
state_id.clone_from_slice(data);
|
|
// 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, Value};
|
|
use sp_client::{
|
|
bitcoin::{secp256k1::SecretKey, Network, Txid},
|
|
silentpayments::SilentPaymentAddress,
|
|
SpClient, SpendKey
|
|
};
|
|
|
|
use crate::pcd::{Member, ValidationRule, ZSTD_COMPRESSION_LEVEL};
|
|
|
|
use super::*;
|
|
|
|
const ALICE_BOB_PAIRING: &str = "fcd21fcf92c8ddd74bb2726138bee9951946ca03b10c1297accd67da159df82b:0";
|
|
const CAROL_PAIRING: &str = "a4f3d2d5ca7af258e6a2c1cfe85b85d4e3f3d1387417fd64012d3c7bfb95a9e9:0";
|
|
const DAVE_PAIRING: &str = "bd21f6acdd0e026e8c02298a51ec40dfaced34d95aec685f407ab5ac91b5f775:0";
|
|
|
|
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 create_carol_wallet() -> SpClient {
|
|
SpClient::new(
|
|
SecretKey::from_str(
|
|
"e4a5906eaa1a7ab24d5fc8d9b600d47f79caa6511c056c111677b7a33e62c5e9",
|
|
)
|
|
.unwrap(),
|
|
SpendKey::Secret(
|
|
SecretKey::from_str(
|
|
"e4c282e14668af1435e39df78403a7b406a791e3c6e666295496a6a865ade162",
|
|
)
|
|
.unwrap(),
|
|
),
|
|
Network::Signet,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn create_dave_wallet() -> SpClient {
|
|
SpClient::new(
|
|
SecretKey::from_str(
|
|
"261d5f9ae4d2b0d8b17ed0c52bd2be7dbce14d9ac1f0f1d4904d3ca7df03766d",
|
|
)
|
|
.unwrap(),
|
|
SpendKey::Secret(
|
|
SecretKey::from_str(
|
|
"8441e2adbb39736f384617fafc61e0d894bf3a5c2b69801fd4476bdcce04fb59",
|
|
)
|
|
.unwrap(),
|
|
),
|
|
Network::Signet,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn get_members_map() -> HashMap<OutPoint, Member> {
|
|
let alice_wallet = create_alice_wallet();
|
|
let bob_wallet = create_bob_wallet();
|
|
let carol_wallet = create_carol_wallet();
|
|
let dave_wallet = create_dave_wallet();
|
|
|
|
let alice_address =
|
|
SilentPaymentAddress::try_from(alice_wallet.get_receiving_address())
|
|
.unwrap();
|
|
let bob_address =
|
|
SilentPaymentAddress::try_from(bob_wallet.get_receiving_address())
|
|
.unwrap();
|
|
let carol_address =
|
|
SilentPaymentAddress::try_from(carol_wallet.get_receiving_address())
|
|
.unwrap();
|
|
let dave_address =
|
|
SilentPaymentAddress::try_from(dave_wallet.get_receiving_address())
|
|
.unwrap();
|
|
|
|
let members_map: HashMap<OutPoint, Member> = HashMap::from([
|
|
(OutPoint::from_str(ALICE_BOB_PAIRING).unwrap(), Member::new(vec![alice_address, bob_address])),
|
|
(OutPoint::from_str(CAROL_PAIRING).unwrap(), Member::new(vec![carol_address])),
|
|
(OutPoint::from_str(DAVE_PAIRING).unwrap(), Member::new(vec![dave_address])),
|
|
]);
|
|
|
|
members_map
|
|
}
|
|
|
|
fn dummy_process_state() -> ProcessState {
|
|
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 validation_rule3 = ValidationRule::new(1.0, vec!["roles".to_owned()], 0.5).unwrap();
|
|
let validation_rule4 = ValidationRule::new(1.0, vec!["public1".to_owned(), "public2".to_owned()], 0.5).unwrap();
|
|
let apophis_rule = ValidationRule::new(1.0, vec!["".to_owned()], 1.0).unwrap();
|
|
|
|
let role_demiurge = RoleDefinition {
|
|
members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()],
|
|
validation_rules: vec![],
|
|
storages: vec![]
|
|
};
|
|
let role_def1 = RoleDefinition {
|
|
members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()],
|
|
validation_rules: vec![validation_rule1],
|
|
storages: vec![]
|
|
};
|
|
let role_def2 = RoleDefinition {
|
|
members: vec![OutPoint::from_str(CAROL_PAIRING).unwrap()],
|
|
validation_rules: vec![validation_rule2],
|
|
storages: vec![]
|
|
};
|
|
|
|
let role_def_roles = RoleDefinition {
|
|
members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()],
|
|
validation_rules: vec![validation_rule3],
|
|
storages: vec![]
|
|
};
|
|
|
|
let role_def_public_data = RoleDefinition {
|
|
members: vec![OutPoint::from_str(ALICE_BOB_PAIRING).unwrap()],
|
|
validation_rules: vec![validation_rule4],
|
|
storages: vec![]
|
|
};
|
|
|
|
let role_def_apophis = RoleDefinition {
|
|
members: vec![OutPoint::from_str(DAVE_PAIRING).unwrap()],
|
|
validation_rules: vec![apophis_rule],
|
|
storages: vec![]
|
|
};
|
|
|
|
let roles: BTreeMap<String, RoleDefinition> = BTreeMap::from([
|
|
("demiurge".to_owned(), role_demiurge),
|
|
("role1".to_owned(), role_def1),
|
|
("role2".to_owned(), role_def2),
|
|
("role_roles".to_owned(), role_def_roles),
|
|
("role_public_data".to_owned(), role_def_public_data),
|
|
("apophis".to_owned(), role_def_apophis)
|
|
]);
|
|
|
|
let private_data: Pcd = json!({
|
|
"field1": "value1",
|
|
"field2": "value2",
|
|
}).try_into().unwrap();
|
|
|
|
let outpoint = OutPoint::null();
|
|
|
|
let public_data: Pcd = json!({
|
|
"public1": "public1",
|
|
"public2": "public2",
|
|
}).try_into().unwrap();
|
|
|
|
ProcessState::new(outpoint, private_data, public_data, Roles::new(roles)).unwrap()
|
|
}
|
|
|
|
fn create_pairing_process_one() -> ProcessState {
|
|
let carol_wallet = create_carol_wallet();
|
|
let carol_address = carol_wallet.get_receiving_address();
|
|
let pairing_rule =
|
|
ValidationRule::new(1.0, vec![PAIREDADDRESSES.to_owned()], 1.0).unwrap();
|
|
let pairing_role_def = RoleDefinition {
|
|
members: vec![],
|
|
validation_rules: vec![pairing_rule],
|
|
storages: vec![]
|
|
};
|
|
|
|
let outpoint = OutPoint::from_str("8425e9749957fd8ca83460c21718be4017692fd3ae61cbb0f0d401e7a5ddce6a:0").unwrap();
|
|
|
|
let private_data: Pcd = json!({"description": "pairing"}).try_into().unwrap();
|
|
|
|
let paired_addresses: Pcd = json!({PAIREDADDRESSES: [carol_address]}).try_into().unwrap();
|
|
|
|
ProcessState::new(outpoint, private_data, paired_addresses, Roles::new(BTreeMap::from([(PAIRING.to_owned(), pairing_role_def)]))).unwrap()
|
|
}
|
|
|
|
fn create_pairing_process_two() -> ProcessState {
|
|
let carol_wallet = create_carol_wallet();
|
|
let carol_address = carol_wallet.get_receiving_address();
|
|
let dave_wallet = create_dave_wallet();
|
|
let dave_address = dave_wallet.get_receiving_address();
|
|
let pairing_rule =
|
|
ValidationRule::new(1.0, vec![PAIREDADDRESSES.to_owned()], 1.0).unwrap();
|
|
let pairing_role_def = RoleDefinition {
|
|
members: vec![],
|
|
validation_rules: vec![pairing_rule],
|
|
storages: vec![]
|
|
};
|
|
|
|
let outpoint = OutPoint::from_str("8425e9749957fd8ca83460c21718be4017692fd3ae61cbb0f0d401e7a5ddce6a:0").unwrap();
|
|
|
|
let private_data: Pcd = json!({"description": "pairing"}).try_into().unwrap();
|
|
|
|
let paired_addresses: Pcd = json!({PAIREDADDRESSES: [carol_address, dave_address]}).try_into().unwrap();
|
|
|
|
ProcessState::new(outpoint, private_data, paired_addresses, Roles::new(BTreeMap::from([(PAIRING.to_owned(), pairing_role_def)]))).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_no_proofs() {
|
|
let state = dummy_process_state();
|
|
let members_map = get_members_map();
|
|
let result = state.is_valid(None, &OutPointMemberMap(members_map));
|
|
assert!(result.is_err());
|
|
assert_eq!(
|
|
result.unwrap_err().to_string(),
|
|
"Can't validate a state with no proofs attached"
|
|
);
|
|
}
|
|
|
|
#[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, &OutPointMemberMap(get_members_map()));
|
|
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_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, &OutPointMemberMap(get_members_map()));
|
|
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_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.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, &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
/// AliceBob is the demiurge
|
|
fn test_valid_demiurge() {
|
|
let mut state = dummy_process_state();
|
|
// We sign with Alice and Carol keys
|
|
let alice_key: SecretKey = create_alice_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let bob_key: SecretKey = create_bob_wallet()
|
|
.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));
|
|
let result = state.is_valid(None, &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
/// Carol tries to bypass demiurge role
|
|
fn test_error_demiurge() {
|
|
let mut state = dummy_process_state();
|
|
// We sign with Alice and Carol keys
|
|
let carol_key: SecretKey = create_alice_wallet()
|
|
.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, &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
/// Carol tries to bypass demiurge role
|
|
fn test_error_demiurge_not_init_state() {
|
|
let mut state = dummy_process_state();
|
|
// We sign with Alice and Carol keys
|
|
let carol_key: SecretKey = create_alice_wallet()
|
|
.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(Some(&dummy_process_state()), &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_err());
|
|
}
|
|
#[test]
|
|
fn test_valid_pairing() {
|
|
let mut pairing_first_state = create_pairing_process_one();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let message_hash = pairing_first_state.get_message_hash(true).unwrap();
|
|
pairing_first_state.validation_tokens.push(Proof::new(message_hash, carol_key));
|
|
let result = pairing_first_state.is_valid(None, &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_pairing_wrong_proof() {
|
|
let mut pairing_first_state = create_pairing_process_one();
|
|
let alice_key: SecretKey = create_alice_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let message_hash = pairing_first_state.get_message_hash(true).unwrap();
|
|
pairing_first_state.validation_tokens.push(Proof::new(message_hash, alice_key));
|
|
let result = pairing_first_state.is_valid(None, &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_valid_pairing_add_device() {
|
|
let pairing_state = create_pairing_process_one();
|
|
let mut paired_addresses: Vec<SilentPaymentAddress> = serde_json::from_value(pairing_state.public_data.get_as_json(PAIREDADDRESSES).unwrap()).unwrap();
|
|
let mut pairing_process = Process::new(pairing_state.commited_in);
|
|
pairing_process.insert_concurrent_state(pairing_state).unwrap();
|
|
let new_commitment = OutPoint::from_str("7f1a6d8923d6ee58a075c0e99e25472bb22a3eea221739281c2beaf829f03f27:0").unwrap();
|
|
pairing_process.update_states_tip(new_commitment).unwrap();
|
|
|
|
let members_list = &OutPointMemberMap(HashMap::from(
|
|
[
|
|
(
|
|
pairing_process.get_process_id().unwrap(),
|
|
Member::new(
|
|
paired_addresses.clone()
|
|
))
|
|
]
|
|
));
|
|
|
|
// Add Dave address
|
|
let dave_address = create_dave_wallet().get_receiving_address();
|
|
paired_addresses.push(dave_address);
|
|
let roles = &pairing_process.get_latest_commited_state().unwrap().roles;
|
|
|
|
let mut add_device_state = ProcessState::new(new_commitment, Pcd::new(BTreeMap::new()), json!({PAIREDADDRESSES: paired_addresses}).try_into().unwrap(), roles.clone()).unwrap();
|
|
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let message_hash = add_device_state.get_message_hash(true).unwrap();
|
|
add_device_state.validation_tokens.push(Proof::new(message_hash, carol_key));
|
|
|
|
let result = add_device_state.is_valid(pairing_process.get_latest_commited_state(), members_list);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_valid_pairing_rm_device() {
|
|
let pairing_state = create_pairing_process_two();
|
|
let paired_addresses: Vec<SilentPaymentAddress> = serde_json::from_value(pairing_state.public_data.get_as_json(PAIREDADDRESSES).unwrap()).unwrap();
|
|
let mut pairing_process = Process::new(pairing_state.commited_in);
|
|
pairing_process.insert_concurrent_state(pairing_state).unwrap();
|
|
let new_commitment = OutPoint::from_str("7f1a6d8923d6ee58a075c0e99e25472bb22a3eea221739281c2beaf829f03f27:0").unwrap();
|
|
pairing_process.update_states_tip(new_commitment).unwrap();
|
|
|
|
let members_list = &OutPointMemberMap(HashMap::from(
|
|
[
|
|
(
|
|
pairing_process.get_process_id().unwrap(),
|
|
Member::new(
|
|
paired_addresses.clone()
|
|
))
|
|
]
|
|
));
|
|
|
|
// Remove Dave address
|
|
let dave_address = create_dave_wallet().get_receiving_address();
|
|
let filtered_paired_addresses: Vec<SilentPaymentAddress> = paired_addresses.into_iter().filter_map(|a| {
|
|
if a != dave_address {
|
|
Some(a)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
let roles = &pairing_process.get_latest_commited_state().unwrap().roles;
|
|
|
|
let mut rm_device_state = ProcessState::new(
|
|
new_commitment,
|
|
Pcd::new(BTreeMap::new()),
|
|
json!({PAIREDADDRESSES: filtered_paired_addresses}).try_into().unwrap(),
|
|
roles.clone()
|
|
).unwrap();
|
|
|
|
// We need both devices to agree to remove one
|
|
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let dave_key: SecretKey = create_dave_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let message_hash = rm_device_state.get_message_hash(true).unwrap();
|
|
rm_device_state.validation_tokens.push(Proof::new(message_hash, carol_key));
|
|
rm_device_state.validation_tokens.push(Proof::new(message_hash, dave_key));
|
|
|
|
let result = rm_device_state.is_valid(pairing_process.get_latest_commited_state(), members_list);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
/// We check that Dave can't add himself to the pairing
|
|
#[test]
|
|
fn test_error_pairing_add_device_wrong_signature() {
|
|
let pairing_state = create_pairing_process_one();
|
|
let mut paired_addresses: Vec<SilentPaymentAddress> = serde_json::from_value(pairing_state.public_data.get_as_json(PAIREDADDRESSES).unwrap()).unwrap();
|
|
let mut pairing_process = Process::new(pairing_state.commited_in);
|
|
pairing_process.insert_concurrent_state(pairing_state).unwrap();
|
|
let new_commitment = OutPoint::from_str("7f1a6d8923d6ee58a075c0e99e25472bb22a3eea221739281c2beaf829f03f27:0").unwrap();
|
|
pairing_process.update_states_tip(new_commitment).unwrap();
|
|
|
|
let members_list = &OutPointMemberMap(HashMap::from(
|
|
[
|
|
(
|
|
pairing_process.get_process_id().unwrap(),
|
|
Member::new(
|
|
paired_addresses.clone()
|
|
))
|
|
]
|
|
));
|
|
|
|
// Add Dave address
|
|
let dave_address = create_dave_wallet().get_receiving_address();
|
|
paired_addresses.push(dave_address);
|
|
let roles = &pairing_process.get_latest_commited_state().unwrap().roles;
|
|
|
|
let mut add_device_state = ProcessState::new(new_commitment, Pcd::new(BTreeMap::new()), json!({PAIREDADDRESSES: paired_addresses}).try_into().unwrap(), roles.clone()).unwrap();
|
|
|
|
let dave_key: SecretKey = create_dave_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let message_hash = add_device_state.get_message_hash(true).unwrap();
|
|
add_device_state.validation_tokens.push(Proof::new(message_hash, dave_key));
|
|
|
|
let result = add_device_state.is_valid(pairing_process.get_latest_commited_state(), members_list);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
/// everyone signs for everything
|
|
fn test_valid_all_signatures() {
|
|
let mut state = dummy_process_state();
|
|
let alice_key: SecretKey = create_alice_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let bob_key: SecretKey = create_bob_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.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, &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
/// Carol tries to obliterate the process but she's not apophis
|
|
fn test_error_invalid_obliteration() {
|
|
let mut state = dummy_process_state();
|
|
let mut process = Process::new(state.commited_in);
|
|
process.insert_concurrent_state(state.clone()).unwrap();
|
|
// We simulate a first commitment
|
|
process.update_states_tip(
|
|
OutPoint::new(
|
|
Txid::from_str(
|
|
"cbeb4455f8d11848809bacd59bfd570243dbe7c4e9a340fa949aae3020fdb127"
|
|
).unwrap()
|
|
, 0
|
|
)
|
|
).unwrap();
|
|
// Now we take the last empty state and try to invalidate it
|
|
let empty_state = process.get_state_for_id(&[0u8; 32]).unwrap();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let message_hash = empty_state.get_message_hash(true).unwrap();
|
|
state.validation_tokens.push(Proof::new(message_hash, carol_key));
|
|
let result = state.is_valid(None, &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
/// Dave signs to obliterate the process
|
|
fn test_valid_obliteration() {
|
|
let state = dummy_process_state();
|
|
let mut process = Process::new(state.commited_in);
|
|
process.insert_concurrent_state(state.clone()).unwrap();
|
|
// We simulate a first commitment
|
|
process.update_states_tip(
|
|
OutPoint::new(
|
|
Txid::from_str(
|
|
"cbeb4455f8d11848809bacd59bfd570243dbe7c4e9a340fa949aae3020fdb127"
|
|
).unwrap()
|
|
, 0
|
|
)
|
|
).unwrap();
|
|
// Now we take the last empty state and try to commit it to invalidate the whole process
|
|
let empty_state = process.get_state_for_id_mut(&[0u8; 32]).unwrap();
|
|
let dave_key: SecretKey = create_dave_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let message_hash = empty_state.get_message_hash(true).unwrap();
|
|
empty_state.validation_tokens.push(Proof::new(message_hash, dave_key));
|
|
let result = empty_state.is_valid(Some(&state), &OutPointMemberMap(get_members_map()));
|
|
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_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let bob_key: SecretKey = create_bob_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.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(Some(&dummy_process_state()), &OutPointMemberMap(get_members_map()));
|
|
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_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let bob_key: SecretKey = create_bob_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.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, &OutPointMemberMap(get_members_map()));
|
|
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.pcd_commitment.keys().into_iter().next().unwrap();
|
|
let mut serialized_value = Vec::new();
|
|
serde_json::to_writer(&mut serialized_value, &Value::String("new_value1".to_owned())).unwrap();
|
|
let updated_value = zstd::encode_all(serialized_value.as_slice(), ZSTD_COMPRESSION_LEVEL).unwrap();
|
|
new_state.update_value(key_to_modify.as_str(), updated_value.as_slice()).unwrap();
|
|
let alice_key: SecretKey = create_alice_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let bob_key: SecretKey = create_bob_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.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), &OutPointMemberMap(get_members_map()));
|
|
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.pcd_commitment.keys().into_iter().next().unwrap();
|
|
let mut serialized_value = Vec::new();
|
|
serde_json::to_writer(&mut serialized_value, &Value::String("new_value1".to_owned())).unwrap();
|
|
let updated_value = zstd::encode_all(serialized_value.as_slice(), ZSTD_COMPRESSION_LEVEL).unwrap();
|
|
new_state.update_value(key_to_modify.as_str(), updated_value.as_slice()).unwrap();
|
|
let carol_key: SecretKey = create_carol_wallet()
|
|
.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), &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_valid_add_someone_role() {
|
|
let mut state = dummy_process_state();
|
|
if let Some(role) = state.roles.get_mut(APOPHIS) {
|
|
role.members = vec![];
|
|
}
|
|
let mut process = Process::new(state.commited_in);
|
|
process.insert_concurrent_state(state).unwrap();
|
|
process.update_states_tip(
|
|
OutPoint::new(
|
|
Txid::from_str(
|
|
"cbeb4455f8d11848809bacd59bfd570243dbe7c4e9a340fa949aae3020fdb127"
|
|
).unwrap()
|
|
, 0
|
|
)
|
|
).unwrap();
|
|
// We now try to add dave into apophis role
|
|
let mut new_roles = process.get_latest_commited_state().unwrap().roles.clone();
|
|
|
|
if let Some(role) = new_roles.get_mut(APOPHIS) {
|
|
role.members = vec![OutPoint::from_str(DAVE_PAIRING).unwrap()];
|
|
}
|
|
|
|
let mut new_state = ProcessState::new(process.get_process_tip().unwrap(), Pcd::new(BTreeMap::new()), Pcd::new(BTreeMap::new()), new_roles).unwrap();
|
|
|
|
// Alice and Bob validate
|
|
let alice_key: SecretKey = create_alice_wallet()
|
|
.get_spend_key()
|
|
.try_into()
|
|
.unwrap();
|
|
let bob_key: SecretKey = create_bob_wallet()
|
|
.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));
|
|
let result = new_state.is_valid(process.get_parent_state(&new_state.commited_in), &OutPointMemberMap(get_members_map()));
|
|
assert!(result.is_ok());
|
|
}
|
|
}
|