Update prd/pcd to keep commitments of each field

This commit is contained in:
Sosthene 2024-11-12 20:52:28 +01:00
parent 4885066bd2
commit c2fde13e95
4 changed files with 237 additions and 156 deletions

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, KeyInit};
use anyhow::{Error, Result};
@ -12,7 +14,7 @@ use tsify::Tsify;
use crate::crypto::AAD;
use crate::error::AnkError;
use crate::pcd::Member;
use crate::pcd::{Member, RoleDefinition};
use crate::signature::Proof;
#[derive(Debug, Serialize, Deserialize, Tsify)]
@ -72,8 +74,8 @@ impl AnkFlag {
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct CommitMessage {
pub init_tx: String, // Can be tx or txid of the first transaction of the chain, which is maybe not ideal
pub encrypted_pcd: Map<String, Value>,
pub keys: Map<String, Value>,
pub pcd_commitment: Value, // map of field <=> hash of the clear value
pub roles: HashMap<String, RoleDefinition>, // Can be hashed and compared with the value above
pub validation_tokens: Vec<Proof>,
pub error: Option<AnkError>,
}
@ -84,13 +86,13 @@ impl CommitMessage {
/// validation_tokens must be empty
pub fn new_first_commitment(
transaction: Transaction,
encrypted_pcd: Map<String, Value>,
keys: Map<String, Value>,
pcd_commitment: Value,
roles: HashMap<String, RoleDefinition>,
) -> Self {
Self {
init_tx: serialize(&transaction).to_lower_hex_string(),
encrypted_pcd,
keys,
pcd_commitment,
roles,
validation_tokens: vec![],
error: None,
}
@ -101,13 +103,13 @@ impl CommitMessage {
/// validation_tokens must be empty
pub fn new_update_commitment(
init_tx: OutPoint,
encrypted_pcd: Map<String, Value>,
keys: Map<String, Value>,
pcd_commitment: Value,
roles: HashMap<String, RoleDefinition>,
) -> Self {
Self {
init_tx: init_tx.to_string(),
encrypted_pcd,
keys,
pcd_commitment,
roles,
validation_tokens: vec![],
error: None,
}

View File

@ -1,5 +1,6 @@
use anyhow::{Error, Result};
use std::{collections::HashSet, str::FromStr};
use rs_merkle::{algorithms::Sha256, Hasher, MerkleTree};
use std::{collections::{HashMap, HashSet}, str::FromStr};
use aes_gcm::{
aead::{Aead, Payload},
@ -11,9 +12,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sp_client::{
bitcoin::{
hashes::{sha256t_hash_newtype, Hash, HashEngine},
hex::{DisplayHex, FromHex},
XOnlyPublicKey,
consensus::serialize, hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, secp256k1::PublicKey, OutPoint, XOnlyPublicKey
},
silentpayments::utils::SilentPaymentAddress,
};
@ -85,17 +84,57 @@ impl AnkPcdHash {
AnkPcdHash::from_engine(eng)
}
pub fn from_map(map: &Map<String, Value>) -> Self {
let value = Value::Object(map.clone());
/// Adding the root_commitment guarantee that the same clear value across different processes wont' produce the same hash
pub fn from_value_with_outpoint(value: &Value, outpoint: &[u8]) -> Self {
let mut eng = AnkPcdHash::engine();
eng.input(outpoint);
eng.input(value.to_string().as_bytes());
AnkPcdHash::from_engine(eng)
}
}
pub trait Pcd<'a>: Serialize + Deserialize<'a> {
fn tagged_hash(&self) -> AnkPcdHash {
AnkPcdHash::from_value(&self.to_value())
fn from_string(str: &str) -> Result<Value> {
let value: Value = serde_json::from_str(str)?;
match value {
Value::Object(_) => Ok(value),
_ => Err(Error::msg("Not a Pcd: not a valid JSON object"))
}
}
fn hash_fields(&self, root_commitment: OutPoint) -> Result<Map<String, Value>> {
let map = self.to_value_object()?;
let outpoint = serialize(&root_commitment);
let mut field2hash = Map::with_capacity(map.len());
// this could be optimised since there's a midstate we're reusing
for (field, value) in map {
let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value, &outpoint);
field2hash.insert(field, Value::String(tagged_hash.to_string()));
}
Ok(field2hash)
}
/// We need to run `hash_fields` before
/// This will just take all the hash value and produces a merkle tree
fn create_merkle_tree(fields2hash: &Value) -> Result<MerkleTree<Sha256>> {
let leaves: Vec<[u8; 32]> = fields2hash
.as_object()
.unwrap()
.iter()
.map(|(_, value)| {
let mut res = [0u8; 32];
res.copy_from_slice(&Vec::from_hex(value.as_str().unwrap()).unwrap());
res
})
.collect();
let merkle_tree = MerkleTree::<Sha256>::from_leaves(&leaves);
Ok(merkle_tree)
}
fn encrypt_fields(
@ -104,14 +143,11 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> {
fields2keys: &mut Map<String, Value>,
fields2cipher: &mut Map<String, Value>,
) -> Result<()> {
let as_value = self.to_value();
let as_map = as_value
.as_object()
.ok_or_else(|| Error::msg("Expected object"))?;
let map = self.to_value_object()?;
let mut rng = thread_rng();
for (field, value) in as_map {
if fields_to_encrypt.contains(field) {
for (field, value) in map {
if fields_to_encrypt.contains(&field) {
let aes_key = Aes256Gcm::generate_key(&mut rng);
let nonce = Aes256Gcm::generate_nonce(&mut rng);
fields2keys.insert(
@ -125,9 +161,9 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> {
msg: value_string.as_bytes(),
aad: AAD,
};
let cipher = encrypt_eng
.encrypt(&nonce, payload)
.map_err(|e| Error::msg(format!("Encryption failed for field {}: {}", field, e)))?;
let cipher = encrypt_eng.encrypt(&nonce, payload).map_err(|e| {
Error::msg(format!("Encryption failed for field {}: {}", field, e))
})?;
let mut res = Vec::with_capacity(nonce.len() + cipher.len());
res.extend_from_slice(&nonce);
@ -147,8 +183,7 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> {
fields2keys: &Map<String, Value>,
fields2plain: &mut Map<String, Value>,
) -> Result<()> {
let value = self.to_value();
let map = value.as_object().unwrap();
let map = self.to_value_object()?;
for (field, encrypted_value) in map.iter() {
if let Some(aes_key) = fields2keys.get(field) {
@ -190,8 +225,41 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> {
Ok(())
}
fn to_value(&self) -> Value {
Value::from_str(&serde_json::to_string(&self).unwrap()).unwrap()
fn to_value_object(&self) -> Result<Map<String, Value>> {
let value = serde_json::to_value(self)?;
match value {
Value::Object(map) => Ok(map),
_ => Err(Error::msg("self is not a valid json object"))
}
}
fn extract_roles(&self) -> Result<HashMap<String, RoleDefinition>> {
let obj = self.to_value_object()?;
let parse_roles_map = |m: &Map<String, Value>| {
let mut res: HashMap<String, RoleDefinition> = HashMap::new();
for (name, role_def) in m {
res.insert(name.clone(), serde_json::from_value(role_def.clone())?);
}
<Result<HashMap<String, RoleDefinition>, Error>>::Ok(res)
};
if let Some(roles) = obj.get("roles") {
match roles {
Value::Object(m) => {
parse_roles_map(m)
},
Value::String(s) => {
let m: Map<String, Value> = serde_json::from_str(&s)?;
parse_roles_map(&m)
}
_ => Err(Error::msg("\"roles\" is not an object"))
}
} else {
Err(Error::msg("No \"roles\" key in this pcd"))
}
}
}
@ -313,7 +381,7 @@ impl ValidationRule {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct RoleDefinition {
pub members: Vec<Member>,

View File

@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine};
use sp_client::bitcoin::hex::FromHex;
use sp_client::bitcoin::secp256k1::SecretKey;
use sp_client::bitcoin::{OutPoint, Psbt, XOnlyPublicKey};
use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey};
use sp_client::bitcoin::{OutPoint, Psbt};
use sp_client::silentpayments::utils::SilentPaymentAddress;
use sp_client::spclient::SpWallet;
use tsify::Tsify;
@ -26,9 +26,9 @@ pub enum PrdType {
Message,
Update, // Update an existing process
List, // request a list of items
Response,
Confirm,
TxProposal, // Send a psbt asking for recipient signature, used for login not sure about other use cases
Response, // Validate (or disagree) with a prd update
Confirm, // Confirm we received an update
TxProposal, // Send a psbt asking for recipient signature
}
sha256t_hash_newtype! {
@ -61,8 +61,9 @@ pub struct Prd {
pub root_commitment: String,
pub sender: String,
pub keys: Map<String, Value>, // key is a key in pcd, value is the key to decrypt it
pub pcd_commitments: Value,
pub validation_tokens: Vec<Proof>,
pub payload: String, // Payload depends on the actual type
pub payload: String, // Additional information depending on the type
pub proof: Option<Proof>, // This must be None up to the creation of the network message
}
@ -76,6 +77,7 @@ impl Prd {
Self {
prd_type: PrdType::Connect,
root_commitment: String::default(),
pcd_commitments: Value::Null,
sender: serde_json::to_string(&sender).unwrap(),
validation_tokens,
keys: Map::new(),
@ -87,8 +89,9 @@ impl Prd {
pub fn new_update(
root_commitment: OutPoint,
sender: String, // Should take Member as argument
encrypted_pcd: Map<String, Value>,
encrypted_values_merkle_root: String,
keys: Map<String, Value>,
pcd_commitments: Value,
) -> Self {
Self {
prd_type: PrdType::Update,
@ -96,19 +99,8 @@ impl Prd {
sender,
validation_tokens: vec![],
keys,
payload: Value::Object(encrypted_pcd).to_string(),
proof: None,
}
}
pub fn new_tx_proposal(root_commitment: OutPoint, sender: Member, psbt: Psbt) -> Self {
Self {
prd_type: PrdType::TxProposal,
root_commitment: root_commitment.to_string(),
sender: serde_json::to_string(&sender).unwrap(),
validation_tokens: vec![],
keys: Map::new(),
payload: serde_json::to_string(&psbt).unwrap(),
pcd_commitments,
payload: encrypted_values_merkle_root,
proof: None,
}
}
@ -117,7 +109,7 @@ impl Prd {
root_commitment: OutPoint,
sender: String,
validation_tokens: Vec<Proof>,
pcd_commitment: AnkPcdHash,
pcd_commitments: Value,
) -> Self {
Self {
prd_type: PrdType::Response,
@ -125,7 +117,8 @@ impl Prd {
sender,
validation_tokens: validation_tokens,
keys: Map::new(),
payload: pcd_commitment.to_string(),
pcd_commitments,
payload: String::default(),
proof: None,
}
}
@ -133,29 +126,22 @@ impl Prd {
pub fn new_confirm(
root_commitment: OutPoint,
sender: Member,
pcd_commitment: AnkPcdHash,
pcd_commitments: Value,
) -> Self {
Self {
prd_type: PrdType::Confirm,
root_commitment: root_commitment.to_string(),
pcd_commitments,
sender: serde_json::to_string(&sender).unwrap(),
validation_tokens: vec![],
keys: Map::new(),
payload: pcd_commitment.to_string(),
payload: String::default(),
proof: None,
}
}
fn _extract_from_message(plain: &[u8], local_address: SilentPaymentAddress, commitment: Option<&AnkPrdHash>) -> Result<Self> {
pub fn extract_from_message(plain: &[u8], local_address: SilentPaymentAddress) -> Result<Self> {
let prd: Prd = serde_json::from_slice(plain)?;
if let Some(commitment) = commitment {
// check that the hash of the prd is consistent with what's commited in the op_return
if prd.create_commitment() != *commitment {
return Err(anyhow::Error::msg(
"Received prd is not what was commited in the transaction",
));
}
}
// check that the proof is consistent
if let Some(proof) = prd.proof {
@ -190,23 +176,9 @@ impl Prd {
} else {
log::warn!("No proof for prd with root_commitment {}", prd.root_commitment);
}
// check that the commitment outpoint is valid, just in case
OutPoint::from_str(&prd.root_commitment)?;
Ok(prd)
}
pub fn extract_from_message(plain: &[u8], local_address: SilentPaymentAddress) -> Result<Self> {
Self::_extract_from_message(plain, local_address, None)
}
pub fn extract_from_message_with_commitment(
plain: &[u8],
local_address: SilentPaymentAddress,
commitment: &AnkPrdHash,
) -> Result<Self> {
Self::_extract_from_message(plain, local_address, Some(commitment))
}
pub fn filter_keys(&mut self, to_keep: HashSet<String>) {
let current_keys = self.keys.clone();
let filtered_keys: Map<String, Value> = current_keys
@ -216,25 +188,6 @@ impl Prd {
self.keys = filtered_keys;
}
/// We commit to everything except the keys and the proof
/// Because 1) we need one commitment to common data for all recipients of the transaction
/// 2) we already commit to the keys in the sender proof anyway
pub fn create_commitment(&self) -> AnkPrdHash {
let mut to_commit = self.clone();
to_commit.keys = Map::new();
to_commit.proof = None;
to_commit.validation_tokens = vec![];
if to_commit.payload.len() != 64 && Vec::from_hex(&to_commit.payload).is_err() {
to_commit.payload = Value::from_str(&to_commit.payload)
.unwrap()
.tagged_hash()
.to_string();
}
AnkPrdHash::from_value(&to_commit.to_value())
}
/// Generate the signed proof and serialize to send over the network
pub fn to_network_msg(&self, sp_wallet: &SpWallet) -> Result<String> {
let spend_sk: SecretKey = sp_wallet.get_client().get_spend_key().try_into()?;
@ -255,6 +208,6 @@ impl Prd {
}
pub fn to_value(&self) -> Value {
Value::from_str(&self.to_string()).unwrap()
serde_json::to_value(self).unwrap()
}
}

View File

@ -5,12 +5,11 @@ use std::{
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress};
use sp_client::bitcoin::OutPoint;
use crate::{
crypto::AnkSharedSecretHash,
pcd::{AnkPcdHash, Pcd, RoleDefinition},
prd::Prd,
prd::{Prd, PrdType},
signature::Proof,
MutexExt,
};
@ -18,12 +17,20 @@ use crate::{
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ProcessState {
pub commited_in: OutPoint,
pub pcd_commitment: Value, // If we can't modify a field, we just copy the previous value
pub encrypted_pcd: Value, // Some fields may be clear, if the owner of the process decides so
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"
}
impl ProcessState {
pub fn decrypt_pcd(&self) -> Value {
// TODO add real error management
let mut fields2plain = Map::new();
let _ = self.encrypted_pcd.decrypt_fields(&self.keys, &mut fields2plain);
Value::Object(fields2plain)
}
fn compute_modified_fields(&self, previous_state: Option<&ProcessState>) -> Vec<String> {
let new_state = &self.encrypted_pcd;
@ -134,10 +141,16 @@ impl ProcessState {
Err(anyhow::anyhow!("Not enough valid proofs"))
}
}
pub fn is_empty(&self) -> bool {
self.encrypted_pcd == Value::Null ||
self.pcd_commitment == Value::Null
}
}
/// A process is basically a succession of states
/// If a process has nothing to do with us, impending_requests will be empty
/// 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)]
pub struct Process {
states: Vec<ProcessState>,
@ -155,8 +168,38 @@ impl Process {
}
}
pub fn insert_state(&mut self, state: ProcessState) {
self.states.push(state);
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)
}
/// We want to always keep an empty state with only the latest unspent commited_in value at the last position
pub fn insert_state(&mut self, prd_update: &Prd) -> anyhow::Result<()> {
if prd_update.prd_type != PrdType::Update { return Err(anyhow::Error::msg("Only update prd allowed")) }
if self.states.is_empty() { return Err(anyhow::Error::msg("Empty Process")); }
let new_encrypted_pcd: Value = serde_json::from_str(&prd_update.payload).unwrap();
let last_index = self.states.len() - 1;
let last_value = self.states.get(last_index).unwrap();
if last_value.encrypted_pcd != Value::Null ||
last_value.keys != Map::new() ||
last_value.pcd_commitment != Value::Null ||
last_value.validation_tokens != vec![]
{
return Err(anyhow::Error::msg("Last state is not empty"));
}
let empty_state = self.states.remove(last_index);
let new_state = ProcessState {
commited_in: empty_state.commited_in,
pcd_commitment: prd_update.pcd_commitments.clone(),
encrypted_pcd: new_encrypted_pcd,
keys: prd_update.keys.clone(),
validation_tokens: vec![]
};
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> {
@ -195,63 +238,79 @@ impl Process {
/// 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) -> Vec<&ProcessState> {
let mut states = vec![];
let latest_state = self.get_latest_state();
if latest_state.is_none() {
return states;
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 latest_state_outpoint = latest_state.unwrap().commited_in;
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 state.commited_in != latest_state_outpoint {
log::debug!("state: {:#?}", state);
if previous_commited_in == OutPoint::null() {
previous_commited_in = state.commited_in;
} else if previous_commited_in != state.commited_in {
break;
}
states.push(state);
}
states
Ok(states)
}
pub fn get_latest_concurrent_states_mut(&mut self) -> Vec<&mut ProcessState> {
let mut states = vec![];
let latest_state = self.get_latest_state();
if latest_state.is_none() {
return 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 latest_state_outpoint = latest_state.unwrap().commited_in;
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 state.commited_in != latest_state_outpoint {
if previous_commited_in == OutPoint::null() {
previous_commited_in = state.commited_in;
} else if previous_commited_in != state.commited_in {
break;
}
states.push(state);
}
states
Ok(states)
}
pub fn remove_latest_concurrent_states(&mut self) -> Vec<ProcessState> {
if self.states.is_empty() {
return vec![];
pub fn remove_latest_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 latest_state = self.get_latest_state().unwrap();
let latest_outpoint = latest_state.commited_in;
let mut previous_commited_in = OutPoint::null();
// Iterate backwards, find the reverse position, and adjust to forward position
let reverse_position = self.states.iter().rev().position(|state| {
if previous_commited_in == OutPoint::null() {
previous_commited_in = state.commited_in;
false // Continue iterating
} else if previous_commited_in != state.commited_in {
true // Stop when a different commited_in is found
} else {
false // Continue iterating
}
});
let pos = self
.states
.iter()
.position(|s| s.commited_in == latest_outpoint)
.unwrap();
let forward_position = reverse_position.map(|pos| self.states.len() - pos - 1);
self.states.split_off(pos)
match forward_position {
Some(pos) => Ok(self.states.split_off(pos)),
None => {
// We take everything out
let res = self.states.to_vec();
self.states = vec![];
Ok(res)
}
}
}
pub fn get_latest_commited_state(&self) -> Option<&ProcessState> {
@ -259,30 +318,18 @@ impl Process {
return None;
}
let latest_state = self.get_latest_state().unwrap();
let last_state = self.states.last().unwrap();
// a commited outpoint with an index of u32::MAX is a pending state
if latest_state.commited_in.vout != u32::MAX {
// This state is commited, there's no pending state
return Some(latest_state);
}
debug_assert!(last_state.is_empty()); // Last state must always be empty
// We look for the last state before all the pending states
let latest_outpoint = latest_state.commited_in;
// We look for the last commited in before all the pending states
let latest_outpoint = last_state.commited_in;
let pos = self
return self
.states
.iter()
.position(|s| s.commited_in == latest_outpoint)
.unwrap();
if pos == 0 {
// There's no commited states, we just return None
return None;
} else {
// The state just before is last commited state
return self.get_state_at(pos-1);
}
.rev()
.find(|s| s.commited_in != latest_outpoint);
}
pub fn insert_impending_request(&mut self, request: Prd) {
@ -297,6 +344,12 @@ impl Process {
self.impending_requests.iter_mut().collect()
}
pub fn prune_impending_requests(&mut self) {
self.impending_requests = self.impending_requests.clone().into_iter()
.filter(|r| r.prd_type != PrdType::None)
.collect();
}
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;
@ -444,6 +497,10 @@ mod tests {
}
});
let outpoint = OutPoint::null();
let pcd_commitment = encrypted_pcd.hash_fields(outpoint).unwrap();
let mut fields2keys = Map::new();
let mut fields2cipher = Map::new();
// let field_to_encrypt: Vec<String> = encrypted_pcd.as_object().unwrap().keys().map(|k| k.clone()).collect();
@ -454,7 +511,8 @@ mod tests {
.unwrap();
ProcessState {
commited_in: OutPoint::null(),
commited_in: outpoint,
pcd_commitment: Value::Object(pcd_commitment),
encrypted_pcd,
keys: Map::new(),
validation_tokens: vec![],