Return transaction to confirm identity

This commit is contained in:
Sosthene 2024-05-13 16:18:17 +02:00
parent bcabeb9a0f
commit fcafcff69e
4 changed files with 212 additions and 73 deletions

View File

@ -5,15 +5,16 @@ use std::str::FromStr;
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use std::sync::{Mutex, OnceLock, PoisonError}; use std::sync::{Mutex, OnceLock, PoisonError};
use log::debug; use log::{debug, warn};
use rand::{Fill, Rng}; use rand::{Fill, Rng};
use anyhow::Error as AnyhowError; use anyhow::Error as AnyhowError;
use sdk_common::crypto::{ use sdk_common::crypto::{
AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose,
}; };
use serde_json::Error as SerdeJsonError; use serde_json::{Error as SerdeJsonError, Value};
use shamir::SecretData; use shamir::SecretData;
use sp_client::bitcoin::blockdata::fee_rate;
use sp_client::bitcoin::consensus::{deserialize, serialize}; use sp_client::bitcoin::consensus::{deserialize, serialize};
use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError}; use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError};
use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point;
@ -27,16 +28,16 @@ use tsify::Tsify;
use wasm_bindgen::convert::FromWasmAbi; use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage}; use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage, UnknownMessage};
use sdk_common::silentpayments::{ use sdk_common::silentpayments::{
check_transaction, create_transaction_for_address_with_shared_secret, check_transaction, create_transaction, create_transaction_for_address_with_shared_secret,
}; };
use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient};
use sp_client::spclient::{SpWallet, SpendKey}; use sp_client::spclient::{SpWallet, SpendKey};
use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER};
use crate::{images, lock_scanned_transactions, Txid2Secrets}; use crate::{images, lock_scanned_transactions, lock_secrets, lock_watched, Txid2Secrets};
use crate::process::Process; use crate::process::Process;
@ -330,16 +331,80 @@ pub fn login_user(
Ok(res) Ok(res)
} }
pub fn scan_for_confirmation_transaction(tx_hex: String) -> anyhow::Result<String> {
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?;
for i in tx.input.iter() {
if let Some(waiting) = lock_watched()?.remove(&i.previous_output) {
match lock_secrets()?.get_mut(&waiting) {
None => return Err(anyhow::Error::msg("No secret match for an error we're waiting for")),
Some(secret) => {
// for now we only handle the case of one secret for one transaction
let res = secret.get_mut(0).unwrap();
res.1.trusted = true;
return Ok(res.0.clone());
}
}
}
}
Err(anyhow::Error::msg("Not spending a watched output"))
}
fn handle_recover_transaction(tx: Transaction, sp_wallet: &mut SpWallet, tweak_data: PublicKey, fee_rate: u32) -> anyhow::Result<Option<Transaction>> {
// does this transaction spent a txid and output we're waiting confirmation for?
let scan_sk = sp_wallet.get_client().get_scan_key();
let txid = tx.txid();
for input in tx.input {
let prevout = input.previous_output;
match lock_secrets()?.get_mut(&prevout.txid) {
None => {
continue;
}
Some(secret) => {
// We found an input spending a notification transaction we sent
if let Some(res) = secret.get_mut(prevout.vout as usize) {
// This is a challenge from a previous message we sent
// we toggle the trusted value
if !res.1.trusted {
res.1.trusted = true;
} else {
return Err(anyhow::Error::msg("Received a confirmation for a transaction we already confirmed"));
}
// We spend the output back to the receiver
let sp_address = SilentPaymentAddress::try_from(res.0.as_str()).expect("Invalid silent payment address");
let response_tx = create_transaction(sp_address, sp_wallet, Amount::from_sat(fee_rate.into()))?;
return Ok(Some(response_tx));
} else {
return Err(anyhow::Error::msg("Received a confirmation from an umapped output"));
}
}
}
}
// If we exhausted all inputs without finding one of our transaction, it means it's a notification
let shared_point =
shared_secret_point(&tweak_data, &scan_sk);
lock_secrets()?.insert(
txid,
vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))],
);
Ok(None)
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn check_transaction_for_silent_payments( pub fn check_transaction_for_silent_payments(
tx_hex: String, tx_hex: String,
blockheight: u32, blockheight: u32,
tweak_data_hex: String, tweak_data_hex: String,
fee_rate: u32,
) -> ApiResult<String> { ) -> ApiResult<String> {
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?; let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?;
// check that we don't already have scanned the tx
if lock_scanned_transactions()?.contains_key(&tx.txid()) { // check that we don't already have scanned the tx, and insert it if we don't
return Err(ApiError { message: "Transaction already scanned".to_owned()}); if !lock_scanned_transactions()?.insert(tx.txid()) {
return Err(ApiError {
message: "Transaction already scanned".to_owned(),
});
} }
let tweak_data = PublicKey::from_str(&tweak_data_hex)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
@ -347,30 +412,28 @@ pub fn check_transaction_for_silent_payments(
let mut connected_user = lock_connected_user()?; let mut connected_user = lock_connected_user()?;
if let Ok(recover) = connected_user.try_get_mut_recover() { if let Ok(recover) = connected_user.try_get_mut_recover() {
if let Ok(txid) = check_transaction(&tx, recover, blockheight, tweak_data) { if let Ok(txid) = check_transaction(&tx, recover, blockheight, tweak_data) {
let shared_point = shared_secret_point(&tweak_data, &recover.get_client().get_scan_key()); if let Err(e) = scan_for_confirmation_transaction(tx_hex) {
lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]); log::error!("{}", e);
handle_recover_transaction(tx, recover, tweak_data, fee_rate)?;
}
return Ok(txid); return Ok(txid);
} }
} }
if let Ok(main) = connected_user.try_get_mut_main() { if let Ok(main) = connected_user.try_get_mut_main() {
if let Ok(txid) = check_transaction(&tx, main, blockheight, tweak_data) { if let Ok(txid) = check_transaction(&tx, main, blockheight, tweak_data) {
let shared_point = shared_secret_point(&tweak_data, &main.get_client().get_scan_key()); // TODO
lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]);
return Ok(txid); return Ok(txid);
} }
} }
if let Ok(revoke) = connected_user.try_get_mut_revoke() { if let Ok(revoke) = connected_user.try_get_mut_revoke() {
if let Ok(txid) = check_transaction(&tx, revoke, blockheight, tweak_data) { if let Ok(txid) = check_transaction(&tx, revoke, blockheight, tweak_data) {
let shared_point = shared_secret_point(&tweak_data, &revoke.get_client().get_scan_key()); // TODO
lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]);
return Ok(txid); return Ok(txid);
} }
} }
// we still want to insert an empty entry in our cache to make sure we don't scan the transaction again
lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::default())]);
Err(ApiError { Err(ApiError {
message: "No output found".to_owned(), message: "No output found".to_owned(),
}) })
@ -385,7 +448,7 @@ pub struct parseNetworkMsgReturn {
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn parse_network_msg(raw: String) -> ApiResult<parseNetworkMsgReturn> { pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<parseNetworkMsgReturn> {
if let Ok(ank_msg) = serde_json::from_str::<AnkNetworkMsg>(&raw) { if let Ok(ank_msg) = serde_json::from_str::<AnkNetworkMsg>(&raw) {
match ank_msg.flag { match ank_msg.flag {
AnkFlag::NewTx => { AnkFlag::NewTx => {
@ -399,6 +462,7 @@ pub fn parse_network_msg(raw: String) -> ApiResult<parseNetworkMsgReturn> {
tx_message.transaction, tx_message.transaction,
0, 0,
tx_message.tweak_data.unwrap(), tx_message.tweak_data.unwrap(),
fee_rate,
)?; )?;
return Ok(parseNetworkMsgReturn { return Ok(parseNetworkMsgReturn {
topic: AnkFlag::NewTx.as_str().to_owned(), topic: AnkFlag::NewTx.as_str().to_owned(),
@ -414,44 +478,60 @@ pub fn parse_network_msg(raw: String) -> ApiResult<parseNetworkMsgReturn> {
} }
AnkFlag::Unknown => { AnkFlag::Unknown => {
// try to decrypt the cipher with all available keys // try to decrypt the cipher with all available keys
let mut plaintext: String = "".to_owned(); for (txid, secret_vec) in lock_secrets()?.iter_mut() {
for (txid, secret_vec) in lock_scanned_transactions()?.iter() { // Actually we probably will ever have only one secret in the case we're receiver
for (shared_with, ank_secret) in secret_vec.iter() { for (shared_with, ank_secret) in secret_vec.iter_mut() {
if *ank_secret == AnkSharedSecret::default() { // if we already have shared_with, that means we already used that key for another message
continue; if !shared_with.is_empty() { continue }
}
let shared_secret = ank_secret.to_byte_array(); let shared_secret = ank_secret.to_byte_array();
if let Ok(msg_decrypt) = Aes256Decryption::new( debug!("{} {}", shared_with, shared_secret.to_lower_hex_string());
let msg_decrypt = Aes256Decryption::new(
Purpose::Arbitrary, Purpose::Arbitrary,
Vec::from_hex(&ank_msg.content.trim_matches('\"'))?, Vec::from_hex(&ank_msg.content.trim_matches('\"'))?,
shared_secret, shared_secret,
) { )?;
match msg_decrypt.decrypt_with_key() { match msg_decrypt.decrypt_with_key() {
Ok(plain) => { Ok(plaintext) => {
plaintext = String::from_utf8(plain)?; let unknown_msg = serde_json::from_slice::<UnknownMessage>(&plaintext);
break; if unknown_msg.is_err() {
}, // The message we were sent is invalid, drop everything
Err(e) => { // for now let's just fill the shared_with with garbage
debug!("{}", e); *shared_with = "a".to_owned();
debug!("Failed to decrypt message {} with key {}", ank_msg.content, shared_secret.to_lower_hex_string()); return Err(ApiError { message: "Invalid msg".to_owned() })
} }
let sender: Result<SilentPaymentAddress, SpError> = unknown_msg.unwrap().sender.try_into();
if sender.is_err() {
// The sender is invalid address
*shared_with = "a".to_owned();
return Err(ApiError { message: "Invalid sp address".to_owned() })
}
// we update our list with the sender address
*shared_with = sender.unwrap().into();
// We return the whole message
// ts is responsible for sending the confirmation message
return Ok(parseNetworkMsgReturn {
topic: AnkFlag::Unknown.as_str().to_owned(),
message: String::from_utf8(plaintext)?,
});
}
Err(e) => {
debug!("{}", e);
debug!(
"Failed to decrypt message {} with key {}",
ank_msg.content,
shared_secret.to_lower_hex_string()
);
} }
} }
} }
} }
if plaintext.is_empty() { // keep the message in cache, just in case?
// keep the message in cache, just in case // return an error
// return an error return Err(ApiError {
return Err(ApiError { message: "No key found".to_owned(),
message: "No key found".to_owned(), });
});
} else {
// return the plain text
return Ok(parseNetworkMsgReturn {
topic: AnkFlag::Unknown.as_str().to_owned(),
message: plaintext,
});
}
} }
_ => unimplemented!(), _ => unimplemented!(),
} }
@ -523,7 +603,7 @@ pub struct createNotificationTransactionReturn {
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_notification_transaction( pub fn create_notification_transaction(
recipient: String, recipient: String,
message: String, message: Option<String>,
fee_rate: u32, fee_rate: u32,
) -> ApiResult<createNotificationTransactionReturn> { ) -> ApiResult<createNotificationTransactionReturn> {
let sp_address: SilentPaymentAddress = recipient.try_into()?; let sp_address: SilentPaymentAddress = recipient.try_into()?;
@ -544,13 +624,16 @@ pub fn create_notification_transaction(
Amount::from_sat(fee_rate.into()), Amount::from_sat(fee_rate.into()),
)?; )?;
debug!("Created transaction with secret {}", shared_secret.to_byte_array().to_lower_hex_string()); debug!(
"Created transaction with secret {}",
shared_secret.to_byte_array().to_lower_hex_string()
);
let mut address2secret: Vec<(String, AnkSharedSecret)> = vec![]; let mut address2secret: Vec<(String, AnkSharedSecret)> = vec![];
address2secret.push((sp_address.into(), shared_secret)); address2secret.push((sp_address.into(), shared_secret));
// update our cache // update our cache
lock_scanned_transactions()?.insert(transaction.txid(), address2secret.clone()); lock_secrets()?.insert(transaction.txid(), address2secret.clone());
Ok(createNotificationTransactionReturn { Ok(createNotificationTransactionReturn {
txid: transaction.txid().to_string(), txid: transaction.txid().to_string(),

View File

@ -2,9 +2,9 @@
use anyhow::Error; use anyhow::Error;
use sdk_common::crypto::AnkSharedSecret; use sdk_common::crypto::AnkSharedSecret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sp_client::bitcoin::Txid; use sp_client::bitcoin::{OutPoint, Txid};
use sp_client::silentpayments::sending::SilentPaymentAddress; use sp_client::silentpayments::sending::SilentPaymentAddress;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::{Mutex, MutexGuard, OnceLock}; use std::sync::{Mutex, MutexGuard, OnceLock};
use tsify::Tsify; use tsify::Tsify;
@ -16,16 +16,40 @@ mod peers;
mod process; mod process;
mod user; mod user;
/// We map txid with one or n secrets
/// Each secret match one sp address
/// When we first detect a transaction, we can't tell who's the sender, so we like sp address empty
/// When we receive the corresponding message, we get a sp address declaration, we complete here
/// Then when we send the confirmation transaction and got the response we can flip the secret to trusted
pub type Txid2Secrets = HashMap<Txid, Vec<(String, AnkSharedSecret)>>; pub type Txid2Secrets = HashMap<Txid, Vec<(String, AnkSharedSecret)>>;
pub static TRANSACTIONCACHE: OnceLock<Mutex<Txid2Secrets>> = OnceLock::new(); pub static SECRETCACHE: OnceLock<Mutex<Txid2Secrets>> = OnceLock::new();
pub fn lock_scanned_transactions() -> Result<MutexGuard<'static, Txid2Secrets>, Error> { pub fn lock_secrets() -> Result<MutexGuard<'static, Txid2Secrets>, Error> {
TRANSACTIONCACHE SECRETCACHE
.get_or_init(|| Mutex::new(Txid2Secrets::new())) .get_or_init(|| Mutex::new(Txid2Secrets::new()))
.lock_anyhow() .lock_anyhow()
} }
/// this is to keep track of transaction we already analysed without finding notification
/// This is not critical and there's no need to keep that in persistent storage, as most transactions would only show up twice
/// Worst case is we will scan again transactions when they got into a block
pub static TRANSACTIONCACHE: OnceLock<Mutex<HashSet<Txid>>> = OnceLock::new();
pub fn lock_scanned_transactions() -> Result<MutexGuard<'static, HashSet<Txid>>, Error> {
TRANSACTIONCACHE
.get_or_init(|| Mutex::new(HashSet::new()))
.lock_anyhow()
}
pub static WATCHEDUTXO: OnceLock<Mutex<HashMap<OutPoint, Txid>>> = OnceLock::new();
pub fn lock_watched() -> Result<MutexGuard<'static, HashMap<OutPoint, Txid>>, Error> {
WATCHEDUTXO
.get_or_init(|| Mutex::new(HashMap::new()))
.lock_anyhow()
}
pub(crate) trait MutexExt<T> { pub(crate) trait MutexExt<T> {
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error>; fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error>;
} }

View File

@ -105,7 +105,7 @@ class Services {
const recipientSpAddress = spAddressElement.value; const recipientSpAddress = spAddressElement.value;
const message = messageElement.value; const message = messageElement.value;
const msg_payload = JSON.stringify({sender: this.sp_address, payload: message}); const msg_payload = JSON.stringify({sender: this.sp_address, message: message});
let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload);
if (notificationInfo) { if (notificationInfo) {
@ -313,14 +313,13 @@ class Services {
services.attachSubmitListener("form4nk", services.updateAnId); services.attachSubmitListener("form4nk", services.updateAnId);
} }
public async parseNetworkMessage(raw: string): Promise<parseNetworkMsgReturn | null> { public async parseNetworkMessage(raw: string, feeRate: number): Promise<parseNetworkMsgReturn> {
const services = await Services.getInstance(); const services = await Services.getInstance();
try { try {
const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw); const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw, feeRate);
return msg; return msg;
} catch (error) { } catch (error) {
console.error(error); throw error;
return null;
} }
} }
@ -802,6 +801,40 @@ class Services {
} }
} }
public async confirm_sender_address(sp_address: string): Promise<void> {
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: createNotificationTransactionReturn;
try {
const feeRate = 1;
notificationInfo = services.sdkClient.create_notification_transaction(sp_address, undefined, 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));
return;
}
public async notify_address_for_message(sp_address: string, message: string): Promise<createNotificationTransactionReturn | null> { public async notify_address_for_message(sp_address: string, message: string): Promise<createNotificationTransactionReturn | null> {
const services = await Services.getInstance(); const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom(); const connection = await services.pickWebsocketConnectionRandom();

View File

@ -25,28 +25,27 @@ class WebSocketClient {
(async () => { (async () => {
if (typeof(msgData) === 'string') { if (typeof(msgData) === 'string') {
console.log("Received text message: "+msgData); // console.log("Received text message: "+msgData);
let res = await services.parseNetworkMessage(msgData); try {
if (res) { const feeRate = 1;
let res = await services.parseNetworkMessage(msgData, feeRate);
if (res.topic === 'new_tx') { if (res.topic === 'new_tx') {
// we received a tx // we received a tx
window.alert(`New tx\n${res.message}`); window.alert(`New tx\n${res.message}`);
await services.updateOwnedOutputsForUser(); await services.updateOwnedOutputsForUser();
} else if (res.topic === 'unknown') { } else if (res.topic === 'unknown') {
// Do we have a json with a sender? let parsed = JSON.parse(res.message);
try { let message = parsed['message'];
let parsed = JSON.parse(res.message); let sender = parsed['sender'];
if (parsed.sender !== undefined) { window.alert(`new message: ${message}\nAsking sender ${sender} to confirm identity...`);
console.info(`Message sent by ${parsed.sender}`); console.debug(`sending confirm message to ${sender}`);
} await services.confirm_sender_address(sender);
window.alert(`new message: ${parsed.payload}`);
} catch (_) {
window.alert(`new message: ${res.message}`);
}
} }
} catch (error) {
console.error('Received an invalid message:', error);
} }
} else { } else {
console.error("Received an invalid message"); console.error('Received a non-string message');
} }
})(); })();
}); });