From b2c070642ee1b5a98909de7c89e018f86d1d4687 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 20 Aug 2024 12:21:15 +0200 Subject: [PATCH] Implement Process --- Cargo.toml | 1 + src/device.rs | 158 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/network.rs | 79 ++++++++++++++++---- src/process.rs | 170 ++++++++++++++++++++++++++++++++++++++++++ src/silentpayments.rs | 75 +++++++++++++++---- 6 files changed, 457 insertions(+), 28 deletions(-) create mode 100644 src/device.rs create mode 100644 src/process.rs diff --git a/Cargo.toml b/Cargo.toml index 991ac1a..271c705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ serde_json = "1.0.108" # sp_client = { path = "../sp-client" } sp_client = { git = "https://github.com/Sosthene00/sp-client.git", branch = "master" } tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } +uuid = { version = "1.10.0", features = ["v4"] } wasm-bindgen = "0.2.91" diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..ed06e05 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; + +use anyhow::{Error, Result}; +use serde_json::{Map, Value}; +use sp_client::bitcoin::consensus::serialize; +use sp_client::bitcoin::hashes::Hash; +use sp_client::bitcoin::secp256k1::SecretKey; +use sp_client::bitcoin::{ + Network, OutPoint, ScriptBuf, Transaction, Txid, XOnlyPublicKey, +}; +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use wasm_bindgen::prelude::*; + +use sp_client::silentpayments::utils::SilentPaymentAddress; +use sp_client::spclient::{OutputList, SpWallet, SpendKey}; + +pub const SESSION_INDEX: u32 = 0; +pub const REVOKATION_INDEX: u32 = 1; + +#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct PairedDevice { + pub address: String, + pub outgoing_pairing_transaction: [u8; 32], + pub revokation_index: u32, + pub incoming_pairing_transaction: [u8; 32], + pub current_remote_key: [u8; 32], + pub current_session_outpoint: OutPoint, // This will be spend by remote device to notify us of next login + pub current_session_revokation_outpoint: OutPoint, // remote device can revoke current session by spending this +} + +impl PairedDevice { + pub fn new(address: SilentPaymentAddress, pairing_txid: Txid, revokation_index: u32) -> Self { + let mut pairing_transaction_buf = [0u8; 32]; + pairing_transaction_buf.copy_from_slice(&serialize(&pairing_txid)); + + Self { + address: address.into(), + outgoing_pairing_transaction: pairing_transaction_buf, + revokation_index, + incoming_pairing_transaction: [0u8; 32], + current_session_revokation_outpoint: OutPoint::default(), + current_session_outpoint: OutPoint::default(), + current_remote_key: [0u8; 32], + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Device { + sp_wallet: SpWallet, + current_session_outpoint: OutPoint, // This is the notification output of incoming login tx + current_session_key: [u8; 32], + current_session_revokation_outpoint: OutPoint, // This is the revokation outpoint of outgoing login tx + paired_device: Option, +} + +impl Device { + pub fn new(sp_wallet: SpWallet) -> Self { + Self { + sp_wallet, + current_session_outpoint: OutPoint::default(), + current_session_key: [0u8; 32], + current_session_revokation_outpoint: OutPoint::default(), + paired_device: None, + } + } + + pub fn get_wallet(&self) -> &SpWallet { + &self.sp_wallet + } + + pub fn get_mut_wallet(&mut self) -> &mut SpWallet { + &mut self.sp_wallet + } + + pub fn is_linked(&self) -> bool { + self.paired_device.is_some() + } + + pub fn is_pairing(&self) -> bool { + self.current_session_key == [0u8; 32] + } + + pub fn get_paired_device_info(&self) -> Option { + self.paired_device.clone() + } + + pub fn get_next_output_to_spend(&self) -> OutPoint { + self.current_session_outpoint + } + + pub fn get_session_revokation_outpoint(&self) -> OutPoint { + self.current_session_revokation_outpoint + } + + pub fn sign_with_current_session_key(&self) -> Result<()> { + unimplemented!(); + } + + pub fn encrypt_with_current_session_key(&self) -> Result<()> { + unimplemented!(); + } + + pub fn new_link( + &mut self, + link_with: SilentPaymentAddress, + outgoing_pairing_tx: Txid, + revokation_output: u32, + incoming_pairing_tx: Txid, + ) -> Result<()> { + // let address_looked_for: String = link_with.into(); + if let Some(paired_device) = self.paired_device.as_ref() { + return Err(Error::msg(format!( + "Found an already paired device with address {} and revokable by {}:{}", + paired_device.address, + Txid::from_byte_array(paired_device.outgoing_pairing_transaction), + paired_device.revokation_index + ))); + } else { + let mut new_device = + PairedDevice::new(link_with, outgoing_pairing_tx, revokation_output); + new_device.incoming_pairing_transaction = incoming_pairing_tx.to_byte_array(); + self.paired_device = Some(new_device); + } + + Ok(()) + } + + // We call that when we spent to the remote device and it similarly spent to us + pub fn update_session( + &mut self, + new_session_key: SecretKey, + new_session_outpoint: OutPoint, + new_revokation_outpoint: OutPoint, + new_remote_key: XOnlyPublicKey, + new_remote_session_outpoint: OutPoint, + new_remote_revokation_outpoint: OutPoint, + ) -> Result<()> { + if !self.is_linked() { + return Err(Error::msg("Can't update an unpaired device")); + } + self.paired_device + .as_mut() + .map(|d| { + d.current_remote_key = new_remote_key.serialize(); + d.current_session_outpoint = new_remote_session_outpoint; + d.current_session_revokation_outpoint = new_remote_revokation_outpoint; + }); + self.current_session_key = new_session_key.secret_bytes(); + self.current_session_outpoint = new_session_outpoint; + self.current_session_revokation_outpoint = new_revokation_outpoint; + + Ok(()) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7d693bb..e4247f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ pub use sp_client; pub mod crypto; +pub mod device; pub mod error; pub mod network; +pub mod process; pub mod silentpayments; diff --git a/src/network.rs b/src/network.rs index 9de12d4..bd962b8 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,5 +1,5 @@ -use std::{default, fmt}; use std::str::FromStr; +use std::{default, fmt}; use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; use anyhow::{Error, Result}; @@ -12,6 +12,7 @@ use sp_client::bitcoin::consensus::serialize; use sp_client::bitcoin::hashes::sha256::Hash; // use sp_client::bitcoin::hashes::Hash; use sp_client::bitcoin::hex::{DisplayHex, FromHex}; +use sp_client::bitcoin::secp256k1::schnorr::Signature; use sp_client::bitcoin::secp256k1::PublicKey; use sp_client::bitcoin::{BlockHash, OutPoint, Transaction}; use sp_client::silentpayments::utils::SilentPaymentAddress; @@ -19,6 +20,7 @@ use tsify::Tsify; use crate::crypto::{Aes256Decryption, Aes256Encryption, Purpose}; use crate::error::AnkError; +use crate::process::{Member, Process}; #[derive(Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] @@ -112,24 +114,67 @@ impl NewTxMessage { } } +#[derive(Debug, Default, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub enum PrdType { + #[default] + None, + Message, + Update, + List, + Response, + Confirm, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +pub struct ValidationToken { + member: Member, + message: Hash, // Hash of Pcd | {"yes/no/blank"} + sig: Signature, + sig_alt: Signature, // User must sign with it's 2 paired devices +} + #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct Prd { + pub prd_type: PrdType, + pub process: Process, pub sender: String, - pub key: [u8;32], + pub key: [u8; 32], + pub validation_tokens: Vec, pub pcd_commitment: Hash, // hash of the pcd pub error: Option, } impl Prd { - pub fn new(sender: SilentPaymentAddress, key: [u8; 32], pcd_commitment: Hash) -> Self { - Self { + pub fn new( + prd_type: PrdType, + process: Process, + sender: SilentPaymentAddress, + key: [u8; 32], + pcd_commitment: Hash, + ) -> Result { + let mut res = Self { + prd_type, + process, sender: sender.into(), + validation_tokens: vec![], key, pcd_commitment, error: None, + }; + + Ok(res) + } + + pub fn add_validation_token(&mut self, validation_token: ValidationToken) -> Result<()> { + match self.prd_type { + PrdType::Confirm => self.validation_tokens.push(validation_token), + _ => return Err(Error::msg("This Prd type doesn't allow validation tokens")) } + Ok(()) } pub fn encrypt_pcd(&self, pcd: &Pcd) -> Result> { @@ -225,6 +270,7 @@ pub enum CachedMessageStatus { pub struct CachedMessage { pub id: u32, pub status: CachedMessageStatus, + pub prd_type: PrdType, pub commited_in: Option, pub tied_by: Option, // index of the output that ties the proposal pub commitment: Option, // content of the op_return @@ -232,9 +278,9 @@ pub struct CachedMessage { pub recipient: Option, // Never None when message sent pub shared_secret: Option, // Never None when message sent pub prd_cipher: Option, // When we receive message we can't decrypt we only have this and commited_in_tx - pub prd: Option, // Never None when message sent - pub pcd_cipher: Option, - pub pcd: Option, // Never None when message sent + pub prd: Option, // Never None when message sent + pub pcd_cipher: Option, + pub pcd: Option, // Never None when message sent pub pcd_commitment: Option, pub confirmed_by: Option, // If this None, Sender keeps sending pub timestamp: u64, @@ -275,11 +321,10 @@ impl CachedMessage { pub fn try_decrypt_pcd(&self, cipher: Vec) -> Result> { if self.prd.is_none() { - return Err(Error::msg( - "Can't try decrypt this message, there's no prd", - )); + return Err(Error::msg("Can't try decrypt this message, there's no prd")); } - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?; + let aes_decrypt = + Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?; aes_decrypt.decrypt_with_key() } @@ -293,7 +338,8 @@ impl CachedMessage { let mut key = [0u8; 32]; key.copy_from_slice(&shared_secret); - let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap()).expect("Shouldn't keep an invalid hex as cipher"); + let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap()) + .expect("Shouldn't keep an invalid hex as cipher"); let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, key)?; aes_decrypt.decrypt_with_key() @@ -356,10 +402,15 @@ impl TrustedChannel { self.revoked_in_block != [0u8; 32] } - pub fn encrypt_msg_for(&self, msg: String) -> Result { + pub fn encrypt_msg_for(&self, msg: String) -> Result { let mut rng = thread_rng(); let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); - let aes256_encryption = Aes256Encryption::import_key(Purpose::Arbitrary, msg.into_bytes(), self.shared_secret, nonce)?; + let aes256_encryption = Aes256Encryption::import_key( + Purpose::Arbitrary, + msg.into_bytes(), + self.shared_secret, + nonce, + )?; let cipher = aes256_encryption.encrypt_with_aes_key()?; Ok(cipher.to_lower_hex_string()) } diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..3fd021f --- /dev/null +++ b/src/process.rs @@ -0,0 +1,170 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sp_client::bitcoin::hashes::Hash; +use sp_client::bitcoin::secp256k1::schnorr::Signature; +use sp_client::bitcoin::{OutPoint, Txid}; +use sp_client::silentpayments::utils::SilentPaymentAddress; +use tsify::Tsify; +use uuid::Uuid; +use wasm_bindgen::prelude::*; + +use crate::device::Device; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Tsify, PartialEq, PartialOrd)] +pub enum Role { + User, + Manager, + Admin, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +pub struct Member { + nym: String, + sp_address: String, + sp_address_alt: String, + role: Role, +} + +impl Member { + pub fn new( + nym: String, + sp_address: SilentPaymentAddress, + sp_address_alt: SilentPaymentAddress, + role: Role, + ) -> Self { + Self { + nym, + sp_address: sp_address.into(), + sp_address_alt: sp_address_alt.into(), + role, + } + } + + pub fn get_nym(&self) -> String { + self.nym.clone() + } + + pub fn get_addresses(&self) -> (String, String) { + (self.sp_address.clone(), self.sp_address_alt.clone()) + } + + pub fn get_role(&self) -> Role { + self.role + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ValidationRules { + quorum: f32, // Must be > 0.0, <= 1.0 + min_permission: Role, // Only users with at least that Role can send a token +} + +impl ValidationRules { + pub fn new(quorum: f32, min_permission: Role) -> Self { + Self { + quorum, + min_permission, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Process { + pub uuid: String, + pub name: String, + pub version: u32, + pub roles: Vec, + pub validation_rules: ValidationRules, + pub initial_commit_tx: Txid, + pub latest_commit_tx: Txid, + pub html: String, + pub style: String, + pub script: String, + pub pcd_template: Value +} + +impl Process { + pub fn new( + name: String, + roles: Vec, + validation_rules: ValidationRules, + initial_commit_tx: Txid, + html: String, + style: String, + script: String, + pcd_template: Value, + ) -> Self { + Self { + uuid: Uuid::new_v4().to_string(), + name, + version: 1, + roles, + validation_rules, + initial_commit_tx, + latest_commit_tx: initial_commit_tx, + html, + style, + script, + pcd_template + } + } + + pub fn new_pairing_process( + pcd: PairingPcd, + initial_commit_tx: Txid, + ) -> Self { + let member = Member::new( + pcd.nym.clone(), + pcd.addresses[0].clone().try_into().unwrap(), + pcd.addresses[1].clone().try_into().unwrap(), + Role::Admin, + ); + let validation_rules = ValidationRules::new(1.0, Role::Admin); + Self::new( + "pairing".to_owned(), + vec![member], + validation_rules, + initial_commit_tx, + "".to_owned(), + "".to_owned(), + "".to_owned(), + Value::from_str(&serde_json::to_string(&pcd).unwrap()).unwrap() + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PairingPcd { + nym: String, + addresses: [String; 2], + session_index: u32, + revokation_index: u32, + pairing_txs: [Txid; 2], + current_session_txs: [Txid; 2] +} + +impl PairingPcd { + pub fn new( + nym: String, + local_address: SilentPaymentAddress, + remote_address: SilentPaymentAddress, + session_index: u32, + revokation_index: u32, + incoming_pairing_tx: Txid, + outgoing_pairing_tx: Txid, + ) -> Self { + let empty_txid = Txid::from_byte_array([0u8; Txid::LEN]); + Self { + nym, + addresses: [local_address.into(), remote_address.into()], + session_index, + revokation_index, + pairing_txs: [incoming_pairing_tx, outgoing_pairing_tx], + current_session_txs: [empty_txid, empty_txid] + } + } +} diff --git a/src/silentpayments.rs b/src/silentpayments.rs index 09d5dca..78dc7b5 100644 --- a/src/silentpayments.rs +++ b/src/silentpayments.rs @@ -6,7 +6,7 @@ use anyhow::{Error, Result}; use rand::{thread_rng, Rng}; use sp_client::bitcoin::consensus::deserialize; use sp_client::bitcoin::psbt::raw; -use sp_client::bitcoin::{Psbt, Amount, OutPoint}; +use sp_client::bitcoin::{Amount, OutPoint, Psbt}; use sp_client::constants::{ self, DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, }; @@ -27,9 +27,7 @@ pub fn create_transaction( .to_spendable_list() // filter out freezed utxos .into_iter() - .filter(|(outpoint, _)| { - !freezed_utxos.contains(outpoint) - }) + .filter(|(outpoint, _)| !freezed_utxos.contains(outpoint)) .collect(); // if we have a payload, it means we are notifying, so let's add a revokation output @@ -37,13 +35,17 @@ pub fn create_transaction( recipients.push(Recipient { address: sp_wallet.get_client().get_receiving_address(), amount: DUST_THRESHOLD, - nb_outputs: 1 + nb_outputs: 1, }) } - let sum_outputs = recipients.iter().fold(Amount::from_sat(0), |acc, x| acc + x.amount); + let sum_outputs = recipients + .iter() + .fold(Amount::from_sat(0), |acc, x| acc + x.amount); - let zero_value_recipient = recipients.iter_mut().find(|r| r.amount == Amount::from_sat(0)); + let zero_value_recipient = recipients + .iter_mut() + .find(|r| r.amount == Amount::from_sat(0)); let mut inputs: HashMap = HashMap::new(); let mut total_available = Amount::from_sat(0); @@ -89,7 +91,8 @@ pub fn create_transaction( if let Some(address) = fee_payer { SpClient::set_fees(&mut new_psbt, fee_rate, address)?; } else { - let candidates: Vec> = new_psbt.outputs + let candidates: Vec> = new_psbt + .outputs .iter() .map(|o| { if let Some(value) = o.proprietary.get(&raw::ProprietaryKey { @@ -112,17 +115,17 @@ pub fn create_transaction( for candidate in candidates { if let Some(c) = candidate { if c == change_address { - SpClient::set_fees(&mut new_psbt, fee_rate, change_address.clone())?; + SpClient::set_fees(&mut new_psbt, fee_rate, change_address.clone())?; fee_set = true; break; } else if c == sender_address { - SpClient::set_fees(&mut new_psbt, fee_rate, sender_address.clone())?; + SpClient::set_fees(&mut new_psbt, fee_rate, sender_address.clone())?; fee_set = true; break; } } } - + if !fee_set { return Err(Error::msg("Must specify payer for fee")); } @@ -172,13 +175,14 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result PublicKey { let prevout = tx.input.get(0).unwrap().to_owned(); @@ -227,7 +232,49 @@ mod tests { let pcd_hash = helper_create_commitment(pcd.to_string()); let mut key = [0u8; 32]; key.copy_from_slice(&Vec::from_hex(KEY).unwrap()); - let prd = Prd::new(ALICE_ADDRESS.try_into().unwrap(), key, pcd_hash); + + let alice_member = Member::new( + "alice".to_owned(), + alice_wallet.get_client().get_receiving_address().try_into().unwrap(), + alice_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), + Role::Admin, + ); + let bob_member = Member::new( + "bob".to_owned(), + bob_wallet.get_client().get_receiving_address().try_into().unwrap(), + bob_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), + Role::User, + ); + + let validation_rules = ValidationRules::new(0.5, Role::User); + + let pcd_template = serde_json::json!({ + "int": 0, + "string": "exemple_data", + "array": [ + "element1", + "element2" + ] + }); + + let process = Process::new( + "default".to_owned(), + vec![alice_member, bob_member], + validation_rules, + Txid::from_str(INITIAL_COMMIT_TX).unwrap(), + "".to_owned(), + "".to_owned(), + "".to_owned(), + pcd_template, + ); + + let prd = Prd::new( + PrdType::Update, + process, + ALICE_ADDRESS.try_into().unwrap(), + key, + pcd_hash, + ).unwrap(); let commitment = helper_create_commitment(serde_json::to_string(&prd).unwrap()); let psbt = create_transaction(