sdk_common/src/prd.rs
2024-10-06 10:40:29 +02:00

236 lines
7.7 KiB
Rust

use std::collections::HashSet;
use std::str::FromStr;
use anyhow::{Error, Result};
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::silentpayments::utils::SilentPaymentAddress;
use sp_client::spclient::SpWallet;
use tsify::Tsify;
use crate::pcd::{AnkPcdHash, Member, Pcd};
use crate::signature::{AnkHash, AnkMessageHash, Proof};
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub enum PrdType {
#[default]
None,
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
}
sha256t_hash_newtype! {
pub struct AnkPrdTag = hash_str("4nk/Prd");
#[hash_newtype(forward)]
pub struct AnkPrdHash(_);
}
impl AnkPrdHash {
pub fn from_value(value: &Value) -> Self {
let mut eng = AnkPrdHash::engine();
eng.input(value.to_string().as_bytes());
AnkPrdHash::from_engine(eng)
}
pub fn from_map(map: &Map<String, Value>) -> Self {
let value = Value::Object(map.clone());
let mut eng = AnkPrdHash::engine();
eng.input(value.to_string().as_bytes());
AnkPrdHash::from_engine(eng)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct Prd {
pub prd_type: PrdType,
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 validation_tokens: Vec<Proof>,
pub payload: String, // Payload depends on the actual type
pub proof: Option<Proof>, // This must be None up to the creation of the network message
}
impl Prd {
pub fn new_update(
root_commitment: OutPoint,
sender: String, // Should take Member as argument
encrypted_pcd: Map<String, Value>,
keys: Map<String, Value>,
) -> Self {
Self {
prd_type: PrdType::Update,
root_commitment: root_commitment.to_string(),
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(),
proof: None,
}
}
pub fn new_response(
root_commitment: OutPoint,
sender: String,
validation_tokens: Vec<Proof>,
pcd_commitment: AnkPcdHash,
) -> Self {
Self {
prd_type: PrdType::Response,
root_commitment: root_commitment.to_string(),
sender,
validation_tokens: validation_tokens,
keys: Map::new(),
payload: pcd_commitment.to_string(),
proof: None,
}
}
pub fn new_confirm(
root_commitment: OutPoint,
sender: Member,
pcd_commitment: AnkPcdHash,
) -> Self {
Self {
prd_type: PrdType::Confirm,
root_commitment: root_commitment.to_string(),
sender: serde_json::to_string(&sender).unwrap(),
validation_tokens: vec![],
keys: Map::new(),
payload: pcd_commitment.to_string(),
proof: None,
}
}
fn _extract_from_message(plain: &[u8], commitment: Option<&AnkPrdHash>) -> 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
let sender: Member = serde_json::from_str(&prd.sender)?;
if let Some(proof) = prd.proof {
// take the spending keys in sender
let addresses = sender.get_addresses();
let mut spend_keys: Vec<XOnlyPublicKey> = vec![];
for address in addresses {
spend_keys.push(
<SilentPaymentAddress>::try_from(address)?
.get_spend_key()
.x_only_public_key()
.0,
);
}
// The key in proof must be one of the sender keys
let proof_key = proof.get_key();
let mut known_key = false;
for key in spend_keys {
if key == proof_key {
known_key = true;
break;
}
}
if !known_key {
return Err(anyhow::Error::msg("Proof signed with an unknown key"));
}
proof.verify()?;
}
// 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]) -> Result<Self> {
Self::_extract_from_message(plain, None)
}
pub fn extract_from_message_with_commitment(
plain: &[u8],
commitment: &AnkPrdHash,
) -> Result<Self> {
Self::_extract_from_message(plain, 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
.into_iter()
.filter(|(field, _)| to_keep.contains(field))
.collect();
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;
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()?;
let to_sign = self.clone(); // we sign the whole prd, incl the keys, for each recipient
let message_hash =
AnkHash::Message(AnkMessageHash::from_message(to_sign.to_string().as_bytes()));
let proof = Proof::new(message_hash, spend_sk);
let mut res = self.clone();
res.proof = Some(proof);
Ok(res.to_string())
}
pub fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
pub fn to_value(&self) -> Value {
Value::from_str(&self.to_string()).unwrap()
}
}