Return transaction to confirm identity
This commit is contained in:
parent
bcabeb9a0f
commit
fcafcff69e
@ -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(),
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user