Heavy refactor of the caching and message structure

This commit is contained in:
Sosthene 2024-05-22 10:15:42 +02:00
parent fcafcff69e
commit 0f7bc644c8
7 changed files with 488 additions and 238 deletions

View File

@ -8,8 +8,8 @@ name = "sdk_client"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
# sp_client= { path = "../../../sp-client" } sp_client= { path = "../../../sp-client" }
sp_client= { git = "https://github.com/Sosthene00/sp-client", branch = "sp_client" } # sp_client= { git = "https://github.com/Sosthene00/sp-client", branch = "sp_client" }
anyhow = "1.0" anyhow = "1.0"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -1,12 +1,14 @@
use std::any::Any; use std::any::Any;
use std::borrow::Borrow;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use std::str::FromStr; 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 std::time::{Duration, Instant};
use log::{debug, warn}; use log::{debug, warn};
use rand::{Fill, Rng}; use rand::{thread_rng, Fill, Rng, RngCore};
use anyhow::Error as AnyhowError; use anyhow::Error as AnyhowError;
use sdk_common::crypto::{ use sdk_common::crypto::{
@ -16,10 +18,13 @@ use serde_json::{Error as SerdeJsonError, Value};
use shamir::SecretData; use shamir::SecretData;
use sp_client::bitcoin::blockdata::fee_rate; 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::hashes::HashEngine;
use sp_client::bitcoin::hashes::{sha256, Hash};
use sp_client::bitcoin::hex::{parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError};
use sp_client::bitcoin::key::Secp256k1;
use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point;
use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey};
use sp_client::bitcoin::{Amount, OutPoint, Transaction, Txid}; use sp_client::bitcoin::{Amount, Network, OutPoint, Psbt, Transaction, Txid};
use sp_client::silentpayments::Error as SpError; use sp_client::silentpayments::Error as SpError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -28,16 +33,21 @@ 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, UnknownMessage}; use sdk_common::network::{
self, AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage, UnknownMessage,
};
use sdk_common::silentpayments::{ use sdk_common::silentpayments::{
check_transaction, create_transaction, create_transaction_for_address_with_shared_secret, create_transaction, create_transaction_for_address_with_shared_secret,
create_transaction_spend_outpoint, map_outputs_to_sp_address
}; };
use sp_client::spclient::{derive_keys_from_seed, OutputList, OwnedOutput, SpClient}; use sp_client::spclient::{
derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, 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, lock_secrets, lock_watched, Txid2Secrets}; use crate::{images, lock_messages};
use crate::process::Process; use crate::process::Process;
@ -82,6 +92,30 @@ impl From<HexToBytesError> for ApiError {
} }
} }
impl From<HexToArrayError> for ApiError {
fn from(value: HexToArrayError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<sp_client::bitcoin::psbt::PsbtParseError> for ApiError {
fn from(value: sp_client::bitcoin::psbt::PsbtParseError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<sp_client::bitcoin::psbt::ExtractTxError> for ApiError {
fn from(value: sp_client::bitcoin::psbt::ExtractTxError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<sp_client::bitcoin::secp256k1::Error> for ApiError { impl From<sp_client::bitcoin::secp256k1::Error> for ApiError {
fn from(value: sp_client::bitcoin::secp256k1::Error) -> Self { fn from(value: sp_client::bitcoin::secp256k1::Error) -> Self {
ApiError { ApiError {
@ -331,112 +365,184 @@ pub fn login_user(
Ok(res) Ok(res)
} }
pub fn scan_for_confirmation_transaction(tx_hex: String) -> anyhow::Result<String> { fn handle_recover_transaction(
let tx = deserialize::<Transaction>(&Vec::from_hex(&tx_hex)?)?; updated: HashMap<OutPoint, OwnedOutput>,
tx: &Transaction,
sp_wallet: &mut SpWallet,
tweak_data: PublicKey,
fee_rate: u32,
) -> anyhow::Result<NetworkMessage> {
// We need to look for different case:
// 1) faucet
// This one is the simplest, we only care about finding the commitment
let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return());
let commitment = if op_return.is_none() {
vec![]
} else {
op_return.unwrap().script_pubkey.as_bytes()[2..].to_vec()
};
let commitment_str = commitment.to_lower_hex_string();
let pos = lock_messages()?
.iter()
.position(|m| m.commitment.as_ref() == Some(&commitment_str));
for i in tx.input.iter() { if pos.is_some() {
if let Some(waiting) = lock_watched()?.remove(&i.previous_output) { let messages = lock_messages()?;
match lock_secrets()?.get_mut(&waiting) { let message = messages.get(pos.unwrap());
None => return Err(anyhow::Error::msg("No secret match for an error we're waiting for")), return Ok(message.cloned().unwrap());
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")) // If we got updates from a transaction, it means that it creates an output to us, spend an output we owned, or both
} // If we destroyed outputs it means we either notified others, or ask confirmation, or confirm
// We probably creates outputs too in this case because of change
// If we only created outputs it means we are being notified
let utxo_destroyed: HashMap<&OutPoint, &OwnedOutput> = updated
.iter()
.filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent)
.collect();
let utxo_created: HashMap<&OutPoint, &OwnedOutput> = updated
.iter()
.filter(|(outpoint, output)| output.spend_status == OutputSpendStatus::Unspent)
.collect();
fn handle_recover_transaction(tx: Transaction, sp_wallet: &mut SpWallet, tweak_data: PublicKey, fee_rate: u32) -> anyhow::Result<Option<Transaction>> { // 2) confirmation
// does this transaction spent a txid and output we're waiting confirmation for? // If the transaction spends one outpoint in `commited_in`, it means we are receiving a confirmation for a notification
let scan_sk = sp_wallet.get_client().get_scan_key(); // if we are receiver, then we must look for `confirmed_by`
let txid = tx.txid(); // if we owned at least one input or no outputs, we can skip the check
for input in tx.input { if utxo_destroyed.is_empty() && !utxo_created.is_empty() {
let prevout = input.previous_output; for input in tx.input.iter() {
match lock_secrets()?.get_mut(&prevout.txid) { // Check for each input if it match a known commitment we made as a sender
None => { // OR a confirmation for the receiver
continue; let pos = lock_messages()?.iter().position(|m| {
} m.commited_in == Some(input.previous_output)
Some(secret) => { || m.confirmed_by == Some(input.previous_output)
// We found an input spending a notification transaction we sent });
if let Some(res) = secret.get_mut(prevout.vout as usize) { if pos.is_some() {
// This is a challenge from a previous message we sent let mut messages = lock_messages()?;
// we toggle the trusted value let message = messages.get_mut(pos.unwrap()).unwrap();
if !res.1.trusted { // If we are receiver, that's pretty much it, just set status to complete
res.1.trusted = true; if message.recipient == Some(sp_wallet.get_client().get_receiving_address()) {
} else { debug_assert!(message.confirmed_by == Some(input.previous_output));
return Err(anyhow::Error::msg("Received a confirmation for a transaction we already confirmed")); message.status = NetworkMessageStatus::Complete;
return Ok(message.clone());
}
// sender needs to spent it back again to receiver
let (outpoint, output) = utxo_created.iter().next().unwrap();
// If we are sender, then we must update the confirmed_by field
message.confirmed_by = Some(**outpoint);
// Caller must interpret this message as "spend confirmed_by outpoint to receiver"
return Ok(message.clone());
} else {
// 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 mut messages = lock_messages()?;
let cipher_pos = messages.iter().position(|m| {
if m.status != NetworkMessageStatus::CipherWaitingTx {
return false;
} }
// We spend the output back to the receiver m.try_decrypt_with_shared_secret(shared_secret.to_byte_array())
let sp_address = SilentPaymentAddress::try_from(res.0.as_str()).expect("Invalid silent payment address"); .is_some()
let response_tx = create_transaction(sp_address, sp_wallet, Amount::from_sat(fee_rate.into()))?; });
return Ok(Some(response_tx));
if cipher_pos.is_some() {
let message = messages.get_mut(cipher_pos.unwrap()).unwrap();
let (outpoint, output) = utxo_created.iter().next().unwrap();
message.commited_in = Some(**outpoint);
message.shared_secret =
Some(shared_secret.to_byte_array().to_lower_hex_string());
message.commitment = Some(commitment.to_lower_hex_string());
let plaintext = message
.try_decrypt_with_shared_secret(shared_secret.to_byte_array())
.unwrap();
let unknown_msg: UnknownMessage = serde_json::from_slice(&plaintext)?;
message.plaintext = Some(unknown_msg.message);
message.sender = Some(unknown_msg.sender);
message.recipient = Some(sp_wallet.get_client().get_receiving_address());
return Ok(message.clone())
} else { } else {
return Err(anyhow::Error::msg("Received a confirmation from an umapped output")); // store it and wait for the message
let mut new_msg = NetworkMessage::default();
let (outpoint, output) = utxo_created.iter().next().unwrap();
new_msg.commited_in = Some(**outpoint);
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.status = NetworkMessageStatus::TxWaitingCipher;
lock_messages()?.push(new_msg.clone());
return Ok(new_msg.clone());
} }
} }
} }
unreachable!("Transaction with no inputs");
} else {
// We are sender of a notification transaction
// We only need to return the message
if let Some(message) = lock_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"));
}
} }
// 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] /// If the transaction has anything to do with us, we create/update the relevant `NetworkMessage`
pub fn check_transaction_for_silent_payments( /// and return it to caller for persistent storage
fn process_transaction(
tx_hex: String, tx_hex: String,
blockheight: u32, blockheight: u32,
tweak_data_hex: String, tweak_data_hex: String,
fee_rate: u32, fee_rate: u32,
) -> ApiResult<String> { ) -> anyhow::Result<NetworkMessage> {
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, and insert it if we don't // check that we don't already have scanned the tx
if !lock_scanned_transactions()?.insert(tx.txid()) { if let Some(_) = lock_messages()?.iter().find(|message| {
return Err(ApiError { if let Some(outpoint) = message.commited_in {
message: "Transaction already scanned".to_owned(), if outpoint.txid == tx.txid() {
}); return true;
}
}
return false;
}) {
return Err(anyhow::Error::msg("Transaction already scanned"));
} }
let tweak_data = PublicKey::from_str(&tweak_data_hex)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
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) { let updated = recover.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
if let Err(e) = scan_for_confirmation_transaction(tx_hex) {
log::error!("{}", e); if updated.len() > 0 {
handle_recover_transaction(tx, recover, tweak_data, fee_rate)?; let updated_msg =
} handle_recover_transaction(updated, &tx, recover, tweak_data, fee_rate)?;
return Ok(txid); return Ok(updated_msg);
} }
} }
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) { let updated = main.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
// TODO unimplemented!();
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) { let updated = revoke.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
// TODO unimplemented!();
return Ok(txid);
}
} }
Err(ApiError { Err(anyhow::Error::msg("No output found"))
message: "No output found".to_owned(),
})
} }
#[derive(Tsify, Serialize, Deserialize)] #[derive(Tsify, Serialize, Deserialize)]
@ -444,7 +550,7 @@ pub fn check_transaction_for_silent_payments(
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub struct parseNetworkMsgReturn { pub struct parseNetworkMsgReturn {
topic: String, topic: String,
message: String, message: NetworkMessage,
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -458,80 +564,57 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<parseNetworkMs
message: "Missing tweak_data".to_owned(), message: "Missing tweak_data".to_owned(),
}); });
} }
let txid = check_transaction_for_silent_payments( let network_msg = process_transaction(
tx_message.transaction, tx_message.transaction,
0, 0,
tx_message.tweak_data.unwrap(), tx_message.tweak_data.unwrap(),
fee_rate, fee_rate,
)?; )?;
debug!("{:?}", network_msg);
return Ok(parseNetworkMsgReturn { return Ok(parseNetworkMsgReturn {
topic: AnkFlag::NewTx.as_str().to_owned(), topic: ank_msg.flag.as_str().to_owned(),
message: txid, message: network_msg,
}); });
} }
AnkFlag::Faucet => unimplemented!(), AnkFlag::Faucet => unimplemented!(),
AnkFlag::Error => { AnkFlag::Error => {
let error_msg = NetworkMessage::new_error(ank_msg.content);
return Ok(parseNetworkMsgReturn { return Ok(parseNetworkMsgReturn {
topic: AnkFlag::Error.as_str().to_owned(), topic: AnkFlag::Error.as_str().to_owned(),
message: ank_msg.content.to_owned(), message: error_msg,
}) })
} }
AnkFlag::Unknown => { AnkFlag::Unknown => {
// try to decrypt the cipher with all available keys // let's try to decrypt with keys we found in transactions but haven't used yet
for (txid, secret_vec) in lock_secrets()?.iter_mut() { let mut messages = lock_messages()?;
// Actually we probably will ever have only one secret in the case we're receiver let cipher = Vec::from_hex(&ank_msg.content)?;
for (shared_with, ank_secret) in secret_vec.iter_mut() { let cipher_pos = messages.iter().position(|m| {
// if we already have shared_with, that means we already used that key for another message if m.status != NetworkMessageStatus::TxWaitingCipher {
if !shared_with.is_empty() { continue } return false;
let shared_secret = ank_secret.to_byte_array();
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(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()
);
}
}
} }
} m.try_decrypt_cipher(cipher.clone()).is_some()
// keep the message in cache, just in case?
// return an error
return Err(ApiError {
message: "No key found".to_owned(),
}); });
if cipher_pos.is_some() {
let mut message = messages.get_mut(cipher_pos.unwrap()).unwrap();
let plain = message.try_decrypt_cipher(cipher).unwrap();
let unknown_msg: UnknownMessage = serde_json::from_slice(&plain)?;
message.plaintext = Some(unknown_msg.message);
message.sender = Some(unknown_msg.sender);
message.ciphertext = Some(ank_msg.content);
return Ok(parseNetworkMsgReturn {
topic: AnkFlag::Unknown.as_str().to_owned(),
message: message.clone(),
});
} else {
// let's keep it in case we receive the transaction later
let mut new_msg = NetworkMessage::default();
new_msg.status = NetworkMessageStatus::CipherWaitingTx;
new_msg.ciphertext = Some(ank_msg.content);
messages.push(new_msg);
return Err(ApiError {
message: "Can't decrypt message".to_owned(),
});
}
} }
_ => unimplemented!(), _ => unimplemented!(),
} }
@ -597,16 +680,20 @@ pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult<bool> {
pub struct createNotificationTransactionReturn { pub struct createNotificationTransactionReturn {
pub txid: String, pub txid: String,
pub transaction: String, pub transaction: String,
pub address2secret: HashMap<String, AnkSharedSecret>, pub new_network_msg: NetworkMessage,
} }
/// This is what we call to confirm as a receiver
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_notification_transaction( pub fn create_confirmation_transaction(
recipient: String, message: NetworkMessage,
message: Option<String>,
fee_rate: u32, fee_rate: u32,
) -> ApiResult<createNotificationTransactionReturn> { ) -> ApiResult<createNotificationTransactionReturn> {
let sp_address: SilentPaymentAddress = recipient.try_into()?; if message.sender.is_none() || message.confirmed_by.is_none() {
return Err(ApiError { message: "Invalid network message".to_owned() });
}
let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?;
let connected_user = lock_connected_user()?; let connected_user = lock_connected_user()?;
@ -617,28 +704,97 @@ pub fn create_notification_transaction(
sp_wallet = connected_user.try_get_main()?; sp_wallet = connected_user.try_get_main()?;
} }
let (transaction, shared_secret) = create_transaction_for_address_with_shared_secret( let recipient = Recipient {
sp_address, address: sp_address.into(),
amount: Amount::from_sat(1200),
nb_outputs: 1,
};
let signed_psbt = create_transaction_spend_outpoint(
&message.confirmed_by.unwrap(),
sp_wallet, sp_wallet,
message, recipient,
Amount::from_sat(fee_rate.into())
)?;
let final_tx = signed_psbt.extract_tx()?;
Ok(createNotificationTransactionReturn {
txid: final_tx.txid().to_string(),
transaction: serialize(&final_tx).to_lower_hex_string(),
new_network_msg: message
})
}
#[wasm_bindgen]
pub fn create_notification_transaction(
address: String,
commitment: Option<String>,
fee_rate: u32,
) -> ApiResult<createNotificationTransactionReturn> {
let sp_address: SilentPaymentAddress = address.as_str().try_into()?;
let connected_user = lock_connected_user()?;
let sp_wallet: &SpWallet;
if sp_address.is_testnet() {
sp_wallet = connected_user.try_get_recover()?;
} else {
sp_wallet = connected_user.try_get_main()?;
}
let recipient = Recipient {
address: sp_address.into(),
amount: Amount::from_sat(1200),
nb_outputs: 1,
};
let signed_psbt = create_transaction_for_address_with_shared_secret(
recipient,
sp_wallet,
commitment.as_deref(),
Amount::from_sat(fee_rate.into()), 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 shared_point = shared_secret_point(
&sp_wallet
.get_client()
.get_scan_key()
.public_key(&Secp256k1::signing_only()),
&partial_secret,
);
let shared_secret = AnkSharedSecret::new(shared_point);
debug!( debug!(
"Created transaction with secret {}", "Created transaction with secret {}",
shared_secret.to_byte_array().to_lower_hex_string() 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 // update our cache
lock_secrets()?.insert(transaction.txid(), address2secret.clone()); let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt)?;
let recipients_vouts = sp_address2vouts.get::<String>(&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 = NetworkMessage::default();
new_msg.commitment = 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());
// plaintext and ciphertext to be added later when sending the encrypted message
lock_messages()?.push(new_msg.clone());
Ok(createNotificationTransactionReturn { Ok(createNotificationTransactionReturn {
txid: transaction.txid().to_string(), txid: final_tx.txid().to_string(),
transaction: serialize(&transaction).to_lower_hex_string(), transaction: serialize(&final_tx).to_lower_hex_string(),
address2secret: address2secret.into_iter().collect(), new_network_msg: new_msg,
}) })
} }
@ -710,6 +866,18 @@ pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult<String> {
Ok(plain) Ok(plain)
} }
#[wasm_bindgen]
pub fn create_faucet_msg() -> ApiResult<FaucetMessage> {
let user = lock_connected_user()?;
let sp_address = user.try_get_recover()?.get_client().get_receiving_address();
let faucet_msg = FaucetMessage::new(sp_address);
// we write the commitment in a networkmessage so that we can keep track
let mut network_msg = NetworkMessage::default();
network_msg.commitment = Some(faucet_msg.commitment.clone());
lock_messages()?.push(network_msg);
Ok(faucet_msg)
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_commitment(payload_to_hash: String) -> String { pub fn create_commitment(payload_to_hash: String) -> String {
let mut engine = sha256::HashEngine::default(); let mut engine = sha256::HashEngine::default();
@ -717,3 +885,110 @@ pub fn create_commitment(payload_to_hash: String) -> String {
let hash = sha256::Hash::from_engine(engine); let hash = sha256::Hash::from_engine(engine);
hash.to_byte_array().to_lower_hex_string() hash.to_byte_array().to_lower_hex_string()
} }
#[derive(Debug, Serialize, Deserialize, PartialEq, Tsify, Clone)]
pub enum NetworkMessageStatus {
NoStatus, // Default
CipherWaitingTx,
TxWaitingCipher,
SentWaitingConfirmation,
MustSpentConfirmation,
Complete,
}
impl Default for NetworkMessageStatus {
fn default() -> Self {
Self::NoStatus
}
}
/// Unique struct for both 4nk messages and notification/key exchange, both rust and ts
/// 1. Faucet: commited_in with nothing else, status is NoStatus
/// 2. notification:
/// 1. sender: ciphertext, plaintext, commited_in, sender, recipient, shared_secret, key
/// 2. receiver (without tx): ciphertext
/// 3. receiver (tx without msg): commited_in, commitment, recipient, shared_secret
/// 4. receiver (receive tx after msg): plaintext, key, sender, commited_in, commitment, recipient, shared_secret
/// 5. receiver (msg after tx): ciphertext, key, plaintext, sender
/// 3. confirmation:
/// 1. receiver (spend the smallest vout that pays him in the first tx): confirmed_by
/// 2. sender (detect a transaction that pays him and spend commited_by): confirmed_by
/// 3. sender toggle status to complete when it spent confirmed_by, receiver when it detects the confirmed_by is spent
#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct NetworkMessage {
pub id: u32,
pub status: NetworkMessageStatus,
pub ciphertext: Option<String>, // When we receive message we can't decrypt we only have this and commited_in_tx
pub plaintext: Option<String>, // Never None when message sent
pub commited_in: Option<OutPoint>,
pub commitment: Option<String>, // content of the op_return
pub sender: Option<String>, // Never None when message sent
pub recipient: Option<String>, // Never None when message sent
pub shared_secret: Option<String>, // Never None when message sent
pub key: Option<String>, // Never None when message sent
pub confirmed_by: Option<OutPoint>, // If this None, Sender keeps sending
pub timestamp: u64,
pub error: Option<String>,
}
impl NetworkMessage {
pub fn new() -> Self {
let mut new = NetworkMessage::default();
let mut buf = [0u8;4];
thread_rng().fill_bytes(&mut buf);
new.id = u32::from_be_bytes(buf);
new
}
pub fn new_error(error: String) -> Self {
let mut new = NetworkMessage::default();
new.error = Some(error);
new
}
pub fn try_decrypt_cipher(&self, cipher: Vec<u8>) -> Option<Vec<u8>> {
if self.ciphertext.is_some() || self.shared_secret.is_none() {
log::error!(
"Can't try decrypt this message, there's already a ciphertext or no shared secret"
);
return None;
}
let mut shared_secret = [0u8; 32];
shared_secret
.copy_from_slice(&Vec::from_hex(self.shared_secret.as_ref().unwrap()).unwrap());
let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, shared_secret);
if aes_decrypt.is_err() {
log::error!("Failed to create decrypt object");
return None;
}
aes_decrypt.unwrap().decrypt_with_key().ok()
}
pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Option<Vec<u8>> {
if self.ciphertext.is_none() || self.shared_secret.is_some() {
log::error!(
"Can't try decrypt this message, ciphertext is none or shared_secret already found"
);
return None;
}
let cipher_bin = Vec::from_hex(self.ciphertext.as_ref().unwrap());
if cipher_bin.is_err() {
let error = cipher_bin.unwrap_err();
log::error!("Invalid hex in ciphertext: {}", error.to_string());
return None;
}
let aes_decrypt =
Aes256Decryption::new(Purpose::Arbitrary, cipher_bin.unwrap(), shared_secret);
if aes_decrypt.is_err() {
log::error!("Failed to create decrypt object");
return None;
}
aes_decrypt.unwrap().decrypt_with_key().ok()
}
}

View File

@ -1,5 +1,6 @@
#![allow(warnings)] #![allow(warnings)]
use anyhow::Error; use anyhow::Error;
use api::NetworkMessage;
use sdk_common::crypto::AnkSharedSecret; use sdk_common::crypto::AnkSharedSecret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sp_client::bitcoin::{OutPoint, Txid}; use sp_client::bitcoin::{OutPoint, Txid};
@ -16,37 +17,11 @@ mod peers;
mod process; mod process;
mod user; mod user;
/// We map txid with one or n secrets pub static NETWORKMESSAGES: OnceLock<Mutex<Vec<NetworkMessage>>> = OnceLock::new();
/// 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 SECRETCACHE: OnceLock<Mutex<Txid2Secrets>> = OnceLock::new(); pub fn lock_messages() -> Result<MutexGuard<'static, Vec<NetworkMessage>>, Error> {
NETWORKMESSAGES
pub fn lock_secrets() -> Result<MutexGuard<'static, Txid2Secrets>, Error> { .get_or_init(|| Mutex::new(vec![]))
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() .lock_anyhow()
} }

View File

@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../../dist/pkg ./crates/sp_client --target bundler", "build_wasm": "wasm-pack build --out-dir ../../dist/pkg ./crates/sp_client --target bundler --dev",
"start": "webpack serve", "start": "webpack serve",
"build": "webpack" "build": "webpack"
}, },

View File

@ -24,6 +24,11 @@ class Database {
'unique': true 'unique': true
} }
}] }]
},
AnkMessages: {
name: "messages",
options: {'keyPath': 'id'},
indices: []
} }
} }

View File

@ -1,4 +1,4 @@
import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret } from '../dist/pkg/sdk_client'; import { createUserReturn, User, Process, createNotificationTransactionReturn, parse_network_msg, outputs_list, parseNetworkMsgReturn, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, NetworkMessage } from '../dist/pkg/sdk_client';
import IndexedDB from './database' import IndexedDB from './database'
import { WebSocketClient } from './websockets'; import { WebSocketClient } from './websockets';
@ -109,30 +109,39 @@ class Services {
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) {
let networkMsg = notificationInfo.new_network_msg;
const msgId = notificationInfo.new_network_msg.id;
let shared_secret = '';
if (networkMsg.shared_secret) {
shared_secret = networkMsg.shared_secret;
} else {
throw 'no shared_secret';
}
let ciphers: string[] = []; let ciphers: string[] = [];
console.info('Successfully sent notification transaction'); console.info('Successfully sent notification transaction');
// Save the secret to db // Save the secret to db
const indexedDb = await IndexedDB.getInstance();
const db = await indexedDb.getDb();
// encrypt the message(s) // encrypt the message(s)
for (const [address, ankSharedSecret] of Object.entries(notificationInfo.address2secret)) { try {
try { const cipher = await services.encryptData(msg_payload, shared_secret);
let cipher = await services.encryptData(msg_payload, ankSharedSecret.secret); let updated_msg = notificationInfo.new_network_msg;
ciphers.push(cipher); updated_msg.plaintext = msg_payload;
} catch (error) { updated_msg.ciphertext = cipher;
throw error; await indexedDb.writeObject(db, indexedDb.getStoreList().AnkMessages, updated_msg, null);
} ciphers.push(cipher);
} catch (error) {
throw error;
} }
const connection = await services.pickWebsocketConnectionRandom(); const connection = await services.pickWebsocketConnectionRandom();
const flag: AnkFlag = "Unknown"; const flag: AnkFlag = "Unknown";
// for testing we only take the first cipher
const payload = ciphers.at(0);
if (!payload) {
console.error("No payload");
return;
}
// add peers list // add peers list
// add processes list // add processes list
// send message (transaction in envelope) // send message (transaction in envelope)
connection?.sendMessage(flag, payload); for (const payload of ciphers) {
connection?.sendMessage(flag, payload);
}
} }
} }
@ -397,19 +406,6 @@ class Services {
} }
} }
public async checkTransaction(tx: string, tweak_data: string, blkheight: number): Promise<string | null> {
const services = await Services.getInstance();
try {
const txid = services.sdkClient.check_transaction_for_silent_payments(tx, blkheight, tweak_data);
await services.updateOwnedOutputsForUser();
return txid;
} catch (error) {
console.error(error);
return null;
}
}
public async getAllProcessForUser(pre_id: string): Promise<Process[]> { public async getAllProcessForUser(pre_id: string): Promise<Process[]> {
const services = await Services.getInstance(); const services = await Services.getInstance();
let user: User; let user: User;
@ -447,6 +443,17 @@ class Services {
return process; return process;
} }
public async updateMessages(message: NetworkMessage): Promise<void> {
const indexedDb = await IndexedDB.getInstance();
const db = await indexedDb.getDb();
try {
await indexedDb.setObject(db, indexedDb.getStoreList().AnkMessages, message, null);
} catch (error) {
throw error;
}
}
public async updateProcesses(): Promise<void> { public async updateProcesses(): Promise<void> {
const services = await Services.getInstance(); const services = await Services.getInstance();
const processList: Process[] = services.sdkClient.get_processes(); const processList: Process[] = services.sdkClient.get_processes();
@ -758,10 +765,8 @@ class Services {
return null; return null;
} }
try { try {
const flag: AnkFlag = "Faucet"; const flag: AnkFlag = 'Faucet';
const faucetMsg: FaucetMessage = { const faucetMsg = services.sdkClient.create_faucet_msg();
'sp_address': spaddress
}
connection.sendMessage(flag, JSON.stringify(faucetMsg)); connection.sendMessage(flag, JSON.stringify(faucetMsg));
} catch (error) { } catch (error) {
console.error("Failed to obtain tokens with relay ", connection.getUrl()); console.error("Failed to obtain tokens with relay ", connection.getUrl());
@ -822,7 +827,7 @@ class Services {
let notificationInfo: createNotificationTransactionReturn; let notificationInfo: createNotificationTransactionReturn;
try { try {
const feeRate = 1; const feeRate = 1;
notificationInfo = services.sdkClient.create_notification_transaction(sp_address, undefined, feeRate); notificationInfo = services.sdkClient.create_notification_transaction(sp_address, null, feeRate);
} catch (error) { } catch (error) {
throw new Error(`Failed to create confirmation transaction: ${error}`); throw new Error(`Failed to create confirmation transaction: ${error}`);
} }
@ -835,23 +840,11 @@ class Services {
return; 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> {
const services = await Services.getInstance(); const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom(); const connection = await services.pickWebsocketConnectionRandom();
if (!connection) { if (!connection) {
return null; throw 'No available connection';
}
let user: User;
try {
let possibleUser = await services.getUserInfo();
if (!possibleUser) {
console.error("No user loaded, please first create a new user or login");
return null;
} else {
user = possibleUser;
}
} catch (error) {
throw error;
} }
try { try {
@ -866,8 +859,7 @@ class Services {
connection.sendMessage(flag, JSON.stringify(newTxMsg)); connection.sendMessage(flag, JSON.stringify(newTxMsg));
return notificationInfo; return notificationInfo;
} catch (error) { } catch (error) {
console.error("Failed to create notification transaction:", error); throw 'Failed to create notification transaction:", error';
return null
} }
} }

View File

@ -34,11 +34,14 @@ class WebSocketClient {
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') {
let parsed = JSON.parse(res.message); let message = res.message['plaintext'];
let message = parsed['message']; let sender = res.message['sender'];
let sender = parsed['sender']; if (!message || !sender) {
throw 'Message missing plaintext and/or sender';
}
window.alert(`new message: ${message}\nAsking sender ${sender} to confirm identity...`); window.alert(`new message: ${message}\nAsking sender ${sender} to confirm identity...`);
console.debug(`sending confirm message to ${sender}`); console.debug(`sending confirm message to ${sender}`);
await services.updateMessages(res.message);
await services.confirm_sender_address(sender); await services.confirm_sender_address(sender);
} }
} catch (error) { } catch (error) {