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::sync::{Mutex, OnceLock, PoisonError};
use log::debug;
use log::{debug, warn};
use rand::{Fill, Rng};
use anyhow::Error as AnyhowError;
use sdk_common::crypto::{
AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose,
};
use serde_json::Error as SerdeJsonError;
use serde_json::{Error as SerdeJsonError, Value};
use shamir::SecretData;
use sp_client::bitcoin::blockdata::fee_rate;
use sp_client::bitcoin::consensus::{deserialize, serialize};
use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToBytesError};
use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point;
@ -27,16 +28,16 @@ use tsify::Tsify;
use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::prelude::*;
use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage};
use sdk_common::network::{AnkFlag, AnkNetworkMsg, NewTxMessage, UnknownMessage};
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::{SpWallet, SpendKey};
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;
@ -330,16 +331,80 @@ pub fn login_user(
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]
pub fn check_transaction_for_silent_payments(
tx_hex: String,
blockheight: u32,
tweak_data_hex: String,
fee_rate: u32,
) -> ApiResult<String> {
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()) {
return Err(ApiError { message: "Transaction already scanned".to_owned()});
// check that we don't already have scanned the tx, and insert it if we don't
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)?;
@ -347,30 +412,28 @@ pub fn check_transaction_for_silent_payments(
let mut connected_user = lock_connected_user()?;
if let Ok(recover) = connected_user.try_get_mut_recover() {
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());
lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]);
if let Err(e) = scan_for_confirmation_transaction(tx_hex) {
log::error!("{}", e);
handle_recover_transaction(tx, recover, tweak_data, fee_rate)?;
}
return Ok(txid);
}
}
if let Ok(main) = connected_user.try_get_mut_main() {
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());
lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]);
// TODO
return Ok(txid);
}
}
if let Ok(revoke) = connected_user.try_get_mut_revoke() {
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());
lock_scanned_transactions()?.insert(tx.txid(), vec![("".to_owned(), AnkSharedSecret::new(shared_point, false))]);
// TODO
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 {
message: "No output found".to_owned(),
})
@ -385,7 +448,7 @@ pub struct parseNetworkMsgReturn {
}
#[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) {
match ank_msg.flag {
AnkFlag::NewTx => {
@ -399,6 +462,7 @@ pub fn parse_network_msg(raw: String) -> ApiResult<parseNetworkMsgReturn> {
tx_message.transaction,
0,
tx_message.tweak_data.unwrap(),
fee_rate,
)?;
return Ok(parseNetworkMsgReturn {
topic: AnkFlag::NewTx.as_str().to_owned(),
@ -414,44 +478,60 @@ pub fn parse_network_msg(raw: String) -> ApiResult<parseNetworkMsgReturn> {
}
AnkFlag::Unknown => {
// try to decrypt the cipher with all available keys
let mut plaintext: String = "".to_owned();
for (txid, secret_vec) in lock_scanned_transactions()?.iter() {
for (shared_with, ank_secret) in secret_vec.iter() {
if *ank_secret == AnkSharedSecret::default() {
continue;
}
for (txid, secret_vec) in lock_secrets()?.iter_mut() {
// Actually we probably will ever have only one secret in the case we're receiver
for (shared_with, ank_secret) in secret_vec.iter_mut() {
// if we already have shared_with, that means we already used that key for another message
if !shared_with.is_empty() { continue }
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,
Vec::from_hex(&ank_msg.content.trim_matches('\"'))?,
shared_secret,
) {
)?;
match msg_decrypt.decrypt_with_key() {
Ok(plain) => {
plaintext = String::from_utf8(plain)?;
break;
},
Ok(plaintext) => {
let unknown_msg = serde_json::from_slice::<UnknownMessage>(&plaintext);
if unknown_msg.is_err() {
// The message we were sent is invalid, drop everything
// for now let's just fill the shared_with with garbage
*shared_with = "a".to_owned();
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());
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 Err(ApiError {
message: "No key found".to_owned(),
});
} else {
// return the plain text
return Ok(parseNetworkMsgReturn {
topic: AnkFlag::Unknown.as_str().to_owned(),
message: plaintext,
});
}
}
_ => unimplemented!(),
}
@ -523,7 +603,7 @@ pub struct createNotificationTransactionReturn {
#[wasm_bindgen]
pub fn create_notification_transaction(
recipient: String,
message: String,
message: Option<String>,
fee_rate: u32,
) -> ApiResult<createNotificationTransactionReturn> {
let sp_address: SilentPaymentAddress = recipient.try_into()?;
@ -544,13 +624,16 @@ pub fn create_notification_transaction(
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![];
address2secret.push((sp_address.into(), shared_secret));
// update our cache
lock_scanned_transactions()?.insert(transaction.txid(), address2secret.clone());
lock_secrets()?.insert(transaction.txid(), address2secret.clone());
Ok(createNotificationTransactionReturn {
txid: transaction.txid().to_string(),

View File

@ -2,9 +2,9 @@
use anyhow::Error;
use sdk_common::crypto::AnkSharedSecret;
use serde::{Deserialize, Serialize};
use sp_client::bitcoin::Txid;
use sp_client::bitcoin::{OutPoint, Txid};
use sp_client::silentpayments::sending::SilentPaymentAddress;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::sync::{Mutex, MutexGuard, OnceLock};
use tsify::Tsify;
@ -16,16 +16,40 @@ mod peers;
mod process;
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 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> {
TRANSACTIONCACHE
pub fn lock_secrets() -> Result<MutexGuard<'static, Txid2Secrets>, Error> {
SECRETCACHE
.get_or_init(|| Mutex::new(Txid2Secrets::new()))
.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> {
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error>;
}

View File

@ -105,7 +105,7 @@ class Services {
const recipientSpAddress = spAddressElement.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);
if (notificationInfo) {
@ -313,14 +313,13 @@ class Services {
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();
try {
const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw);
const msg: parseNetworkMsgReturn = services.sdkClient.parse_network_msg(raw, feeRate);
return msg;
} catch (error) {
console.error(error);
return null;
throw error;
}
}
@ -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> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();

View File

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