From 5cddf0566a8911a9378e7d86564fba5ee5fb1e83 Mon Sep 17 00:00:00 2001 From: Sosthene00 Date: Fri, 19 Apr 2024 00:23:09 +0200 Subject: [PATCH] WIP --- crates/sp_client/src/api.rs | 22 ++--- crates/sp_client/src/silentpayments.rs | 125 ++++++++++++++++++++++--- src/services.ts | 21 ++++- 3 files changed, 140 insertions(+), 28 deletions(-) diff --git a/crates/sp_client/src/api.rs b/crates/sp_client/src/api.rs index 6046111..14c43cc 100644 --- a/crates/sp_client/src/api.rs +++ b/crates/sp_client/src/api.rs @@ -12,7 +12,6 @@ use sdk_common::crypto::AnkSharedSecret; use serde_json::Error as SerdeJsonError; use shamir::SecretData; use sp_client::bitcoin::consensus::{deserialize, serialize}; -use sp_client::silentpayments::bitcoin_hashes::{HashEngine, sha256, Hash}; use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; use sp_client::bitcoin::secp256k1::ecdh::SharedSecret; use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; @@ -31,7 +30,9 @@ use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClie use sp_client::spclient::{SpWallet, SpendKey}; use crate::images; -use crate::silentpayments::{check_transaction, create_transaction_for_address}; +use crate::silentpayments::{ + check_transaction, create_transaction_for_address_with_shared_secret, ScannedTransaction, +}; use crate::user::{lock_connected_users, User, UserWallets, CONNECTED_USERS}; use crate::process::Process; @@ -392,7 +393,7 @@ pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult { #[allow(non_camel_case_types)] pub struct createNotificationTransactionReturn { pub transaction: String, - pub spaddress2secret: HashMap, + pub transaction2secret: ScannedTransaction, } #[wasm_bindgen] @@ -402,22 +403,17 @@ pub fn create_notification_transaction( ) -> ApiResult { let sp_address: SilentPaymentAddress = recipient.try_into()?; - let (transaction, shared_point) = + let (transaction, shared_secret) = create_transaction_for_address_with_shared_secret(sp_address, message)?; - // The shared_point *must* be hashed to produce a proper ecdh secret - // For now we propose to implement a tagged hash for it - // It could be interesting to add some piece of data to allow for the derivation of multiple secrets - let mut eng = sha256::HashEngine::default(); - eng.write_all(&shared_point); - let shared_secret = sha256::Hash::from_engine(eng); - let mut spaddress2secret: HashMap = HashMap::new(); + // update our cache - spaddress2secret.insert(sp_address.into(), shared_secret.as_byte_array().to_lower_hex_string()); + let mut transaction2secret = ScannedTransaction::new(); + transaction2secret.get_mut().insert(transaction.txid(), vec![shared_secret]); Ok(createNotificationTransactionReturn { transaction: serialize(&transaction).to_lower_hex_string(), - spaddress2secret, + transaction2secret, }) } diff --git a/crates/sp_client/src/silentpayments.rs b/crates/sp_client/src/silentpayments.rs index ac2a223..1b5d35e 100644 --- a/crates/sp_client/src/silentpayments.rs +++ b/crates/sp_client/src/silentpayments.rs @@ -1,14 +1,23 @@ -use std::collections::HashMap; +// This file should move to common + +use std::collections::{HashMap, HashSet}; +use std::io::Write; +use std::iter::Once; +use std::sync::{Mutex, MutexGuard, OnceLock}; use anyhow::{Error, Result}; +use log::debug; use rand::Rng; +use sdk_common::crypto::{Aes256Encryption, Aes256Gcm, AeadCore, Purpose}; +use serde::{Deserialize, Serialize}; use sp_client::bitcoin::policy::DUST_RELAY_TX_FEE; use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; -use sp_client::bitcoin::{block, Amount, OutPoint}; +use sp_client::bitcoin::{block, Amount, OutPoint, Txid}; use sp_client::silentpayments::sending::SilentPaymentAddress; use sp_client::silentpayments::utils::receiving::calculate_shared_secret; use sp_client::spclient::{OutputList, OwnedOutput, Recipient, SpClient, SpWallet}; +use sp_client::silentpayments::bitcoin_hashes::{sha256, Hash, HashEngine}; use sp_client::{ bitcoin::{ secp256k1::{PublicKey, Scalar, XOnlyPublicKey}, @@ -16,17 +25,75 @@ use sp_client::{ }, silentpayments::receiving::Label, }; +use tsify::Tsify; use crate::user::{lock_connected_users, CONNECTED_USERS}; +use crate::MutexExt; + +#[derive(Debug, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ScannedTransaction(HashMap>); + +impl ScannedTransaction { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn get_mut(&mut self) -> &mut HashMap> { + &mut self.0 + } +} + +pub static TRANSACTIONCACHE: OnceLock> = OnceLock::new(); + +pub fn lock_scanned_transactions() -> Result> { + TRANSACTIONCACHE + .get_or_init(|| Mutex::new(ScannedTransaction::new())) + .lock_anyhow() +} type FoundOutputs = HashMap, HashMap>; -type SharedPoint = [u8;64]; +#[derive(Debug)] +pub struct SharedPoint([u8; 64]); + +impl SharedPoint { + pub fn as_inner(&self) -> &[u8; 64] { + &self.0 + } +} + +#[derive(Debug, Serialize, Deserialize, Tsify)] +#[tsify(from_wasm_abi, into_wasm_abi)] +pub struct SharedSecret { + secret: [u8; 32], + shared_with: Option, // SilentPaymentAddress +} + +impl SharedSecret { + pub fn new(secret: [u8;32], shared_with: Option) -> Result { + if let Some(ref shared) = shared_with { + if let Ok(_) = SilentPaymentAddress::try_from(shared.as_str()) { + Ok(Self { + secret, + shared_with + }) + } else { + Err(Error::msg("Invalid silent payment address")) + } + } else { + Ok(Self { + secret, + shared_with: None + }) + } + } +} pub fn create_transaction_for_address_with_shared_secret( sp_address: SilentPaymentAddress, message: String, -) -> Result<(Transaction, SharedPoint)> { +) -> Result<(Transaction, SharedSecret)> { let connected_users = lock_connected_users()?; let (_, wallets) = connected_users @@ -34,6 +101,8 @@ pub fn create_transaction_for_address_with_shared_secret( .last() .ok_or(Error::msg("Unknown sender"))?; + debug!("Got user wallets"); + let sp_wallet = if sp_address.is_testnet() { &wallets.recover } else { @@ -77,11 +146,38 @@ pub fn create_transaction_for_address_with_shared_secret( let mut new_psbt = sp_wallet .get_client() - .create_new_psbt(inputs, vec![recipient], None)?; + .create_new_psbt(inputs, vec![recipient], Some(message.as_bytes()))?; log::debug!("Created psbt: {}", new_psbt); SpClient::set_fees(&mut new_psbt, Amount::from_sat(1000), sp_address.into())?; - let partial_secret = sp_wallet.get_client().get_partial_secret_from_psbt(&new_psbt)?; - sp_wallet.get_client().fill_sp_outputs(&mut new_psbt, partial_secret)?; + + let partial_secret = sp_wallet + .get_client() + .get_partial_secret_from_psbt(&new_psbt)?; + + // This wouldn't work with many recipients in the same transaction + // each address (or more precisely each scan public key) would have its own point + let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret); + + // The shared_point *must* be hashed to produce a proper ecdh secret + let mut eng = sha256::HashEngine::default(); + eng.write_all(&shared_point); + let shared_secret = sha256::Hash::from_engine(eng); + + // encrypt the message with the new shared_secret + let message_encryption = Aes256Encryption::import_key( + Purpose::Arbitrary, + message.into_bytes(), + shared_secret.to_byte_array(), + Aes256Gcm::generate_nonce(&mut rand::thread_rng()).into(), + )?; + + let cipher = message_encryption.encrypt_with_aes_key()?; + + sp_client::spclient::SpClient::replace_op_return_with(&mut new_psbt, &cipher)?; + + sp_wallet + .get_client() + .fill_sp_outputs(&mut new_psbt, partial_secret)?; log::debug!("Definitive psbt: {}", new_psbt); let mut aux_rand = [0u8; 32]; rand::thread_rng().fill(&mut aux_rand); @@ -91,17 +187,24 @@ pub fn create_transaction_for_address_with_shared_secret( let final_tx = signed.extract_tx()?; - // This should not be directly used without hashing - let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret); - - Ok((final_tx, shared_point)) + Ok((final_tx, SharedSecret { + secret: shared_secret.to_byte_array(), + shared_with: Some(sp_address.into()) + })) } +// This need to go pub fn check_transaction( tx: &Transaction, blockheight: u32, tweak_data: PublicKey, ) -> Result { + // first check that we haven't scanned this transaction yet + if let Some((txid, _)) = lock_scanned_transactions()?.0.get_key_value(&tx.txid()) { + let err_msg = format!("Already scanned tx {}", txid); + return Err(Error::msg(err_msg)); + } + let connected_users = lock_connected_users()?; let txid = tx.txid().to_string(); diff --git a/src/services.ts b/src/services.ts index 56d26a2..5a3344e 100644 --- a/src/services.ts +++ b/src/services.ts @@ -1,4 +1,4 @@ -import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag } from '../dist/pkg/sdk_client'; +import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; @@ -86,7 +86,10 @@ class Services { const message = messageElement.value; const services = await Services.getInstance(); - let notificationInfo = services.notify_address_for_message(recipientSpAddress, message); + let notificationInfo = await services.notify_address_for_message(recipientSpAddress, message); + if (notificationInfo) { + notificationInfo.transaction + } console.log(notificationInfo); } @@ -751,6 +754,10 @@ class Services { public async notify_address_for_message(sp_address: string, message: string): Promise { const services = await Services.getInstance(); + const connection = await services.pickWebsocketConnectionRandom(); + if (!connection) { + return null; + } let user: User; try { let possibleUser = await services.getUserInfo(); @@ -766,9 +773,15 @@ class Services { try { let notificationInfo: createNotificationTransactionReturn = services.sdkClient.create_notification_transaction(user, sp_address, message); + const flag: AnkFlag = "NewTx"; + const newTxMsg: NewTxMessage = { + 'transaction': notificationInfo.transaction, + 'tweak_data': null + } + connection.sendMessage(flag, JSON.stringify(newTxMsg)); return notificationInfo; - } catch { - console.error("Failed to create notification transaction for user", user); + } catch (error) { + console.error("Failed to create notification transaction:", error); return null } }