From bc6f95a98fc9933eb0fe8dd1f3732ed3c16676d8 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 24 May 2024 22:40:40 +0200 Subject: [PATCH] Message processing heavy refactoring --- crates/sp_client/src/api.rs | 206 +++++++++++++++++++++++------------- src/services.ts | 111 ++++++++----------- src/websockets.ts | 15 ++- 3 files changed, 191 insertions(+), 141 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index b3b4b34..d1edc07 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -25,7 +25,8 @@ use sp_client::bitcoin::key::Secp256k1; use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; use sp_client::bitcoin::{Amount, Network, OutPoint, Psbt, Transaction, Txid}; -use sp_client::silentpayments::Error as SpError; +use sp_client::silentpayments::utils as sp_utils; +use sp_client::silentpayments::{Error as SpError, Network as SpNetwork}; use serde::{Deserialize, Serialize}; use sp_client::silentpayments::sending::SilentPaymentAddress; @@ -34,11 +35,12 @@ use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::prelude::*; use sdk_common::network::{ - self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, UnknownMessage + self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, + UnknownMessage, }; use sdk_common::silentpayments::{ create_transaction, create_transaction_for_address_with_shared_secret, - create_transaction_spend_outpoint, map_outputs_to_sp_address + create_transaction_spend_outpoint, map_outputs_to_sp_address, }; use sp_client::spclient::{ @@ -402,23 +404,29 @@ fn handle_recover_transaction( } else { false } - }) - { + }) { let message = messages.get_mut(pos).unwrap(); match message.status { CachedMessageStatus::FaucetWaiting => { message.status = CachedMessageStatus::FaucetComplete; - message.commited_in = utxo_created.into_iter().next().map(|(outpoint, _)| *outpoint); + message.commited_in = utxo_created + .into_iter() + .next() + .map(|(outpoint, _)| *outpoint); return Ok(message.clone()); - }, + } + // Actually this is unreachable CachedMessageStatus::FaucetComplete => return Ok(message.clone()), - _ => () + _ => (), } } // we inspect inputs looking for links with previous tx for input in tx.input.iter() { - if let Some(pos) = messages.iter().position(|m| { + if let Some(pos) = messages + .iter() + .position(|m| { + debug!("{:?}", Some(input.previous_output)); m.confirmed_by == Some(input.previous_output) }) { @@ -426,9 +434,9 @@ fn handle_recover_transaction( // If we are receiver, that's pretty much it, just set status to complete message.status = CachedMessageStatus::Complete; return Ok(message.clone()); - } else if let Some(pos) = messages.iter().position(|m| { - m.commited_in == Some(input.previous_output) - }) + } else if let Some(pos) = messages + .iter() + .position(|m| m.commited_in == Some(input.previous_output)) { // sender needs to spent it back again to receiver let (outpoint, output) = utxo_created.into_iter().next().unwrap(); @@ -444,9 +452,16 @@ fn handle_recover_transaction( } // if we've found nothing we are being notified - let shared_point = - shared_secret_point(&tweak_data, &sp_wallet.get_client().get_scan_key()); - let shared_secret = AnkSharedSecret::new(shared_point); + let shared_point = sp_utils::receiving::calculate_shared_point( + &tweak_data, + &sp_wallet.get_client().get_scan_key(), + ); + let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?); + + debug!( + "Shared secret: {}", + shared_secret.to_byte_array().to_lower_hex_string() + ); if let Some(cipher_pos) = messages.iter().position(|m| { if m.status != CachedMessageStatus::CipherWaitingTx { @@ -454,15 +469,13 @@ fn handle_recover_transaction( } m.try_decrypt_with_shared_secret(shared_secret.to_byte_array()) .is_ok() - }) - { + }) { let message = messages.get_mut(cipher_pos).unwrap(); let (outpoint, output) = utxo_created.into_iter().next().unwrap(); message.commited_in = Some(outpoint.clone()); - message.shared_secret = - Some(shared_secret.to_byte_array().to_lower_hex_string()); + message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); message.commitment = Some(commitment_str); let plaintext = message @@ -474,16 +487,18 @@ fn handle_recover_transaction( message.recipient = Some(sp_wallet.get_client().get_receiving_address()); message.status = CachedMessageStatus::ReceivedMustConfirm; - return Ok(message.clone()) + return Ok(message.clone()); } else { // store it and wait for the message let mut new_msg = CachedMessage::new(); - let (outpoint, output) = utxo_created.into_iter().next().expect("utxo_created shouldn't be empty"); + let (outpoint, output) = utxo_created + .into_iter() + .next() + .expect("utxo_created shouldn't be empty"); new_msg.commited_in = Some(outpoint.clone()); new_msg.commitment = Some(commitment.to_lower_hex_string()); new_msg.recipient = Some(sp_wallet.get_client().get_receiving_address()); - new_msg.shared_secret = - Some(shared_secret.to_byte_array().to_lower_hex_string()); + new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.status = CachedMessageStatus::TxWaitingCipher; messages.push(new_msg.clone()); return Ok(new_msg.clone()); @@ -491,14 +506,15 @@ fn handle_recover_transaction( } else { // We are sender of a transaction // We only need to return the message - if let Some(message) = messages.iter() - .find(|m| { - m.commitment.as_ref() == Some(&commitment_str) - }) + if let Some(message) = messages + .iter() + .find(|m| m.commitment.as_ref() == Some(&commitment_str)) { return Ok(message.clone()); } else { - return Err(anyhow::Error::msg("We spent a transaction for a commitment we don't know")); + return Err(anyhow::Error::msg( + "We spent a transaction for a commitment we don't know", + )); } } } @@ -577,7 +593,7 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult AnkFlag::Faucet => unimplemented!(), AnkFlag::Error => { let error_msg = CachedMessage::new_error(ank_msg.content); - return Ok(error_msg) + return Ok(error_msg); } AnkFlag::Unknown => { // let's try to decrypt with keys we found in transactions but haven't used yet @@ -597,16 +613,15 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult message.plaintext = Some(unknown_msg.message); message.sender = Some(unknown_msg.sender); message.ciphertext = Some(ank_msg.content); + message.status = CachedMessageStatus::ReceivedMustConfirm; return Ok(message.clone()); } else { // let's keep it in case we receive the transaction later let mut new_msg = CachedMessage::new(); new_msg.status = CachedMessageStatus::CipherWaitingTx; new_msg.ciphertext = Some(ank_msg.content); - messages.push(new_msg); - return Err(ApiError { - message: "Can't decrypt message".to_owned(), - }); + messages.push(new_msg.clone()); + return Ok(new_msg); } } _ => unimplemented!(), @@ -676,22 +691,33 @@ pub struct createTransactionReturn { pub new_network_msg: CachedMessage, } -/// This is what we call to answer a confirmation as a sender +/// This is what we call to answer a confirmation as a sender #[wasm_bindgen] pub fn answer_confirmation_transaction( - message: CachedMessage, + message_id: u32, fee_rate: u32, ) -> ApiResult { - if message.recipient.is_none() || message.confirmed_by.is_none() { - return Err(ApiError { message: "Invalid network message".to_owned() }); + let mut messages = lock_messages()?; + let message: &mut CachedMessage; + if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) { + if m.sender.is_none() || m.commited_in.is_none() { + return Err(ApiError { + message: "Invalid network message".to_owned(), + }); + } + + message = m; + } else { + return Err(ApiError { message: format!("Can't find message for id {}", message_id) }); } - let sp_address: SilentPaymentAddress = message.recipient.as_ref().unwrap().as_str().try_into()?; + let sp_address: SilentPaymentAddress = + message.recipient.as_ref().unwrap().as_str().try_into()?; let connected_user = lock_connected_user()?; let sp_wallet: &SpWallet; - if sp_address.is_testnet() { + if sp_address.get_network() != SpNetwork::Mainnet { sp_wallet = connected_user.try_get_recover()?; } else { sp_wallet = connected_user.try_get_main()?; @@ -703,38 +729,53 @@ pub fn answer_confirmation_transaction( nb_outputs: 1, }; + let confirmed_by = message.confirmed_by.clone().unwrap(); + let commited_in = message.commited_in.clone().unwrap(); + let signed_psbt = create_transaction_spend_outpoint( - &message.confirmed_by.unwrap(), - sp_wallet, - recipient, - Amount::from_sat(fee_rate.into()) + &confirmed_by, + sp_wallet, + recipient, + &commited_in.txid, + Amount::from_sat(fee_rate.into()), )?; let final_tx = signed_psbt.extract_tx()?; + message.status = CachedMessageStatus::Complete; + Ok(createTransactionReturn { txid: final_tx.txid().to_string(), transaction: serialize(&final_tx).to_lower_hex_string(), - new_network_msg: message + new_network_msg: message.clone(), }) } /// This is what we call to confirm as a receiver #[wasm_bindgen] pub fn create_confirmation_transaction( - message: CachedMessage, + message_id: u32, fee_rate: u32, ) -> ApiResult { - if message.sender.is_none() || message.confirmed_by.is_none() { - return Err(ApiError { message: "Invalid network message".to_owned() }); + let mut messages = lock_messages()?; + let message: &mut CachedMessage; + if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) { + if m.sender.is_none() || m.commited_in.is_none() { + return Err(ApiError { + message: "Invalid network message".to_owned(), + }); + } + + message = m; + } else { + return Err(ApiError { message: format!("Can't find message for id {}", message_id) }); } let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?; - let connected_user = lock_connected_user()?; let sp_wallet: &SpWallet; - if sp_address.is_testnet() { + if sp_address.get_network() != SpNetwork::Mainnet { sp_wallet = connected_user.try_get_recover()?; } else { sp_wallet = connected_user.try_get_main()?; @@ -746,26 +787,38 @@ pub fn create_confirmation_transaction( nb_outputs: 1, }; + let commited_in = message.commited_in.clone().unwrap(); + let signed_psbt = create_transaction_spend_outpoint( - &message.confirmed_by.unwrap(), - sp_wallet, - recipient, - Amount::from_sat(fee_rate.into()) + &commited_in, + sp_wallet, + recipient, + &commited_in.txid, + Amount::from_sat(fee_rate.into()), )?; + // what's the vout of the output sent to sender? + let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?; + let recipients_vouts = sp_address2vouts + .get::(&sp_address.into()) + .expect("recipients didn't change") + .as_slice(); + let final_tx = signed_psbt.extract_tx()?; + message.confirmed_by = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 }); + Ok(createTransactionReturn { txid: final_tx.txid().to_string(), transaction: serialize(&final_tx).to_lower_hex_string(), - new_network_msg: message + new_network_msg: message.clone(), }) } #[wasm_bindgen] pub fn create_notification_transaction( address: String, - commitment: Option, + message: UnknownMessage, fee_rate: u32, ) -> ApiResult { let sp_address: SilentPaymentAddress = address.as_str().try_into()?; @@ -773,7 +826,7 @@ pub fn create_notification_transaction( let connected_user = lock_connected_user()?; let sp_wallet: &SpWallet; - if sp_address.is_testnet() { + if sp_address.get_network() != SpNetwork::Mainnet { sp_wallet = connected_user.try_get_recover()?; } else { sp_wallet = connected_user.try_get_main()?; @@ -785,42 +838,47 @@ pub fn create_notification_transaction( nb_outputs: 1, }; + let commitment = create_commitment(serde_json::to_string(&message)?); + let signed_psbt = create_transaction_for_address_with_shared_secret( recipient, sp_wallet, - commitment.as_deref(), + Some(&commitment), Amount::from_sat(fee_rate.into()), )?; let psbt = Psbt::from_str(&signed_psbt)?; - let partial_secret = sp_wallet - .get_client() - .get_partial_secret_from_psbt(&psbt)?; + let partial_secret = sp_wallet.get_client().get_partial_secret_from_psbt(&psbt)?; - let shared_point = shared_secret_point( - &sp_wallet - .get_client() - .get_scan_key() - .public_key(&Secp256k1::signing_only()), - &partial_secret, - ); + let shared_point = + sp_utils::sending::calculate_shared_point(&sp_address.get_scan_key(), &partial_secret); - let shared_secret = AnkSharedSecret::new(shared_point); + let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?); debug!( "Created transaction with secret {}", shared_secret.to_byte_array().to_lower_hex_string() ); + let cipher = encrypt_with_key(serde_json::to_string(&message)?, shared_secret.to_byte_array().to_lower_hex_string())?; + // update our cache let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt)?; - let recipients_vouts = sp_address2vouts.get::(&address).expect("recipients didn't change").as_slice(); + let recipients_vouts = sp_address2vouts + .get::(&address) + .expect("recipients didn't change") + .as_slice(); // for now let's just take the smallest vout that belongs to the recipient let final_tx = psbt.extract_tx()?; - let mut new_msg = CachedMessage::default(); - new_msg.commitment = commitment; - new_msg.commited_in = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 }); + let mut new_msg = CachedMessage::new(); + new_msg.plaintext = Some(message.message); + new_msg.ciphertext = Some(cipher); + new_msg.commitment = Some(commitment); + new_msg.commited_in = Some(OutPoint { + txid: final_tx.txid(), + vout: recipients_vouts[0] as u32, + }); new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.recipient = Some(address); new_msg.sender = Some(sp_wallet.get_client().get_receiving_address()); @@ -908,11 +966,11 @@ pub fn create_faucet_msg() -> ApiResult { let user = lock_connected_user()?; let sp_address = user.try_get_recover()?.get_client().get_receiving_address(); - let mut commitment = [0u8;64]; + let mut commitment = [0u8; 64]; thread_rng().fill_bytes(&mut commitment); let mut cached_msg = CachedMessage::new(); - cached_msg.recipient = Some(sp_address); + cached_msg.recipient = Some(sp_address); cached_msg.commitment = Some(commitment.to_lower_hex_string()); cached_msg.status = CachedMessageStatus::FaucetWaiting; lock_messages()?.push(cached_msg.clone()); diff --git a/src/services.ts b/src/services.ts index 7ad857d..d02fb70 100644 --- a/src/services.ts +++ b/src/services.ts @@ -1,4 +1,4 @@ -import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage, UnknownMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -105,36 +105,24 @@ class Services { const recipientSpAddress = spAddressElement.value; const message = messageElement.value; - const msg_payload = JSON.stringify({sender: this.sp_address, message: message}); + const msg_payload: UnknownMessage = {sender: this.sp_address!, message: message}; let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); if (notificationInfo) { let networkMsg = notificationInfo.new_network_msg; - let shared_secret = ''; - if (networkMsg.shared_secret) { - shared_secret = networkMsg.shared_secret; - } else { - throw 'no shared_secret'; - } - console.info('Successfully sent notification transaction'); + console.debug(networkMsg); const connection = await services.pickWebsocketConnectionRandom(); const flag: AnkFlag = "Unknown"; - // encrypt the message(s) - // TODO we'd rather do that in the wasm as part of notify_address_for_message try { - const cipher = await services.encryptData(msg_payload, shared_secret); - let updated_msg = notificationInfo.new_network_msg; - updated_msg.plaintext = msg_payload; - updated_msg.ciphertext = cipher; - await services.updateMessages(updated_msg); - connection?.sendMessage(flag, cipher); + // send message (transaction in envelope) + await services.updateMessages(networkMsg); + connection?.sendMessage(flag, networkMsg.ciphertext!); } catch (error) { throw error; } // add peers list // add processes list - // send message (transaction in envelope) } } @@ -820,6 +808,41 @@ class Services { } } + public async answer_confirmation_message(msg: CachedMessage): Promise { + const services = await Services.getInstance(); + const connection = await services.pickWebsocketConnectionRandom(); + if (!connection) { + throw new Error("No connection to relay"); + } + let user: User; + try { + let possibleUser = await services.getUserInfo(); + if (!possibleUser) { + throw new Error("No user loaded, please first create a new user or login"); + } else { + user = possibleUser; + } + } catch (error) { + throw error; + } + + let notificationInfo: createTransactionReturn; + try { + const feeRate = 1; + notificationInfo = services.sdkClient.answer_confirmation_transaction(msg.id, feeRate); + } catch (error) { + throw new Error(`Failed to create confirmation transaction: ${error}`); + } + const flag: AnkFlag = "NewTx"; + const newTxMsg: NewTxMessage = { + 'transaction': notificationInfo.transaction, + 'tweak_data': null + } + connection.sendMessage(flag, JSON.stringify(newTxMsg)); + await services.updateMessages(notificationInfo.new_network_msg); + return; + } + public async confirm_sender_address(msg: CachedMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); @@ -841,7 +864,7 @@ class Services { let notificationInfo: createTransactionReturn; try { const feeRate = 1; - notificationInfo = services.sdkClient.answer_confirmation_transaction(msg, feeRate); + notificationInfo = services.sdkClient.create_confirmation_transaction(msg.id, feeRate); } catch (error) { throw new Error(`Failed to create confirmation transaction: ${error}`); } @@ -851,10 +874,11 @@ class Services { 'tweak_data': null } connection.sendMessage(flag, JSON.stringify(newTxMsg)); + await services.updateMessages(notificationInfo.new_network_msg); return; } - public async notify_address_for_message(sp_address: string, message: string): Promise { + public async notify_address_for_message(sp_address: string, message: UnknownMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { @@ -863,62 +887,19 @@ class Services { try { const feeRate = 1; - const commitment = services.sdkClient.create_commitment(message); - let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, commitment, feeRate); + let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate); const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, 'tweak_data': null } connection.sendMessage(flag, JSON.stringify(newTxMsg)); + console.info('Successfully sent notification transaction'); return notificationInfo; } catch (error) { throw 'Failed to create notification transaction:", error'; } } - - // public async encryptData(data: string, sharedSecret: Record): Promise> { - // const services = await Services.getInstance(); - // let msg_cipher: encryptWithNewKeyResult; - // try { - // msg_cipher = services.sdkClient.encrypt_with_new_key(data); - // } catch (error) { - // throw error; - // } - - // let res = new Map(); - // for (const [recipient, secret] of Object.entries(sharedSecret)) { - // try { - // const key = secret.secret; - // const encryptedKey: string = await services.sdkClient.encrypt_with_key(msg_cipher.key, key); - // res.set(recipient, encryptedKey); - // } catch (error) { - // throw new Error(`Failed to encrypt key for recipient ${recipient}: ${error}`); - // } - // } - - // return res; - // } - public async encryptData(data: string, key: string): Promise { - const services = await Services.getInstance(); - - try { - let res: string = services.sdkClient.encrypt_with_key(data, key); - return res; - } catch (error) { - throw error; - } - } - - public async decryptData(cipher: string, key: string): Promise { - const services = await Services.getInstance(); - try { - let res = services.sdkClient.try_decrypt_with_key(cipher, key); - return res; - } catch (error) { - throw error; - } - } } export default Services; diff --git a/src/websockets.ts b/src/websockets.ts index 67d8434..6cc35c5 100644 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -34,7 +34,7 @@ class WebSocketClient { if (res.status === 'FaucetComplete') { // we received a faucet tx, there's nothing else to do window.alert(`New faucet output\n${res.commited_in}`); - await services.removeMessage(res.id); + await services.updateMessages(res); await services.updateOwnedOutputsForUser(); } else if (res.status === 'TxWaitingCipher') { // we received a tx but we don't have the cipher @@ -47,6 +47,7 @@ class WebSocketClient { await services.updateMessages(res); } else if (res.status === 'SentWaitingConfirmation') { // We are sender and we're waiting for the challenge that will confirm recipient got the transaction and the message + await services.updateMessages(res); await services.updateOwnedOutputsForUser(); } else if (res.status === 'MustSpendConfirmation') { // we received a challenge for a notification we made @@ -54,7 +55,17 @@ class WebSocketClient { window.alert(`Spending ${res.confirmed_by} to prove our identity`); console.debug(`sending confirm message to ${res.recipient}`); await services.updateMessages(res); + await services.answer_confirmation_message(res); + } else if (res.status === 'ReceivedMustConfirm') { + // we found a notification and decrypted the cipher + window.alert(`Received message from ${res.sender}\n${res.plaintext}`); + // we must spend the commited_in output to sender + await services.updateMessages(res); await services.confirm_sender_address(res); + } else if (res.status === 'Complete') { + window.alert(`Received confirmation that ${res.sender} is the author of message ${res.plaintext}`) + await services.updateMessages(res); + await services.updateOwnedOutputsForUser(); } else { console.debug('Received an unimplemented valid message'); } @@ -88,7 +99,7 @@ class WebSocketClient { // console.debug("Sending message:", JSON.stringify(networkMessage)); this.ws.send(JSON.stringify(networkMessage)); } else { - console.error('WebSocket is not open. ReadyState:', this.ws.readyState); + console.warn('WebSocket is not open. ReadyState:', this.ws.readyState); this.messageQueue.push(message); } }