Message processing heavy refactoring

This commit is contained in:
Sosthene 2024-05-24 22:40:40 +02:00
parent c3e435a228
commit bc6f95a98f
3 changed files with 191 additions and 141 deletions

View File

@ -25,7 +25,8 @@ use sp_client::bitcoin::key::Secp256k1;
use sp_client::bitcoin::secp256k1::ecdh::shared_secret_point;
use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey};
use sp_client::bitcoin::{Amount, Network, OutPoint, Psbt, Transaction, Txid};
use sp_client::silentpayments::Error as SpError;
use sp_client::silentpayments::utils as sp_utils;
use sp_client::silentpayments::{Error as SpError, Network as SpNetwork};
use serde::{Deserialize, Serialize};
use sp_client::silentpayments::sending::SilentPaymentAddress;
@ -34,11 +35,12 @@ use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::prelude::*;
use sdk_common::network::{
self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage, UnknownMessage
self, AnkFlag, AnkNetworkMsg, CachedMessage, CachedMessageStatus, FaucetMessage, NewTxMessage,
UnknownMessage,
};
use sdk_common::silentpayments::{
create_transaction, create_transaction_for_address_with_shared_secret,
create_transaction_spend_outpoint, map_outputs_to_sp_address
create_transaction_spend_outpoint, map_outputs_to_sp_address,
};
use sp_client::spclient::{
@ -402,23 +404,29 @@ fn handle_recover_transaction(
} else {
false
}
})
{
}) {
let message = messages.get_mut(pos).unwrap();
match message.status {
CachedMessageStatus::FaucetWaiting => {
message.status = CachedMessageStatus::FaucetComplete;
message.commited_in = utxo_created.into_iter().next().map(|(outpoint, _)| *outpoint);
message.commited_in = utxo_created
.into_iter()
.next()
.map(|(outpoint, _)| *outpoint);
return Ok(message.clone());
},
}
// Actually this is unreachable
CachedMessageStatus::FaucetComplete => return Ok(message.clone()),
_ => ()
_ => (),
}
}
// we inspect inputs looking for links with previous tx
for input in tx.input.iter() {
if let Some(pos) = messages.iter().position(|m| {
if let Some(pos) = messages
.iter()
.position(|m| {
debug!("{:?}", Some(input.previous_output));
m.confirmed_by == Some(input.previous_output)
})
{
@ -426,9 +434,9 @@ fn handle_recover_transaction(
// If we are receiver, that's pretty much it, just set status to complete
message.status = CachedMessageStatus::Complete;
return Ok(message.clone());
} else if let Some(pos) = messages.iter().position(|m| {
m.commited_in == Some(input.previous_output)
})
} else if let Some(pos) = messages
.iter()
.position(|m| m.commited_in == Some(input.previous_output))
{
// sender needs to spent it back again to receiver
let (outpoint, output) = utxo_created.into_iter().next().unwrap();
@ -444,9 +452,16 @@ fn handle_recover_transaction(
}
// if we've found nothing 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 shared_point = sp_utils::receiving::calculate_shared_point(
&tweak_data,
&sp_wallet.get_client().get_scan_key(),
);
let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?);
debug!(
"Shared secret: {}",
shared_secret.to_byte_array().to_lower_hex_string()
);
if let Some(cipher_pos) = messages.iter().position(|m| {
if m.status != CachedMessageStatus::CipherWaitingTx {
@ -454,15 +469,13 @@ fn handle_recover_transaction(
}
m.try_decrypt_with_shared_secret(shared_secret.to_byte_array())
.is_ok()
})
{
}) {
let message = messages.get_mut(cipher_pos).unwrap();
let (outpoint, output) = utxo_created.into_iter().next().unwrap();
message.commited_in = Some(outpoint.clone());
message.shared_secret =
Some(shared_secret.to_byte_array().to_lower_hex_string());
message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
message.commitment = Some(commitment_str);
let plaintext = message
@ -474,16 +487,18 @@ fn handle_recover_transaction(
message.recipient = Some(sp_wallet.get_client().get_receiving_address());
message.status = CachedMessageStatus::ReceivedMustConfirm;
return Ok(message.clone())
return Ok(message.clone());
} else {
// store it and wait for the message
let mut new_msg = CachedMessage::new();
let (outpoint, output) = utxo_created.into_iter().next().expect("utxo_created shouldn't be empty");
let (outpoint, output) = utxo_created
.into_iter()
.next()
.expect("utxo_created shouldn't be empty");
new_msg.commited_in = Some(outpoint.clone());
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.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
new_msg.status = CachedMessageStatus::TxWaitingCipher;
messages.push(new_msg.clone());
return Ok(new_msg.clone());
@ -491,14 +506,15 @@ fn handle_recover_transaction(
} else {
// We are sender of a transaction
// We only need to return the message
if let Some(message) = messages.iter()
.find(|m| {
m.commitment.as_ref() == Some(&commitment_str)
})
if let Some(message) = 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"));
return Err(anyhow::Error::msg(
"We spent a transaction for a commitment we don't know",
));
}
}
}
@ -577,7 +593,7 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage>
AnkFlag::Faucet => unimplemented!(),
AnkFlag::Error => {
let error_msg = CachedMessage::new_error(ank_msg.content);
return Ok(error_msg)
return Ok(error_msg);
}
AnkFlag::Unknown => {
// let's try to decrypt with keys we found in transactions but haven't used yet
@ -597,16 +613,15 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage>
message.plaintext = Some(unknown_msg.message);
message.sender = Some(unknown_msg.sender);
message.ciphertext = Some(ank_msg.content);
message.status = CachedMessageStatus::ReceivedMustConfirm;
return Ok(message.clone());
} else {
// let's keep it in case we receive the transaction later
let mut new_msg = CachedMessage::new();
new_msg.status = CachedMessageStatus::CipherWaitingTx;
new_msg.ciphertext = Some(ank_msg.content);
messages.push(new_msg);
return Err(ApiError {
message: "Can't decrypt message".to_owned(),
});
messages.push(new_msg.clone());
return Ok(new_msg);
}
}
_ => unimplemented!(),
@ -676,22 +691,33 @@ pub struct createTransactionReturn {
pub new_network_msg: CachedMessage,
}
/// This is what we call to answer a confirmation as a sender
/// This is what we call to answer a confirmation as a sender
#[wasm_bindgen]
pub fn answer_confirmation_transaction(
message: CachedMessage,
message_id: u32,
fee_rate: u32,
) -> ApiResult<createTransactionReturn> {
if message.recipient.is_none() || message.confirmed_by.is_none() {
return Err(ApiError { message: "Invalid network message".to_owned() });
let mut messages = lock_messages()?;
let message: &mut CachedMessage;
if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) {
if m.sender.is_none() || m.commited_in.is_none() {
return Err(ApiError {
message: "Invalid network message".to_owned(),
});
}
message = m;
} else {
return Err(ApiError { message: format!("Can't find message for id {}", message_id) });
}
let sp_address: SilentPaymentAddress = message.recipient.as_ref().unwrap().as_str().try_into()?;
let sp_address: SilentPaymentAddress =
message.recipient.as_ref().unwrap().as_str().try_into()?;
let connected_user = lock_connected_user()?;
let sp_wallet: &SpWallet;
if sp_address.is_testnet() {
if sp_address.get_network() != SpNetwork::Mainnet {
sp_wallet = connected_user.try_get_recover()?;
} else {
sp_wallet = connected_user.try_get_main()?;
@ -703,38 +729,53 @@ pub fn answer_confirmation_transaction(
nb_outputs: 1,
};
let confirmed_by = message.confirmed_by.clone().unwrap();
let commited_in = message.commited_in.clone().unwrap();
let signed_psbt = create_transaction_spend_outpoint(
&message.confirmed_by.unwrap(),
sp_wallet,
recipient,
Amount::from_sat(fee_rate.into())
&confirmed_by,
sp_wallet,
recipient,
&commited_in.txid,
Amount::from_sat(fee_rate.into()),
)?;
let final_tx = signed_psbt.extract_tx()?;
message.status = CachedMessageStatus::Complete;
Ok(createTransactionReturn {
txid: final_tx.txid().to_string(),
transaction: serialize(&final_tx).to_lower_hex_string(),
new_network_msg: message
new_network_msg: message.clone(),
})
}
/// This is what we call to confirm as a receiver
#[wasm_bindgen]
pub fn create_confirmation_transaction(
message: CachedMessage,
message_id: u32,
fee_rate: u32,
) -> ApiResult<createTransactionReturn> {
if message.sender.is_none() || message.confirmed_by.is_none() {
return Err(ApiError { message: "Invalid network message".to_owned() });
let mut messages = lock_messages()?;
let message: &mut CachedMessage;
if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) {
if m.sender.is_none() || m.commited_in.is_none() {
return Err(ApiError {
message: "Invalid network message".to_owned(),
});
}
message = m;
} else {
return Err(ApiError { message: format!("Can't find message for id {}", message_id) });
}
let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?;
let connected_user = lock_connected_user()?;
let sp_wallet: &SpWallet;
if sp_address.is_testnet() {
if sp_address.get_network() != SpNetwork::Mainnet {
sp_wallet = connected_user.try_get_recover()?;
} else {
sp_wallet = connected_user.try_get_main()?;
@ -746,26 +787,38 @@ pub fn create_confirmation_transaction(
nb_outputs: 1,
};
let commited_in = message.commited_in.clone().unwrap();
let signed_psbt = create_transaction_spend_outpoint(
&message.confirmed_by.unwrap(),
sp_wallet,
recipient,
Amount::from_sat(fee_rate.into())
&commited_in,
sp_wallet,
recipient,
&commited_in.txid,
Amount::from_sat(fee_rate.into()),
)?;
// what's the vout of the output sent to sender?
let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?;
let recipients_vouts = sp_address2vouts
.get::<String>(&sp_address.into())
.expect("recipients didn't change")
.as_slice();
let final_tx = signed_psbt.extract_tx()?;
message.confirmed_by = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 });
Ok(createTransactionReturn {
txid: final_tx.txid().to_string(),
transaction: serialize(&final_tx).to_lower_hex_string(),
new_network_msg: message
new_network_msg: message.clone(),
})
}
#[wasm_bindgen]
pub fn create_notification_transaction(
address: String,
commitment: Option<String>,
message: UnknownMessage,
fee_rate: u32,
) -> ApiResult<createTransactionReturn> {
let sp_address: SilentPaymentAddress = address.as_str().try_into()?;
@ -773,7 +826,7 @@ pub fn create_notification_transaction(
let connected_user = lock_connected_user()?;
let sp_wallet: &SpWallet;
if sp_address.is_testnet() {
if sp_address.get_network() != SpNetwork::Mainnet {
sp_wallet = connected_user.try_get_recover()?;
} else {
sp_wallet = connected_user.try_get_main()?;
@ -785,42 +838,47 @@ pub fn create_notification_transaction(
nb_outputs: 1,
};
let commitment = create_commitment(serde_json::to_string(&message)?);
let signed_psbt = create_transaction_for_address_with_shared_secret(
recipient,
sp_wallet,
commitment.as_deref(),
Some(&commitment),
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 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_point =
sp_utils::sending::calculate_shared_point(&sp_address.get_scan_key(), &partial_secret);
let shared_secret = AnkSharedSecret::new(shared_point);
let shared_secret = AnkSharedSecret::new(PublicKey::from_slice(&shared_point)?);
debug!(
"Created transaction with secret {}",
shared_secret.to_byte_array().to_lower_hex_string()
);
let cipher = encrypt_with_key(serde_json::to_string(&message)?, shared_secret.to_byte_array().to_lower_hex_string())?;
// update our cache
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();
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 = CachedMessage::default();
new_msg.commitment = commitment;
new_msg.commited_in = Some(OutPoint { txid: final_tx.txid(), vout: recipients_vouts[0] as u32 });
let mut new_msg = CachedMessage::new();
new_msg.plaintext = Some(message.message);
new_msg.ciphertext = Some(cipher);
new_msg.commitment = Some(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());
@ -908,11 +966,11 @@ pub fn create_faucet_msg() -> ApiResult<CachedMessage> {
let user = lock_connected_user()?;
let sp_address = user.try_get_recover()?.get_client().get_receiving_address();
let mut commitment = [0u8;64];
let mut commitment = [0u8; 64];
thread_rng().fill_bytes(&mut commitment);
let mut cached_msg = CachedMessage::new();
cached_msg.recipient = Some(sp_address);
cached_msg.recipient = Some(sp_address);
cached_msg.commitment = Some(commitment.to_lower_hex_string());
cached_msg.status = CachedMessageStatus::FaucetWaiting;
lock_messages()?.push(cached_msg.clone());

View File

@ -1,4 +1,4 @@
import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage } from '../dist/pkg/sdk_client';
import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage, UnknownMessage } from '../dist/pkg/sdk_client';
import IndexedDB from './database'
import { WebSocketClient } from './websockets';
@ -105,36 +105,24 @@ class Services {
const recipientSpAddress = spAddressElement.value;
const message = messageElement.value;
const msg_payload = JSON.stringify({sender: this.sp_address, message: message});
const msg_payload: UnknownMessage = {sender: this.sp_address!, message: message};
let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload);
if (notificationInfo) {
let networkMsg = notificationInfo.new_network_msg;
let shared_secret = '';
if (networkMsg.shared_secret) {
shared_secret = networkMsg.shared_secret;
} else {
throw 'no shared_secret';
}
console.info('Successfully sent notification transaction');
console.debug(networkMsg);
const connection = await services.pickWebsocketConnectionRandom();
const flag: AnkFlag = "Unknown";
// encrypt the message(s)
// TODO we'd rather do that in the wasm as part of notify_address_for_message
try {
const cipher = await services.encryptData(msg_payload, shared_secret);
let updated_msg = notificationInfo.new_network_msg;
updated_msg.plaintext = msg_payload;
updated_msg.ciphertext = cipher;
await services.updateMessages(updated_msg);
connection?.sendMessage(flag, cipher);
// send message (transaction in envelope)
await services.updateMessages(networkMsg);
connection?.sendMessage(flag, networkMsg.ciphertext!);
} catch (error) {
throw error;
}
// add peers list
// add processes list
// send message (transaction in envelope)
}
}
@ -820,6 +808,41 @@ class Services {
}
}
public async answer_confirmation_message(msg: CachedMessage): 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: createTransactionReturn;
try {
const feeRate = 1;
notificationInfo = services.sdkClient.answer_confirmation_transaction(msg.id, 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));
await services.updateMessages(notificationInfo.new_network_msg);
return;
}
public async confirm_sender_address(msg: CachedMessage): Promise<void> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();
@ -841,7 +864,7 @@ class Services {
let notificationInfo: createTransactionReturn;
try {
const feeRate = 1;
notificationInfo = services.sdkClient.answer_confirmation_transaction(msg, feeRate);
notificationInfo = services.sdkClient.create_confirmation_transaction(msg.id, feeRate);
} catch (error) {
throw new Error(`Failed to create confirmation transaction: ${error}`);
}
@ -851,10 +874,11 @@ class Services {
'tweak_data': null
}
connection.sendMessage(flag, JSON.stringify(newTxMsg));
await services.updateMessages(notificationInfo.new_network_msg);
return;
}
public async notify_address_for_message(sp_address: string, message: string): Promise<createTransactionReturn> {
public async notify_address_for_message(sp_address: string, message: UnknownMessage): Promise<createTransactionReturn> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();
if (!connection) {
@ -863,62 +887,19 @@ class Services {
try {
const feeRate = 1;
const commitment = services.sdkClient.create_commitment(message);
let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, commitment, feeRate);
let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate);
const flag: AnkFlag = "NewTx";
const newTxMsg: NewTxMessage = {
'transaction': notificationInfo.transaction,
'tweak_data': null
}
connection.sendMessage(flag, JSON.stringify(newTxMsg));
console.info('Successfully sent notification transaction');
return notificationInfo;
} catch (error) {
throw 'Failed to create notification transaction:", error';
}
}
// public async encryptData(data: string, sharedSecret: Record<string, AnkSharedSecret>): Promise<Map<string, string>> {
// const services = await Services.getInstance();
// let msg_cipher: encryptWithNewKeyResult;
// try {
// msg_cipher = services.sdkClient.encrypt_with_new_key(data);
// } catch (error) {
// throw error;
// }
// let res = new Map<string, string>();
// for (const [recipient, secret] of Object.entries(sharedSecret)) {
// try {
// const key = secret.secret;
// const encryptedKey: string = await services.sdkClient.encrypt_with_key(msg_cipher.key, key);
// res.set(recipient, encryptedKey);
// } catch (error) {
// throw new Error(`Failed to encrypt key for recipient ${recipient}: ${error}`);
// }
// }
// return res;
// }
public async encryptData(data: string, key: string): Promise<string> {
const services = await Services.getInstance();
try {
let res: string = services.sdkClient.encrypt_with_key(data, key);
return res;
} catch (error) {
throw error;
}
}
public async decryptData(cipher: string, key: string): Promise<string> {
const services = await Services.getInstance();
try {
let res = services.sdkClient.try_decrypt_with_key(cipher, key);
return res;
} catch (error) {
throw error;
}
}
}
export default Services;

View File

@ -34,7 +34,7 @@ class WebSocketClient {
if (res.status === 'FaucetComplete') {
// we received a faucet tx, there's nothing else to do
window.alert(`New faucet output\n${res.commited_in}`);
await services.removeMessage(res.id);
await services.updateMessages(res);
await services.updateOwnedOutputsForUser();
} else if (res.status === 'TxWaitingCipher') {
// we received a tx but we don't have the cipher
@ -47,6 +47,7 @@ class WebSocketClient {
await services.updateMessages(res);
} else if (res.status === 'SentWaitingConfirmation') {
// We are sender and we're waiting for the challenge that will confirm recipient got the transaction and the message
await services.updateMessages(res);
await services.updateOwnedOutputsForUser();
} else if (res.status === 'MustSpendConfirmation') {
// we received a challenge for a notification we made
@ -54,7 +55,17 @@ class WebSocketClient {
window.alert(`Spending ${res.confirmed_by} to prove our identity`);
console.debug(`sending confirm message to ${res.recipient}`);
await services.updateMessages(res);
await services.answer_confirmation_message(res);
} else if (res.status === 'ReceivedMustConfirm') {
// we found a notification and decrypted the cipher
window.alert(`Received message from ${res.sender}\n${res.plaintext}`);
// we must spend the commited_in output to sender
await services.updateMessages(res);
await services.confirm_sender_address(res);
} else if (res.status === 'Complete') {
window.alert(`Received confirmation that ${res.sender} is the author of message ${res.plaintext}`)
await services.updateMessages(res);
await services.updateOwnedOutputsForUser();
} else {
console.debug('Received an unimplemented valid message');
}
@ -88,7 +99,7 @@ class WebSocketClient {
// console.debug("Sending message:", JSON.stringify(networkMessage));
this.ws.send(JSON.stringify(networkMessage));
} else {
console.error('WebSocket is not open. ReadyState:', this.ws.readyState);
console.warn('WebSocket is not open. ReadyState:', this.ws.readyState);
this.messageQueue.push(message);
}
}