diff --git a/Cargo.toml b/Cargo.toml index 991ac1a..3fc049f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,10 @@ crate-type = ["cdylib", "rlib"] aes-gcm = "0.10.3" anyhow = "1.0" js-sys = "0.3.69" +log = "0.4.6" rand = "0.8.5" serde = { version = "1.0.193", features = ["derive"] } -serde_json = "1.0.108" +serde_json = { version = "1.0.108", features = ["preserve_order"]} # 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" } diff --git a/src/crypto.rs b/src/crypto.rs index 6caefa5..7fecd13 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,63 +1,11 @@ -use std::collections::HashMap; - use anyhow::{Error, Result}; -use serde::{Deserialize, Serialize}; -use sp_client::{ - bitcoin::{ - hex::{DisplayHex, FromHex}, - key::constants::SECRET_KEY_SIZE, - Txid, - }, - silentpayments::{ - bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine}, - secp256k1::PublicKey, - utils::SilentPaymentAddress, - }, -}; -use tsify::Tsify; +use sp_client::silentpayments::{bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine}, secp256k1::PublicKey}; use aes_gcm::aead::{Aead, Payload}; pub use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; use rand::thread_rng; -const AAD: &[u8] = "4nk".as_bytes(); - -const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2; - -const THIRTYTWO: usize = 32; - -#[derive(Debug)] -pub struct SharedPoint([u8; 64]); - -impl SharedPoint { - pub fn as_inner(&self) -> &[u8; 64] { - &self.0 - } -} - -#[derive(Debug, Serialize, Deserialize, Tsify, Clone, Default, PartialEq)] -#[tsify(from_wasm_abi, into_wasm_abi)] -pub struct AnkSharedSecret { - secret: String, -} - -impl AnkSharedSecret { - pub fn new(shared_point: PublicKey) -> Self { - let mut shared_point_bin = [0u8; 64]; - shared_point_bin.copy_from_slice(&shared_point.serialize_uncompressed()[1..]); - let secret = AnkSharedSecretHash::from_shared_point(shared_point_bin).to_byte_array(); - Self { - secret: secret.to_lower_hex_string(), - } - } - - pub fn to_byte_array(&self) -> [u8; 32] { - let bytes = Vec::from_hex(&self.secret).unwrap(); - let mut buf = [0u8; 32]; - buf.copy_from_slice(&bytes); - buf - } -} +pub const AAD: &[u8] = "4nk".as_bytes(); sha256t_hash_newtype! { pub struct AnkSharedSecretTag = hash_str("4nk/AnkSharedSecret"); @@ -67,317 +15,39 @@ sha256t_hash_newtype! { } impl AnkSharedSecretHash { - pub fn from_shared_point(shared_point: [u8; 64]) -> Self { + pub fn from_shared_point(shared_point: PublicKey) -> Self { let mut eng = AnkSharedSecretHash::engine(); - eng.input(&shared_point); + eng.input(&shared_point.serialize_uncompressed()[1..]); AnkSharedSecretHash::from_engine(eng) } } -pub struct HalfKey([u8; HALFKEYSIZE]); +pub fn encrypt_with_key(key: &[u8; 32], plaintext: &[u8]) -> Result> { + let encryption_eng = Aes256Gcm::new(key.into()); + let nonce = Aes256Gcm::generate_nonce(&mut thread_rng()); + let payload = Payload { + msg: plaintext, + aad: AAD, + }; + let ciphertext = encryption_eng.encrypt(&nonce, payload) + .map_err(|e| anyhow::anyhow!(e))?; -impl TryFrom> for HalfKey { - type Error = anyhow::Error; - fn try_from(value: Vec) -> std::prelude::v1::Result { - if value.len() == HALFKEYSIZE { - let mut buf = [0u8; HALFKEYSIZE]; - buf.copy_from_slice(&value); - Ok(HalfKey(buf)) - } else { - Err(Error::msg("Invalid length for HalfKey")) - } - } + let mut res: Vec = Vec::with_capacity(nonce.len() + ciphertext.len()); + res.extend_from_slice(&nonce); + res.extend_from_slice(&ciphertext); + + Ok(res) } -impl HalfKey { - pub fn as_slice(&self) -> &[u8] { - &self.0 - } +pub fn decrypt_with_key(key: &[u8; 32], ciphertext: &[u8]) -> Result> { + let decryption_eng = Aes256Gcm::new(key.into()); + let nonce = &ciphertext[..12]; + let payload = Payload { + msg: &ciphertext[12..], + aad: AAD, + }; + let plaintext = decryption_eng.decrypt(nonce.into(), payload) + .map_err(|e| anyhow::anyhow!(e))?; - pub fn to_inner(&self) -> Vec { - self.0.to_vec() - } -} - -pub enum Purpose { - Login, - ThirtyTwoBytes, - Arbitrary, -} - -pub type CipherText = Vec; - -pub type EncryptedKey = Vec; - -pub struct Aes256Decryption { - pub purpose: Purpose, - cipher_text: CipherText, - aes_key: [u8; 32], - nonce: [u8; 12], -} - -impl Aes256Decryption { - pub fn new(purpose: Purpose, cipher_text: CipherText, aes_key: [u8; 32]) -> Result { - if cipher_text.len() <= 12 { - return Err(Error::msg("cipher_text is shorter than nonce length")); - } - let (message_nonce, message_cipher) = cipher_text.split_at(12); - let mut nonce = [0u8; 12]; - nonce.copy_from_slice(message_nonce); - Ok(Self { - purpose, - cipher_text: message_cipher.to_vec(), - aes_key, - nonce, - }) - } - - pub fn decrypt_with_key(&self) -> Result> { - match self.purpose { - Purpose::Login => { - let half_key = self.decrypt_login()?; - Ok(half_key.to_inner()) - } - Purpose::ThirtyTwoBytes => { - let thirty_two_buf = self.decrypt_thirty_two()?; - Ok(thirty_two_buf.to_vec()) - } - Purpose::Arbitrary => { - let arbitrary = self.decrypt_arbitrary()?; - Ok(arbitrary) - } - } - } - - fn decrypt_login(&self) -> Result { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let plain = cipher - .decrypt(&self.nonce.into(), &*self.cipher_text) - .map_err(|e| Error::msg(format!("{}", e)))?; - if plain.len() != SECRET_KEY_SIZE / 2 { - return Err(Error::msg("Plain text of invalid lenght for a login")); - } - let mut key_half = [0u8; SECRET_KEY_SIZE / 2]; - key_half.copy_from_slice(&plain); - Ok(HalfKey(key_half)) - } - - fn decrypt_thirty_two(&self) -> Result<[u8; THIRTYTWO]> { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let plain = cipher - .decrypt(&self.nonce.into(), &*self.cipher_text) - .map_err(|e| Error::msg(format!("{}", e)))?; - if plain.len() != THIRTYTWO { - return Err(Error::msg("Plain text of invalid length, should be 32")); - } - let mut thirty_two = [0u8; THIRTYTWO]; - thirty_two.copy_from_slice(&plain); - Ok(thirty_two) - } - - fn decrypt_arbitrary(&self) -> Result> { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let payload = Payload { - msg: &self.cipher_text, - aad: AAD, - }; - let plain = cipher - .decrypt(&self.nonce.into(), payload) - .map_err(|e| Error::msg(format!("{}", e)))?; - Ok(plain) - } -} - -pub struct Aes256Encryption { - pub purpose: Purpose, - plaintext: Vec, - aes_key: [u8; 32], - nonce: [u8; 12], - shared_secrets: HashMap>, -} - -impl Aes256Encryption { - pub fn new(purpose: Purpose, plaintext: Vec) -> Result { - let mut rng = thread_rng(); - let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into(); - let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); - Self::import_key(purpose, plaintext, aes_key, nonce) - } - - pub fn set_shared_secret( - &mut self, - shared_secrets: HashMap>, - ) { - self.shared_secrets = shared_secrets; - } - - pub fn encrypt_keys_with_shared_secrets( - &self, - ) -> Result> { - let mut res = HashMap::new(); - let mut rng = thread_rng(); - - for (_, sp_address2shared_secret) in self.shared_secrets.iter() { - for (sp_address, shared_secret) in sp_address2shared_secret { - let cipher = Aes256Gcm::new_from_slice(&shared_secret.to_byte_array()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let nonce = Aes256Gcm::generate_nonce(&mut rng); - let encrypted_key = cipher - .encrypt(&nonce, self.aes_key.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - - let mut ciphertext = Vec::::with_capacity(nonce.len() + encrypted_key.len()); - ciphertext.extend(nonce); - ciphertext.extend(encrypted_key); - - res.insert(sp_address.to_owned(), ciphertext); - } - } - Ok(res) - } - - pub fn import_key( - purpose: Purpose, - plaintext: Vec, - aes_key: [u8; 32], - nonce: [u8; 12], - ) -> Result { - if plaintext.len() == 0 { - return Err(Error::msg("Can't create encryption for an empty message")); - } - Ok(Self { - purpose, - plaintext, - aes_key, - nonce, - shared_secrets: HashMap::new(), - }) - } - - pub fn export_key(&self) -> [u8; 32] { - self.aes_key - } - - pub fn encrypt_with_aes_key(&self) -> Result { - match self.purpose { - Purpose::Login => self.encrypt_login(), - Purpose::ThirtyTwoBytes => self.encrypt_thirty_two(), - Purpose::Arbitrary => self.encrypt_arbitrary(), - } - } - - fn encrypt_login(&self) -> Result { - let half_key: HalfKey = self.plaintext.clone().try_into()?; - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let cipher_text = cipher - .encrypt(&self.nonce.into(), half_key.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } - - fn encrypt_thirty_two(&self) -> Result { - if self.plaintext.len() != 32 { - return Err(Error::msg("Invalid length, should be 32")); - } - let mut thirty_two = [0u8; 32]; - thirty_two.copy_from_slice(&self.plaintext); - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let cipher_text = cipher - .encrypt(&self.nonce.into(), thirty_two.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } - - fn encrypt_arbitrary(&self) -> Result { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let payload = Payload { - msg: &self.plaintext, - aad: AAD, - }; - let cipher_text = cipher - .encrypt(&self.nonce.into(), payload) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } -} - -#[cfg(test)] -mod tests { - use std::{io::Read, str::FromStr}; - - use sp_client::bitcoin::hex::FromHex; - - use super::*; - - const ALICE_SP_ADDRESS: &str = "tsp1qqw3lqr6xravz9nf8ntazgwwl0fqv47kfjdxsnxs6eutavqfwyv5q6qk97mmyf6dtkdyzqlu2zv6h9j2ggclk7vn705q5u2phglpq7yw3dg5rwpdz"; - const BOB_SP_ADDRESS: &str = "tsp1qq2hlsgrj0gz8kcfkf9flqw5llz0u2vr04telqndku9mcqm6dl4fhvq60t8r78srrf56w9yr7w9e9dusc2wjqc30up6fjwnh9mw3e3veqegdmtf08"; - const TRANSACTION: &str = "4e6d03dec558e1b6624f813bf2da7cd8d8fb1c2296684c08cf38724dcfd8d10b"; - const ALICE_SHARED_SECRET: &str = - "ccf02d364c2641ca129a3fdf49de57b705896e233f7ba6d738991993ea7e2106"; - const BOB_SHARED_SECRET: &str = - "15ef3e377fb842e81de52dbaaea8ba30aeb051a81043ee19264afd27353da521"; - - #[test] - fn new_aes_empty_plaintext() { - let plaintext = Vec::new(); - let aes_enc = Aes256Encryption::new(Purpose::Login, plaintext); - - assert!(aes_enc.is_err()); - } - - #[test] - fn aes_encrypt_login_invalid_length() { - let plaintext = "example"; - let aes_enc_short = Aes256Encryption::new(Purpose::Login, plaintext.as_bytes().to_vec()); - - assert!(aes_enc_short.is_ok()); - - let cipher = aes_enc_short.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_err()); - - let plaintext = [1u8; 64]; - let aes_enc_long = Aes256Encryption::new(Purpose::Login, plaintext.to_vec()); - - assert!(aes_enc_long.is_ok()); - - let cipher = aes_enc_long.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_err()); - } - - #[test] - fn aes_encrypt_login() { - let plaintext = [1u8; HALFKEYSIZE]; - let aes_key = Aes256Gcm::generate_key(&mut thread_rng()); - let nonce = Aes256Gcm::generate_nonce(&mut thread_rng()); - let aes_enc = Aes256Encryption::import_key( - Purpose::Login, - plaintext.to_vec(), - aes_key.into(), - nonce.into(), - ); - - assert!(aes_enc.is_ok()); - - let cipher = aes_enc.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_ok()); - - let mut plain_key = [0u8; 32]; - plain_key.copy_from_slice(&aes_key.to_vec()); - - let aes_dec = Aes256Decryption::new(Purpose::Login, cipher.unwrap(), plain_key); - - assert!(aes_dec.is_ok()); - } + Ok(plaintext) } diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..76c1b01 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use wasm_bindgen::prelude::*; + +use sp_client::{bitcoin::{hashes::Hash, OutPoint, Txid}, spclient::SpWallet}; + +use crate::pcd::Member; + +#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Device { + sp_wallet: SpWallet, + pairing_process_commitment: Option, + paired_member: Option, +} + +impl Device { + pub fn new(sp_wallet: SpWallet) -> Self { + Self { + sp_wallet, + pairing_process_commitment: None, + paired_member: 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_linking(&self) -> bool { + match self.pairing_process_commitment { + Some(ref value) => value.as_raw_hash().as_byte_array().iter().all(|&b| b == 0), + None => false, + } + } + + pub fn is_linked(&self) -> bool { + match self.pairing_process_commitment { + Some(ref value) => !value.as_raw_hash().as_byte_array().iter().all(|&b| b == 0), + None => false, + } + } + + pub fn get_process_commitment(&self) -> Option { + self.pairing_process_commitment.clone() + } + + pub fn pair(&mut self, commitment_tx: Txid, member: Member) { + self.pairing_process_commitment = Some(commitment_tx); + self.paired_member = Some(member); + } + + pub fn to_member(&self) -> Option { + self.paired_member.clone() + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7d693bb..4e56d23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,29 @@ +use std::sync::{Mutex, MutexGuard}; +use std::fmt::Debug; + pub use sp_client; +pub use log; +pub use aes_gcm; pub mod crypto; +pub mod device; pub mod error; pub mod network; +pub mod pcd; +pub mod prd; +pub mod process; pub mod silentpayments; +pub mod signature; + +pub const MAX_PRD_PAYLOAD_SIZE: usize = u16::MAX as usize; // 64KiB sounds reasonable for now + +pub trait MutexExt { + fn lock_anyhow(&self) -> Result, anyhow::Error>; +} + +impl MutexExt for Mutex { + fn lock_anyhow(&self) -> Result, anyhow::Error> { + self.lock() + .map_err(|e| anyhow::Error::msg(format!("Failed to lock: {}", e))) + } +} diff --git a/src/network.rs b/src/network.rs index 586bdb1..cc438dc 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,21 +1,17 @@ -use std::default; -use std::str::FromStr; - -use aes_gcm::{AeadCore, Aes256Gcm}; +use aes_gcm::aead::{Aead, Payload}; +use aes_gcm::{Aes256Gcm, KeyInit}; use anyhow::{Error, Result}; use js_sys::Date; use rand::{thread_rng, RngCore}; use serde::{Deserialize, Serialize}; -use sp_client::bitcoin::consensus::serialize; -use sp_client::bitcoin::hashes::Hash; +use serde_json::Value; use sp_client::bitcoin::hex::{DisplayHex, FromHex}; -use sp_client::bitcoin::secp256k1::PublicKey; -use sp_client::bitcoin::{BlockHash, OutPoint, Transaction}; -use sp_client::silentpayments::utils::SilentPaymentAddress; +use sp_client::bitcoin::OutPoint; use tsify::Tsify; -use crate::crypto::{Aes256Decryption, Aes256Encryption, Purpose}; +use crate::crypto::AAD; use crate::error::AnkError; +use crate::pcd::Member; #[derive(Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] @@ -109,36 +105,13 @@ impl NewTxMessage { } } -#[derive(Debug, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct CipherMessage { - pub sender: String, - pub message: String, - pub error: Option, -} - -impl CipherMessage { - pub fn new(sender: String, message: String) -> Self { - Self { - sender, - message, - error: None, - } - } - - pub fn to_string(&self) -> String { - serde_json::to_string(self).unwrap() - } -} - -#[derive(Debug, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct AnkNetworkMsg { +#[derive(Debug, Serialize, Deserialize)] +pub struct Envelope { pub flag: AnkFlag, pub content: String, } -impl AnkNetworkMsg { +impl Envelope { pub fn new(flag: AnkFlag, raw: &str) -> Self { Self { flag, @@ -160,48 +133,28 @@ impl AnkNetworkMsg { pub enum CachedMessageStatus { #[default] NoStatus, - FaucetWaiting, - Pairing, - Login, CipherWaitingTx, - TxWaitingCipher, - SentWaitingConfirmation, // we're sender and wait for commited_in to be spent - ReceivedMustConfirm, // we're receiver and we spend commited_in to sender - MustSpendConfirmation, // we're sender and we must spend confirmed_by - Trusted, // Can receive more messages - Closed, // No more messages will be trusted from this handshake + TxWaitingPrd, + Opened, // Can receive more messages } /// Unique struct for both 4nk messages and notification/key exchange, both rust and ts -/// 0. Faucet: commited_in with nothing else, status is NoStatus -/// 1. notification: -/// 0. sender: ciphertext, plaintext, commited_in, sender, recipient, shared_secret, key -/// 1. receiver (without tx): ciphertext -/// 2. receiver (tx without msg): commited_in, commitment, recipient, shared_secret -/// 3. receiver (receive tx after msg): plaintext, key, sender, commited_in, commitment, recipient, shared_secret -/// 4. receiver (msg after tx): ciphertext, key, plaintext, sender -/// 2. confirmation: -/// 0. receiver (spend the smallest vout that pays him in the first tx): confirmed_by -/// 1. sender (detect a transaction that pays him and spend commited_by): confirmed_by -/// 2. 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)] +#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone, PartialEq)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct CachedMessage { pub id: u32, pub status: CachedMessageStatus, - pub ciphertext: Option, // Needed when we are waiting for a key in transaction, discarded after - pub plaintext: Vec, // Append new messages received while in Trusted state - pub commited_in: Option, - pub tied_by: Option, // index of the output that ties the proposal + pub transaction: Option, pub commitment: Option, // content of the op_return - pub sender: Option, // Never None when message sent - pub recipient: Option, // Never None when message sent - pub shared_secret: Option, // Never None when message sent - pub key: Option, // Never None when message sent + pub sender: Option, // Never None when message sent + pub recipient: Option, // Never None when message sent + pub shared_secrets: Vec, // Max 2 secrets in case we send to both address of the recipient + pub cipher: Vec, // Max 2 ciphers in case we send to both address of the recipient + pub prd: Option, // Never None when message sent + pub pcd: Option, // Value is here an alias for impl Pcd pub confirmed_by: Option, // If this None, Sender keeps sending pub timestamp: u64, - pub error: Option, } impl CachedMessage { @@ -223,93 +176,54 @@ impl CachedMessage { serde_json::to_string(self).unwrap() } - pub fn try_decrypt_cipher(&self, cipher: Vec) -> Result> { - if self.shared_secret.is_none() { + pub fn try_decrypt_message(&self, cipher: Vec) -> Result> { + if self.shared_secrets.is_empty() { return Err(Error::msg( "Can't try decrypt this message, there's no shared secret", )); } - let mut shared_secret = [0u8; 32]; - shared_secret.copy_from_slice(&Vec::from_hex(self.shared_secret.as_ref().unwrap())?); - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, shared_secret)?; + for shared_secret in &self.shared_secrets { + let mut key = [0u8; 32]; + let mut nonce = [0u8; 12]; + key.copy_from_slice(&Vec::from_hex(shared_secret)?); + nonce.copy_from_slice(&cipher[..12]); - aes_decrypt.decrypt_with_key() + let engine = Aes256Gcm::new(&key.into()); + let payload = Payload { + msg: &cipher[12..], + aad: AAD, + }; + match engine.decrypt(&nonce.into(), payload) { + Ok(plain) => return Ok(plain), + Err(_) => continue + } + } + + Err(Error::msg("Failed to decrypt message")) } pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Result> { - if self.ciphertext.is_none() || self.shared_secret.is_some() { + if self.cipher.is_empty() || !self.shared_secrets.is_empty() { return Err(Error::msg( "Can't try decrypt this message, ciphertext is none or shared_secret already found", )); } - let cipher_bin = Vec::from_hex(self.ciphertext.as_ref().unwrap())?; - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher_bin, shared_secret)?; + for prd_cipher in &self.cipher { + let cipher = Vec::from_hex(prd_cipher)?; + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&cipher[..12]); - aes_decrypt.decrypt_with_key() - } -} - -#[derive(Debug, Serialize, Deserialize, Tsify, Default)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct TrustedChannel { - id: u32, // Just take the id of the cached message? - revokation_outpoint: OutPoint, // revokation output is locked by the shared_secret - shared_secret: [u8; 32], - revoked_in_block: [u8; 32], - with: String, // Silent payment address -} - -impl TrustedChannel { - pub fn new(with: String) -> Result { - // check that with is valid silent payment address - SilentPaymentAddress::try_from(with.as_str())?; - - // Generating random id - let mut new = Self::default(); - let mut buf = [0u8; 4]; - thread_rng().fill_bytes(&mut buf); - new.id = u32::from_be_bytes(buf); - new.with = with; - - Ok(new) - } - - pub fn set_revokation( - &mut self, - revokation_outpoint: String, - shared_secret: String, - ) -> Result<()> { - let mut buf = [0u8; 32]; - buf.copy_from_slice(&Vec::from_hex(&shared_secret)?); - self.revokation_outpoint = OutPoint::from_str(&revokation_outpoint)?; - self.shared_secret.copy_from_slice(&buf); - - Ok(()) - } - - pub fn revoke(&mut self, revoked_in_block: String) -> Result<()> { - let block_hash = BlockHash::from_str(&revoked_in_block)?; - let mut buf = [0u8; 32]; - buf.copy_from_slice(&serialize::(&block_hash)); - self.revoked_in_block = buf; - - Ok(()) - } - - pub fn is_set(&self) -> bool { - self.revokation_outpoint == OutPoint::default() - } - - pub fn is_revoked(&self) -> bool { - self.revoked_in_block != [0u8; 32] - } - - 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 cipher = aes256_encryption.encrypt_with_aes_key()?; - Ok(cipher.to_lower_hex_string()) + let engine = Aes256Gcm::new(&shared_secret.into()); + let payload = Payload { + msg: &cipher[12..], + aad: AAD, + }; + match engine.decrypt(&nonce.into(), payload) { + Ok(plain) => return Ok(plain), + Err(_) => continue + } + } + + Err(Error::msg("Failed to decrypt message")) } } diff --git a/src/pcd.rs b/src/pcd.rs new file mode 100644 index 0000000..1cc64fa --- /dev/null +++ b/src/pcd.rs @@ -0,0 +1,233 @@ +use std::{collections::HashSet, str::FromStr}; +use anyhow::{Result, Error}; + +use aes_gcm::{aead::{Aead, Payload}, AeadCore, Aes256Gcm, KeyInit}; +use log::debug; +use rand::thread_rng; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use sp_client::{bitcoin::{hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, Txid}, silentpayments::utils::SilentPaymentAddress}; +use tsify::Tsify; + +use crate::crypto::AAD; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Member { + sp_addresses: Vec +} + +impl Member { + pub fn new( + sp_addresses: Vec, + ) -> Result { + if sp_addresses.is_empty() { + return Err(Error::msg("empty address set")); + } + + let mut seen = HashSet::new(); + for s in sp_addresses.iter() { + if !seen.insert(s.clone()) { + return Err(Error::msg("Duplicate addresses found")); + } + } + + let res: Vec = sp_addresses.iter() + .map(|a| Into::::into(*a)) + .collect(); + + Ok(Self { + sp_addresses: res + }) + } + + pub fn get_addresses(&self) -> Vec { + self.sp_addresses.clone() + } +} + +sha256t_hash_newtype! { + pub struct AnkPcdTag = hash_str("4nk/Pcd"); + + #[hash_newtype(forward)] + pub struct AnkPcdHash(_); +} + +impl AnkPcdHash { + pub fn from_value(value: &Value) -> Self { + let mut eng = AnkPcdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPcdHash::from_engine(eng) + } + + pub fn from_map(map: &Map) -> Self { + let value = Value::Object(map.clone()); + let mut eng = AnkPcdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPcdHash::from_engine(eng) + } +} + +pub trait Pcd<'a>: Serialize + Deserialize<'a> { + fn tagged_hash(&self) -> AnkPcdHash { + AnkPcdHash::from_value(&self.to_value()) + } + + fn encrypt_fields(&self, fields2keys: &mut Map, fields2cipher: &mut Map) -> Result<()> { + let as_value = self.to_value(); + let as_map = as_value.as_object().ok_or_else(|| Error::msg("Expected object"))?; + let mut rng = thread_rng(); + + for (field, value) in as_map { + let aes_key = Aes256Gcm::generate_key(&mut rng); + let nonce = Aes256Gcm::generate_nonce(&mut rng); + fields2keys.insert(field.to_owned(), Value::String(aes_key.to_lower_hex_string())); + + let encrypt_eng = Aes256Gcm::new(&aes_key); + let value_string = value.to_string(); + let payload = Payload { + msg: value_string.as_bytes(), + aad: AAD, + }; + let cipher = encrypt_eng.encrypt(&nonce, payload) + .map_err(|e| Error::msg(format!("Encryption failed for field {}: {}", field, e)))?; + + let mut res = Vec::with_capacity(nonce.len() + cipher.len()); + res.extend_from_slice(&nonce); + res.extend_from_slice(&cipher); + + fields2cipher.insert(field.to_owned(), Value::String(res.to_lower_hex_string())); + } + + Ok(()) + } + + fn decrypt_fields(&self, fields2keys: &Map, fields2plain: &mut Map) -> Result<()> { + let value = self.to_value(); + let map = value.as_object().unwrap(); + + for (field, encrypted_value) in map.iter() { + if let Some(aes_key) = fields2keys.get(field) { + + let key_buf = Vec::from_hex(&aes_key.to_string().trim_matches('\"'))?; + + let decrypt_eng = Aes256Gcm::new(key_buf.as_slice().into()); + + let raw_cipher = Vec::from_hex(&encrypted_value.as_str().ok_or_else(|| Error::msg("Expected string"))?.trim_matches('\"'))?; + + if raw_cipher.len() < 28 { + return Err(Error::msg(format!("Invalid ciphertext length for field {}", field))); + } + + let payload = Payload { + msg: &raw_cipher[12..], + aad: AAD, + }; + + let plain = decrypt_eng.decrypt(raw_cipher[..12].into(), payload) + .map_err(|_| Error::msg(format!("Failed to decrypt field {}", field)))?; + let decrypted_value: String = String::from_utf8(plain)?; + + fields2plain.insert(field.to_owned(), Value::String(decrypted_value)); + } else { + fields2plain.insert(field.to_owned(), Value::Null); + } + } + + Ok(()) + } + + fn to_value(&self) -> Value { + Value::from_str(&serde_json::to_string(&self).unwrap()).unwrap() + } +} + +impl Pcd<'_> for Value {} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ValidationRule { + quorum: f32, // Must be >= 0.0, <= 1.0, 0.0 means reading right + pub fields: Vec, // Which fields are concerned by this rule + min_sig_member: f32, // Must be >= 0.0, <= 1.0, does each member need to sign with all it's devices? +} + +impl ValidationRule { + pub fn new(quorum: f32, fields: Vec, min_sig_member: f32) -> Result { + if quorum < 0.0 || quorum > 1.0 { + return Err(Error::msg("quorum must be 0.0 < quorum <= 1.0")); + } + + if min_sig_member < 0.0 || min_sig_member > 1.0 { + return Err(Error::msg("min_signatures_member must be 0.0 < min_signatures_member <= 1.0")); + } + + if fields.is_empty() { + return Err(Error::msg("Fields can't be empty")); + } + + let res = Self { + quorum, + fields, + min_sig_member, + }; + + Ok(res) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct RoleDefinition { + pub members: Vec, + pub validation_rules: Vec, +} + +pub fn compare_maps(map1: &Map, map2: &Map) -> bool { + // First, check if both maps have the same keys + if map1.keys().collect::>() != map2.keys().collect::>() { + return false; + } + + // Then, check if the corresponding values have the same type + for key in map1.keys() { + let value1 = map1.get(key).unwrap(); + let value2 = map2.get(key).unwrap(); + + if !compare_values(value1, value2) { + return false; + } + } + + true +} + +fn compare_values(value1: &Value, value2: &Value) -> bool { + if value1.is_null() && value2.is_null() { + return true; + } else if value1.is_boolean() && value2.is_boolean() { + return true; + } else if value1.is_number() && value2.is_number() { + return true; + } else if value1.is_string() && value2.is_string() { + return true; + } else if value1.is_array() && value2.is_array() { + return compare_arrays(value1.as_array().unwrap(), value2.as_array().unwrap()); + } else if value1.is_object() && value2.is_object() { + // Recursive comparison for nested objects + return compare_maps(value1.as_object().unwrap(), value2.as_object().unwrap()); + } else { + return false; + } +} + +fn compare_arrays(array1: &Vec, array2: &Vec) -> bool { + // Compare the type of each element in the arrays + for (elem1, elem2) in array1.iter().zip(array2.iter()) { + if !compare_values(elem1, elem2) { + return false; + } + } + + true +} diff --git a/src/prd.rs b/src/prd.rs new file mode 100644 index 0000000..9362da8 --- /dev/null +++ b/src/prd.rs @@ -0,0 +1,209 @@ +use std::collections::HashSet; +use std::str::FromStr; + +use anyhow::{Result, Error}; +use serde::{Serialize, Deserialize}; + +use serde_json::{Map, Value}; +use sp_client::bitcoin::hex::FromHex; +use sp_client::bitcoin::secp256k1::SecretKey; +use sp_client::bitcoin::{OutPoint, XOnlyPublicKey}; +use sp_client::silentpayments::utils::SilentPaymentAddress; +use sp_client::spclient::SpWallet; +use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; +use tsify::Tsify; + +use crate::pcd::{AnkPcdHash, Member, Pcd}; +use crate::signature::{AnkHash, AnkMessageHash, Proof}; + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub enum PrdType { + #[default] + None, + Message, + Update, // Update an existing process + List, // request a list of items + Response, + Confirm, + TxProposal, // Send a psbt asking for recipient signature, used for login not sure about other use cases +} + +sha256t_hash_newtype! { + pub struct AnkPrdTag = hash_str("4nk/Prd"); + + #[hash_newtype(forward)] + pub struct AnkPrdHash(_); +} + +impl AnkPrdHash { + pub fn from_value(value: &Value) -> Self { + let mut eng = AnkPrdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPrdHash::from_engine(eng) + } + + pub fn from_map(map: &Map) -> Self { + let value = Value::Object(map.clone()); + let mut eng = AnkPrdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPrdHash::from_engine(eng) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct Prd { + pub prd_type: PrdType, + pub root_commitment: String, + pub sender: String, + pub keys: Map, // key is a key in pcd, value is the key to decrypt it + pub validation_tokens: Vec, + pub payload: String, // Payload depends on the actual type + pub proof: Option, // This must be None up to the creation of the network message +} + +impl Prd { + pub fn new_update( + root_commitment: OutPoint, + sender: String, // Should take Member as argument + encrypted_pcd: Map, + keys: Map + ) -> Self { + Self { + prd_type: PrdType::Update, + root_commitment: root_commitment.to_string(), + sender, + validation_tokens: vec![], + keys, + payload: Value::Object(encrypted_pcd).to_string(), + proof: None, + } + } + + pub fn new_response( + root_commitment: OutPoint, + sender: String, + validation_token: Proof, + pcd_commitment: AnkPcdHash, + ) -> Self { + Self { + prd_type: PrdType::Response, + root_commitment: root_commitment.to_string(), + sender, + validation_tokens: vec![validation_token], + keys: Map::new(), + payload: pcd_commitment.to_string(), + proof: None, + } + } + + pub fn new_confirm( + root_commitment: OutPoint, + sender: Member, + pcd_commitment: AnkPcdHash, + ) -> Self { + Self { + prd_type: PrdType::Confirm, + root_commitment: root_commitment.to_string(), + sender: serde_json::to_string(&sender).unwrap(), + validation_tokens: vec![], + keys: Map::new(), + payload: pcd_commitment.to_string(), + proof: None, + } + } + + + fn _extract_from_message(plain: &[u8], commitment: Option<&AnkPrdHash>) -> Result { + let prd: Prd = serde_json::from_slice(plain)?; + if let Some(commitment) = commitment { + // check that the hash of the prd is consistent with what's commited in the op_return + if prd.create_commitment() != *commitment { + return Err(anyhow::Error::msg("Received prd is not what was commited in the transaction")); + } + } + // check that the proof is consistent + let sender: Member = serde_json::from_str(&prd.sender)?; + if let Some(proof) = prd.proof { + // take the spending keys in sender + let addresses = sender.get_addresses(); + let mut spend_keys: Vec = vec![]; + for address in addresses { + spend_keys.push(::try_from(address)?.get_spend_key().x_only_public_key().0); + } + // The key in proof must be one of the sender keys + let proof_key = proof.get_key(); + let mut known_key = false; + for key in spend_keys { + if key == proof_key { + known_key = true; + break; + } + } + if !known_key { + return Err(anyhow::Error::msg("Proof signed with an unknown key")); + } + proof.verify()?; + } + // check that the commitment outpoint is valid, just in case + OutPoint::from_str(&prd.root_commitment)?; + Ok(prd) + } + + pub fn extract_from_message(plain: &[u8]) -> Result { + Self::_extract_from_message(plain, None) + } + + pub fn extract_from_message_with_commitment(plain: &[u8], commitment: &AnkPrdHash) -> Result { + Self::_extract_from_message(plain, Some(commitment)) + } + + pub fn filter_keys(&mut self, to_keep: HashSet) { + let current_keys = self.keys.clone(); + let filtered_keys: Map = current_keys.into_iter() + .filter(|(field, _)| to_keep.contains(field)) + .collect(); + self.keys = filtered_keys; + } + + /// We commit to everything except the keys and the proof + /// Because 1) we need one commitment to common data for all recipients of the transaction + /// 2) we already commit to the keys in the sender proof anyway + pub fn create_commitment(&self) -> AnkPrdHash { + let mut to_commit = self.clone(); + to_commit.keys = Map::new(); + to_commit.proof = None; + + if to_commit.payload.len() != 64 && Vec::from_hex(&to_commit.payload).is_err() { + to_commit.payload = Value::from_str(&to_commit.payload).unwrap().tagged_hash().to_string(); + } + + AnkPrdHash::from_value(&to_commit.to_value()) + } + + /// Generate the signed proof and serialize to send over the network + pub fn to_network_msg(&self, sp_wallet: &SpWallet) -> Result { + let spend_sk: SecretKey = sp_wallet.get_client().get_spend_key().try_into()?; + let to_sign = self.clone(); // we sign the whole prd, incl the keys, for each recipient + + let message_hash = AnkHash::Message(AnkMessageHash::from_message(to_sign.to_string().as_bytes())); + + let proof = Proof::new(message_hash, spend_sk); + + let mut res = self.clone(); + res.proof = Some(proof); + + Ok(res.to_string()) + } + + pub fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } + + pub fn to_value(&self) -> Value { + Value::from_str(&self.to_string()).unwrap() + } +} diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..b785313 --- /dev/null +++ b/src/process.rs @@ -0,0 +1,86 @@ +use std::{collections::HashMap, sync::{Mutex, MutexGuard, OnceLock}}; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress}; + +use crate::{crypto::AnkSharedSecretHash, prd::Prd, signature::Proof, MutexExt}; + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct ProcessState { + pub commited_in: OutPoint, + pub encrypted_pcd: Value, // Some fields may be clear, if the owner of the process decides so + pub keys: Map, // We may not always have all the keys + pub validation_token: Vec, // This signs the encrypted pcd +} + +/// A process is basically a succession of states +/// If a process has nothing to do with us, shared_secrets and impending_requests will be empty +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct Process { + states: Vec, + shared_secrets: HashMap, + impending_requests: Vec, +} + +impl Process { + pub fn new(states: Vec, shared_secrets: HashMap, impending_requests: Vec) -> Self { + Self { + states, + shared_secrets: shared_secrets.into_iter().map(|(k, v)| (k.to_string(), v)).collect(), + impending_requests, + } + } + + pub fn insert_shared_secret(&mut self, address: SilentPaymentAddress, secret: AnkSharedSecretHash) { + self.shared_secrets.insert(address.to_string(), secret); + } + + pub fn get_shared_secret_for_address(&self, address: &SilentPaymentAddress) -> Option { + self.shared_secrets.get(&address.to_string()).cloned() + } + + pub fn get_all_secrets(&self) -> HashMap { + self.shared_secrets.clone().into_iter().map(|(k, v)| (SilentPaymentAddress::try_from(k.as_str()).unwrap(), v)).collect() + } + + pub fn insert_state(&mut self, state: ProcessState) { + self.states.push(state); + } + + pub fn get_status_at(&self, index: usize) -> Option<&ProcessState> { + self.states.get(index) + } + + pub fn get_status_at_mut(&mut self, index: usize) -> Option<&mut ProcessState> { + self.states.get_mut(index) + } + + pub fn get_latest_state(&self) -> Option<&ProcessState> { + self.states.last() + } + + pub fn get_latest_state_mut(&mut self) -> Option<&mut ProcessState> { + self.states.last_mut() + } + + pub fn insert_impending_request(&mut self, request: Prd) { + self.impending_requests.push(request); + } + + pub fn get_impending_requests(&self) -> Vec<&Prd> { + self.impending_requests.iter().collect() + } + + pub fn get_impending_requests_mut(&mut self) -> Vec<&mut Prd> { + self.impending_requests.iter_mut().collect() + } +} + +pub static CACHEDPROCESSES: OnceLock>> = OnceLock::new(); + +pub fn lock_processes() -> Result>, anyhow::Error> { + CACHEDPROCESSES + .get_or_init(|| Mutex::new(HashMap::new())) + .lock_anyhow() +} diff --git a/src/signature.rs b/src/signature.rs new file mode 100644 index 0000000..d58ecd6 --- /dev/null +++ b/src/signature.rs @@ -0,0 +1,111 @@ +use anyhow::Result; +use rand::{thread_rng, RngCore}; +use serde::{Serialize, Deserialize}; +use sp_client::bitcoin::key::Secp256k1; +use sp_client::bitcoin::secp256k1::schnorr::Signature; +use sp_client::bitcoin::secp256k1::{Keypair, Message, SecretKey, XOnlyPublicKey}; +use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; + +use crate::pcd::AnkPcdHash; + +sha256t_hash_newtype! { + pub struct AnkMessageTag = hash_str("4nk/Message"); + + #[hash_newtype(forward)] + pub struct AnkMessageHash(_); + + pub struct AnkValidationYesTag = hash_str("4nk/yes"); + + #[hash_newtype(forward)] + pub struct AnkValidationYesHash(_); + + pub struct AnkValidationNoTag = hash_str("4nk/no"); + + #[hash_newtype(forward)] + pub struct AnkValidationNoHash(_); +} + +impl AnkMessageHash { + pub fn from_message(message: &[u8]) -> Self { + let mut eng = AnkMessageHash::engine(); + eng.input(&message); + AnkMessageHash::from_engine(eng) + } +} + +impl AnkValidationYesHash { + pub fn from_commitment(commitment: AnkPcdHash) -> Self { + let mut eng = AnkValidationYesHash::engine(); + eng.input(&commitment.to_byte_array()); + AnkValidationYesHash::from_engine(eng) + } +} + +impl AnkValidationNoHash { + pub fn from_commitment(commitment: AnkPcdHash) -> Self { + let mut eng = AnkValidationNoHash::engine(); + eng.input(&commitment.to_byte_array()); + AnkValidationNoHash::from_engine(eng) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum AnkHash { + Message(AnkMessageHash), + ValidationYes(AnkValidationYesHash), + ValidationNo(AnkValidationNoHash), +} + +impl AnkHash { + pub fn to_byte_array(&self) -> [u8; 32] { + match self { + AnkHash::Message(hash) => hash.to_byte_array(), + AnkHash::ValidationYes(hash) => hash.to_byte_array(), + AnkHash::ValidationNo(hash) => hash.to_byte_array(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Proof { + signature: Signature, + message: AnkHash, + key: XOnlyPublicKey +} + +impl Proof { + pub fn new(message_hash: AnkHash, signing_key: SecretKey) -> Self { + let secp = Secp256k1::signing_only(); + + let keypair = Keypair::from_secret_key(&secp, &signing_key); + + let mut aux_rand = [0u8; 32]; + + thread_rng().fill_bytes(&mut aux_rand); + + let sig = secp.sign_schnorr_with_aux_rand(&Message::from_digest(message_hash.to_byte_array()), &keypair, &aux_rand); + + Self { + signature: sig, + message: message_hash, + key: keypair.x_only_public_key().0 + } + } + + pub fn get_key(&self) -> XOnlyPublicKey { + self.key + } + + pub fn verify(&self) -> Result<()> { + let secp = Secp256k1::verification_only(); + secp.verify_schnorr(&self.signature, &Message::from_digest(self.message.to_byte_array()), &self.key)?; + + Ok(()) + } + + pub fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } + +} + diff --git a/src/silentpayments.rs b/src/silentpayments.rs index b1088de..1e29c98 100644 --- a/src/silentpayments.rs +++ b/src/silentpayments.rs @@ -5,10 +5,8 @@ use anyhow::{Error, Result}; use rand::{thread_rng, Rng}; use sp_client::bitcoin::consensus::deserialize; -use sp_client::bitcoin::key::{Keypair, Secp256k1, TapTweak}; -use sp_client::bitcoin::psbt::{raw, Output}; -use sp_client::bitcoin::{Address, Psbt, ScriptBuf, Transaction, Txid}; -use sp_client::bitcoin::{Amount, OutPoint, TxOut}; +use sp_client::bitcoin::psbt::raw; +use sp_client::bitcoin::{Amount, OutPoint, Psbt}; use sp_client::constants::{ self, DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, }; @@ -29,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 @@ -39,20 +35,24 @@ 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); for outpoint in mandatory_inputs { let (must_outpoint, must_output) = available_outpoints .remove_entry(&outpoint) - .ok_or_else(|| Error::msg("Mandatory outpoint unknown"))?; + .ok_or_else(|| Error::msg(format!("Mandatory outpoint unknown: {}", outpoint)))?; total_available += must_output.amount; inputs.insert(must_outpoint, must_output); } @@ -91,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 { @@ -114,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")); } @@ -170,199 +171,247 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result PublicKey { - let prevout = tx.input.get(0).unwrap().to_owned(); - let outpoint_data = ( - prevout.previous_output.txid.to_string(), - prevout.previous_output.vout, - ); - let input_pubkey = - get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap(); - let tweak_data = - calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap(); - tweak_data - } +// fn helper_get_tweak_data(tx: &Transaction, spk: ScriptBuf) -> PublicKey { +// let prevout = tx.input.get(0).unwrap().to_owned(); +// let outpoint_data = ( +// prevout.previous_output.txid.to_string(), +// prevout.previous_output.vout, +// ); +// let input_pubkey = +// get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap(); +// let tweak_data = +// calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap(); +// tweak_data +// } - fn helper_create_commitment(payload_to_hash: String) -> String { - let mut engine = sha256::HashEngine::default(); - engine.write_all(&payload_to_hash.as_bytes()); - let hash = sha256::Hash::from_engine(engine); - hash.to_byte_array().to_lower_hex_string() - } +// fn helper_create_commitment(payload_to_hash: String) -> sha256::Hash { +// let mut engine = sha256::HashEngine::default(); +// engine.write_all(&payload_to_hash.as_bytes()).unwrap(); +// let hash = sha256::Hash::from_engine(engine); +// hash +// } - #[test] - fn it_creates_notification_transaction() { - let recipient = Recipient { - address: BOB_ADDRESS.to_owned(), - amount: Amount::from_sat(1200), - nb_outputs: 1, - }; - let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap(); - let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap(); - let message: CipherMessage = CipherMessage::new(ALICE_ADDRESS.to_owned(), "TEST".to_owned()); - let commitment = helper_create_commitment(serde_json::to_string(&message).unwrap()); +// #[test] +// fn it_creates_notification_transaction() { +// let recipient = Recipient { +// address: BOB_ADDRESS.to_owned(), +// amount: Amount::from_sat(1200), +// nb_outputs: 1, +// }; +// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap(); +// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap(); +// let pcd = Pcd::new(Value::String("TEST".to_owned())); +// let pcd_hash = helper_create_commitment(pcd.to_string()); +// let mut key = [0u8; 32]; +// key.copy_from_slice(&Vec::from_hex(KEY).unwrap()); - assert!(commitment == "d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99"); +// 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 psbt = create_transaction( - &vec![], - &HashSet::new(), - &alice_wallet, - vec![recipient], - Some(Vec::from_hex(COMMITMENT).unwrap()), - FEE_RATE, - None, - ) - .unwrap(); +// let validation_rules = ValidationRules::new(0.5, Role::User); - let final_tx = psbt.extract_tx().unwrap(); - let spk = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1"; +// let pcd_template = serde_json::json!({ +// "int": 0, +// "string": "exemple_data", +// "array": [ +// "element1", +// "element2" +// ] +// }); - let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); +// 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, +// ); - // Check that Alice and Bob are both able to find that transaction - let alice_update = alice_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(alice_update.len() > 0); - let bob_update = bob_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(bob_update.len() > 0); - println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); - println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); - } +// 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()); - #[test] - fn it_creates_confirmation_transaction() { - let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_CONFIRMATION).unwrap(); - let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_CONFIRMATION).unwrap(); +// let psbt = create_transaction( +// &vec![], +// &HashSet::new(), +// &alice_wallet, +// vec![recipient], +// Some(commitment.as_byte_array().to_vec()), +// FEE_RATE, +// None, +// ) +// .unwrap(); - // Bob must spend notification output - let (confirmation_outpoint, _) = bob_wallet - .get_outputs() - .get_outpoint( - OutPoint::from_str( - "148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0", - ) - .unwrap(), - ) - .unwrap(); +// let final_tx = psbt.extract_tx().unwrap(); +// let spk = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1"; - let recipient = Recipient { - address: ALICE_ADDRESS.to_owned(), - amount: Amount::from_sat(0), - nb_outputs: 1, - }; +// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); - let psbt = create_transaction( - &vec![&confirmation_outpoint], - &HashSet::new(), - &bob_wallet, - vec![recipient], - None, - FEE_RATE, - Some(ALICE_ADDRESS.to_owned()), - ) - .unwrap(); +// // Check that Alice and Bob are both able to find that transaction +// let alice_update = alice_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(alice_update.len() > 0); +// let bob_update = bob_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(bob_update.len() > 0); +// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); +// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); +// } - let final_tx = psbt.extract_tx().unwrap(); - // println!( - // "{}", - // serialize::(&final_tx).to_lower_hex_string() - // ); - let spk = "512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c"; +// #[test] +// fn it_creates_confirmation_transaction() { +// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_CONFIRMATION).unwrap(); +// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_CONFIRMATION).unwrap(); - let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); +// // Bob must spend notification output +// let (confirmation_outpoint, _) = bob_wallet +// .get_outputs() +// .get_outpoint( +// OutPoint::from_str( +// "148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0", +// ) +// .unwrap(), +// ) +// .unwrap(); - // Check that Alice and Bob are both able to find that transaction - let alice_update = alice_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(alice_update.len() > 0); - let bob_update = bob_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(bob_update.len() > 0); - println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); - println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); - } +// let recipient = Recipient { +// address: ALICE_ADDRESS.to_owned(), +// amount: Amount::from_sat(0), +// nb_outputs: 1, +// }; - #[test] - fn it_creates_answer_transaction() { - let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_ANSWER).unwrap(); - let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_ANSWER).unwrap(); +// let psbt = create_transaction( +// &vec![&confirmation_outpoint], +// &HashSet::new(), +// &bob_wallet, +// vec![recipient], +// None, +// FEE_RATE, +// Some(ALICE_ADDRESS.to_owned()), +// ) +// .unwrap(); - // Bob must spend notification output - let (confirmation_outpoint, _) = alice_wallet - .get_outputs() - .get_outpoint( - OutPoint::from_str( - "bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0", - ) - .unwrap(), - ) - .unwrap(); +// let final_tx = psbt.extract_tx().unwrap(); +// // println!( +// // "{}", +// // serialize::(&final_tx).to_lower_hex_string() +// // ); +// let spk = "512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c"; - let recipient = Recipient { - address: BOB_ADDRESS.to_owned(), - amount: Amount::from_sat(0), - nb_outputs: 1, - }; +// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); - let psbt = create_transaction( - &vec![&confirmation_outpoint], - &HashSet::new(), - &alice_wallet, - vec![recipient], - None, - FEE_RATE, - Some(BOB_ADDRESS.to_owned()), - ) - .unwrap(); +// // Check that Alice and Bob are both able to find that transaction +// let alice_update = alice_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(alice_update.len() > 0); +// let bob_update = bob_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(bob_update.len() > 0); +// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); +// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); +// } - let final_tx = psbt.extract_tx().unwrap(); - // println!("{}", serialize::(&final_tx).to_lower_hex_string()); - let spk = "5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa"; +// #[test] +// fn it_creates_answer_transaction() { +// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_ANSWER).unwrap(); +// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_ANSWER).unwrap(); - let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); +// // Bob must spend notification output +// let (confirmation_outpoint, _) = alice_wallet +// .get_outputs() +// .get_outpoint( +// OutPoint::from_str( +// "bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0", +// ) +// .unwrap(), +// ) +// .unwrap(); - // Check that Alice and Bob are both able to find that transaction - let alice_update = alice_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(alice_update.len() > 0); - let bob_update = bob_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(bob_update.len() > 0); - println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); - println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); - } -} +// let recipient = Recipient { +// address: BOB_ADDRESS.to_owned(), +// amount: Amount::from_sat(0), +// nb_outputs: 1, +// }; + +// let psbt = create_transaction( +// &vec![&confirmation_outpoint], +// &HashSet::new(), +// &alice_wallet, +// vec![recipient], +// None, +// FEE_RATE, +// Some(BOB_ADDRESS.to_owned()), +// ) +// .unwrap(); + +// let final_tx = psbt.extract_tx().unwrap(); +// // println!("{}", serialize::(&final_tx).to_lower_hex_string()); +// let spk = "5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa"; + +// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); + +// // Check that Alice and Bob are both able to find that transaction +// let alice_update = alice_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(alice_update.len() > 0); +// let bob_update = bob_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(bob_update.len() > 0); +// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); +// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); +// } +// }