use std::any::Any; use std::borrow::Borrow; use std::collections::{BTreeMap, HashMap, HashSet}; use std::io::Write; use std::ops::Index; use std::str::FromStr; use std::string::FromUtf8Error; use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError}; use std::time::{Duration, Instant}; use std::u32; use rand::{thread_rng, Fill, Rng, RngCore}; use sdk_common::aes_gcm::aead::generic_array::GenericArray; use sdk_common::aes_gcm::aes::cipher::ArrayLength; use sdk_common::aes_gcm::Nonce; use sdk_common::log::{self, debug, info, warn}; use anyhow::Context; use anyhow::Error as AnyhowError; use anyhow::Result as AnyhowResult; use sdk_common::aes_gcm::aead::{Aead, Payload}; use sdk_common::crypto::{ decrypt_with_key, encrypt_with_key, generate_key, AeadCore, Aes256Gcm, AnkSharedSecretHash, KeyInit, AAD }; use sdk_common::process::{check_tx_for_process_updates, lock_processes, Process, ProcessState}; use sdk_common::signature::{AnkHash, AnkMessageHash, AnkValidationNoHash, AnkValidationYesHash, Proof}; use sdk_common::sp_client::bitcoin::blockdata::fee_rate; use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize}; use sdk_common::sp_client::bitcoin::hashes::{sha256, sha256t, Hash}; use sdk_common::sp_client::bitcoin::hashes::{FromSliceError, HashEngine}; use sdk_common::sp_client::bitcoin::hex::{ self, parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, }; use sdk_common::sp_client::bitcoin::key::{Keypair, Parity, Secp256k1}; use sdk_common::sp_client::bitcoin::network::ParseNetworkError; use sdk_common::sp_client::bitcoin::p2p::message::NetworkMessage; use sdk_common::sp_client::bitcoin::psbt::raw; use sdk_common::sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, Scalar, SecretKey}; use sdk_common::sp_client::bitcoin::transaction::ParseOutPointError; use sdk_common::sp_client::bitcoin::{ Amount, Network, OutPoint, Psbt, Transaction, Txid, XOnlyPublicKey, }; use sdk_common::sp_client::constants::{ DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, }; use sdk_common::sp_client::silentpayments::utils as sp_utils; use sdk_common::sp_client::silentpayments::{ utils::{Network as SpNetwork, SilentPaymentAddress}, Error as SpError, }; use sdk_common::{signature, MutexExt, MAX_PRD_PAYLOAD_SIZE}; use serde_json::{json, Error as SerdeJsonError, Map, Value}; use serde::{de, Deserialize, Serialize}; use tsify::{JsValueSerdeExt, Tsify}; use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi}; use wasm_bindgen::prelude::*; use sdk_common::device::Device; use sdk_common::network::{ self, AnkFlag, CommitMessage, Envelope, FaucetMessage, NewTxMessage, }; use sdk_common::pcd::{ AnkPcdHash, AnkPcdTag, Member, Pcd, PcdCommitments, RoleDefinition, Roles, ValidationRule }; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; use sdk_common::sp_client::spclient::{ derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, }; use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; use sdk_common::secrets::SecretsStore; use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub enum DiffStatus { #[default] None, Rejected, Validated, } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct UserDiff { pub process_id: String, pub state_id: String, // TODO add a merkle proof that the new_value belongs to that state pub value_commitment: String, pub field: String, pub roles: Roles, pub description: Option, pub notify_user: bool, pub need_validation: bool, pub validation_status: DiffStatus, } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct UpdatedProcess { pub process_id: OutPoint, pub current_process: Process, pub diffs: Vec, // All diffs should have the same state_id pub encrypted_data: BTreeMap, // hashes in pcd commitment to ciphers pub validated_state: Option<[u8; 32]>, // when we add/receive validation proofs for a state } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct ApiReturn { pub secrets: Option, pub updated_process: Option, pub new_tx_to_send: Option, pub ciphers_to_send: Vec, pub commit_to_send: Option, pub push_to_storage: Vec, // hash of the requested data, must be in db } pub type ApiResult = Result; const IS_TESTNET: bool = true; const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000); pub static SHAREDSECRETS: OnceLock> = OnceLock::new(); pub fn lock_shared_secrets() -> Result, anyhow::Error> { SHAREDSECRETS .get_or_init(|| Mutex::new(SecretsStore::new())) .lock_anyhow() } #[derive(Debug, PartialEq, Eq)] pub struct ApiError { pub message: String, } impl ApiError { pub fn new(message: String) -> Self { ApiError { message } } } impl From for ApiError { fn from(value: AnyhowError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: serde_wasm_bindgen::Error) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: SpError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: FromSliceError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: SerdeJsonError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: HexToBytesError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: HexToArrayError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::PsbtParseError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::psbt::ExtractTxError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::secp256k1::Error) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: sdk_common::sp_client::bitcoin::consensus::encode::Error) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: FromUtf8Error) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: ParseNetworkError) -> Self { ApiError::new(value.to_string()) } } impl From for ApiError { fn from(value: ParseOutPointError) -> Self { ApiError::new(value.to_string()) } } impl Into for ApiError { fn into(self) -> JsValue { JsValue::from_str(&self.message) } } #[wasm_bindgen] pub fn setup() { wasm_logger::init(wasm_logger::Config::default()); } #[wasm_bindgen] pub fn get_address() -> ApiResult { let local_device = lock_local_device()?; Ok(local_device .get_wallet() .get_client() .get_receiving_address()) } #[wasm_bindgen] pub fn get_member() -> ApiResult { let local_device = lock_local_device()?; let us = local_device.to_member(); Ok(us) } #[wasm_bindgen] pub fn restore_device(device_str: String) -> ApiResult<()> { let device: Device = serde_json::from_str(&device_str)?; let mut local_device = lock_local_device()?; *local_device = device; Ok(()) } #[wasm_bindgen] pub fn create_device_from_sp_wallet(sp_wallet: String) -> ApiResult { let sp_wallet: SpWallet = serde_json::from_str(&sp_wallet)?; let our_address = set_new_device(sp_wallet)?; Ok(our_address) } #[wasm_bindgen] pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult { let sp_wallet = generate_sp_wallet(None, Network::from_core_arg(&network_str)?)?; let our_address = set_new_device(sp_wallet)?; Ok(our_address) } #[wasm_bindgen] pub fn is_paired() -> ApiResult { let local_device = lock_local_device()?; Ok(local_device.get_pairing_commitment().is_some()) } #[wasm_bindgen] pub fn pair_device(process_id: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; if local_device.get_pairing_commitment().is_some() { return Err(ApiError::new("Already paired".to_owned())); } let local_address = local_device .get_wallet() .get_client() .get_receiving_address(); if !sp_addresses.iter().any(|a| *a == local_address) { sp_addresses.push(local_address); } local_device.pair( OutPoint::from_str(&process_id)?, Member::new( sp_addresses .into_iter() .map(|a| TryInto::::try_into(a).unwrap()) .collect(), )?, ); Ok(()) } #[wasm_bindgen] pub fn unpair_device() -> ApiResult<()> { let mut local_device = lock_local_device()?; local_device.unpair(); Ok(()) } #[wasm_bindgen] pub fn dump_wallet() -> ApiResult { let device = lock_local_device()?; Ok(serde_json::to_string(device.get_wallet()).unwrap()) } #[wasm_bindgen] pub fn reset_process_cache() -> ApiResult<()> { let mut cached_processes = lock_processes()?; *cached_processes = HashMap::new(); debug_assert!(cached_processes.is_empty()); Ok(()) } #[wasm_bindgen] pub fn dump_process_cache() -> ApiResult { let cached_processes = lock_processes()?; let serializable_cache = cached_processes .iter() .map(|(outpoint, process)| { ( outpoint.to_string(), serde_json::to_value(&process).unwrap(), ) }) .collect::>(); let json_string = serde_json::to_string(&serializable_cache) .map_err(|e| ApiError::new(format!("Failed to serialize process cache: {}", e)))?; Ok(json_string) } #[wasm_bindgen] pub fn set_process_cache(processes: String) -> ApiResult<()> { let processes: Map = serde_json::from_str(&processes)?; let parsed_processes: anyhow::Result> = processes.into_iter() .map(|(key, value)| { Ok((OutPoint::from_str(&key)?, serde_json::from_value(value)?)) }) .collect(); let mut cached_processes = lock_processes()?; if !cached_processes.is_empty() { // Don't overwrite processes in memory return Err(ApiError::new("Processes cache is not empty".to_owned())); } *cached_processes = parsed_processes?; Ok(()) } #[wasm_bindgen] pub fn add_to_process_cache(process_id: String, process: String) -> ApiResult<()> { let process_id = OutPoint::from_str(&process_id)?; let process: Process = serde_json::from_str(&process)?; let mut cached_processes = lock_processes()?; cached_processes.insert(process_id, process); Ok(()) } #[wasm_bindgen] pub fn reset_shared_secrets() -> ApiResult<()> { let mut shared_secrets = lock_shared_secrets()?; *shared_secrets = SecretsStore::new(); Ok(()) } #[wasm_bindgen] pub fn set_shared_secrets(secrets: String) -> ApiResult<()>{ let mut shared_secrets = lock_shared_secrets()?; *shared_secrets = serde_json::from_str(&secrets)?; Ok(()) } #[wasm_bindgen] pub fn get_pairing_process_id() -> ApiResult { let local_device = lock_local_device()?; let pairing_commitment = local_device.get_pairing_commitment().ok_or(ApiError::new("Device is not paired".to_owned()))?; Ok(pairing_commitment.to_string()) } #[wasm_bindgen] pub fn dump_device() -> ApiResult { let local_device = lock_local_device()?; Ok(serde_json::to_string(&local_device.clone())?) } #[wasm_bindgen] pub fn reset_device() -> ApiResult<()> { let mut device = lock_local_device()?; *device = Device::default(); reset_shared_secrets()?; reset_process_cache()?; Ok(()) } #[wasm_bindgen] pub fn get_txid(transaction: String) -> ApiResult { let tx: Transaction = deserialize(&Vec::from_hex(&transaction)?)?; Ok(tx.txid().to_string()) } fn handle_transaction( updated: HashMap, tx: &Transaction, public_data: PublicKey, ) -> AnyhowResult { let b_scan: SecretKey; let local_member: Member; let sp_wallet: SpWallet; { let local_device = lock_local_device()?; sp_wallet = local_device.get_wallet().clone(); b_scan = local_device.get_wallet().get_client().get_scan_key(); local_member = local_device.to_member(); } let op_return: Vec<&sdk_common::sp_client::bitcoin::TxOut> = tx .output .iter() .filter(|o| o.script_pubkey.is_op_return()) .collect(); if op_return.len() != 1 { return Err(AnyhowError::msg( "Transaction must have exactly one op_return output", )); } let commitment = AnkPrdHash::from_slice(&op_return.first().unwrap().script_pubkey.as_bytes()[2..])?; // Basically a transaction that destroyed utxo is a transaction we sent. let utxo_destroyed: HashMap<&OutPoint, &OwnedOutput> = updated .iter() .filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent) .collect(); let mut shared_secrets = lock_shared_secrets()?; // empty utxo_destroyed means we received this transaction if utxo_destroyed.is_empty() { let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret( &public_data, &b_scan, ); let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); // We keep the shared_secret as unconfirmed shared_secrets.add_unconfirmed_secret(shared_secret); // We also return it let mut new_secret = SecretsStore::new(); new_secret.add_unconfirmed_secret(shared_secret); // We hash the shared secret to commit into the prd connect let secret_hash = AnkMessageHash::from_message(shared_secret.as_byte_array()); // We still don't know who sent it, so we reply with a `Connect` prd let prd_connect = Prd::new_connect(local_member, secret_hash, None); let msg = prd_connect.to_network_msg(&sp_wallet)?; // We encrypt the prd connect with the same secret let cipher = encrypt_with_key(shared_secret.as_byte_array(), msg.as_bytes())?; return Ok(ApiReturn { secrets: Some(new_secret), ciphers_to_send: vec![cipher.to_lower_hex_string()], ..Default::default() }) } else { // We're sender of the transaction, do nothing return Ok(ApiReturn { ..Default::default() }); } } /// If the transaction has anything to do with us, we create/update the relevant process /// and return it to caller for persistent storage fn process_transaction( tx_hex: String, blockheight: u32, tweak_data_hex: String, ) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; // Before anything, check if this transaction spends the tip of a process we know about match check_tx_for_process_updates(&tx) { Ok(outpoint) => { let processes = lock_processes()?; let process = processes.get(&outpoint).unwrap(); let new_state = process.get_latest_commited_state().unwrap(); let diffs = if let Ok(diffs) = create_diffs(process, new_state) { diffs } else { vec![] }; let updated_process = UpdatedProcess { process_id: outpoint, current_process: process.clone(), diffs, ..Default::default() }; let api_return = ApiReturn { updated_process: Some(updated_process), ..Default::default() }; debug!("Found an update for process {:?}", api_return.updated_process.as_ref().unwrap().process_id); return Ok(api_return); } Err(e) => debug!("Failed to find process update: {}", e) } let tweak_data = PublicKey::from_str(&tweak_data_hex)?; let updated: HashMap; { let mut device = lock_local_device()?; let wallet = device.get_mut_wallet(); updated = wallet.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; } if updated.len() > 0 { return handle_transaction(updated, &tx, tweak_data); } Err(anyhow::Error::msg("Transaction is not our")) } #[wasm_bindgen] pub fn parse_new_tx(new_tx_msg: String, block_height: u32) -> ApiResult { let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?; if let Some(error) = new_tx.error { return Err(ApiError::new(format!( "NewTx returned with an error: {}", error ))); } if new_tx.tweak_data.is_none() { return Err(ApiError::new("Missing tweak_data".to_owned())); } Ok(process_transaction( new_tx.transaction, block_height, new_tx.tweak_data.unwrap(), )?) } fn confirm_prd(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { match prd.prd_type { PrdType::Confirm | PrdType::Response | PrdType::List => { return Err(AnyhowError::msg("Invalid prd type")); } _ => (), } let outpoint = prd.process_id; let local_device = lock_local_device()?; let sender = local_device.to_member(); let prd_confirm = Prd::new_confirm(outpoint, sender, prd.pcd_commitments.clone()); let prd_msg = prd_confirm.to_network_msg(local_device.get_wallet())?; Ok(encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?.to_lower_hex_string()) } fn create_diffs(process: &Process, new_state: &ProcessState) -> AnyhowResult> { let new_state_commitments = &new_state.pcd_commitment; let device = lock_local_device()?; let our_id = device.to_member(); let fields_to_validate = new_state.get_fields_to_validate_for_member(&our_id)?; let new_state_id = &new_state.state_id; let new_public_data = &new_state.public_data; let process_id = process.get_process_id()?.to_string(); let mut diffs = vec![]; for (field, hash) in new_state_commitments.iter() { let description_field = new_public_data.get(field); let has_description = description_field.as_ref().is_some_and(|v| v.is_string()); let description = if has_description { description_field.unwrap().as_str().map(|s| s.to_owned()) } else { None }; let need_validation = fields_to_validate.contains(field); diffs.push(UserDiff { process_id: process_id.clone(), state_id: new_state_id.to_lower_hex_string(), value_commitment: hash.to_lower_hex_string(), field: field.to_owned(), description, notify_user: false, need_validation, validation_status: DiffStatus::None, roles: new_state.roles.clone(), }); } Ok(diffs) } fn handle_prd_connect(prd: Prd, secret: AnkSharedSecretHash) -> AnyhowResult { let local_device = lock_local_device()?; let local_member = local_device.to_member(); let sp_wallet = local_device.get_wallet(); let secret_hash = AnkMessageHash::from_message(secret.as_byte_array()); let mut shared_secrets = lock_shared_secrets()?; if let Some(prev_proof) = prd.validation_tokens.get(0) { // check that the proof is valid prev_proof.verify()?; // Check it's signed with our key let local_address = SilentPaymentAddress::try_from(sp_wallet.get_client().get_receiving_address())?; if prev_proof.get_key() != local_address.get_spend_key() { return Err(anyhow::Error::msg("Previous proof of a prd connect isn't signed by us")); } // Check it signs a prd connect that contains the commitment to the shared secret let empty_prd = Prd::new_connect(local_member, secret_hash, None); let msg = AnkMessageHash::from_message(empty_prd.to_string().as_bytes()); if *msg.as_byte_array() != prev_proof.get_message() { return Err(anyhow::Error::msg("Previous proof signs another message")); } // Now we can confirm the secret and link it to an address let proof = prd.proof.unwrap(); let actual_sender = prd.sender.get_address_for_key(&proof.get_key()) .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?); let mut secrets_return = SecretsStore::new(); secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?); return Ok(ApiReturn { secrets: Some(secrets_return), ..Default::default() }) } else { let proof = prd.proof.unwrap(); let actual_sender = prd.sender.get_address_for_key(&proof.get_key()) .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?); let mut secrets_return = SecretsStore::new(); secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?); let prd_connect = Prd::new_connect(local_member, secret_hash, prd.proof); let msg = prd_connect.to_network_msg(sp_wallet)?; let cipher = encrypt_with_key(secret.as_byte_array(), msg.as_bytes())?; return Ok(ApiReturn { ciphers_to_send: vec![cipher.to_lower_hex_string()], secrets: Some(secrets_return), ..Default::default() }) } } fn handle_prd( prd: Prd, secret: AnkSharedSecretHash ) -> AnyhowResult { debug!("handle_prd: {:#?}", prd); // Connect is a bit different here because there's no associated process // Let's handle that case separately if prd.prd_type == PrdType::Connect { return handle_prd_connect(prd, secret); } let outpoint = prd.process_id; let mut processes = lock_processes()?; let relevant_process = match processes.entry(outpoint) { std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), std::collections::hash_map::Entry::Vacant(entry) => { debug!("Creating new process for outpoint: {}", outpoint); entry.insert(Process::new(outpoint)) } }; match prd.prd_type { PrdType::Update => { // Compute the merkle tree root for the proposed new state to see if we already know about it let update_merkle_root = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?; if relevant_process.get_state_for_id(&update_merkle_root).is_ok() { // We already know about that state return Err(AnyhowError::msg("Received update for a state we already know")); } let commited_in = relevant_process.get_process_tip()?; let new_state = ProcessState { commited_in, pcd_commitment: prd.pcd_commitments, state_id: update_merkle_root.clone(), keys: prd.keys, roles: prd.roles, public_data: prd.public_data, ..Default::default() }; // Compute the diffs let diffs = create_diffs(&relevant_process, &new_state)?; relevant_process.insert_concurrent_state(new_state)?; let updated_process = UpdatedProcess { process_id: outpoint, current_process: relevant_process.clone(), diffs, ..Default::default() }; return Ok(ApiReturn { updated_process: Some(updated_process), ..Default::default() }); } PrdType::Response => { let update_state_id = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?; let mut to_update = relevant_process.get_state_for_id_mut(&update_state_id)?; let new_validations = prd.validation_tokens; let mut to_add: Vec<&Proof> = vec![]; for token in &new_validations { let key = token.get_key(); // Check if the token key already exists let already_exists = to_update .validation_tokens .iter() .any(|existing_token| existing_token.get_key() == key); if !already_exists { debug!("Adding token with key {}", key); to_add.push(token); } else { debug!("Token with key {} already exists, skipping", key); } } // If there's no new proofs return early if to_add.is_empty() { return Err(AnyhowError::msg("No new validation tokens found in prd response")); } // We add the new tokens and return all that was updated to_update.validation_tokens.extend(to_add); let updated_state = to_update.clone(); let validated_state = Some(to_update.state_id); let mut commit_msg = CommitMessage::new_update_commitment( prd.process_id, updated_state.pcd_commitment, updated_state.roles, updated_state.public_data ); commit_msg.set_validation_tokens(updated_state.validation_tokens); // We must return an update of the process let updated_process = UpdatedProcess { process_id: prd.process_id, current_process: relevant_process.clone(), validated_state, ..Default::default() }; return Ok(ApiReturn { updated_process: Some(updated_process), commit_to_send: Some(commit_msg), ..Default::default() }); } PrdType::Request => { // We are being requested encrypted data for one or more states, to be uploaded on storage let states: Vec<[u8; 32]> = serde_json::from_str(&prd.payload)?; let requester = prd.sender; // diffs will trigger upload of the encrypted data on storage let mut diffs = vec![]; // This will notify the requester and provide relevant information if needed let mut ciphers = vec![]; let mut push_to_storage = vec![]; for state_id in states { let state = match relevant_process.get_state_for_id(&state_id) { Ok(state) => state, Err(_) => { debug!("Ignoring request for unknown state {}", state_id.to_lower_hex_string()); continue; } }; let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); let mut relevant_fields: HashSet = HashSet::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in state.roles.iter() { if !role.members.contains(&requester) { // This role doesn't concern requester continue; } let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); relevant_fields.extend(fields); } let sender: Member = local_device .to_member(); let mut full_prd = Prd::new_update( outpoint, sender, state.roles.clone(), state.public_data.clone(), state.keys.clone(), state.pcd_commitment.clone(), ); full_prd.filter_keys(&relevant_fields); let prd_msg = full_prd.to_network_msg(sp_wallet)?; let addresses = requester.get_addresses(); for sp_address in addresses.into_iter() { // We skip our own device address, no point sending ourself a cipher if sp_address == local_address { continue; } // We shouldn't ever have error here since we're answering a cipher let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?) .ok_or(AnyhowError::msg(format!("No secret for address {}", sp_address.as_str())))?; let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } let pcd_commitment = &state.pcd_commitment; for (field, hash) in pcd_commitment.iter() { // We only need field that are visible by requester if !relevant_fields.contains(field.as_str()) { continue; } let diff = UserDiff { process_id: outpoint.to_string(), state_id: state_id.to_lower_hex_string(), value_commitment: hash.to_lower_hex_string(), field: field.to_owned(), ..Default::default() }; diffs.push(diff); push_to_storage.push(hash.to_lower_hex_string()); } } let updated_process = Some(UpdatedProcess { process_id: outpoint, current_process: relevant_process.clone(), diffs, ..Default::default() }); return Ok(ApiReturn { updated_process, ciphers_to_send: ciphers, push_to_storage, ..Default::default() }); } _ => unimplemented!(), } } fn handle_decrypted_message( secret: AnkSharedSecretHash, plain: Vec, ) -> anyhow::Result { let local_address: SilentPaymentAddress = lock_local_device()?.get_wallet().get_client().get_receiving_address().try_into()?; if let Ok(prd) = Prd::extract_from_message(&plain, local_address) { handle_prd(prd, secret) } else { Err(anyhow::Error::msg("Failed to handle decrypted message")) } } #[wasm_bindgen] pub fn parse_cipher(cipher_msg: String) -> ApiResult { // Check that the cipher is not empty or too long if cipher_msg.is_empty() || cipher_msg.len() > MAX_PRD_PAYLOAD_SIZE { return Err(ApiError::new( "Invalid cipher: empty or too long".to_owned(), )); } let cipher = Vec::from_hex(cipher_msg.trim_matches('"'))?; let decrypt_res = lock_shared_secrets()?.try_decrypt(&cipher); if let Ok((secret, plain)) = decrypt_res { return handle_decrypted_message(secret, plain) .map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e))); } Err(ApiError::new("Failed to decrypt cipher".to_owned())) } #[wasm_bindgen] pub fn get_outputs() -> ApiResult { let device = lock_local_device()?; let outputs = device.get_wallet().get_outputs().clone(); Ok(JsValue::from_serde(&outputs.to_outpoints_list())?) } #[wasm_bindgen] pub fn get_available_amount() -> ApiResult { let device = lock_local_device()?; Ok(device.get_wallet().get_outputs().get_balance().to_sat()) } fn get_shared_secrets_in_transaction( psbt: &Psbt, addresses: Vec ) -> anyhow::Result> { let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let partial_secret = sp_wallet .get_client() .get_partial_secret_from_psbt(&psbt)?; let mut new_secrets = HashMap::new(); for address in addresses { let sp_address = SilentPaymentAddress::try_from(address.as_str())?; let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( &sp_address.get_scan_key(), &partial_secret, ); let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); new_secrets.insert(sp_address, shared_secret); } Ok(new_secrets) } fn create_transaction_for_addresses(addresses: Vec, fee_rate: u32) -> anyhow::Result { let mut sp_addresses: Vec = Vec::with_capacity(addresses.len()); for address in &addresses { let sp_address = SilentPaymentAddress::try_from(address.as_str())?; sp_addresses.push(sp_address); } let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let mut freezed_utxos = lock_freezed_utxos()?; let mut recipients = Vec::with_capacity(addresses.len()); for address in addresses { let recipient = Recipient { address, amount: DEFAULT_AMOUNT, nb_outputs: 1, }; recipients.push(recipient); } let signed_psbt = create_transaction( vec![], &freezed_utxos, sp_wallet, recipients, None, Amount::from_sat(fee_rate.into()), None, )?; // We add the used inputs in freezed utxos to prevent accidental double spends for input in &signed_psbt.unsigned_tx.input { freezed_utxos.insert(input.previous_output); } Ok(signed_psbt) } #[wasm_bindgen] /// We send a transaction that pays at least one output to each address /// The goal is to establish a shared_secret to be used as an encryption key for further communication pub fn create_connect_transaction(addresses: Vec, fee_rate: u32) -> ApiResult { if addresses.is_empty() { return Err(ApiError::new("No addresses to connect to".to_owned())); } let psbt = create_transaction_for_addresses(addresses.clone(), fee_rate)?; let new_secrets = get_shared_secrets_in_transaction(&psbt, addresses)?; let transaction = psbt.extract_tx()?; let mut shared_secrets = lock_shared_secrets()?; let mut secrets_return = SecretsStore::new(); for (address, secret) in new_secrets { shared_secrets.confirm_secret_for_address(secret, address); secrets_return.confirm_secret_for_address(secret, address); } Ok(ApiReturn { new_tx_to_send: Some(NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None)), secrets: Some(secrets_return), ..Default::default() }) } #[wasm_bindgen] pub fn create_new_process( init_state: JsValue, roles: JsValue, public_data: JsValue, relay_address: String, fee_rate: u32, ) -> ApiResult { let init_state: Pcd = serde_wasm_bindgen::from_value(init_state)?; let roles: Roles = serde_wasm_bindgen::from_value(roles)?; let public_data: Pcd = serde_wasm_bindgen::from_value(public_data)?; // We create a transaction that spends to the relay address let psbt = create_transaction_for_addresses(vec![relay_address.clone()], fee_rate)?; // We take the secret out let new_secrets = get_shared_secrets_in_transaction(&psbt, vec![relay_address])?; let mut shared_secrets = lock_shared_secrets()?; let mut secrets_return = SecretsStore::new(); for (address, secret) in new_secrets { shared_secrets.confirm_secret_for_address(secret, address); secrets_return.confirm_secret_for_address(secret, address); } let transaction = psbt.extract_tx()?; // We now have the outpoint that will serve as id for the whole process let process_id = OutPoint::new(transaction.txid(), 0); let new_tx_msg = NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None); let mut new_state = ProcessState::new(process_id, init_state.clone(), public_data.clone(), roles.clone())?; let pcd_commitment = new_state.pcd_commitment.clone(); let mut process = Process::new(process_id); let diffs = create_diffs(&process, &new_state)?; let all_fields: Vec = init_state.iter().map(|(field, _)| field.clone()).collect(); let mut fields2keys = BTreeMap::new(); let mut encrypted_data = BTreeMap::new(); let mut rng = thread_rng(); for (field, plain_value) in init_state.iter() { let hash = pcd_commitment.get(field).ok_or(anyhow::Error::msg("Missing commitment"))?; let key = generate_key(&mut rng); let serialized = serde_json::to_string(plain_value)?; let cipher = encrypt_with_key(&key, serialized.as_bytes())?; fields2keys.insert(field.to_owned(), key); encrypted_data.insert(hash.to_lower_hex_string(), cipher.to_lower_hex_string()); } new_state.keys = fields2keys; process.insert_concurrent_state(new_state.clone())?; { let mut processes = lock_processes()?; // If we already have an entry with this outpoint, something's wrong if processes.contains_key(&process_id) { return Err(ApiError::new("There's already a process for this outpoint".to_owned())); } processes.insert(process_id, process.clone()); } let commit_msg = CommitMessage::new_update_commitment( process_id, pcd_commitment, roles, public_data, ); let updated_process = UpdatedProcess { process_id, current_process: process, diffs, encrypted_data, ..Default::default() }; Ok(ApiReturn { secrets: Some(secrets_return), commit_to_send: Some(commit_msg), updated_process: Some(updated_process), new_tx_to_send: Some(new_tx_msg), ..Default::default() }) } #[wasm_bindgen] /// TODO allow modifications from user that doesn't have read access to all attributes pub fn update_process( mut process: Process, new_attributes: JsValue, roles: JsValue, new_public_data: JsValue, ) -> ApiResult { let new_attributes: Pcd = serde_wasm_bindgen::from_value(new_attributes)?; let roles: Roles = serde_wasm_bindgen::from_value(roles)?; let new_public_data: Pcd = serde_wasm_bindgen::from_value(new_public_data)?; let process_id = process.get_process_id()?; let prev_state = process.get_latest_commited_state() .ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?; let mut prev_public_data = prev_state.public_data.clone(); for (field, value) in new_public_data.into_iter() { prev_public_data.insert(field, value); } // We expect the whole set of attributes for now, even if value doesn't change since previous state // We rehash everything with the new txid, so we need the clear value // eventually we would like to be able to create partial states even if we don't have read access to some attributes let mut new_state = ProcessState::new( process.get_process_tip()?, new_attributes.clone(), prev_public_data, roles.clone() )?; // We compare the new state with the previous one let last_state_merkle_root = &prev_state.state_id; if *last_state_merkle_root == new_state.state_id { return Err(ApiError::new("new proposed state is identical to the previous commited state".to_owned())); } // We check that we don't have already a similar concurrent state let concurrent_processes = process.get_latest_concurrent_states()?; if concurrent_processes.iter().any(|p| p.state_id == new_state.state_id) { return Err(ApiError::new("New state already known".to_owned())); } let diffs = create_diffs(&process, &new_state)?; let all_fields: Vec = new_attributes.iter().map(|(field, _)| field.clone()).collect(); let mut encrypted_data = BTreeMap::new(); let mut rng = thread_rng(); for (field, plain_value) in new_attributes.iter() { let hash = new_state.pcd_commitment.get(field).ok_or(anyhow::Error::msg("Missing commitment"))?; let key = generate_key(&mut rng); new_state.keys.insert(field.to_owned(), key); let serialized = serde_json::to_string(plain_value)?; let cipher = encrypt_with_key(&key, serialized.as_bytes())?; encrypted_data.insert(hash.to_lower_hex_string(), cipher.to_lower_hex_string()); } // Add the new state to the process process.insert_concurrent_state(new_state.clone())?; { // save the process to wasm memory let mut processes = lock_processes()?; // To keep it simple, assume that db is true and replace what we have in memory processes.insert(process_id, process.clone()); } let updated_process = UpdatedProcess { process_id, current_process: process, diffs, encrypted_data, ..Default::default() }; let commit_msg = CommitMessage::new_update_commitment( process_id, new_state.pcd_commitment, roles, new_state.public_data, ); Ok(ApiReturn { updated_process: Some(updated_process), commit_to_send: Some(commit_msg), ..Default::default() }) } #[wasm_bindgen] pub fn request_data(process_id: String, state_ids_str: Vec, roles: JsValue) -> ApiResult { let process_id = OutPoint::from_str(&process_id)?; let local_device = lock_local_device()?; let sender = local_device.to_member(); let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); let roles: Vec = serde_wasm_bindgen::from_value(roles)?; let mut state_ids: Vec<[u8; 32]> = vec![]; for s in state_ids_str { if (s.len() == 0 || s == String::from_utf8(Vec::from([0u8; 32])).unwrap()) { continue; } let state_id: Result<[u8; 32], _> = Vec::from_hex(&s)?.try_into().map_err(|_| ApiError::new("Invalid state id".to_owned())); if let Ok(state_id) = state_id { state_ids.push(state_id); } } let mut send_to: HashSet = HashSet::new(); for role in roles { for (_, role_def) in role { let members = &role_def.members; if !members.contains(&sender) { continue; } for member in members { for address in member.get_addresses() { if address == local_address { continue }; send_to.insert(SilentPaymentAddress::try_from(address)?); } } } } let prd_request = Prd::new_request( process_id, sender, state_ids ); let prd_msg = prd_request.to_network_msg(sp_wallet)?; // For now, we just send the request to all members we share the data with, but this could be refined let shared_secrets = lock_shared_secrets()?; let mut ciphers = vec![]; for address in send_to { if let Some(secret) = shared_secrets.get_secret_for_address(address) { let cipher = encrypt_with_key(secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } else { debug!("No shared secret"); } } Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } #[wasm_bindgen] pub fn create_update_message( process_id: String, state_id: String, ) -> ApiResult { let mut processes = lock_processes()?; let process_id = OutPoint::from_str(&process_id)?; let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?; let process = processes.get_mut(&process_id) .ok_or(ApiError::new("Unknown process".to_owned()))?; let update_state = process.get_state_for_id(&state_id)?; let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in update_state.roles.iter() { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); for member in &role.members { // Check that we have a shared_secret with all members if let Some(no_secret_address) = member.get_addresses().iter() .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) { // We ignore it if we don't have a secret with ourselves if *no_secret_address != local_address { // for now we return an error to keep it simple return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member))); } } if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); } all_members.get_mut(&member).unwrap().extend(fields.clone()); } } let sender: Member = local_device .to_member(); let full_prd = Prd::new_update( process_id, sender, update_state.roles.clone(), update_state.public_data.clone(), update_state.keys.clone(), update_state.pcd_commitment.clone(), ); let mut ciphers = vec![]; for (member, visible_fields) in all_members { let mut prd = full_prd.clone(); prd.filter_keys(&visible_fields); let prd_msg = prd.to_network_msg(sp_wallet)?; let addresses = member.get_addresses(); for sp_address in addresses.into_iter() { // We skip our own device address, no point sending ourself a cipher if sp_address == local_address { continue; } // We shouldn't ever have error here since we already checked above let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap(); let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } } Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } #[wasm_bindgen] pub fn validate_state(process: Process, state_id: String) -> ApiResult { add_validation_token(process, state_id, true) } #[wasm_bindgen] pub fn refuse_state(process: Process, state_id: String) -> ApiResult { add_validation_token(process, state_id, false) } #[wasm_bindgen] pub fn evaluate_state(process_id: String, previous_state: Option, process_state: ProcessState) -> ApiResult { process_state.is_valid(previous_state.as_ref())?; // We create a commit msg with the valid state let outpoint: OutPoint = OutPoint::from_str(&process_id)?; let commit_msg = CommitMessage::new_update_commitment( outpoint, process_state.pcd_commitment, process_state.roles, process_state.public_data, ); Ok(ApiReturn { commit_to_send: Some(commit_msg), ..Default::default() }) } fn add_validation_token(mut process: Process, state_id: String, approval: bool) -> ApiResult { let process_id = process.get_process_id()?; let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?; if state_id == [0u8; 32] { return Err(ApiError::new("Can't validate empty state".to_owned())); } { let update_state: &mut ProcessState = process.get_state_for_id_mut(&state_id)?; let message_hash = if approval { AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(state_id)) } else { AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(state_id)) }; let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); update_state.validation_tokens.push(proof); } let commit_msg: Option = { let update_state = process.get_state_for_id(&state_id)?; // if the state is valid we also add a commit msg let update_is_valid = update_state.is_valid(process.get_parent_state(&update_state.commited_in)); if update_is_valid.is_ok() { let mut commit_msg = CommitMessage::new_update_commitment( process.get_process_id()?, update_state.pcd_commitment.clone(), update_state.roles.clone(), update_state.public_data.clone(), ); commit_msg.set_validation_tokens(update_state.validation_tokens.clone()); Some(commit_msg) } else { debug!("No commit msg"); None } }; let updated_process = UpdatedProcess { process_id, current_process: process.clone(), validated_state: Some(state_id.clone()), ..Default::default() }; let ciphers_to_send = new_response_prd(process_id, process.get_state_for_id_mut(&state_id)?)?; Ok(ApiReturn { updated_process: Some(updated_process), commit_to_send: commit_msg, ciphers_to_send, ..Default::default() }) } #[wasm_bindgen] pub fn create_response_prd(process_id: String, state_id: String) -> ApiResult { let mut processes = lock_processes()?; let state_id: [u8; 32] = Vec::from_hex(&state_id)?.try_into().map_err(|_| ApiError::new("Invalid state_id".to_owned()))?; let outpoint = OutPoint::from_str(&process_id)?; let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; let update_state: &mut ProcessState = process.get_state_for_id_mut(&state_id)?; let ciphers = new_response_prd(outpoint, update_state)?; Ok(ApiReturn { ciphers_to_send: ciphers, ..Default::default() }) } fn new_response_prd(process_id: OutPoint, update_state: &mut ProcessState) -> AnyhowResult> { let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in update_state.roles.iter() { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); for member in &role.members { // Check that we have a shared_secret with all members if let Some(no_secret_address) = member.get_addresses().iter() .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) { // We ignore it if we don't have a secret with ourselves if *no_secret_address != local_address { // for now we return an error to keep it simple return Err(AnyhowError::msg(format!("No shared secret for all addresses of {:?}\nPlease first connect", member))); } } if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); } all_members.get_mut(&member).unwrap().extend(fields.clone()); } } let our_key = SilentPaymentAddress::try_from(local_address.as_str())?.get_spend_key(); let proof = update_state.validation_tokens.iter().find(|t| t.get_key() == our_key) .ok_or(AnyhowError::msg("We haven't added our validation token yet".to_owned()))?; let sender: Member = local_device .to_member(); let response_prd = Prd::new_response( process_id, sender, vec![*proof], update_state.pcd_commitment.clone(), ); let prd_msg = response_prd.to_network_msg(sp_wallet)?; let mut ciphers = vec![]; for (member, visible_fields) in all_members { let addresses = member.get_addresses(); for sp_address in addresses.into_iter() { // We skip our own device address, no point sending ourself a cipher if sp_address == local_address { continue; } // We shouldn't ever have error here since we already checked above let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?) .ok_or(AnyhowError::msg("Failed to retrieve secret".to_owned()))?; let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); } } Ok(ciphers) } #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct encryptWithNewKeyResult { pub cipher: String, pub key: String, } #[wasm_bindgen] pub fn create_faucet_msg() -> ApiResult { let sp_address = lock_local_device()? .get_wallet() .get_client() .get_receiving_address(); let faucet_msg = FaucetMessage::new(sp_address.clone()); Ok(faucet_msg.to_string()) } #[wasm_bindgen] pub fn get_storages(process_outpoint: String) -> ApiResult> { let outpoint = OutPoint::from_str(&process_outpoint)?; Ok(vec![]) } #[wasm_bindgen] pub fn is_child_role(parent_roles: String, child_roles: String) -> ApiResult<()> { let parent_roles: BTreeMap = serde_json::from_str(&parent_roles)?; let child_roles: BTreeMap = serde_json::from_str(&child_roles)?; for (_, child_role) in &child_roles { for child_member in &child_role.members { let mut is_in_parent = false; for (_, parent_role) in &parent_roles { if parent_role.members.contains(&child_member) { is_in_parent = true; } if is_in_parent { break } } if !is_in_parent { return Err(ApiError::new("child role contains a member not in parent".to_owned())); } } } Ok(()) } #[wasm_bindgen] pub fn decrypt_data(key: &[u8], data: &[u8]) -> ApiResult { let mut key_buf = [0u8; 32]; if key.len() != 32 { return Err(ApiError::new("key must be 32B long".to_owned())); } key_buf.copy_from_slice(key); // decrypt the data let clear = decrypt_with_key(&key_buf, data)?; Ok(String::from_utf8(clear)?) }