diff --git a/src/api.rs b/src/api.rs index bebdf80..24088a9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -23,7 +23,7 @@ use sdk_common::crypto::{ encrypt_with_key, AeadCore, Aes256Gcm, AnkSharedSecretHash, KeyInit, AAD, }; use sdk_common::process::{lock_processes, Process, ProcessState}; -use sdk_common::signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}; +use sdk_common::signature::{AnkHash, AnkMessageHash, AnkValidationNoHash, AnkValidationYesHash, Proof}; use sdk_common::sp_client::bitcoin::blockdata::fee_rate; use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize}; use sdk_common::sp_client::bitcoin::hashes::{sha256, sha256t, Hash}; @@ -85,6 +85,7 @@ pub struct ApiReturn { pub new_tx_to_send: Option, pub ciphers_to_send: Vec, pub commit_to_send: Option, + pub decrypted_pcds: Vec, } pub type ApiResult = Result; @@ -266,7 +267,7 @@ pub fn pair_device(commitment_tx: String, mut sp_addresses: Vec) -> ApiR ); local_device.pair( - OutPoint::from_str(&commitment_tx)?.txid, + OutPoint::from_str(&commitment_tx)?, Member::new( sp_addresses .into_iter() @@ -298,93 +299,93 @@ impl outputs_list { } } -#[wasm_bindgen] -pub fn login(previous_login_tx: String, fee_rate: u32) -> ApiResult { - // We first create a transaction that spends both pairing tx outputs - let previous_tx: Txid = deserialize(&Vec::from_hex(&previous_login_tx)?)?; +// #[wasm_bindgen] +// pub fn login(previous_login_tx: String, fee_rate: u32) -> ApiResult { +// // We first create a transaction that spends both pairing tx outputs +// let previous_tx: Txid = deserialize(&Vec::from_hex(&previous_login_tx)?)?; - let device = lock_local_device()?; - if !device.is_linked() { - return Err(ApiError::new("Device is not linked".to_owned())); - } +// let device = lock_local_device()?; +// if !device.is_linked() { +// return Err(ApiError::new("Device is not linked".to_owned())); +// } - let member = device.to_member().unwrap(); - let nb_outputs = member.get_addresses().len(); +// let member = device.to_member().unwrap(); +// let nb_outputs = member.get_addresses().len(); - let other_addresses = device.get_other_addresses(); +// let other_addresses = device.get_other_addresses(); - // We get the pairing process out of cache - let commitment_txid = device.get_process_commitment().unwrap(); - let commitment_outpoint = OutPoint::new(commitment_txid, 0); +// // We get the pairing process out of cache +// let commitment_txid = device.get_process_commitment().unwrap(); +// let commitment_outpoint = OutPoint::new(commitment_txid, 0); - let process = lock_processes()?.get(&commitment_outpoint).unwrap().clone(); - let state = process.get_latest_state().unwrap().clone(); +// let process = lock_processes()?.get(&commitment_outpoint).unwrap().clone(); +// let state = process.get_latest_state().unwrap().clone(); - let mut shared_secrets = Vec::new(); - for address in other_addresses { - let shared_secret = - process.get_shared_secret_for_address(&SilentPaymentAddress::try_from(address)?); - if let Some(shared_secret) = shared_secret { - shared_secrets.push(shared_secret); - } - } +// let mut shared_secrets = Vec::new(); +// for address in other_addresses { +// let shared_secret = +// process.get_shared_secret_for_address(&SilentPaymentAddress::try_from(address)?); +// if let Some(shared_secret) = shared_secret { +// shared_secrets.push(shared_secret); +// } +// } - let mut decrypted_pcd = Map::new(); - state - .encrypted_pcd - .decrypt_fields(&state.keys, &mut decrypted_pcd)?; +// let mut decrypted_pcd = Map::new(); +// state +// .encrypted_pcd +// .decrypt_fields(&state.keys, &mut decrypted_pcd)?; - let pairing_tx = decrypted_pcd.get("pairing_tx").unwrap().as_str().unwrap(); +// let pairing_tx = decrypted_pcd.get("pairing_tx").unwrap().as_str().unwrap(); - let wallet = device.get_wallet(); +// let wallet = device.get_wallet(); - let freezed_utxos = lock_freezed_utxos()?; +// let freezed_utxos = lock_freezed_utxos()?; - let recipients: Vec = device - .to_member() - .unwrap() - .get_addresses() - .iter() - .map(|a| Recipient { - address: a.clone(), - amount: DEFAULT_AMOUNT, - nb_outputs: 1, - }) - .collect(); +// let recipients: Vec = device +// .to_member() +// .unwrap() +// .get_addresses() +// .iter() +// .map(|a| Recipient { +// address: a.clone(), +// amount: DEFAULT_AMOUNT, +// nb_outputs: 1, +// }) +// .collect(); - let mut mandatory_inputs = Vec::new(); - for i in 0u32..nb_outputs.try_into().unwrap() { - mandatory_inputs.push(OutPoint::new(previous_tx, i)); - } +// let mut mandatory_inputs = Vec::new(); +// for i in 0u32..nb_outputs.try_into().unwrap() { +// mandatory_inputs.push(OutPoint::new(previous_tx, i)); +// } - let signed_psbt = create_transaction( - mandatory_inputs, - &freezed_utxos, - wallet, - recipients, - None, - Amount::from_sat(fee_rate.into()), - None, - )?; +// let signed_psbt = create_transaction( +// mandatory_inputs, +// &freezed_utxos, +// wallet, +// recipients, +// None, +// Amount::from_sat(fee_rate.into()), +// None, +// )?; - // We send it in a TxProposal prd - let tx_proposal = Prd::new_tx_proposal(commitment_outpoint, member, signed_psbt); +// // We send it in a TxProposal prd +// let tx_proposal = Prd::new_tx_proposal(commitment_outpoint, member, signed_psbt); - debug!("tx_proposal: {:?}", tx_proposal); - // We encrypt the prd with the shared_secret for pairing process - let prd_msg = tx_proposal.to_network_msg(wallet)?; - debug!("prd_msg: {:?}", prd_msg); - let mut ciphers = Vec::new(); - for shared_secret in shared_secrets { - let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; - ciphers.push(cipher.to_lower_hex_string()); - } - // We return the cipher - Ok(ApiReturn { - ciphers_to_send: ciphers, - ..Default::default() - }) -} +// debug!("tx_proposal: {:?}", tx_proposal); +// // We encrypt the prd with the shared_secret for pairing process +// let prd_msg = tx_proposal.to_network_msg(wallet)?; +// debug!("prd_msg: {:?}", prd_msg); +// let mut ciphers = Vec::new(); +// for shared_secret in shared_secrets { +// let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; +// ciphers.push(cipher.to_lower_hex_string()); +// } +// // We return the cipher +// Ok(ApiReturn { +// ciphers_to_send: ciphers, +// ..Default::default() +// }) +// } #[wasm_bindgen] pub fn logout() -> ApiResult<()> { @@ -608,125 +609,87 @@ pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> Api )?) } -#[wasm_bindgen] -/// Produce a proof and append it to a prd -pub fn add_validation_token_to_prd( - root_commitment: String, - prd_commitment: String, - approval: bool, -) -> ApiResult { - let prd_hash = AnkPrdHash::from_str(&prd_commitment)?; - let outpoint = OutPoint::from_str(&root_commitment)?; +// #[wasm_bindgen] +// pub fn response_prd( +// root_commitment: String, +// prd_commitment: String, // The commitment to the Prd we respond to +// approval: bool, +// ) -> ApiResult { +// let prd_hash = AnkPrdHash::from_str(&prd_commitment)?; +// let outpoint = OutPoint::from_str(&root_commitment)?; +// let local_device = lock_local_device()?; +// let member = local_device +// .to_member(); - // find the prd in the registered process - let mut processes = lock_processes()?; - let process = processes - .get_mut(&outpoint) - .ok_or(ApiError::new("Unknown process".to_owned()))?; +// // find the prd in the registered process +// let mut processes = lock_processes()?; +// let process = processes +// .get_mut(&outpoint) +// .ok_or(ApiError::new("Unknown process".to_owned()))?; - let prd_ref = process - .get_impending_requests_mut() - .into_iter() - .find(|r| r.create_commitment() == prd_hash) - .ok_or(ApiError::new( - "Failed to find the prd in registered processes".to_owned(), - ))?; +// let prd_ref = process +// .get_impending_requests_mut() +// .into_iter() +// .find(|r| r.create_commitment() == prd_hash) +// .ok_or(ApiError::new( +// "Failed to find the prd in registered processes".to_owned(), +// ))?; - let local_device = lock_local_device()?; +// match prd_ref.prd_type { +// PrdType::Update => { +// let pcd = Value::from_str(&prd_ref.payload)?; +// let pcd_hash: AnkPcdHash = AnkPcdHash::from_value(&pcd); - let wallet = local_device.get_wallet(); +// let prd_response = Prd::new_response( +// OutPoint::from_str(&root_commitment)?, +// serde_json::to_string(&member)?, +// prd_ref.validation_tokens.clone(), +// pcd_hash, +// ); - let spend_key: SecretKey = wallet.get_client().get_spend_key().try_into()?; +// let prd_msg = prd_response.to_network_msg(local_device.get_wallet())?; - match prd_ref.prd_type { - PrdType::Update => { - let new_state = Value::from_str(&prd_ref.payload)?; +// let roles = &pcd +// .get("roles") +// .ok_or(ApiError::new("No roles in pcd we respond to".to_owned()))?; +// let roles_map = roles +// .as_object() +// .ok_or(ApiError::new("roles is not an object".to_owned()))? +// .clone(); +// let shared_secrets = lock_shared_secrets()?; +// let mut ciphers = vec![]; +// for (_, role_def) in roles_map { +// let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?; +// for member in role.members { +// for sp_address in member.get_addresses() { +// if sp_address.to_string() +// == local_device +// .get_wallet() +// .get_client() +// .get_receiving_address() +// { +// continue; +// } +// if let Some(shared_secret) = shared_secrets.get_secret_for_address(sp_address.try_into()?) { +// let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; +// ciphers.push(cipher.to_lower_hex_string()); +// } else { +// continue; +// } +// } +// } +// } - let new_state_commitment = new_state.tagged_hash(); +// return Ok(ApiReturn { +// ciphers_to_send: ciphers, +// ..Default::default() +// }); +// } +// _ => unimplemented!(), +// }; +// } - let message_hash = if approval { - AnkHash::ValidationYes(AnkValidationYesHash::from_commitment(new_state_commitment)) - } else { - AnkHash::ValidationNo(AnkValidationNoHash::from_commitment(new_state_commitment)) - }; - - let proof = Proof::new(message_hash, spend_key); - - prd_ref.validation_tokens.push(proof); - - Ok(ApiReturn { - updated_process: Some((root_commitment, process.clone())), - ..Default::default() - }) - } - _ => return Err(ApiError::new("Can't validate that prd".to_owned())), - } -} - -#[wasm_bindgen] -pub fn response_prd( - root_commitment: String, - prd_commitment: String, // The commitment to the Prd we respond to - approval: bool, -) -> ApiResult { - let prd_hash = AnkPrdHash::from_str(&prd_commitment)?; - let outpoint = OutPoint::from_str(&root_commitment)?; - let local_device = lock_local_device()?; - let member = local_device - .to_member() - .ok_or(ApiError::new("Unpaired device".to_owned()))?; - - // find the prd in the registered process - let mut processes = lock_processes()?; - let process = processes - .get_mut(&outpoint) - .ok_or(ApiError::new("Unknown process".to_owned()))?; - - let prd_ref = process - .get_impending_requests_mut() - .into_iter() - .find(|r| r.create_commitment() == prd_hash) - .ok_or(ApiError::new( - "Failed to find the prd in registered processes".to_owned(), - ))?; - - match prd_ref.prd_type { - PrdType::Update => { - let pcd_hash: AnkPcdHash = AnkPcdHash::from_value(&Value::from_str(&prd_ref.payload)?); - - let prd_response = Prd::new_response( - OutPoint::from_str(&root_commitment)?, - serde_json::to_string(&member)?, - prd_ref.validation_tokens.clone(), - pcd_hash, - ); - - let prd_msg = prd_response.to_network_msg(local_device.get_wallet())?; - - let mut ciphers = vec![]; - for (sp_address, shared_secret) in process.get_all_secrets() { - if sp_address.to_string() - == local_device - .get_wallet() - .get_client() - .get_receiving_address() - { - continue; - } - let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; - ciphers.push(cipher.to_lower_hex_string()); - } - - return Ok(ApiReturn { - ciphers_to_send: ciphers, - ..Default::default() - }); - } - _ => unimplemented!(), - }; -} - -fn confirm_prd(prd: Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { +fn confirm_prd(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { match prd.prd_type { PrdType::Confirm | PrdType::Response | PrdType::List => { return Err(AnyhowError::msg("Invalid prd type")); @@ -737,27 +700,9 @@ fn confirm_prd(prd: Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult member, - None => { - // This might be because we're pairing, let's see if our address is part of sender of the initial prd - let remote_member: Member = serde_json::from_str(&prd.sender)?; - let addresses = remote_member.get_addresses(); - let this_device_address = local_device - .get_wallet() - .get_client() - .get_receiving_address(); - if let Some(_) = addresses.into_iter().find(|a| *a == this_device_address) { - remote_member - } else { - return Err(AnyhowError::msg("Must pair device first")); - } - } - }; + let member = local_device.to_member(); - let pcd_commitment = AnkPcdHash::from_str(&prd.payload)?; - - let prd_confirm = Prd::new_confirm(outpoint, member, pcd_commitment); + let prd_confirm = Prd::new_confirm(outpoint, member, prd.pcd_commitments.clone()); debug!("Sending confirm prd: {:?}", prd_confirm); @@ -766,6 +711,57 @@ fn confirm_prd(prd: Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult AnyhowResult { + let local_device = lock_local_device()?; + let local_member = local_device.to_member(); + let sp_wallet = local_device.get_wallet(); + let secret_hash = AnkMessageHash::from_message(secret.as_byte_array()); + let mut shared_secrets = lock_shared_secrets()?; + if let Some(prev_proof) = prd.validation_tokens.get(0) { + // check that the proof is valid + prev_proof.verify()?; + // Check it's signed with our key + let local_address = SilentPaymentAddress::try_from(sp_wallet.get_client().get_receiving_address())?; + if prev_proof.get_key() != local_address.get_spend_key() { + return Err(anyhow::Error::msg("Previous proof of a prd connect isn't signed by us")); + } + // Check it signs a prd connect that contains the commitment to the shared secret + let empty_prd = Prd::new_connect(local_member, secret_hash, None); + let msg = AnkMessageHash::from_message(empty_prd.to_string().as_bytes()); + if *msg.as_byte_array() != prev_proof.get_message() { + return Err(anyhow::Error::msg("Previous proof signs another message")); + } + // Now we can confirm the secret and link it to an address + let sender = serde_json::from_str::(&prd.sender)?; + let proof = prd.proof.unwrap(); + let actual_sender = sender.get_address_for_key(&proof.get_key()) + .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; + shared_secrets.confirm_secret_for_address(secret, actual_sender.try_into()?); + debug!("updated secrets"); + return Ok(ApiReturn { + secrets: shared_secrets.to_owned(), + ..Default::default() + }) + } else { + let proof = prd.proof.unwrap(); + let sender = serde_json::from_str::(&prd.sender)?; + let actual_sender = sender.get_address_for_key(&proof.get_key()) + .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; + + shared_secrets.confirm_secret_for_address(secret, actual_sender.try_into()?); + + let prd_connect = Prd::new_connect(local_member, secret_hash, prd.proof); + let msg = prd_connect.to_network_msg(sp_wallet)?; + let cipher = encrypt_with_key(secret.as_byte_array(), msg.as_bytes())?; + + return Ok(ApiReturn { + ciphers_to_send: vec![cipher.to_lower_hex_string()], + secrets: shared_secrets.to_owned(), + ..Default::default() + }) + } +} + fn handle_prd( prd: Prd, secret: AnkSharedSecretHash @@ -773,54 +769,7 @@ fn handle_prd( // Connect is a bit different here because there's no associated process // Let's handle that case separately if prd.prd_type == PrdType::Connect { - let local_device = lock_local_device()?; - let local_member = local_device.to_member(); - let sp_wallet = local_device.get_wallet(); - let secret_hash = AnkMessageHash::from_message(secret.as_byte_array()); - let mut shared_secrets = lock_shared_secrets()?; - if let Some(prev_proof) = prd.validation_tokens.get(0) { - // check that the proof is valid - prev_proof.verify()?; - // Check it's signed with our key - let local_address = SilentPaymentAddress::try_from(sp_wallet.get_client().get_receiving_address())?; - if prev_proof.get_key() != local_address.get_spend_key() { - return Err(anyhow::Error::msg("Previous proof of a prd connect isn't signed by us")); - } - // Check it signs a prd connect that contains the commitment to the shared secret - let empty_prd = Prd::new_connect(local_member, secret_hash, None); - let msg = AnkMessageHash::from_message(empty_prd.to_string().as_bytes()); - if *msg.as_byte_array() != prev_proof.get_message() { - return Err(anyhow::Error::msg("Previous proof signs another message")); - } - // Now we can confirm the secret and link it to an address - let sender = serde_json::from_str::(&prd.sender)?; - let proof = prd.proof.unwrap(); - let actual_sender = sender.get_address_for_key(&proof.get_key()) - .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; - shared_secrets.confirm_secret_for_address(secret, actual_sender.try_into()?); - debug!("updated secrets"); - return Ok(ApiReturn { - secrets: shared_secrets.to_owned(), - ..Default::default() - }) - } else { - let proof = prd.proof.unwrap(); - let sender = serde_json::from_str::(&prd.sender)?; - let actual_sender = sender.get_address_for_key(&proof.get_key()) - .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; - - shared_secrets.confirm_secret_for_address(secret, actual_sender.try_into()?); - - let prd_connect = Prd::new_connect(local_member, secret_hash, prd.proof); - let msg = prd_connect.to_network_msg(sp_wallet)?; - let cipher = encrypt_with_key(secret.as_byte_array(), msg.as_bytes())?; - - return Ok(ApiReturn { - ciphers_to_send: vec![cipher.to_lower_hex_string()], - secrets: shared_secrets.to_owned(), - ..Default::default() - }) - } + return handle_prd_connect(prd, secret); } let outpoint = OutPoint::from_str(&prd.root_commitment)?; @@ -846,8 +795,7 @@ fn handle_prd( if r.prd_type != PrdType::Update { return false; } - let hash = Value::from_str(&r.payload).unwrap().tagged_hash(); - hash.to_string() == prd.payload + r.pcd_commitments == prd.pcd_commitments }) .ok_or(anyhow::Error::msg("Original request not found"))?; let member: Member = serde_json::from_str(&prd.sender)?; @@ -909,8 +857,7 @@ fn handle_prd( if r.prd_type != PrdType::Update { return false; } - let hash = Value::from_str(&r.payload).unwrap().tagged_hash(); - hash.to_string() == prd.payload + r.pcd_commitments == prd.pcd_commitments }) .ok_or(anyhow::Error::msg("Original request not found"))?; @@ -931,28 +878,31 @@ fn handle_prd( } } -fn handle_pcd(plain: Vec, root_commitment: OutPoint) -> AnyhowResult { - let pcd = Value::from_str(&String::from_utf8(plain)?)?; - - let pcd_commitment = pcd.tagged_hash(); - +fn handle_pcd(pcd: Value) -> AnyhowResult { + // We received an encrypted pcd, so we can compute the merkle root of a tree where all the encrypted values are the leaves + // Like the sender of the update did + // We pass an empty Outpoint as salt, as there's no point salting hash of encrypted values + let encrypted_pcd_commitments = pcd.hash_fields(OutPoint::null())?; let mut processes = lock_processes()?; - let relevant_process = processes.get_mut(&root_commitment).unwrap(); + for (outpoint, process) in processes.iter_mut() { + // We check all pending requests and match the payload with the hash of this pcd + if let Some(prd) = process + .get_impending_requests_mut() + .into_iter() + .find(|r| *r.payload == pcd.to_string()) + { + // We update the process and return it + prd.payload = pcd.to_string(); + return Ok(ApiReturn { + updated_process: Some((outpoint.to_string(), process.clone())), + ..Default::default() + }); + } else { + continue; + } + } - // We match the pcd with a prd and act accordingly - let prd = relevant_process - .get_impending_requests_mut() - .into_iter() - .find(|r| *r.payload == pcd_commitment.to_string()) - .ok_or(AnyhowError::msg("Failed to retrieve the matching prd"))?; - - // We update the process and return it - prd.payload = pcd.to_string(); - - return Ok(ApiReturn { - updated_process: Some((root_commitment.to_string(), relevant_process.clone())), - ..Default::default() - }); + Err(anyhow::Error::msg("Failed to find matching prd")) } fn handle_decrypted_message( @@ -1003,86 +953,67 @@ pub fn get_available_amount() -> ApiResult { Ok(device.get_wallet().get_outputs().get_balance().to_sat()) } -#[wasm_bindgen] -/// This takes a reference to a process and creates a commit msg for the latest state -pub fn create_commit_message( - init_commitment_outpoint: String, - relay_address: String, - fee_rate: u32, -) -> ApiResult { - let outpoint = OutPoint::from_str(&init_commitment_outpoint)?; +fn get_shared_secrets_in_transaction( + psbt: &Psbt, + addresses: Vec +) -> anyhow::Result> { + let local_device = lock_local_device()?; - if let Some(process) = lock_processes()?.get(&outpoint) { - match process.get_number_of_states() { - 0 => Err(ApiError::new("Process has no states".to_owned())), - 1 => { - // This is a creation - let state = process.get_latest_state().unwrap(); - if state.commited_in.vout != u32::MAX { - return Err(ApiError::new("Latest state is already commited".to_owned())); - } - let encrypted_pcd = state.encrypted_pcd.clone(); - let keys = state.keys.clone(); + let sp_wallet = local_device.get_wallet(); - let freezed_utxos = lock_freezed_utxos()?; + let partial_secret = sp_wallet + .get_client() + .get_partial_secret_from_psbt(&psbt)?; - let local_device = lock_local_device()?; + let mut new_secrets = HashMap::new(); + for address in addresses { + let sp_address = SilentPaymentAddress::try_from(address.as_str())?; + let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( + &sp_address.get_scan_key(), + &partial_secret, + ); - let sp_wallet = local_device.get_wallet(); + let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); - let signed_psbt = create_transaction( - vec![], - &freezed_utxos, - sp_wallet, - vec![Recipient { - address: relay_address, - amount: Amount::from_sat(1000), - nb_outputs: 1, - }], - None, - Amount::from_sat(fee_rate.into()), - None, - )?; - - let tx = signed_psbt.extract_tx()?; - - Ok(ApiReturn { - commit_to_send: Some(CommitMessage::new_first_commitment( - tx, - encrypted_pcd.as_object().unwrap().clone(), - keys, - )), - ..Default::default() - }) - } - _ => { - // We're updating an existing process - // Check that initial outpoint is not a placeholder and that latest state has a commited_in of null - if outpoint.vout != u32::MAX { - return Err(ApiError::new( - "Initial outpoint is a placeholder".to_owned(), - )); - } - let state = process.get_latest_state().unwrap(); - if state.commited_in != OutPoint::null() { - return Err(ApiError::new("Latest state is already commited".to_owned())); - } - let encrypted_pcd = state.encrypted_pcd.clone(); - let keys = state.keys.clone(); - // We just send the message with the outpoint - return Ok(ApiReturn { - commit_to_send: Some(CommitMessage::new_update_commitment( - outpoint, - encrypted_pcd.as_object().unwrap().clone(), - keys, - )), - ..Default::default() - }); - } - } - } else { - return Err(ApiError::new("Process not found".to_owned())); + new_secrets.insert(sp_address, shared_secret); } + + Ok(new_secrets) +} + +fn create_transaction_for_addresses(addresses: Vec, fee_rate: u32) -> anyhow::Result { + let mut sp_addresses: Vec = Vec::with_capacity(addresses.len()); + for address in &addresses { + let sp_address = SilentPaymentAddress::try_from(address.as_str())?; + sp_addresses.push(sp_address); + } + + let local_device = lock_local_device()?; + + let sp_wallet = local_device.get_wallet(); + let freezed_utxos = lock_freezed_utxos()?; + + let mut recipients = Vec::with_capacity(addresses.len()); + for address in addresses { + let recipient = Recipient { + address: address, + amount: DEFAULT_AMOUNT, + nb_outputs: 1, + }; + recipients.push(recipient); + } + + let signed_psbt = create_transaction( + vec![], + &freezed_utxos, + sp_wallet, + recipients, + None, + Amount::from_sat(fee_rate.into()), + None, + )?; + + Ok(signed_psbt) } #[wasm_bindgen] @@ -1095,57 +1026,21 @@ pub fn create_connect_transaction(members_str: Vec, fee_rate: u32) -> Ap members.push(serde_json::from_str(&member)?) } - let local_device = lock_local_device()?; - - let sp_wallet = local_device.get_wallet(); - let freezed_utxos = lock_freezed_utxos()?; - - let recipients = members.iter() - .flat_map(|member| { - member.get_addresses() - }) - .map(|address| { - Recipient { - address: address.clone(), - amount: DEFAULT_AMOUNT, - nb_outputs: 1 - } - }) - .collect(); - - let signed_psbt = create_transaction( - vec![], - &freezed_utxos, - sp_wallet, - recipients, - None, - Amount::from_sat(fee_rate.into()), - None, - )?; - - let partial_secret = sp_wallet - .get_client() - .get_partial_secret_from_psbt(&signed_psbt)?; - - // We now generate the shared secret for each address - let mut shared_secrets = lock_shared_secrets()?; + let mut addresses = vec![]; for member in members { - let addresses = member.get_addresses(); - - for address in addresses { - let sp_address = SilentPaymentAddress::try_from(address.as_str())?; - let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( - &sp_address.get_scan_key(), - &partial_secret, - ); - - let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); - - shared_secrets.confirm_secret_for_address(shared_secret, sp_address); - } + addresses.extend(member.get_addresses().into_iter()); } - let transaction = signed_psbt.extract_tx()?; + let psbt = create_transaction_for_addresses(addresses.clone(), fee_rate)?; + + let new_secrets = get_shared_secrets_in_transaction(&psbt, addresses)?; + + let transaction = psbt.extract_tx()?; + + let mut shared_secrets = lock_shared_secrets()?; + for (address, secret) in new_secrets { + shared_secrets.confirm_secret_for_address(secret, address); + } Ok(ApiReturn { new_tx_to_send: Some(NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None)), @@ -1155,72 +1050,170 @@ pub fn create_connect_transaction(members_str: Vec, fee_rate: u32) -> Ap } #[wasm_bindgen] -/// We assume that the provided tx outpoint exist -pub fn create_update_transaction( - init_commitment: Option, - new_state: String, +pub fn create_new_process( + init_state: String, + relay_address: String, fee_rate: u32, ) -> ApiResult { - let pcd = Value::from_str(&new_state)?; - let pcd_map = pcd + let pcd = ::from_string(&init_state)?; + + // check that we have a proper roles map + let roles = pcd.extract_roles()?; + + // Step 1: we create the encryption keys for each field and encrypt them + let mut fields2keys = Map::new(); + let mut fields2cipher = Map::new(); + let fields_to_encrypt: Vec = pcd .as_object() - .ok_or(ApiError::new("new_state must be an object".to_owned()))?; + .unwrap() + .keys() + .map(|k| k.clone()) + .collect(); - let mut processes = lock_processes()?; + pcd.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); - let commitment_outpoint: OutPoint; - let relevant_process: &mut Process; - if let Some(s) = init_commitment { - // We're updating an existing contract - let outpoint = OutPoint::from_str(&s)?; + // We create a transaction that spends to the relay address + let psbt = create_transaction_for_addresses(vec![relay_address.clone()], fee_rate)?; - if let Some(p) = processes.get_mut(&outpoint) { - // compare the provided new_state with the process defined template - let previous_state = &p.get_state_at(0).unwrap().encrypted_pcd; - if !compare_maps(previous_state.as_object().unwrap(), pcd_map) { - return Err(ApiError::new( - "Provided updated state is not consistent with the process template".to_owned(), - )); - } - relevant_process = p; - commitment_outpoint = outpoint; - } else { - // This is a process we don't know about, so we insert a new entry - processes.insert(outpoint, Process::default()); - relevant_process = processes.get_mut(&outpoint).unwrap(); - commitment_outpoint = outpoint; - } - } else { - // This is a creation with an init state, the commitment will come later - // We need a placeholder to keep track of the process before it's commited on chain - // We can take the hash of the init_state as a txid, and set the vout to the max as it is very unlikely to ever have a real commitment that will look like this - let dummy = pcd.tagged_hash(); + // We take the secret out + let new_secrets = get_shared_secrets_in_transaction(&psbt, vec![relay_address])?; - let dummy_outpoint = OutPoint::new(Txid::from_slice(dummy.as_byte_array())?, u32::MAX); - - processes.insert(dummy_outpoint, Process::default()); - - relevant_process = processes.get_mut(&dummy_outpoint).unwrap(); - commitment_outpoint = dummy_outpoint; + let mut shared_secrets = lock_shared_secrets()?; + for (address, secret) in new_secrets { + shared_secrets.confirm_secret_for_address(secret, address); } - // We assume that all processes must have a roles key - let roles = pcd - .get("roles") - .ok_or(ApiError::new("No roles in new_state".to_owned()))?; - let roles_map = roles + let transaction = psbt.extract_tx()?; + + // We now have the outpoint that will serve as id for the whole process + let outpoint = OutPoint::new(transaction.txid(), 0); + + // We now need a hash that commits to the clear value of each field + the process id (or outpoint) + let fields_commitment = pcd.hash_fields(outpoint)?; + + // We now create the first process state with all that data + let process_state = ProcessState { + commited_in: outpoint, + pcd_commitment: Value::Object(fields_commitment.clone()), + encrypted_pcd: Value::Object(fields2cipher.clone()), + keys: fields2keys.clone(), + validation_tokens: vec![], + }; + + let process = Process::new(vec![process_state], vec![]); + + { + let mut processes = lock_processes()?; + // If we already have an entry with this outpoint, something's wrong + if processes.contains_key(&outpoint) { + return Err(ApiError::new("There's already a process for this outpoint".to_owned())); + } + processes.insert(outpoint.clone(), process.clone()); + } + + let commit_msg = CommitMessage::new_first_commitment(transaction, Value::Object(fields_commitment), roles); + + Ok(ApiReturn { + secrets: shared_secrets.clone(), + commit_to_send: Some(commit_msg), + updated_process: Some((outpoint.to_string(), process)), + ..Default::default() + }) +} + +#[wasm_bindgen] +pub fn update_process( + init_commitment: String, + new_state: String, +) -> ApiResult { + let outpoint = OutPoint::from_str(&init_commitment)?; + + let mut processes = lock_processes()?; + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + let last_state = process.get_latest_commited_state() + .ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?; + + let last_state_encrypted_val = &last_state.encrypted_pcd; + + let new_state_val = Value::from_str(&new_state)?; + + let mut fields2keys = Map::new(); + let mut fields2cipher = Map::new(); + let fields_to_encrypt: Vec = new_state_val .as_object() - .ok_or(ApiError::new("roles is not an object".to_owned()))? - .clone(); + .unwrap() + .keys() + .map(|k| k.clone()) + .collect(); + new_state_val.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); + + // TODO what are the actual differences? + if *last_state_encrypted_val == new_state_val { + return Err(ApiError::new("New state is identical to last state".to_owned())); + } + + let mut to_update = process.get_latest_state().unwrap().clone(); // This is an empty state with `commited_in` set as the last unspent output + + to_update.encrypted_pcd = Value::Object(fields2cipher); + to_update.keys = fields2keys; + + // Add the new state to the process + process.insert_state(to_update); + + Ok(ApiReturn { + updated_process: Some((init_commitment, process.clone())), + ..Default::default() + }) +} + +#[wasm_bindgen] +pub fn create_update_message( + init_commitment: String, + pcd_commitment: String, +) -> ApiResult { + let mut processes = lock_processes()?; + + let outpoint = OutPoint::from_str(&init_commitment)?; + + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + let latest_states = process.get_latest_concurrent_states()?; + // This is a map of keys to hash of the clear values + let new_state_commitments = ::from_string(&pcd_commitment)?; + + let update_state: &ProcessState; + if let Some(state) = latest_states.into_iter().find(|state| state.encrypted_pcd == new_state_commitments) + { + update_state = state; + } else { + return Err(ApiError::new("Can't find the state to update".to_owned())); + } + + // We must have at least the key for the roles field, otherwise we don't know who to send the message to + let clear_state = update_state.decrypt_pcd().as_object().unwrap().clone(); + + // debug!("clear_state: {:#?}", clear_state); + let roles = Value::Object(clear_state).extract_roles()?; + let mut all_members: HashMap> = HashMap::new(); - for (name, role_def) in roles_map { - let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?; + let shared_secrets = lock_shared_secrets()?; + for (name, role) in roles { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); for member in role.members { + // Check that we have a shared_secret with all members + if member.get_addresses().iter() + .any(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) + { + // for now we return an error to keep it simple + return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member))); + } if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); } @@ -1228,86 +1221,31 @@ pub fn create_update_transaction( } } - let nb_recipients = all_members.len(); - if nb_recipients == 0 { - return Err(ApiError::new( - "Can't create a process with 0 member".to_owned(), - )); - } - - let mut recipients: Vec = Vec::with_capacity(nb_recipients * 2); // We suppose that will work most of the time - // we actually have multiple "recipients" in a technical sense for each social recipient - // that's necessary because we don't want to miss a notification because we don't have a device atm - for member in all_members.keys() { - let addresses = member.get_addresses(); - for sp_address in addresses.into_iter() { - recipients.push(Recipient { - address: sp_address.into(), - amount: DEFAULT_AMOUNT, - nb_outputs: 1, - }); - } - } - - let mut fields2keys = Map::new(); - let mut fields2cipher = Map::new(); - let encrypted_pcd = pcd.clone(); - let fields_to_encrypt: Vec = encrypted_pcd - .as_object() - .unwrap() - .keys() - .map(|k| k.clone()) - .collect(); - encrypted_pcd.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); - let local_device = lock_local_device()?; let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); let sender: Member = local_device - .to_member() - .ok_or(ApiError::new("unpaired device".to_owned()))?; + .to_member(); + + // To allow the recipient to identify the pcd that contains only encrypted values, we compute the merkle tree of the encrypted pcd + // we then put the root in the payload of the prd update + let encrypted_pcd_hash = update_state.encrypted_pcd.hash_fields(OutPoint::null())?; + let encrypted_pcd_merkle_root = ::create_merkle_tree(Value::Object(encrypted_pcd_hash))?.root().unwrap(); - // We first generate the prd with all the keys that we will keep to ourselves let full_prd = Prd::new_update( - commitment_outpoint, + outpoint, serde_json::to_string(&sender)?, - fields2cipher.clone(), - fields2keys.clone(), + serialize(&encrypted_pcd_merkle_root).to_lower_hex_string(), + update_state.keys.clone(), + new_state_commitments ); - let prd_commitment = full_prd.create_commitment(); - - let freezed_utxos = lock_freezed_utxos()?; - - let signed_psbt = create_transaction( - vec![], - &freezed_utxos, - sp_wallet, - recipients, - Some(prd_commitment.as_byte_array().to_vec()), - Amount::from_sat(fee_rate.into()), - None, - )?; - - let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?; - - let partial_secret = sp_wallet - .get_client() - .get_partial_secret_from_psbt(&signed_psbt)?; - - let final_tx = signed_psbt.extract_tx()?; - let mut ciphers = vec![]; for (member, visible_fields) in all_members { let mut prd = full_prd.clone(); prd.filter_keys(visible_fields); - // we hash the payload - prd.payload = Value::from_str(&prd.payload) - .unwrap() - .tagged_hash() - .to_string(); let prd_msg = prd.to_network_msg(sp_wallet)?; let addresses = member.get_addresses(); @@ -1316,33 +1254,18 @@ pub fn create_update_transaction( if sp_address == local_address { continue; } - let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( - &::try_from(sp_address.as_str())?.get_scan_key(), - &partial_secret, - ); - let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); + // We shouldn't ever have error here since we already checked above + let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap(); let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); - relevant_process - .insert_shared_secret(SilentPaymentAddress::try_from(sp_address)?, shared_secret); } } - relevant_process.insert_impending_request(full_prd); - relevant_process.insert_state(ProcessState { - commited_in: OutPoint::null(), - encrypted_pcd: Value::Object(fields2cipher), - keys: fields2keys, - validation_tokens: vec![], - }); - - // Create the new_tx message - let new_tx_msg = NewTxMessage::new(serialize(&final_tx).to_lower_hex_string(), None); + process.insert_impending_request(full_prd); Ok(ApiReturn { - new_tx_to_send: Some(new_tx_msg), - updated_process: Some((commitment_outpoint.to_string(), relevant_process.clone())), + updated_process: Some((outpoint.to_string(), process.clone())), ciphers_to_send: ciphers, ..Default::default() }) @@ -1371,7 +1294,7 @@ pub fn create_faucet_msg() -> ApiResult { /// Get active update proposals for a given process outpoint /// Returns a vector with the latest commited state first, if any, and all active proposals #[wasm_bindgen] -pub fn get_update_proposals(process_outpoint: String) -> ApiResult> { +pub fn get_update_proposals(process_outpoint: String) -> ApiResult { let outpoint = OutPoint::from_str(&process_outpoint)?; let mut processes = lock_processes()?; @@ -1396,11 +1319,15 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult> ))); } - let mut res = vec![]; + let mut decrypted_pcds = vec![]; // We first push the last commited state, if any match relevant_process.get_latest_commited_state() { - Some(state) => res.push(serde_json::to_string(state)?), + Some(state) => { + let mut decrypted_pcd = Map::new(); + state.encrypted_pcd.decrypt_fields(&state.keys, &mut decrypted_pcd); + decrypted_pcds.push(Value::Object(decrypted_pcd)); + } None => () } @@ -1417,10 +1344,10 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult> debug!("found pcd {:#?}", pcd); let pcd_hash = AnkPcdHash::from_value(&pcd); // We look for a pending state for the exact same state as the one in the proposal - if let None = relevant_process.get_latest_concurrent_states() + if let None = relevant_process.get_latest_concurrent_states()? .into_iter() .find(|state| { - AnkPcdHash::from_value(&state.encrypted_pcd) == pcd_hash + state.pcd_commitment == proposal.pcd_commitments }) { // If not, we first add a new state @@ -1428,21 +1355,26 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult> commited_in: OutPoint::new(Txid::from_str(&pcd_hash.to_string())?, u32::MAX), encrypted_pcd: pcd.clone(), keys: proposal.keys.clone(), - validation_tokens: proposal.validation_tokens.clone() + validation_tokens: proposal.validation_tokens.clone(), + pcd_commitment: proposal.pcd_commitments.clone() }); update_states = true; } // We add the decrypted state to our return variable let mut decrypted_pcd = Map::new(); pcd.decrypt_fields(&proposal.keys, &mut decrypted_pcd)?; - res.push(serde_json::to_string(&decrypted_pcd)?); + decrypted_pcds.push(Value::Object(decrypted_pcd)); } + let mut res = ApiReturn::default(); if update_states { // We replace the process - processes.insert(outpoint, updated_process); + processes.insert(outpoint, updated_process.clone()); + res.updated_process = Some((outpoint.to_string(), updated_process)); } // else we do nothing + res.decrypted_pcds = decrypted_pcds; + Ok(res) }