From 7ba532cebec3d281a5f9faacd611809089dfcdfd Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 30 Oct 2024 15:09:27 +0100 Subject: [PATCH 01/43] [tests/pairing] Add Alice and Bob secrets store --- tests/pairing.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/pairing.rs b/tests/pairing.rs index 15f1dcb..73d08a2 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -10,6 +10,8 @@ use sdk_common::sp_client::bitcoin::consensus::deserialize; use sdk_common::sp_client::bitcoin::hex::FromHex; use sdk_common::sp_client::bitcoin::{OutPoint, Transaction}; use sdk_common::sp_client::spclient::OwnedOutput; +use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; +use sdk_common::secrets::SecretsStore; use serde_json::{json, Value}; use tsify::JsValueSerdeExt; @@ -26,6 +28,8 @@ fn test_pairing() { setup(); let mut alice_process_cache = HashMap::new(); let mut bob_process_cache = HashMap::new(); + let mut alice_secrets_store = SecretsStore::new(); + let mut bob_secrets_store = SecretsStore::new(); debug!("==============================================\nStarting test_pairing\n=============================================="); @@ -39,6 +43,11 @@ fn test_pairing() { // we scan the qr code or get the address by any other means let bob_address = helper_get_bob_address(); + // we add some shared_secret in both secrets_store + let shared_secret = "c3f1a64e15d2e8d50f852c20b7f0b47cbe002d9ef80bc79582d09d6f38612d45"; + alice_secrets_store.confirm_secret_for_address(shared_secret, bob_address.try_into().unwrap()); + bob_secrets_store.confirm_secret_for_address(shared_secret, alice_address.try_into().unwrap()); + // Alice creates the new member with Bob address let new_member = Member::new(vec![ alice_address.as_str().try_into().unwrap(), From 0a81ce12df772d0b010072f48589cba01c99277f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 30 Oct 2024 15:10:33 +0100 Subject: [PATCH 02/43] [tests/pairing] Replace create_update_transaction with *message --- tests/pairing.rs | 42 ++++++------------------------------------ 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/tests/pairing.rs b/tests/pairing.rs index 73d08a2..1622b51 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::str::FromStr; use sdk_client::api::{ - add_validation_token_to_prd, create_commit_message, create_device_from_sp_wallet, create_update_transaction, dump_device, dump_process_cache, get_address, get_outputs, get_update_proposals, pair_device, parse_cipher, reset_device, response_prd, restore_device, set_process_cache, setup, ApiReturn + add_validation_token_to_prd, create_commit_message, create_connect_transaction, create_device_from_sp_wallet, create_update_message, dump_device, dump_process_cache, get_address, get_outputs, get_update_proposals, pair_device, parse_cipher, reset_device, response_prd, restore_device, set_process_cache, setup, ApiReturn }; use sdk_common::log::{debug, info}; use sdk_common::pcd::{Member, RoleDefinition}; @@ -91,27 +91,14 @@ fn test_pairing() { // we can update our local device now, first with an empty txid pair_device(OutPoint::null().to_string(), vec![helper_get_bob_address()]).unwrap(); - debug!("Alice sends a transaction commiting to an update prd to Bob"); + debug!("Alice sends an update prd to Bob"); let alice_pairing_return = - create_update_transaction(None, pairing_init_state.to_string(), 1).unwrap(); + create_update_message(None, pairing_init_state.to_string()).unwrap(); let (root_outpoint, alice_init_process) = alice_pairing_return.updated_process.unwrap(); alice_process_cache.insert(root_outpoint.clone(), alice_init_process.clone()); - let pairing_tx_msg = alice_pairing_return.new_tx_to_send.unwrap(); - - // This is only for testing, the relay takes care of that in prod - let get_outputs_result = get_outputs().unwrap(); - - let alice_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - - let alice_pairing_tweak_data = - helper_get_tweak_data(&pairing_tx_msg.transaction, alice_outputs); - - // End of the test only part - - // Alice parses her own transaction - helper_parse_transaction(&pairing_tx_msg.transaction, &alice_pairing_tweak_data); + let alice_to_bob_cipher = alice_pairing_return.ciphers_to_send[0]; // this is only for testing, as we're playing both parts let alice_device = dump_device().unwrap(); @@ -121,25 +108,8 @@ fn test_pairing() { reset_device().unwrap(); create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap(); - // Bob receives Alice pairing transaction - debug!("Bob parses Alice pairing transaction"); - helper_parse_transaction(&pairing_tx_msg.transaction, &alice_pairing_tweak_data); - - debug!("Bob receives the prd"); - let mut bob_retrieved_prd: ApiReturn = ApiReturn::default(); - for cipher in alice_pairing_return.ciphers_to_send.iter() { - // debug!("Parsing cipher: {:#?}", cipher); - match parse_cipher(cipher.clone()) { - Ok(res) => bob_retrieved_prd = res, - Err(e) => { - debug!("Error parsing cipher: {:#?}", e); - continue; - } - } - } - - assert!(bob_retrieved_prd.ciphers_to_send.len() == 1); - assert!(bob_retrieved_prd.updated_process.is_some()); + debug!("Bob receives the update prd"); + let bob_parsed_return = parse_cipher(alice_to_bob_cipher).unwrap(); debug!("Bob retrieved prd: {:#?}", bob_retrieved_prd); From bfa60aa0230f153653f813910f506110fcbcd421 Mon Sep 17 00:00:00 2001 From: Omar Oughriss Date: Wed, 30 Oct 2024 15:26:08 +0100 Subject: [PATCH 03/43] Doc connect.rs --- tests/connect.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/connect.rs b/tests/connect.rs index 1e4b87a..5a85de8 100644 --- a/tests/connect.rs +++ b/tests/connect.rs @@ -24,6 +24,33 @@ use utils::*; wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] +/// Tests the connection process between two devices, Alice and Bob, by executing a series of secure +/// transactions to establish a shared secret for encrypted communication. +/// +/// # Process Summary +/// +/// ## Alice's Initialization: +/// - Alice initializes her device from an `sp_wallet` object and sets it as the local device. +/// - She retrieves her own address and obtains Bob’s address. +/// - Alice creates a new member using Bob’s address to identify him within the transaction. +/// - She generates a connection transaction (`connect_tx`) targeting Bob's device, initiating the process for secure message sharing. +/// - Alice processes her own transaction and stores the derived shared secrets in `alice_secrets_store`, +/// associating each shared secret with Bob's addresses. +/// +/// ## Bob's Initialization: +/// - Bob initializes his device from his own `sp_wallet`. +/// - He parses Alice’s connection transaction to retrieve the shared secret Alice created for him, +/// then responds by sending an encrypted message back to Alice in a follow-up transaction. +/// - Bob saves these derived shared secrets in `bob_secrets_store`. +/// +/// ## Message Exchange: +/// - **Alice’s Response**: Alice receives and decrypts the message from Bob’s response transaction. +/// - She replies to Bob by encrypting a confirmation message, updating her secrets in `alice_secrets_store`. +/// - **Bob’s Confirmation**: Bob receives Alice’s confirmation message, decrypts it, and updates his secrets in `bob_secrets_store`. +/// +/// ## Verification: +/// - Finally, the function asserts that Alice and Bob now share the same secrets, confirming successful +/// connection and mutual authentication between the devices. fn test_connect() { setup(); // let mut alice_process_cache = HashMap::new(); From e9421f2e040e22b99ba81f5556ffe6a514face5b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 30 Oct 2024 16:06:52 +0100 Subject: [PATCH 04/43] Fix connect.rs documentation --- tests/connect.rs | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/tests/connect.rs b/tests/connect.rs index 5a85de8..83df1ef 100644 --- a/tests/connect.rs +++ b/tests/connect.rs @@ -24,29 +24,44 @@ use utils::*; wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] -/// Tests the connection process between two devices, Alice and Bob, by executing a series of secure -/// transactions to establish a shared secret for encrypted communication. +/// Tests the connection process between two devices, Alice and Bob, by executing a secure +/// transaction to establish a shared secret for encrypted communication. +/// +/// The basics are that one device will initiate the process by sending a transaction that pays another device. +/// The recipient of the transaction as soon as it finds it, can extract a shared secret and send an encrypted +/// message back. Upon receiving this message, the initiator answers with a similar message similarly encrypted. +/// Upon receiving this message, the recipient can be assured that the communication is safe, and start using +/// the secret to communicate. +/// +/// The security of the shared secret rest on the soundness of the silent payment protocol for Bitcoin. +/// In its encrypted response, the initiator adds a signature that is proof that it indeed controls the +/// private key for the silent payment address it announced, so recipient knows there's no mitm or impostor. /// -/// # Process Summary +/// # Detailed Process /// -/// ## Alice's Initialization: +/// ## Alice sends a transaction that pays Bob: /// - Alice initializes her device from an `sp_wallet` object and sets it as the local device. /// - She retrieves her own address and obtains Bob’s address. -/// - Alice creates a new member using Bob’s address to identify him within the transaction. -/// - She generates a connection transaction (`connect_tx`) targeting Bob's device, initiating the process for secure message sharing. +/// - Alice creates a new member using Bob’s device address (this is mainly for testing purpose, +/// because `create_connection_transaction` would take members as argument). +/// - She generates a connection transaction (`connect_tx`) targeting Bob's device. /// - Alice processes her own transaction and stores the derived shared secrets in `alice_secrets_store`, -/// associating each shared secret with Bob's addresses. +/// associating the shared secret with Bob's addresses. /// -/// ## Bob's Initialization: +/// ## Bob parses the transaction: /// - Bob initializes his device from his own `sp_wallet`. -/// - He parses Alice’s connection transaction to retrieve the shared secret Alice created for him, -/// then responds by sending an encrypted message back to Alice in a follow-up transaction. -/// - Bob saves these derived shared secrets in `bob_secrets_store`. +/// - He parses Alice’s connection transaction to retrieve the shared secret Alice created for him. +/// - Bob saves these derived shared secrets in `bob_secrets_store` but can't index it with Alice's address yet. /// -/// ## Message Exchange: -/// - **Alice’s Response**: Alice receives and decrypts the message from Bob’s response transaction. -/// - She replies to Bob by encrypting a confirmation message, updating her secrets in `alice_secrets_store`. -/// - **Bob’s Confirmation**: Bob receives Alice’s confirmation message, decrypts it, and updates his secrets in `bob_secrets_store`. +/// ## Prd Connect exchange +/// - Bob then responds by sending a prd connect back to Alice encrypted with the shared secret. +/// This prd is very simple and basically contains the following: +/// * All Bob's devices addresses +/// * a commitment to the shared secret +/// * a proof signed with Bob's device spend key +/// - Alice receives and decrypts the message from Bob. +/// - She replies to Bob by encrypting another prd connect which is basically the same, but keeping Bob's proof and adding her own. +/// - **Bob’s Confirmation**: Bob receives Alice’s confirmation message, decrypts it, and updates his secret in `bob_secrets_store`. /// /// ## Verification: /// - Finally, the function asserts that Alice and Bob now share the same secrets, confirming successful From 30d7ddcf1abe940dc50c3beb0e5fb08cd971c594 Mon Sep 17 00:00:00 2001 From: Omar Oughriss Date: Thu, 31 Oct 2024 14:58:30 +0100 Subject: [PATCH 05/43] pairing.rs documentation --- tests/pairing.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/pairing.rs b/tests/pairing.rs index 1622b51..e45ebf8 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -23,6 +23,66 @@ use utils::*; wasm_bindgen_test_configure!(run_in_browser); +/// # Pairing Process Documentation between Alice and Bob +/// +/// This test describes the secure pairing process between two devices, Alice and Bob, +/// across several steps, utilizing `process_cache` to store and exchange the process state +/// while mutually validating their commitments. Each stage is designed to establish a strong +/// validation between the two devices via secure exchanges of `prd` messages (updates and confirmations). +/// +/// ## Step 1 - Pairing Preparation by Alice +/// 1. **Establishing a Shared Secret**: A shared secret is established via `connect.rs` to secure +/// communication between Alice and Bob. +/// 2. **Pairing Status Check**: Alice verifies that the pairing is not already active. +/// 3. **Adding Bob's Address**: Alice adds Bob’s address to her own, setting the base for creating +/// a new `Member` object. +/// 4. **Initiating Pairing**: Alice initializes pairing using the transaction’s `commitment` and +/// the created member, which contains the list of devices. +/// 5. **Updating `prd`**: Alice creates an update `prd`, stores this new state in +/// `alice_process_cache`, and sends the `prd` to Bob. +/// +/// ## Step 2 - Receiving and Confirming the `prd` by Bob +/// 1. **Receiving and Verifying**: Bob receives and decrypts the update `prd` message sent by Alice. +/// 2. **Updating Process State**: Bob updates his process state using the `prd` data and stores +/// this new state in `bob_process_cache`. +/// 3. **Creating and Sending `Prd Confirm`**: Bob creates a confirmation `prd`, which he then +/// sends to Alice to proceed with the pairing. +/// +/// ## Step 3 - Confirmation of `Prd Confirm` by Alice +/// 1. **Receiving and Verifying**: Alice receives the `Prd Confirm` sent by Bob. +/// 2. **Creating Commitment**: Alice uses the received `prd` to generate a commitment based on the +/// recorded process state. +/// 3. **Proof Generation**: Alice creates a proof using her private spend key and response to the +/// commitment, which is then added to the `prd`. +/// 4. **Updating `process_cache`**: Alice stores the new state in `alice_process_cache`. +/// 5. **Sending `Prd Response`**: Alice creates and sends a `Prd Response` to Bob, including +/// the commitment of the `pcd` which will validate the pairing. +/// +/// ## Step 4 - Finalizing Pairing by Bob +/// 1. **Receiving and Verifying `Prd Response` and `pcd`**: Bob receives Alice’s `Prd Response` +/// and updates `bob_process_cache` with the new state. +/// 2. **Validating Pairing State**: Bob retrieves the latest process state and the state change +/// request, in this case, the pairing. +/// - He validates the current process state. +/// - He decrypts the `pcd` to retrieve the request. +/// 3. **Verifying Roles and Addresses**: Bob extracts the roles associated with the state change, +/// retrieves the addresses of involved members, and displays them to the user to confirm pairing. +/// 4. **Adding Final Proof**: Upon user confirmation, Bob generates and adds his proof to the `prd`, +/// then records the final state in `bob_process_cache`. +/// +/// ## Creation of Pairing Transaction +/// 1. **Creating the `commit_msg`**: A validation message (`commit_msg`) containing proofs from +/// both Alice and Bob is generated. +/// 2. **Creating the Transaction**: A transaction containing the final commitment is created and +/// shared between Alice and Bob. +/// 3. **Device Pairing**: Using the transaction `txid` and the list of member addresses, the +/// pairing is officially initiated, validated by a new `Member` object grouping Alice and Bob’s +/// addresses along with the transaction. +/// +/// ## Final Outcome +/// The pairing is now active between Alice and Bob, ensuring the mutual validation of their +/// respective identities and shared commitment to the validated `prd`. + #[wasm_bindgen_test] fn test_pairing() { setup(); From 124718a2070ff69c300587f464cfac25cf4be933 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 31 Oct 2024 15:54:24 +0100 Subject: [PATCH 06/43] Update pairing.rs documentation --- tests/pairing.rs | 100 +++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/tests/pairing.rs b/tests/pairing.rs index e45ebf8..3eab488 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -25,63 +25,71 @@ wasm_bindgen_test_configure!(run_in_browser); /// # Pairing Process Documentation between Alice and Bob /// -/// This test describes the secure pairing process between two devices, Alice and Bob, -/// across several steps, utilizing `process_cache` to store and exchange the process state -/// while mutually validating their commitments. Each stage is designed to establish a strong -/// validation between the two devices via secure exchanges of `prd` messages (updates and confirmations). -/// +/// This test describes the secure pairing process between two devices, Alice and Bob. +/// +/// ## What's pairing? +/// Pairing is a process, and abide by the same rules than any other process. The goal of pairing +/// is to define an identity on the network as a set of devices (defined by their sp_address). +/// Being a process it is public and can be audited by anyone, and be used as one's proof of identity. +/// It also contains a session keypair that is updated as necessary. Since all devices are needed to +/// update the key in the process it can then be used to sign a proof that someone was indeed in control +/// of all the devices for some amount of time in a MFA setup. +/// It contains the following mandatory fields: +/// * `roles`: multiple devices represented as sp adresses linked together in the same member. It is recommended +/// to have one `owner` role with one member which is the actual identity and whose signatures are all +/// needed to modify anything in the process. +/// * `session_privkey`: a private key visible by all devices of the member defined in the process, but +/// not by other members. It *must* be changed at every update of the process. This key will be used +/// to sign documents and validate actions for other processes. It's valid as soon as the commitment +/// transaction for the process udpate is seen and it stays valid for _n_ blocks after the update being mined. +/// * `session_pubkey`: the x-only public key derived from the session private key. It's visible by everyone and +/// used for validation by any third party. Obviously it changes with the private key at any update. +/// * `parity`: the parity of the session_pubkey. We could use 33 bytes compressed public key format +/// but using 32 bytes publick key + parity allows for more standard serialization. +/// +/// ## Detailed protocol +/// (Here Alice and Bob are used as a convention, but keep in mind they're not 2 different users, but +/// 2 devices belonging to the same user) +/// ## Step 0 - Preliminary step +/// 1. **Establishing a Shared Secret**: A shared secret is established to secure +/// communication between Alice and Bob (see `connect.rs`). /// ## Step 1 - Pairing Preparation by Alice -/// 1. **Establishing a Shared Secret**: A shared secret is established via `connect.rs` to secure -/// communication between Alice and Bob. -/// 2. **Pairing Status Check**: Alice verifies that the pairing is not already active. -/// 3. **Adding Bob's Address**: Alice adds Bob’s address to her own, setting the base for creating +/// 1. **Pairing Status Check**: Alice verifies that it's not already paired. +/// 2. **Adding Bob's Address**: Alice adds Bob’s address to her own, setting the base for creating /// a new `Member` object. -/// 4. **Initiating Pairing**: Alice initializes pairing using the transaction’s `commitment` and -/// the created member, which contains the list of devices. -/// 5. **Updating `prd`**: Alice creates an update `prd`, stores this new state in -/// `alice_process_cache`, and sends the `prd` to Bob. +/// 3. **Creation of the pairing process**: Alice initializes pairing by creating a prd update that contains +/// both its address and Bob's, and send it to Bob. /// /// ## Step 2 - Receiving and Confirming the `prd` by Bob /// 1. **Receiving and Verifying**: Bob receives and decrypts the update `prd` message sent by Alice. -/// 2. **Updating Process State**: Bob updates his process state using the `prd` data and stores -/// this new state in `bob_process_cache`. +/// 2. **Updating Process State**: Bob identifies the new process and store it, but it doesn't have access +/// to the actual data for now. /// 3. **Creating and Sending `Prd Confirm`**: Bob creates a confirmation `prd`, which he then -/// sends to Alice to proceed with the pairing. +/// sends to Alice to get the pcd containing the state for this new process. /// -/// ## Step 3 - Confirmation of `Prd Confirm` by Alice +/// ## Step 3 - Alice gets confirmation and answers with a pcd /// 1. **Receiving and Verifying**: Alice receives the `Prd Confirm` sent by Bob. -/// 2. **Creating Commitment**: Alice uses the received `prd` to generate a commitment based on the -/// recorded process state. -/// 3. **Proof Generation**: Alice creates a proof using her private spend key and response to the -/// commitment, which is then added to the `prd`. -/// 4. **Updating `process_cache`**: Alice stores the new state in `alice_process_cache`. -/// 5. **Sending `Prd Response`**: Alice creates and sends a `Prd Response` to Bob, including -/// the commitment of the `pcd` which will validate the pairing. -/// +/// 2. **Sending PCD**: Alice having confirmation that Bob got the update proposal, +/// it now sends the actual data in a pcd. +/// 3. **User confirmation**: At this step we must get the approval of the user. If user confirms +/// the pairing we create a prd response with a valid signature from Alice spend key and send +/// it to Bob. +/// /// ## Step 4 - Finalizing Pairing by Bob -/// 1. **Receiving and Verifying `Prd Response` and `pcd`**: Bob receives Alice’s `Prd Response` -/// and updates `bob_process_cache` with the new state. +/// 1. **Receiving and Verifying `pcd`**: Bob received the `pcd` and only now can tell what's the +/// process was about. /// 2. **Validating Pairing State**: Bob retrieves the latest process state and the state change -/// request, in this case, the pairing. -/// - He validates the current process state. -/// - He decrypts the `pcd` to retrieve the request. -/// 3. **Verifying Roles and Addresses**: Bob extracts the roles associated with the state change, -/// retrieves the addresses of involved members, and displays them to the user to confirm pairing. -/// 4. **Adding Final Proof**: Upon user confirmation, Bob generates and adds his proof to the `prd`, -/// then records the final state in `bob_process_cache`. +/// request, in this case, the pairing. User is prompted for validation, and if confirmed a prd response +/// is created and sent(see the **User confirmation** step for Alice). /// -/// ## Creation of Pairing Transaction -/// 1. **Creating the `commit_msg`**: A validation message (`commit_msg`) containing proofs from -/// both Alice and Bob is generated. -/// 2. **Creating the Transaction**: A transaction containing the final commitment is created and -/// shared between Alice and Bob. -/// 3. **Device Pairing**: Using the transaction `txid` and the list of member addresses, the -/// pairing is officially initiated, validated by a new `Member` object grouping Alice and Bob’s -/// addresses along with the transaction. -/// -/// ## Final Outcome -/// The pairing is now active between Alice and Bob, ensuring the mutual validation of their -/// respective identities and shared commitment to the validated `prd`. +/// ## Commiting the process state +/// 1. **Creating the `commit_msg`**: The first device that got both validations creates the commit_msg that +/// contains a transaction paying a relay to generate the first outpoint to commit the state of the process, +/// the hash of the encrypted state of the process (relay must have access to roles though, either it is clear +/// all along or it was provided with the encryption keys) and the proofs that all devices validated this state. +/// 2. **Actual commitment**: As soon as the relay validated the proofs it spends the outpoint and puts the hash of +/// the whole prd response (including pcd hash and all the proofs) in an OP_RETURN output. The process is now +/// public and can be used to prove identity for other processes. #[wasm_bindgen_test] fn test_pairing() { From 494270b636ae43897d93ef1e8fd4f39c6c84bcab Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 8 Nov 2024 13:00:16 +0100 Subject: [PATCH 07/43] Clean up connect.rs test --- tests/connect.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/connect.rs b/tests/connect.rs index 83df1ef..1173b90 100644 --- a/tests/connect.rs +++ b/tests/connect.rs @@ -1,18 +1,14 @@ use std::collections::HashMap; -use std::str::FromStr; use sdk_client::api::{ - add_validation_token_to_prd, create_commit_message, create_connect_transaction, create_device_from_sp_wallet, create_update_message, dump_device, dump_process_cache, get_address, get_outputs, get_update_proposals, pair_device, parse_cipher, reset_device, reset_shared_secrets, response_prd, restore_device, set_process_cache, set_shared_secrets, setup, ApiReturn + create_connect_transaction, create_device_from_sp_wallet, dump_device, get_address, get_outputs, parse_cipher, reset_device, reset_shared_secrets, restore_device, set_shared_secrets, setup, ApiReturn }; -use sdk_common::log::{debug, info}; -use sdk_common::pcd::{Member, RoleDefinition}; +use sdk_common::log::debug; +use sdk_common::pcd::Member; use sdk_common::secrets::SecretsStore; -use sdk_common::sp_client::bitcoin::consensus::deserialize; -use sdk_common::sp_client::bitcoin::hex::FromHex; -use sdk_common::sp_client::bitcoin::{OutPoint, Transaction}; +use sdk_common::sp_client::bitcoin::OutPoint; use sdk_common::sp_client::spclient::OwnedOutput; use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; -use serde_json::{json, Value}; use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; @@ -68,8 +64,6 @@ wasm_bindgen_test_configure!(run_in_browser); /// connection and mutual authentication between the devices. fn test_connect() { setup(); - // let mut alice_process_cache = HashMap::new(); - // let mut bob_process_cache = HashMap::new(); let mut alice_secrets_store = SecretsStore::new(); let mut bob_secrets_store = SecretsStore::new(); From 2463b8f05b32ab0587d081f2125823ec71d43caa Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 8 Nov 2024 12:59:52 +0100 Subject: [PATCH 08/43] Update api --- src/api.rs | 1012 ++++++++++++++++++++++++---------------------------- 1 file changed, 472 insertions(+), 540 deletions(-) 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) } From 93ae65ece7e2b750536f2ccf7ccf8157158f6284 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 8 Nov 2024 13:00:33 +0100 Subject: [PATCH 09/43] Update pairing test --- src/api.rs | 296 ++++++++++++++++++++++++++++++++--------------- tests/pairing.rs | 177 ++++++++++++---------------- 2 files changed, 274 insertions(+), 199 deletions(-) diff --git a/src/api.rs b/src/api.rs index 24088a9..492071e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -85,7 +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 decrypted_pcds: HashMap, } pub type ApiResult = Result; @@ -779,7 +779,11 @@ fn handle_prd( std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), std::collections::hash_map::Entry::Vacant(entry) => { debug!("Creating new process for outpoint: {}", outpoint); - entry.insert(Process::new(vec![], vec![])) + let empty_state = ProcessState { + commited_in: outpoint, + ..Default::default() + }; + entry.insert(Process::new(vec![empty_state], vec![])) } }; @@ -788,23 +792,22 @@ fn handle_prd( // It must match a prd we sent previously // We send the whole data in a pcd debug!("Received confirm prd {:#?}", prd); - let original_request = relevant_process - .get_impending_requests() + let relevant_state = relevant_process + .get_latest_concurrent_states()? .into_iter() - .find(|r| { - if r.prd_type != PrdType::Update { - return false; - } - r.pcd_commitments == prd.pcd_commitments + .find(|state| { + state.pcd_commitment == prd.pcd_commitments }) .ok_or(anyhow::Error::msg("Original request not found"))?; + let member: Member = serde_json::from_str(&prd.sender)?; // We send the data to all addresses of the member we know a secret for let mut ciphers = vec![]; for address in member.get_addresses() { if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) { - let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd.payload.as_bytes()); + let cipher = encrypt_with_key(shared_secret.as_byte_array(), serde_json::to_string(&relevant_state.encrypted_pcd)?.as_bytes())?; + ciphers.push(cipher.to_lower_hex_string()); } else { // For now we don't fail if we're missing an address for a member but maybe we should warn!("Failed to find secret for address {}", address); @@ -831,6 +834,7 @@ fn handle_prd( for address in member.get_addresses() { if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) { let cipher = confirm_prd(&prd, &shared_secret)?; + ciphers.push(cipher); } else { // For now we don't fail if we're missing an address for a member but maybe we should warn!("Failed to find secret for address {}", address); @@ -880,26 +884,33 @@ fn handle_prd( 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 encrypted_pcd_root = ::create_merkle_tree(&Value::Object(encrypted_pcd_commitments))?.root().unwrap().to_lower_hex_string(); let mut processes = lock_processes()?; + let updated_prd: Prd; 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()) + .find(|r| *r.payload == encrypted_pcd_root) { // 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() - }); + updated_prd = prd.clone(); + // We can now safely mark the prd to be remove from the process + prd.prd_type = PrdType::None; } else { continue; } + debug!("Updating process states with {:#?}", updated_prd); + process.insert_state(&updated_prd)?; + process.prune_impending_requests(); + return Ok(ApiReturn { + updated_process: Some((outpoint.to_string(), process.clone())), + ..Default::default() + }); } Err(anyhow::Error::msg("Failed to find matching prd")) @@ -1135,10 +1146,22 @@ pub fn update_process( 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 last_state_commitments = &last_state.pcd_commitment; let new_state_val = Value::from_str(&new_state)?; + // We hash all the new values + let pcd_commitment = new_state_val.hash_fields(outpoint)?; + let new_state_merkle_root = ::create_merkle_tree(&Value::Object(pcd_commitment))?.root().unwrap(); + + // We compare the new state with the previous one + let last_state_merkle_root = ::create_merkle_tree(last_state_commitments)?.root().unwrap(); + + if last_state_merkle_root == new_state_merkle_root { + return Err(ApiError::new("new proposed state is identical to the previous commited state".to_owned())); + } + + // We create the encrypted pcd let mut fields2keys = Map::new(); let mut fields2cipher = Map::new(); let fields_to_encrypt: Vec = new_state_val @@ -1149,18 +1172,25 @@ pub fn update_process( .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())); - } + // We create an encrypted values merkle root + let new_state_encrypted_commitments = Value::Object(fields2cipher).hash_fields(OutPoint::null())?; + let new_state_encrypted_root = ::create_merkle_tree(&Value::Object(new_state_encrypted_commitments.clone()))?.root().unwrap(); - let mut to_update = process.get_latest_state().unwrap().clone(); // This is an empty state with `commited_in` set as the last unspent output + let to_update = process.get_latest_state().unwrap(); // 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; + let device = lock_local_device()?; + let sender = device.to_member(); + + let update_prd = Prd::new_update( + outpoint, + serde_json::to_string(&sender)?, + new_state_encrypted_root.to_lower_hex_string(), + fields2keys, + Value::Object(new_state_encrypted_commitments), + ); // Add the new state to the process - process.insert_state(to_update); + process.insert_state(&update_prd); Ok(ApiReturn { updated_process: Some((init_commitment, process.clone())), @@ -1185,7 +1215,7 @@ pub fn create_update_message( 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) + if let Some(state) = latest_states.into_iter().find(|state| state.pcd_commitment == new_state_commitments) { update_state = state; } else { @@ -1195,9 +1225,13 @@ pub fn create_update_message( // 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 local_device = lock_local_device()?; + + let sp_wallet = local_device.get_wallet(); + let local_address = sp_wallet.get_client().get_receiving_address(); + let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; for (name, role) in roles { @@ -1207,12 +1241,16 @@ pub fn create_update_message( .flat_map(|rule| rule.fields.clone()) .collect(); for member in role.members { + debug!("member: {:?}", member); // 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()) + if let Some(no_secret_address) = member.get_addresses().iter() + .find(|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))); + // We ignore it if we don't have a secret with ourselves + if *no_secret_address != local_address { + // 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()); @@ -1221,18 +1259,13 @@ pub fn create_update_message( } } - 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(); // 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(); + let encrypted_pcd_merkle_root = ::create_merkle_tree(&Value::Object(encrypted_pcd_hash))?.root().unwrap(); let full_prd = Prd::new_update( outpoint, @@ -1262,6 +1295,11 @@ pub fn create_update_message( ciphers.push(cipher.to_lower_hex_string()); } } + + if ciphers.is_empty() { + return Err(ApiError::new("Empty ciphers list".to_owned())); + } + process.insert_impending_request(full_prd); Ok(ApiReturn { @@ -1271,6 +1309,116 @@ pub fn create_update_message( }) } +#[wasm_bindgen] +pub fn create_response_message(init_commitment: String, pcd_commitment: String, approval: bool) -> 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_mut()?; + // This is a map of keys to hash of the clear values + let new_state_commitments = ::from_string(&pcd_commitment)?; + + let update_state: &mut ProcessState; + if let Some(state) = latest_states.into_iter().find(|state| state.pcd_commitment == 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(); + + let roles = Value::Object(clear_state).extract_roles()?; + + let local_device = lock_local_device()?; + + let sp_wallet = local_device.get_wallet(); + let local_address = sp_wallet.get_client().get_receiving_address(); + + let mut all_members: HashMap> = HashMap::new(); + 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 { + debug!("member: {:?}", member); + // Check that we have a shared_secret with all members + if let Some(no_secret_address) = member.get_addresses().iter() + .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) + { + // We ignore it if we don't have a secret with ourselves + if *no_secret_address != local_address { + // 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()); + } + all_members.get_mut(&member).unwrap().extend(fields.clone()); + } + } + + let sender: Member = local_device + .to_member(); + + 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(); + + let root = ::create_merkle_tree(&new_state_commitments)?.root().unwrap(); + let message_hash = if approval { + AnkHash::ValidationYes(AnkValidationYesHash::from_byte_array(root)) + } else { + AnkHash::ValidationNo(AnkValidationNoHash::from_byte_array(root)) + }; + let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); + + let response_prd = Prd::new_response( + outpoint, + serde_json::to_string(&sender)?, + vec![], + new_state_commitments + ); + let prd_msg = response_prd.to_network_msg(sp_wallet)?; + + let mut ciphers = vec![]; + for (member, visible_fields) in all_members { + let addresses = member.get_addresses(); + for sp_address in addresses.into_iter() { + // We skip our own device address, no point sending ourself a cipher + if sp_address == local_address { + continue; + } + + // 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()); + } + } + + if ciphers.is_empty() { + return Err(ApiError::new("Empty ciphers list".to_owned())); + } + + update_state.validation_tokens.push(proof); + + Ok(ApiReturn { + updated_process: Some((outpoint.to_string(), process.clone())), + ciphers_to_send: ciphers, + ..Default::default() + }) +} + #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] @@ -1304,77 +1452,33 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { .get(&outpoint) .ok_or(ApiError::new("process not found".to_owned()))?; - let mut updated_process = relevant_process.clone(); - - let update_proposals: Vec<&Prd> = relevant_process - .get_impending_requests() - .into_iter() - .filter(|r| r.prd_type == PrdType::Update) - .collect(); - - if update_proposals.is_empty() { - return Err(ApiError::new(format!( - "No active update proposals for process {}", - process_outpoint - ))); - } - - let mut decrypted_pcds = vec![]; + let mut decrypted_pcds = HashMap::new(); // We first push the last commited state, if any match relevant_process.get_latest_commited_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)); + let root = ::create_merkle_tree(&state.pcd_commitment)?.root().unwrap(); + decrypted_pcds.insert(root.to_lower_hex_string(), Value::Object(decrypted_pcd)); } None => () } - // Maybe that's the right place for adding a new state with what we've got from the prd update - // We should probably iterate on every update proposals and see which one don't have a state yet - let mut update_states = false; - for proposal in update_proposals { - // Is there a state that matches this proposal? If not, let's add it - debug!("Trying proposal {:#?}", proposal); - let pcd = match Value::from_str(&proposal.payload) { - Ok(value) => value, - Err(e) => continue - }; - 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()? - .into_iter() - .find(|state| { - state.pcd_commitment == proposal.pcd_commitments - }) - { - // If not, we first add a new state - updated_process.insert_state(ProcessState { - 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(), - pcd_commitment: proposal.pcd_commitments.clone() - }); - update_states = true; + for state in relevant_process.get_latest_concurrent_states()? { + if state.encrypted_pcd == Value::Null { + // This is the last empty state, ignore it + continue; } - // We add the decrypted state to our return variable + let mut decrypted_pcd = Map::new(); - pcd.decrypt_fields(&proposal.keys, &mut decrypted_pcd)?; - decrypted_pcds.push(Value::Object(decrypted_pcd)); + state.encrypted_pcd.decrypt_fields(&state.keys, &mut decrypted_pcd)?; + let root = ::create_merkle_tree(&state.pcd_commitment)?.root().unwrap(); + decrypted_pcds.insert(root.to_lower_hex_string(), Value::Object(decrypted_pcd)); } - let mut res = ApiReturn::default(); - if update_states { - // We replace the 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) + Ok(ApiReturn { + decrypted_pcds, + ..Default::default() + }) } diff --git a/tests/pairing.rs b/tests/pairing.rs index 3eab488..2d04431 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -2,19 +2,15 @@ use std::collections::HashMap; use std::str::FromStr; use sdk_client::api::{ - add_validation_token_to_prd, create_commit_message, create_connect_transaction, create_device_from_sp_wallet, create_update_message, dump_device, dump_process_cache, get_address, get_outputs, get_update_proposals, pair_device, parse_cipher, reset_device, response_prd, restore_device, set_process_cache, setup, ApiReturn + create_device_from_sp_wallet, create_new_process, create_response_message, create_update_message, dump_device, get_address, get_update_proposals, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup }; -use sdk_common::log::{debug, info}; -use sdk_common::pcd::{Member, RoleDefinition}; -use sdk_common::sp_client::bitcoin::consensus::deserialize; -use sdk_common::sp_client::bitcoin::hex::FromHex; -use sdk_common::sp_client::bitcoin::{OutPoint, Transaction}; -use sdk_common::sp_client::spclient::OwnedOutput; -use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; +use sdk_common::crypto::AnkSharedSecretHash; +use sdk_common::log::debug; +use sdk_common::pcd::{Member, Pcd}; +use sdk_common::sp_client::bitcoin::hex::DisplayHex; use sdk_common::secrets::SecretsStore; use serde_json::{json, Value}; -use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; mod utils; @@ -93,6 +89,8 @@ wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn test_pairing() { + const RELAY_ADDRESS: &str = "tsp1qqvfm6wvd55r68ltysdhmagg7qavxrzlmm9a7tujsp8qqy6x2vr0muqajt5p2jdxfw450wyeygevypxte29sxlxzgprmh2gwnutnt09slrcqqy5h4"; + setup(); let mut alice_process_cache = HashMap::new(); let mut bob_process_cache = HashMap::new(); @@ -107,14 +105,18 @@ fn test_pairing() { // we get our own address let alice_address = get_address().unwrap(); + debug!("alice address: {}", alice_address); // we scan the qr code or get the address by any other means let bob_address = helper_get_bob_address(); + debug!("bob_address: {}", bob_address); // we add some shared_secret in both secrets_store - let shared_secret = "c3f1a64e15d2e8d50f852c20b7f0b47cbe002d9ef80bc79582d09d6f38612d45"; - alice_secrets_store.confirm_secret_for_address(shared_secret, bob_address.try_into().unwrap()); - bob_secrets_store.confirm_secret_for_address(shared_secret, alice_address.try_into().unwrap()); + let shared_secret = AnkSharedSecretHash::from_str("c3f1a64e15d2e8d50f852c20b7f0b47cbe002d9ef80bc79582d09d6f38612d45").unwrap(); + alice_secrets_store.confirm_secret_for_address(shared_secret, bob_address.as_str().try_into().unwrap()); + bob_secrets_store.confirm_secret_for_address(shared_secret, alice_address.as_str().try_into().unwrap()); + + set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap(); // Alice creates the new member with Bob address let new_member = Member::new(vec![ @@ -127,6 +129,10 @@ fn test_pairing() { let initial_session_pubkey = [0u8; 32]; let pairing_init_state = json!({ + "html": "", + "js": "", + "style": "", + "zones": [], "description": "AliceBob", "roles": { "owner": { @@ -155,48 +161,63 @@ fn test_pairing() { "key_parity": true, // This allows us to use a 32 bytes array in serialization }); + let create_process_return = create_new_process(pairing_init_state.to_string(), RELAY_ADDRESS.to_owned(), 1).unwrap(); + + let commit_msg = create_process_return.commit_to_send.unwrap(); + alice_secrets_store = create_process_return.secrets; + let (outpoint, new_process) = create_process_return.updated_process.unwrap(); + alice_process_cache.insert(outpoint.clone(), new_process); + + // We send the commit_msg to the relay we got the address from + + // now we create prd update for this new process + let create_update_return = create_update_message(outpoint, commit_msg.pcd_commitment.to_string()).unwrap(); + + let (root_outpoint, alice_init_process) = create_update_return.updated_process.unwrap(); + alice_process_cache.insert(root_outpoint.clone(), alice_init_process); + debug!("Alice pairs her device"); // we can update our local device now, first with an empty txid - pair_device(OutPoint::null().to_string(), vec![helper_get_bob_address()]).unwrap(); + pair_device(root_outpoint, vec![helper_get_bob_address()]).unwrap(); debug!("Alice sends an update prd to Bob"); let alice_pairing_return = - create_update_message(None, pairing_init_state.to_string()).unwrap(); + create_update_message(alice_process_cache.keys().next().unwrap().to_owned(), commit_msg.pcd_commitment.to_string()).unwrap(); + + // debug!("{:#?}", alice_pairing_return); let (root_outpoint, alice_init_process) = alice_pairing_return.updated_process.unwrap(); alice_process_cache.insert(root_outpoint.clone(), alice_init_process.clone()); - let alice_to_bob_cipher = alice_pairing_return.ciphers_to_send[0]; + let alice_to_bob_cipher = &alice_pairing_return.ciphers_to_send[0]; // this is only for testing, as we're playing both parts let alice_device = dump_device().unwrap(); - let alice_processes = dump_process_cache().unwrap(); // ======================= Bob reset_device().unwrap(); create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap(); + set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap(); debug!("Bob receives the update prd"); - let bob_parsed_return = parse_cipher(alice_to_bob_cipher).unwrap(); + let bob_parsed_return = parse_cipher(alice_to_bob_cipher.to_owned()).unwrap(); - debug!("Bob retrieved prd: {:#?}", bob_retrieved_prd); - - let (root_commitment, relevant_process) = bob_retrieved_prd.updated_process.unwrap(); + let (root_commitment, relevant_process) = bob_parsed_return.updated_process.unwrap(); bob_process_cache.insert(root_commitment.clone(), relevant_process); - let prd_confirm_cipher = bob_retrieved_prd.ciphers_to_send.iter().next().unwrap(); + let prd_confirm_cipher = bob_parsed_return.ciphers_to_send.iter().next().unwrap(); debug!("Bob sends a Confirm Prd to Alice"); // this is only for testing, as we're playing both parts let bob_device = dump_device().unwrap(); - let bob_processes = dump_process_cache().unwrap(); // ======================= Alice reset_device().unwrap(); restore_device(alice_device).unwrap(); - set_process_cache(alice_processes).unwrap(); + set_process_cache(serde_json::to_string(&alice_process_cache).unwrap()).unwrap(); + set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap(); debug!("Alice receives the Confirm Prd"); let alice_parsed_confirm = parse_cipher(prd_confirm_cipher.clone()).unwrap(); @@ -209,81 +230,35 @@ fn test_pairing() { // Alice simply shoots back the return value in the ws let bob_received_pcd = alice_parsed_confirm.ciphers_to_send[0].clone(); - // Now that we're sure that bob got the prd udpate we also produce the prd response and shoot it - let alice_prd_update_commitment = alice_init_process - .get_impending_requests() - .get(0) - .unwrap() - .create_commitment(); - let (_, alice_validated_prd) = add_validation_token_to_prd( - root_outpoint.clone(), - alice_prd_update_commitment.to_string(), - true, - ) - .unwrap() - .updated_process - .unwrap(); - - alice_process_cache.insert(root_outpoint.clone(), alice_validated_prd); - - let alice_prd_response = - response_prd(root_outpoint, alice_prd_update_commitment.to_string(), true).unwrap(); - - let bob_received_response = alice_prd_response.ciphers_to_send.get(0).unwrap().clone(); - // ======================= Bob reset_device().unwrap(); restore_device(bob_device).unwrap(); - set_process_cache(bob_processes).unwrap(); + set_process_cache(serde_json::to_string(&bob_process_cache).unwrap()).unwrap(); + set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap(); debug!("Bob parses Alice's pcd"); let bob_parsed_pcd_return = parse_cipher(bob_received_pcd).unwrap(); - debug!("bob_parsed_pcd: {:#?}", bob_parsed_pcd_return); - + let (root_outpoint, updated_process) = bob_parsed_pcd_return.updated_process.unwrap(); + // Here we would update our database bob_process_cache.insert( - root_commitment.clone(), - bob_parsed_pcd_return.updated_process.unwrap().1, + root_outpoint.clone(), + updated_process ); - // We now need Alice prd response, and update our process with it - debug!("Bob also parses alice prd response"); - let bob_parsed_response = parse_cipher(bob_received_response).unwrap(); - - debug!("bob_parsed_response: {:#?}", bob_parsed_response); - - bob_process_cache.insert( - root_commitment.clone(), - bob_parsed_response.updated_process.unwrap().1, - ); - - debug!("{:#?}", bob_process_cache.get(&root_commitment).unwrap()); - // At this point, user must validate the pairing proposal received from Alice // We decrypt the content of the pcd so that we can display to user what matters - let alice_proposal = get_update_proposals(root_commitment.clone()).unwrap(); + let alice_proposal = get_update_proposals(root_outpoint.clone()).unwrap().decrypted_pcds; debug!("Alice proposal: {:#?}", alice_proposal); - let proposal = Value::from_str(&alice_proposal.get(0).unwrap()).unwrap(); - debug!("proposal: {:#?}", proposal); + let (pcd_commitment_root, proposal) = alice_proposal.iter().next().unwrap(); + + // debug!("proposal: {:#?}", proposal); // get the roles from the proposal - let roles = proposal - .get("roles") - .and_then(|v| Value::from_str(v.as_str().unwrap()).ok()) - .unwrap() - .as_object() - .unwrap() - .iter() - .map(|(role_name, role_value)| { - let role_def: RoleDefinition = serde_json::from_value(role_value.clone())?; - Ok((role_name.clone(), role_def)) - }) - .collect::, anyhow::Error>>(); - - let roles = roles.unwrap(); + let roles = proposal.extract_roles().unwrap(); // we check that the proposal contains only one member assert!(roles.len() == 1); @@ -309,37 +284,33 @@ fn test_pairing() { debug!("proposal_members: {:?}", proposal_members); // we can now show all the addresses to the user on device to prompt confirmation - info!("Pop-up: User confirmation"); + debug!("Pop-up: User confirmation"); // If user is ok, we can add our own validation token - let prd_to_respond = bob_process_cache - .get(&root_commitment) - .unwrap() - .get_impending_requests() - .get(0) - .unwrap() - .to_owned(); - let bob_added_validation = - add_validation_token_to_prd(root_commitment.clone(), prd_to_respond.create_commitment().to_string(), true).unwrap(); + // Get the whole commitment from the process + let process = alice_process_cache.get(&root_outpoint).unwrap(); + let mut pcd_commitment = String::default(); + for p in process.get_latest_concurrent_states().unwrap() { + let root = ::create_merkle_tree(&p.pcd_commitment).unwrap().root().unwrap(); + if *pcd_commitment_root == root.to_lower_hex_string() { + pcd_commitment = p.pcd_commitment.to_string(); + break; + } + } + let bob_response = create_response_message(root_outpoint, pcd_commitment, true).unwrap(); + + let (root_outpoint, updated_process) = bob_response.updated_process.unwrap(); + let ciphers = bob_response.ciphers_to_send; // We would send it to Alice to let her know we agree bob_process_cache.insert( - root_commitment.clone(), - bob_added_validation.updated_process.unwrap().1, + root_outpoint.clone(), + updated_process, ); - // We create the commit msg for the relay that includes Alice and Bob's proofs - let commit_msg = create_commit_message(root_commitment.clone(), "tsp1qqvfm6wvd55r68ltysdhmagg7qavxrzlmm9a7tujsp8qqy6x2vr0muqajt5p2jdxfw450wyeygevypxte29sxlxzgprmh2gwnutnt09slrcqqy5h4".to_owned(), 1).unwrap().commit_to_send.unwrap(); - - let tx: Transaction = deserialize(&Vec::from_hex(&commit_msg.init_tx).unwrap()).unwrap(); - - // We send the commit_msg to the relay we got the address from - // We also send - - // We can just take the txid of the transaction we created for the commitment - let commitment_outpoint = OutPoint::new(tx.txid(), 0); + // We also send an update to the other members debug!("Bob pairs device with Alice"); - pair_device(commitment_outpoint.to_string(), proposal_members).unwrap(); + pair_device(root_outpoint, proposal_members).unwrap(); // To make the pairing effective, alice and bob must now creates a new transaction where they both control one output From 0d522182d61d30258d0223562dda91e38aa252b1 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 19 Nov 2024 17:01:10 +0100 Subject: [PATCH 10/43] Update to latest common --- src/api.rs | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/api.rs b/src/api.rs index 492071e..cc102a5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -904,8 +904,14 @@ fn handle_pcd(pcd: Value) -> AnyhowResult { } else { continue; } - debug!("Updating process states with {:#?}", updated_prd); - process.insert_state(&updated_prd)?; + let new_state = ProcessState { + commited_in: *outpoint, + pcd_commitment: updated_prd.pcd_commitments, + encrypted_pcd: pcd, + keys: updated_prd.keys, + validation_tokens: vec![] + }; + process.insert_concurrent_state(new_state)?; process.prune_impending_requests(); return Ok(ApiReturn { updated_process: Some((outpoint.to_string(), process.clone())), @@ -1151,8 +1157,8 @@ pub fn update_process( let new_state_val = Value::from_str(&new_state)?; // We hash all the new values - let pcd_commitment = new_state_val.hash_fields(outpoint)?; - let new_state_merkle_root = ::create_merkle_tree(&Value::Object(pcd_commitment))?.root().unwrap(); + let pcd_commitment = Value::Object(new_state_val.hash_fields(outpoint)?); + let new_state_merkle_root = ::create_merkle_tree(&pcd_commitment)?.root().unwrap(); // We compare the new state with the previous one let last_state_merkle_root = ::create_merkle_tree(last_state_commitments)?.root().unwrap(); @@ -1172,25 +1178,26 @@ pub fn update_process( .collect(); new_state_val.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); + let encrypted_pcd = Value::Object(fields2cipher); + // We create an encrypted values merkle root - let new_state_encrypted_commitments = Value::Object(fields2cipher).hash_fields(OutPoint::null())?; + let new_state_encrypted_commitments = encrypted_pcd.hash_fields(OutPoint::null())?; let new_state_encrypted_root = ::create_merkle_tree(&Value::Object(new_state_encrypted_commitments.clone()))?.root().unwrap(); let to_update = process.get_latest_state().unwrap(); // This is an empty state with `commited_in` set as the last unspent output let device = lock_local_device()?; - let sender = device.to_member(); - let update_prd = Prd::new_update( - outpoint, - serde_json::to_string(&sender)?, - new_state_encrypted_root.to_lower_hex_string(), - fields2keys, - Value::Object(new_state_encrypted_commitments), - ); - + let new_state = ProcessState { + commited_in: outpoint, + pcd_commitment: pcd_commitment, + encrypted_pcd: encrypted_pcd, + keys: fields2keys, + validation_tokens: vec![] + }; + // Add the new state to the process - process.insert_state(&update_prd); + process.insert_concurrent_state(new_state)?; Ok(ApiReturn { updated_process: Some((init_commitment, process.clone())), From 359312574f6175198bc4c433a2fa5e32ad17277d Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 19 Nov 2024 22:03:57 +0100 Subject: [PATCH 11/43] Fix response logic --- src/api.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/api.rs b/src/api.rs index cc102a5..5938723 100644 --- a/src/api.rs +++ b/src/api.rs @@ -819,8 +819,15 @@ fn handle_prd( return Err(anyhow::Error::msg(format!("No available secrets for member {:?}", member))); } + let clear_pcd = relevant_state.decrypt_pcd(); + + let roles = clear_pcd.extract_roles()?; + + let commit_msg = CommitMessage::new_update_commitment(outpoint, relevant_state.pcd_commitment.clone(), roles); + return Ok(ApiReturn { ciphers_to_send: ciphers, + commit_to_send: Some(commit_msg), ..Default::default() }) } @@ -854,19 +861,16 @@ fn handle_prd( } PrdType::Response => { // We must know of a prd update that the response answers to - let original_request = relevant_process - .get_impending_requests_mut() + let mut updated_state = relevant_process + .get_latest_concurrent_states_mut()? .into_iter() .find(|r| { - if r.prd_type != PrdType::Update { - return false; - } - r.pcd_commitments == prd.pcd_commitments + r.pcd_commitment == prd.pcd_commitments }) .ok_or(anyhow::Error::msg("Original request not found"))?; // Once we found the prd update, we can add the received proofs as validation tokens - original_request + updated_state .validation_tokens .extend(prd.validation_tokens); @@ -1391,7 +1395,7 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, let response_prd = Prd::new_response( outpoint, serde_json::to_string(&sender)?, - vec![], + vec![proof], new_state_commitments ); let prd_msg = response_prd.to_network_msg(sp_wallet)?; From d6f22026274bdd7f25d39633d8dbac56b0e80594 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 19 Nov 2024 22:04:25 +0100 Subject: [PATCH 12/43] Update pairing test --- tests/pairing.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/pairing.rs b/tests/pairing.rs index 2d04431..60822e0 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -125,7 +125,7 @@ fn test_pairing() { ]) .unwrap(); - let initial_session_privkey = [0u8; 32]; + let initial_session_privkey = [0u8; 32]; // In reality we would generate a random new key here let initial_session_pubkey = [0u8; 32]; let pairing_init_state = json!({ @@ -229,6 +229,20 @@ fn test_pairing() { // Alice simply shoots back the return value in the ws let bob_received_pcd = alice_parsed_confirm.ciphers_to_send[0].clone(); + let commit_msg = alice_parsed_confirm.commit_to_send.unwrap(); + + // Take the relevant state out of the process + let relevant_process = alice_process_cache.get(&commit_msg.init_tx).unwrap(); + + let concurrent_states = relevant_process.get_latest_concurrent_states().unwrap(); + let relevant_state = concurrent_states.into_iter().find(|s| s.pcd_commitment == commit_msg.pcd_commitment).unwrap(); + + // Alice can also sign her response and send it to Bob + let alice_response = create_response_message(commit_msg.init_tx, relevant_state.pcd_commitment.to_string(), true).unwrap(); + + let (outpoint, updated_process) = alice_response.updated_process.unwrap(); + + alice_process_cache.insert(outpoint, updated_process); // ======================= Bob reset_device().unwrap(); @@ -307,12 +321,15 @@ fn test_pairing() { updated_process, ); - // We also send an update to the other members - debug!("Bob pairs device with Alice"); pair_device(root_outpoint, proposal_members).unwrap(); - // To make the pairing effective, alice and bob must now creates a new transaction where they both control one output + // We can also check alice response + let parsed_alice_response = parse_cipher(alice_response.ciphers_to_send[0].clone()).unwrap(); + + debug!("parsed_alice_response: {:#?}", parsed_alice_response.updated_process.unwrap()); + + // Since we have enough validation we can send it directly to relay for commitment // login(); } From 5c77ae1b4ffa139d985065157e05d2c258d86600 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 19 Nov 2024 23:55:24 +0100 Subject: [PATCH 13/43] ApiReturn secrets is Option with only new secrets --- src/api.rs | 30 +++++++++++++++++-------- tests/connect.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++---- tests/pairing.rs | 16 ++++++++++++- 3 files changed, 90 insertions(+), 14 deletions(-) diff --git a/src/api.rs b/src/api.rs index 5938723..eda3e8a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -80,7 +80,7 @@ use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct ApiReturn { - pub secrets: SecretsStore, + pub secrets: Option, pub updated_process: Option<(String, Process)>, pub new_tx_to_send: Option, pub ciphers_to_send: Vec, @@ -538,6 +538,10 @@ fn handle_transaction( // We keep the shared_secret as unconfirmed shared_secrets.add_unconfirmed_secret(shared_secret); + // We also return it + let mut new_secret = SecretsStore::new(); + new_secret.add_unconfirmed_secret(shared_secret); + // We hash the shared secret to commit into the prd connect let secret_hash = AnkMessageHash::from_message(shared_secret.as_byte_array()); @@ -550,7 +554,7 @@ fn handle_transaction( let cipher = encrypt_with_key(shared_secret.as_byte_array(), msg.as_bytes())?; return Ok(ApiReturn { - secrets: shared_secrets.to_owned(), + secrets: Some(new_secret), ciphers_to_send: vec![cipher.to_lower_hex_string()], ..Default::default() }) @@ -736,10 +740,11 @@ fn handle_prd_connect(prd: Prd, secret: AnkSharedSecretHash) -> AnyhowResult AnyhowResult AnyhowResult, fee_rate: u32) -> Ap let transaction = psbt.extract_tx()?; let mut shared_secrets = lock_shared_secrets()?; + let mut secrets_return = SecretsStore::new(); for (address, secret) in new_secrets { shared_secrets.confirm_secret_for_address(secret, address); + secrets_return.confirm_secret_for_address(secret, address); } Ok(ApiReturn { new_tx_to_send: Some(NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None)), - secrets: shared_secrets.to_owned(), + secrets: Some(secrets_return), ..Default::default() }) } @@ -1100,8 +1110,10 @@ pub fn create_new_process( let new_secrets = get_shared_secrets_in_transaction(&psbt, vec![relay_address])?; let mut shared_secrets = lock_shared_secrets()?; + let mut secrets_return = SecretsStore::new(); for (address, secret) in new_secrets { shared_secrets.confirm_secret_for_address(secret, address); + secrets_return.confirm_secret_for_address(secret, address); } let transaction = psbt.extract_tx()?; @@ -1135,7 +1147,7 @@ pub fn create_new_process( let commit_msg = CommitMessage::new_first_commitment(transaction, Value::Object(fields_commitment), roles); Ok(ApiReturn { - secrets: shared_secrets.clone(), + secrets: Some(secrets_return), commit_to_send: Some(commit_msg), updated_process: Some((outpoint.to_string(), process)), ..Default::default() diff --git a/tests/connect.rs b/tests/connect.rs index 1173b90..c560fdc 100644 --- a/tests/connect.rs +++ b/tests/connect.rs @@ -106,7 +106,21 @@ fn test_connect() { let alice_connect_transaction = connect_tx_msg.transaction; let alice_device = dump_device().unwrap(); - alice_secrets_store = alice_connect_return.secrets; + + // Below is how to update our secrets store when secrets is Some + let secrets_update = alice_connect_return.secrets.unwrap(); + let unconfirmed_secrets = secrets_update.get_all_unconfirmed_secrets(); + if !unconfirmed_secrets.is_empty() { + for secret in unconfirmed_secrets { + alice_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = secrets_update.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + alice_secrets_store.confirm_secret_for_address(secret, address); + } + } // ======================= Bob reset_device().unwrap(); @@ -119,7 +133,19 @@ fn test_connect() { let bob_to_alice_cipher = &bob_parsed_transaction_return.ciphers_to_send[0]; let bob_device = dump_device().unwrap(); - bob_secrets_store = bob_parsed_transaction_return.secrets; + let updated_secrets = bob_parsed_transaction_return.secrets.unwrap(); + let updated_unconfirmed_secrets = updated_secrets.get_all_unconfirmed_secrets(); + if !updated_unconfirmed_secrets.is_empty() { + for secret in updated_unconfirmed_secrets { + bob_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = updated_secrets.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + bob_secrets_store.confirm_secret_for_address(secret, address); + } + } // ======================= Alice reset_device().unwrap(); @@ -132,7 +158,19 @@ fn test_connect() { // debug!("alice_parsed_confirm: {:#?}", alice_parsed_confirm); let alice_to_bob_cipher = alice_parsed_connect.ciphers_to_send.get(0).unwrap(); - alice_secrets_store = alice_parsed_connect.secrets; + let secrets_update = alice_parsed_connect.secrets.unwrap(); + let unconfirmed_secrets = secrets_update.get_all_unconfirmed_secrets(); + if !unconfirmed_secrets.is_empty() { + for secret in unconfirmed_secrets { + alice_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = secrets_update.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + alice_secrets_store.confirm_secret_for_address(secret, address); + } + } // ======================= Bob reset_device().unwrap(); @@ -142,7 +180,19 @@ fn test_connect() { debug!("Bob parses alice prd connect"); let bob_parsed_connect = parse_cipher(alice_to_bob_cipher.clone()).unwrap(); - bob_secrets_store = bob_parsed_connect.secrets; + let updated_secrets = bob_parsed_connect.secrets.unwrap(); + let updated_unconfirmed_secrets = updated_secrets.get_all_unconfirmed_secrets(); + if !updated_unconfirmed_secrets.is_empty() { + for secret in updated_unconfirmed_secrets { + bob_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = updated_secrets.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + bob_secrets_store.confirm_secret_for_address(secret, address); + } + } // Assert that Alice and Bob now has the same secret assert!(alice_secrets_store.get_secret_for_address(bob_address.try_into().unwrap()) == bob_secrets_store.get_secret_for_address(alice_address.try_into().unwrap())); diff --git a/tests/pairing.rs b/tests/pairing.rs index 60822e0..d3ef4ae 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -164,7 +164,21 @@ fn test_pairing() { let create_process_return = create_new_process(pairing_init_state.to_string(), RELAY_ADDRESS.to_owned(), 1).unwrap(); let commit_msg = create_process_return.commit_to_send.unwrap(); - alice_secrets_store = create_process_return.secrets; + + let secrets_update = create_process_return.secrets.unwrap(); + let unconfirmed_secrets = secrets_update.get_all_unconfirmed_secrets(); + if !unconfirmed_secrets.is_empty() { + for secret in unconfirmed_secrets { + alice_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = secrets_update.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + alice_secrets_store.confirm_secret_for_address(secret, address); + } + } + let (outpoint, new_process) = create_process_return.updated_process.unwrap(); alice_process_cache.insert(outpoint.clone(), new_process); From 6350b293793801c6139a7960246ddb188270afb2 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 25 Nov 2024 22:28:49 +0100 Subject: [PATCH 14/43] Add get_new_keypair api --- src/api.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/api.rs b/src/api.rs index eda3e8a..fb930e9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -31,7 +31,7 @@ use sdk_common::sp_client::bitcoin::hashes::{FromSliceError, HashEngine}; use sdk_common::sp_client::bitcoin::hex::{ self, parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, }; -use sdk_common::sp_client::bitcoin::key::{Parity, Secp256k1}; +use sdk_common::sp_client::bitcoin::key::{Keypair, Parity, Secp256k1}; use sdk_common::sp_client::bitcoin::network::ParseNetworkError; use sdk_common::sp_client::bitcoin::p2p::message::NetworkMessage; use sdk_common::sp_client::bitcoin::psbt::raw; @@ -90,6 +90,15 @@ pub struct ApiReturn { pub type ApiResult = Result; +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct NewKey { + pub private_key: String, + pub x_only_public_key: String, + pub key_parity: bool +} + const IS_TESTNET: bool = true; const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000); @@ -211,6 +220,22 @@ pub fn get_address() -> ApiResult { .get_receiving_address()) } +#[wasm_bindgen] +pub fn get_new_keypair() -> NewKey { + let secp = Secp256k1::new(); + let mut rng = thread_rng(); + let keypair = Keypair::new(&secp, &mut rng); + + let secret_hex = keypair.secret_bytes().to_lower_hex_string(); + let (xonly, parity) = keypair.x_only_public_key(); + + NewKey { + private_key: secret_hex, + x_only_public_key: xonly.to_string(), + key_parity: if parity == Parity::Even { true } else { false } + } +} + #[wasm_bindgen] pub fn restore_device(device_str: String) -> ApiResult<()> { let device: Device = serde_json::from_str(&device_str)?; From 0aa0c6817a88443d4ae1347820da42625c480de1 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 25 Nov 2024 22:29:10 +0100 Subject: [PATCH 15/43] parse_new_tx doesn't need fee_rate --- src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.rs b/src/api.rs index fb930e9..c806e10 100644 --- a/src/api.rs +++ b/src/api.rs @@ -617,7 +617,7 @@ fn process_transaction( } #[wasm_bindgen] -pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> ApiResult { +pub fn parse_new_tx(new_tx_msg: String, block_height: u32) -> ApiResult { let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?; if let Some(error) = new_tx.error { From ff47da3083299d24a462bfd49fae4d0d28b2a28a Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 25 Nov 2024 22:32:56 +0100 Subject: [PATCH 16/43] Minor fixes --- src/api.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/api.rs b/src/api.rs index c806e10..d09515c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -812,11 +812,7 @@ fn handle_prd( std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), std::collections::hash_map::Entry::Vacant(entry) => { debug!("Creating new process for outpoint: {}", outpoint); - let empty_state = ProcessState { - commited_in: outpoint, - ..Default::default() - }; - entry.insert(Process::new(vec![empty_state], vec![])) + entry.insert(Process::new(outpoint)) } }; @@ -1085,6 +1081,10 @@ pub fn create_connect_transaction(members_str: Vec, fee_rate: u32) -> Ap addresses.extend(member.get_addresses().into_iter()); } + if addresses.is_empty() { + return Err(ApiError::new("No addresses to connect to".to_owned())); + } + let psbt = create_transaction_for_addresses(addresses.clone(), fee_rate)?; let new_secrets = get_shared_secrets_in_transaction(&psbt, addresses)?; @@ -1149,6 +1149,8 @@ pub fn create_new_process( // 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)?; + let mut process = Process::new(outpoint); + // We now create the first process state with all that data let process_state = ProcessState { commited_in: outpoint, @@ -1156,9 +1158,9 @@ pub fn create_new_process( encrypted_pcd: Value::Object(fields2cipher.clone()), keys: fields2keys.clone(), validation_tokens: vec![], - }; + }; - let process = Process::new(vec![process_state], vec![]); + process.insert_concurrent_state(process_state)?; { let mut processes = lock_processes()?; From ad632e40ce3a8dea433f70a30ec1c2ea464a9309 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 25 Nov 2024 23:23:38 +0100 Subject: [PATCH 17/43] create_response_message takes the merkle root --- src/api.rs | 29 +++++++++++------------------ tests/pairing.rs | 15 ++++----------- tests/utils.rs | 2 +- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/api.rs b/src/api.rs index d09515c..8e742a4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1361,6 +1361,13 @@ pub fn create_update_message( #[wasm_bindgen] pub fn create_response_message(init_commitment: String, pcd_commitment: String, approval: bool) -> ApiResult { + let mut merkle_root = [0u8; 32]; + let pcd_commitment_vec = Vec::from_hex(&pcd_commitment)?; + if pcd_commitment_vec.len() != 32 { + return Err(ApiError::new("pcd_commitment must be 32B long".to_owned())); + } + merkle_root.copy_from_slice(&pcd_commitment_vec); + let mut processes = lock_processes()?; let outpoint = OutPoint::from_str(&init_commitment)?; @@ -1368,17 +1375,7 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; - let latest_states = process.get_latest_concurrent_states_mut()?; - // This is a map of keys to hash of the clear values - let new_state_commitments = ::from_string(&pcd_commitment)?; - - let update_state: &mut ProcessState; - if let Some(state) = latest_states.into_iter().find(|state| state.pcd_commitment == new_state_commitments) - { - update_state = state; - } else { - return Err(ApiError::new("Can't find the state to update".to_owned())); - } + let update_state: &mut ProcessState = process.get_state_for_commitments_root(merkle_root)?; // 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(); @@ -1420,14 +1417,10 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, let sender: Member = local_device .to_member(); - 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(); - - let root = ::create_merkle_tree(&new_state_commitments)?.root().unwrap(); let message_hash = if approval { - AnkHash::ValidationYes(AnkValidationYesHash::from_byte_array(root)) + AnkHash::ValidationYes(AnkValidationYesHash::from_byte_array(merkle_root)) } else { - AnkHash::ValidationNo(AnkValidationNoHash::from_byte_array(root)) + AnkHash::ValidationNo(AnkValidationNoHash::from_byte_array(merkle_root)) }; let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); @@ -1435,7 +1428,7 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, outpoint, serde_json::to_string(&sender)?, vec![proof], - new_state_commitments + update_state.pcd_commitment.clone(), ); let prd_msg = response_prd.to_network_msg(sp_wallet)?; diff --git a/tests/pairing.rs b/tests/pairing.rs index d3ef4ae..0ab0eb3 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -251,8 +251,10 @@ fn test_pairing() { let concurrent_states = relevant_process.get_latest_concurrent_states().unwrap(); let relevant_state = concurrent_states.into_iter().find(|s| s.pcd_commitment == commit_msg.pcd_commitment).unwrap(); + let root = ::create_merkle_tree(&relevant_state.pcd_commitment).unwrap().root().unwrap(); + // Alice can also sign her response and send it to Bob - let alice_response = create_response_message(commit_msg.init_tx, relevant_state.pcd_commitment.to_string(), true).unwrap(); + let alice_response = create_response_message(commit_msg.init_tx, root.to_lower_hex_string(), true).unwrap(); let (outpoint, updated_process) = alice_response.updated_process.unwrap(); @@ -316,16 +318,7 @@ fn test_pairing() { // If user is ok, we can add our own validation token // Get the whole commitment from the process - let process = alice_process_cache.get(&root_outpoint).unwrap(); - let mut pcd_commitment = String::default(); - for p in process.get_latest_concurrent_states().unwrap() { - let root = ::create_merkle_tree(&p.pcd_commitment).unwrap().root().unwrap(); - if *pcd_commitment_root == root.to_lower_hex_string() { - pcd_commitment = p.pcd_commitment.to_string(); - break; - } - } - let bob_response = create_response_message(root_outpoint, pcd_commitment, true).unwrap(); + let bob_response = create_response_message(root_outpoint, pcd_commitment_root.to_string(), true).unwrap(); let (root_outpoint, updated_process) = bob_response.updated_process.unwrap(); let ciphers = bob_response.ciphers_to_send; // We would send it to Alice to let her know we agree diff --git a/tests/utils.rs b/tests/utils.rs index e04736c..53cd531 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -69,7 +69,7 @@ pub fn helper_parse_transaction(transaction: &str, tweak_data: &str) -> ApiRetur )) .unwrap(); // debug!("new_tx_msg: {:?}", new_tx_msg); - let result = parse_new_tx(new_tx_msg, 0, 1); + let result = parse_new_tx(new_tx_msg, 0); match result { Ok(m) => m, Err(e) => panic!("Unexpected error: {}", e.message), From e90473d8bea1404e79b08558fc568cc3bf141708 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 26 Nov 2024 21:12:34 +0100 Subject: [PATCH 18/43] Refactoring of the response api --- src/api.rs | 89 +++++++++++++++++++++++++++++++++--------------- tests/pairing.rs | 40 +++++++++------------- 2 files changed, 79 insertions(+), 50 deletions(-) diff --git a/src/api.rs b/src/api.rs index 8e742a4..a26eeac 100644 --- a/src/api.rs +++ b/src/api.rs @@ -889,7 +889,6 @@ fn handle_prd( }); } PrdType::Response => { - // We must know of a prd update that the response answers to let mut updated_state = relevant_process .get_latest_concurrent_states_mut()? .into_iter() @@ -1251,7 +1250,7 @@ pub fn update_process( #[wasm_bindgen] pub fn create_update_message( init_commitment: String, - pcd_commitment: String, + merkle_root_hex: String, ) -> ApiResult { let mut processes = lock_processes()?; @@ -1260,18 +1259,17 @@ pub fn create_update_message( 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 mut merkle_root_bin = [0u8; 32]; + let merkle_root_vec = Vec::from_hex(&merkle_root_hex)?; - let update_state: &ProcessState; - if let Some(state) = latest_states.into_iter().find(|state| state.pcd_commitment == new_state_commitments) - { - update_state = state; - } else { - return Err(ApiError::new("Can't find the state to update".to_owned())); + if merkle_root_vec.len() != 32 { + return Err(ApiError::new("merkle root must be 32B long".to_owned())); } + merkle_root_bin.copy_from_slice(&merkle_root_vec); + + let update_state = process.get_state_for_commitments_root(merkle_root_bin)?; + // 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(); @@ -1322,7 +1320,7 @@ pub fn create_update_message( serde_json::to_string(&sender)?, serialize(&encrypted_pcd_merkle_root).to_lower_hex_string(), update_state.keys.clone(), - new_state_commitments + update_state.pcd_commitment.clone(), ); let mut ciphers = vec![]; @@ -1360,9 +1358,54 @@ pub fn create_update_message( } #[wasm_bindgen] -pub fn create_response_message(init_commitment: String, pcd_commitment: String, approval: bool) -> ApiResult { +pub fn validate_state(init_commitment: String, merkle_root_hex: String) -> ApiResult { + add_validation_token(init_commitment, merkle_root_hex, true) +} + +#[wasm_bindgen] +pub fn refuse_state(init_commitment: String, merkle_root_hex: String) -> ApiResult { + add_validation_token(init_commitment, merkle_root_hex, false) +} + +fn add_validation_token(init_commitment: String, merkle_root_hex: String, approval: bool) -> ApiResult { let mut merkle_root = [0u8; 32]; - let pcd_commitment_vec = Vec::from_hex(&pcd_commitment)?; + let pcd_commitment_vec = Vec::from_hex(&merkle_root_hex)?; + if pcd_commitment_vec.len() != 32 { + return Err(ApiError::new("pcd_commitment must be 32B long".to_owned())); + } + merkle_root.copy_from_slice(&pcd_commitment_vec); + + 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 update_state: &mut ProcessState = process.get_state_for_commitments_root(merkle_root)?; + + let message_hash = if approval { + AnkHash::ValidationYes(AnkValidationYesHash::from_byte_array(merkle_root)) + } else { + AnkHash::ValidationNo(AnkValidationNoHash::from_byte_array(merkle_root)) + }; + + let local_device = lock_local_device()?; + let sp_wallet = local_device.get_wallet(); + let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); + + update_state.validation_tokens.push(proof); + + Ok(ApiReturn { + updated_process: Some((init_commitment, process.clone())), + ..Default::default() + }) +} + +#[wasm_bindgen] +pub fn create_response_prd(init_commitment: String, merkle_root_hex: String) -> ApiResult { + let mut merkle_root = [0u8; 32]; + let pcd_commitment_vec = Vec::from_hex(&merkle_root_hex)?; if pcd_commitment_vec.len() != 32 { return Err(ApiError::new("pcd_commitment must be 32B long".to_owned())); } @@ -1383,7 +1426,6 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, let roles = Value::Object(clear_state).extract_roles()?; let local_device = lock_local_device()?; - let sp_wallet = local_device.get_wallet(); let local_address = sp_wallet.get_client().get_receiving_address(); @@ -1396,7 +1438,6 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, .flat_map(|rule| rule.fields.clone()) .collect(); for member in role.members { - debug!("member: {:?}", member); // Check that we have a shared_secret with all members if let Some(no_secret_address) = member.get_addresses().iter() .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) @@ -1414,20 +1455,17 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, } } + let our_key = SilentPaymentAddress::try_from(local_address.as_str())?.get_spend_key(); + let proof = update_state.validation_tokens.iter().find(|t| t.get_key() == our_key) + .ok_or(ApiError::new("We haven't added our validation token yet".to_owned()))?; + let sender: Member = local_device .to_member(); - let message_hash = if approval { - AnkHash::ValidationYes(AnkValidationYesHash::from_byte_array(merkle_root)) - } else { - AnkHash::ValidationNo(AnkValidationNoHash::from_byte_array(merkle_root)) - }; - let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); - let response_prd = Prd::new_response( outpoint, serde_json::to_string(&sender)?, - vec![proof], + vec![*proof], update_state.pcd_commitment.clone(), ); let prd_msg = response_prd.to_network_msg(sp_wallet)?; @@ -1453,10 +1491,7 @@ pub fn create_response_message(init_commitment: String, pcd_commitment: String, return Err(ApiError::new("Empty ciphers list".to_owned())); } - update_state.validation_tokens.push(proof); - Ok(ApiReturn { - updated_process: Some((outpoint.to_string(), process.clone())), ciphers_to_send: ciphers, ..Default::default() }) diff --git a/tests/pairing.rs b/tests/pairing.rs index 0ab0eb3..8e446ae 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::str::FromStr; use sdk_client::api::{ - create_device_from_sp_wallet, create_new_process, create_response_message, create_update_message, dump_device, get_address, get_update_proposals, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup + create_device_from_sp_wallet, create_new_process, create_response_prd, create_update_message, dump_device, get_address, get_update_proposals, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup, validate_state }; use sdk_common::crypto::AnkSharedSecretHash; use sdk_common::log::debug; @@ -185,25 +185,17 @@ fn test_pairing() { // We send the commit_msg to the relay we got the address from // now we create prd update for this new process - let create_update_return = create_update_message(outpoint, commit_msg.pcd_commitment.to_string()).unwrap(); + debug!("Alice sends an update prd to Bob"); + let root = ::create_merkle_tree(&commit_msg.pcd_commitment).unwrap().root().unwrap(); + let create_update_return = create_update_message(outpoint, root.to_lower_hex_string()).unwrap(); let (root_outpoint, alice_init_process) = create_update_return.updated_process.unwrap(); alice_process_cache.insert(root_outpoint.clone(), alice_init_process); debug!("Alice pairs her device"); - // we can update our local device now, first with an empty txid pair_device(root_outpoint, vec![helper_get_bob_address()]).unwrap(); - debug!("Alice sends an update prd to Bob"); - let alice_pairing_return = - create_update_message(alice_process_cache.keys().next().unwrap().to_owned(), commit_msg.pcd_commitment.to_string()).unwrap(); - - // debug!("{:#?}", alice_pairing_return); - - let (root_outpoint, alice_init_process) = alice_pairing_return.updated_process.unwrap(); - alice_process_cache.insert(root_outpoint.clone(), alice_init_process.clone()); - - let alice_to_bob_cipher = &alice_pairing_return.ciphers_to_send[0]; + let alice_to_bob_cipher = &create_update_return.ciphers_to_send[0]; // this is only for testing, as we're playing both parts let alice_device = dump_device().unwrap(); @@ -254,11 +246,13 @@ fn test_pairing() { let root = ::create_merkle_tree(&relevant_state.pcd_commitment).unwrap().root().unwrap(); // Alice can also sign her response and send it to Bob - let alice_response = create_response_message(commit_msg.init_tx, root.to_lower_hex_string(), true).unwrap(); + let validate_state_return = validate_state(commit_msg.init_tx, root.to_lower_hex_string()).unwrap(); - let (outpoint, updated_process) = alice_response.updated_process.unwrap(); + let (outpoint, validated_process) = validate_state_return.updated_process.unwrap(); - alice_process_cache.insert(outpoint, updated_process); + alice_process_cache.insert(outpoint.clone(), validated_process); + + let alice_response = create_response_prd(outpoint, root.to_lower_hex_string()).unwrap(); // ======================= Bob reset_device().unwrap(); @@ -318,16 +312,16 @@ fn test_pairing() { // If user is ok, we can add our own validation token // Get the whole commitment from the process - let bob_response = create_response_message(root_outpoint, pcd_commitment_root.to_string(), true).unwrap(); + let bob_validated_process = validate_state(root_outpoint.clone(), pcd_commitment_root.to_string()).unwrap(); + + let (_, validated_process) = bob_validated_process.updated_process.unwrap(); + + bob_process_cache.insert(root_outpoint.clone(), validated_process); + + let bob_response = create_response_prd(root_outpoint.clone(), pcd_commitment_root.to_string()).unwrap(); - let (root_outpoint, updated_process) = bob_response.updated_process.unwrap(); let ciphers = bob_response.ciphers_to_send; // We would send it to Alice to let her know we agree - bob_process_cache.insert( - root_outpoint.clone(), - updated_process, - ); - debug!("Bob pairs device with Alice"); pair_device(root_outpoint, proposal_members).unwrap(); From 041cf55f58de343c58231000eaad8f761bac6575 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 26 Nov 2024 22:55:31 +0100 Subject: [PATCH 19/43] Add UpdatedProcess type --- src/api.rs | 105 ++++++++++++++++++++++++++++++++++++----------- tests/pairing.rs | 43 +++++++++---------- 2 files changed, 102 insertions(+), 46 deletions(-) diff --git a/src/api.rs b/src/api.rs index a26eeac..0d93712 100644 --- a/src/api.rs +++ b/src/api.rs @@ -77,11 +77,22 @@ use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct UpdatedProcess { + pub commitment_tx: OutPoint, + pub current_process: Process, + pub new_state: Option, + pub modified_state: Option<(ProcessState, ProcessState)>, // first the previous state, then the current one + pub user_validation_required: bool, +} + +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct ApiReturn { pub secrets: Option, - pub updated_process: Option<(String, Process)>, + pub updated_process: Option, pub new_tx_to_send: Option, pub ciphers_to_send: Vec, pub commit_to_send: Option, @@ -729,11 +740,11 @@ fn confirm_prd(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult AnyhowResult { + debug!("handle_prd: {:#?}", 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 { @@ -820,7 +832,7 @@ fn handle_prd( PrdType::Confirm => { // It must match a prd we sent previously // We send the whole data in a pcd - debug!("Received confirm prd {:#?}", prd); + debug!("Received confirm prd with commitments {:#?}", prd.pcd_commitments); let relevant_state = relevant_process .get_latest_concurrent_states()? .into_iter() @@ -877,19 +889,25 @@ fn handle_prd( } } - // This should never happen since we sent a message to get a confirmation back + // This should never happen if ciphers.is_empty() { return Err(anyhow::Error::msg(format!("No available secrets for member {:?}", member))); } + let updated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: relevant_process.clone(), + ..Default::default() + }; + return Ok(ApiReturn { ciphers_to_send: ciphers, - updated_process: Some((outpoint.to_string(), relevant_process.clone())), + updated_process: Some(updated_process), ..Default::default() }); } PrdType::Response => { - let mut updated_state = relevant_process + let mut to_update = relevant_process .get_latest_concurrent_states_mut()? .into_iter() .find(|r| { @@ -898,12 +916,21 @@ fn handle_prd( .ok_or(anyhow::Error::msg("Original request not found"))?; // Once we found the prd update, we can add the received proofs as validation tokens - updated_state + let previous_state = to_update.clone(); + + to_update .validation_tokens .extend(prd.validation_tokens); + let updated_state = to_update.clone(); + // We must return an update of the process - let updated_process = (prd.root_commitment, relevant_process.clone()); + let updated_process = UpdatedProcess { + commitment_tx: OutPoint::from_str(&prd.root_commitment)?, + current_process: relevant_process.clone(), + modified_state: Some((previous_state, updated_state.clone())), + ..Default::default() + }; return Ok(ApiReturn { updated_process: Some(updated_process), @@ -943,10 +970,18 @@ fn handle_pcd(pcd: Value) -> AnyhowResult { keys: updated_prd.keys, validation_tokens: vec![] }; - process.insert_concurrent_state(new_state)?; + process.insert_concurrent_state(new_state.clone())?; process.prune_impending_requests(); + + let udpated_process = UpdatedProcess { + commitment_tx: *outpoint, + current_process: process.clone(), + new_state: Some(new_state), + ..Default::default() + }; + return Ok(ApiReturn { - updated_process: Some((outpoint.to_string(), process.clone())), + updated_process: Some(udpated_process), ..Default::default() }); } @@ -1159,7 +1194,7 @@ pub fn create_new_process( validation_tokens: vec![], }; - process.insert_concurrent_state(process_state)?; + process.insert_concurrent_state(process_state.clone())?; { let mut processes = lock_processes()?; @@ -1172,10 +1207,17 @@ pub fn create_new_process( let commit_msg = CommitMessage::new_first_commitment(transaction, Value::Object(fields_commitment), roles); + let updated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: process, + new_state: Some(process_state), + ..Default::default() + }; + Ok(ApiReturn { secrets: Some(secrets_return), commit_to_send: Some(commit_msg), - updated_process: Some((outpoint.to_string(), process)), + updated_process: Some(updated_process), ..Default::default() }) } @@ -1222,12 +1264,6 @@ pub fn update_process( let encrypted_pcd = Value::Object(fields2cipher); - // We create an encrypted values merkle root - let new_state_encrypted_commitments = encrypted_pcd.hash_fields(OutPoint::null())?; - let new_state_encrypted_root = ::create_merkle_tree(&Value::Object(new_state_encrypted_commitments.clone()))?.root().unwrap(); - - let to_update = process.get_latest_state().unwrap(); // This is an empty state with `commited_in` set as the last unspent output - let device = lock_local_device()?; let new_state = ProcessState { @@ -1239,10 +1275,17 @@ pub fn update_process( }; // Add the new state to the process - process.insert_concurrent_state(new_state)?; + process.insert_concurrent_state(new_state.clone())?; + + let updated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: process.clone(), + new_state: Some(new_state), + ..Default::default() + }; Ok(ApiReturn { - updated_process: Some((init_commitment, process.clone())), + updated_process: Some(updated_process), ..Default::default() }) } @@ -1350,8 +1393,14 @@ pub fn create_update_message( process.insert_impending_request(full_prd); + let updated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: process.clone(), + ..Default::default() + }; + Ok(ApiReturn { - updated_process: Some((outpoint.to_string(), process.clone())), + updated_process: Some(updated_process), ciphers_to_send: ciphers, ..Default::default() }) @@ -1396,8 +1445,14 @@ fn add_validation_token(init_commitment: String, merkle_root_hex: String, approv update_state.validation_tokens.push(proof); + let updated_process = UpdatedProcess { + commitment_tx: OutPoint::from_str(&init_commitment)?, + current_process: process.clone(), + ..Default::default() + }; + Ok(ApiReturn { - updated_process: Some((init_commitment, process.clone())), + updated_process: Some(updated_process), ..Default::default() }) } diff --git a/tests/pairing.rs b/tests/pairing.rs index 8e446ae..2fc9d31 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -9,6 +9,7 @@ use sdk_common::log::debug; use sdk_common::pcd::{Member, Pcd}; use sdk_common::sp_client::bitcoin::hex::DisplayHex; use sdk_common::secrets::SecretsStore; +use sdk_common::sp_client::bitcoin::OutPoint; use serde_json::{json, Value}; use wasm_bindgen_test::*; @@ -179,21 +180,21 @@ fn test_pairing() { } } - let (outpoint, new_process) = create_process_return.updated_process.unwrap(); - alice_process_cache.insert(outpoint.clone(), new_process); + let updated_process = create_process_return.updated_process.unwrap(); + alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); // We send the commit_msg to the relay we got the address from // now we create prd update for this new process debug!("Alice sends an update prd to Bob"); let root = ::create_merkle_tree(&commit_msg.pcd_commitment).unwrap().root().unwrap(); - let create_update_return = create_update_message(outpoint, root.to_lower_hex_string()).unwrap(); + let create_update_return = create_update_message(updated_process.commitment_tx.to_string(), root.to_lower_hex_string()).unwrap(); - let (root_outpoint, alice_init_process) = create_update_return.updated_process.unwrap(); - alice_process_cache.insert(root_outpoint.clone(), alice_init_process); + let updated_process = create_update_return.updated_process.unwrap(); + alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); debug!("Alice pairs her device"); - pair_device(root_outpoint, vec![helper_get_bob_address()]).unwrap(); + pair_device(updated_process.commitment_tx.to_string(), vec![helper_get_bob_address()]).unwrap(); let alice_to_bob_cipher = &create_update_return.ciphers_to_send[0]; @@ -208,9 +209,9 @@ fn test_pairing() { debug!("Bob receives the update prd"); let bob_parsed_return = parse_cipher(alice_to_bob_cipher.to_owned()).unwrap(); - let (root_commitment, relevant_process) = bob_parsed_return.updated_process.unwrap(); + let updated_process = bob_parsed_return.updated_process.unwrap(); - bob_process_cache.insert(root_commitment.clone(), relevant_process); + bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); let prd_confirm_cipher = bob_parsed_return.ciphers_to_send.iter().next().unwrap(); @@ -238,7 +239,7 @@ fn test_pairing() { let commit_msg = alice_parsed_confirm.commit_to_send.unwrap(); // Take the relevant state out of the process - let relevant_process = alice_process_cache.get(&commit_msg.init_tx).unwrap(); + let relevant_process = alice_process_cache.get(&OutPoint::from_str(&commit_msg.init_tx).unwrap()).unwrap(); let concurrent_states = relevant_process.get_latest_concurrent_states().unwrap(); let relevant_state = concurrent_states.into_iter().find(|s| s.pcd_commitment == commit_msg.pcd_commitment).unwrap(); @@ -248,11 +249,11 @@ fn test_pairing() { // Alice can also sign her response and send it to Bob let validate_state_return = validate_state(commit_msg.init_tx, root.to_lower_hex_string()).unwrap(); - let (outpoint, validated_process) = validate_state_return.updated_process.unwrap(); + let updated_process = validate_state_return.updated_process.unwrap(); - alice_process_cache.insert(outpoint.clone(), validated_process); + alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let alice_response = create_response_prd(outpoint, root.to_lower_hex_string()).unwrap(); + let alice_response = create_response_prd(updated_process.commitment_tx.to_string(), root.to_lower_hex_string()).unwrap(); // ======================= Bob reset_device().unwrap(); @@ -263,17 +264,17 @@ fn test_pairing() { debug!("Bob parses Alice's pcd"); let bob_parsed_pcd_return = parse_cipher(bob_received_pcd).unwrap(); - let (root_outpoint, updated_process) = bob_parsed_pcd_return.updated_process.unwrap(); + let updated_process = bob_parsed_pcd_return.updated_process.unwrap(); // Here we would update our database bob_process_cache.insert( - root_outpoint.clone(), - updated_process + updated_process.commitment_tx, + updated_process.current_process ); // At this point, user must validate the pairing proposal received from Alice // We decrypt the content of the pcd so that we can display to user what matters - let alice_proposal = get_update_proposals(root_outpoint.clone()).unwrap().decrypted_pcds; + let alice_proposal = get_update_proposals(updated_process.commitment_tx.to_string()).unwrap().decrypted_pcds; debug!("Alice proposal: {:#?}", alice_proposal); @@ -312,18 +313,18 @@ fn test_pairing() { // If user is ok, we can add our own validation token // Get the whole commitment from the process - let bob_validated_process = validate_state(root_outpoint.clone(), pcd_commitment_root.to_string()).unwrap(); + let bob_validated_process = validate_state(updated_process.commitment_tx.to_string(), pcd_commitment_root.to_string()).unwrap(); - let (_, validated_process) = bob_validated_process.updated_process.unwrap(); + let updated_process = bob_validated_process.updated_process.unwrap(); - bob_process_cache.insert(root_outpoint.clone(), validated_process); + bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let bob_response = create_response_prd(root_outpoint.clone(), pcd_commitment_root.to_string()).unwrap(); + let bob_response = create_response_prd(updated_process.commitment_tx.to_string(), pcd_commitment_root.to_string()).unwrap(); let ciphers = bob_response.ciphers_to_send; // We would send it to Alice to let her know we agree debug!("Bob pairs device with Alice"); - pair_device(root_outpoint, proposal_members).unwrap(); + pair_device(updated_process.commitment_tx.to_string(), proposal_members).unwrap(); // We can also check alice response let parsed_alice_response = parse_cipher(alice_response.ciphers_to_send[0].clone()).unwrap(); From ed7c5e5b0c604715437817ad2dc05fe5ad011d19 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 29 Nov 2024 11:52:30 +0100 Subject: [PATCH 20/43] Minor updates --- src/api.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api.rs b/src/api.rs index 0d93712..b90f2e9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -860,7 +860,7 @@ fn handle_prd( return Err(anyhow::Error::msg(format!("No available secrets for member {:?}", member))); } - let clear_pcd = relevant_state.decrypt_pcd(); + let clear_pcd = relevant_state.decrypt_pcd()?; let roles = clear_pcd.extract_roles()?; @@ -1314,7 +1314,7 @@ pub fn create_update_message( let update_state = process.get_state_for_commitments_root(merkle_root_bin)?; // 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(); + let clear_state = update_state.decrypt_pcd()?.to_value_object()?; let roles = Value::Object(clear_state).extract_roles()?; @@ -1476,7 +1476,7 @@ pub fn create_response_prd(init_commitment: String, merkle_root_hex: String) -> let update_state: &mut ProcessState = process.get_state_for_commitments_root(merkle_root)?; // 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(); + let clear_state = update_state.decrypt_pcd()?.to_value_object()?; let roles = Value::Object(clear_state).extract_roles()?; From f95b6473e5857c3da1ff3cbefea1d04a0bd5833f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 29 Nov 2024 12:24:05 +0100 Subject: [PATCH 21/43] Add PcdUpdates and UserDiff --- src/api.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/src/api.rs b/src/api.rs index b90f2e9..7106e89 100644 --- a/src/api.rs +++ b/src/api.rs @@ -52,7 +52,7 @@ use sdk_common::sp_client::silentpayments::{ use sdk_common::{signature, MutexExt, MAX_PRD_PAYLOAD_SIZE}; use serde_json::{Error as SerdeJsonError, Map, Value}; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Serialize}; use tsify::{JsValueSerdeExt, Tsify}; use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi}; use wasm_bindgen::prelude::*; @@ -96,7 +96,6 @@ pub struct ApiReturn { pub new_tx_to_send: Option, pub ciphers_to_send: Vec, pub commit_to_send: Option, - pub decrypted_pcds: HashMap, } pub type ApiResult = Result; @@ -1572,31 +1571,53 @@ pub fn create_faucet_msg() -> ApiResult { Ok(faucet_msg.to_string()) } +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +struct UserDiff { + new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state + field: String, + previous_value: Value, + new_value: Value, + notify_user: bool, + need_validation: bool, + proof: Option, // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such +} + +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct PcdUpdates { + pub previous_pcd: Option, // We don't have a previous state for creation + pub decrypted_pcds: HashMap, // Key is the merkle root of the whole state + pub modified_values: Vec, +} + /// 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()?; - // TODO: We clone the process to prevent double borrowing issue, this can certainly be improved let relevant_process = processes .get(&outpoint) .ok_or(ApiError::new("process not found".to_owned()))?; let mut decrypted_pcds = HashMap::new(); + let mut modified_values = Vec::new(); - // We first push the last commited state, if any - match relevant_process.get_latest_commited_state() { + let previous_pcd = match relevant_process.get_latest_commited_state() { Some(state) => { let mut decrypted_pcd = Map::new(); - state.encrypted_pcd.decrypt_fields(&state.keys, &mut decrypted_pcd); - let root = ::create_merkle_tree(&state.pcd_commitment)?.root().unwrap(); - decrypted_pcds.insert(root.to_lower_hex_string(), Value::Object(decrypted_pcd)); + state.encrypted_pcd.decrypt_fields(&state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd); + Some(Value::Object(decrypted_pcd)) } - None => () - } + None => None + }; + + let member = lock_local_device()?.to_member(); for state in relevant_process.get_latest_concurrent_states()? { if state.encrypted_pcd == Value::Null { @@ -1604,14 +1625,52 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { continue; } + let fields_to_validate = state.get_fields_to_validate_for_member(&member)?; + let mut decrypted_pcd = Map::new(); - state.encrypted_pcd.decrypt_fields(&state.keys, &mut decrypted_pcd)?; - let root = ::create_merkle_tree(&state.pcd_commitment)?.root().unwrap(); - decrypted_pcds.insert(root.to_lower_hex_string(), Value::Object(decrypted_pcd)); + state.encrypted_pcd.decrypt_fields(&state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd)?; + let root = state.pcd_commitment.create_merkle_tree()?.root_hex().unwrap(); + + if let Some(ref previous_state) = previous_pcd { + for (key, value) in &decrypted_pcd { + let previous_value = previous_state.get(key).or_else(|| Some(&Value::Null)).unwrap(); + if previous_value == value { continue; } + let need_validation = if fields_to_validate.iter().any(|f| *key == **f) { true } else { false }; + let notify_user = if need_validation { true } else if !value.is_hex_string() { true } else { false }; + let diff = UserDiff { + new_state_merkle_root: root.clone(), + field: key.to_owned(), + previous_value: previous_value.clone(), + new_value: value.clone(), + need_validation, + notify_user, + proof: None + }; + modified_values.push(diff); + } + } else { + for (key, value) in &decrypted_pcd { + let need_validation = if fields_to_validate.iter().any(|f| *key == **f) { true } else { false }; + let notify_user = if need_validation { true } else if !value.is_hex_string() { true } else { false }; + let diff = UserDiff { + new_state_merkle_root: root.clone(), + field: key.to_owned(), + previous_value: Value::Null, + new_value: value.clone(), + need_validation, + notify_user, + proof: None + }; + modified_values.push(diff); + } + + } + decrypted_pcds.insert(root, Value::Object(decrypted_pcd)); } - Ok(ApiReturn { - decrypted_pcds, - ..Default::default() + Ok(PcdUpdates { + previous_pcd, + decrypted_pcds, + modified_values, }) } From 544c3256d69a36bc773e7d681e0bc9c96e83e01f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 30 Nov 2024 20:01:40 +0100 Subject: [PATCH 22/43] [bug] pairing: `roles` needs validation when pairing --- src/api.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api.rs b/src/api.rs index 7106e89..e31a987 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1617,7 +1617,10 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { None => None }; - let member = lock_local_device()?.to_member(); + let device = lock_local_device()?; + let member = device.to_member(); + + let is_pairing = device.get_pairing_commitment().is_none(); for state in relevant_process.get_latest_concurrent_states()? { if state.encrypted_pcd == Value::Null { @@ -1635,7 +1638,7 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { for (key, value) in &decrypted_pcd { let previous_value = previous_state.get(key).or_else(|| Some(&Value::Null)).unwrap(); if previous_value == value { continue; } - let need_validation = if fields_to_validate.iter().any(|f| *key == **f) { true } else { false }; + let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) }; let notify_user = if need_validation { true } else if !value.is_hex_string() { true } else { false }; let diff = UserDiff { new_state_merkle_root: root.clone(), @@ -1650,7 +1653,7 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { } } else { for (key, value) in &decrypted_pcd { - let need_validation = if fields_to_validate.iter().any(|f| *key == **f) { true } else { false }; + let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) }; let notify_user = if need_validation { true } else if !value.is_hex_string() { true } else { false }; let diff = UserDiff { new_state_merkle_root: root.clone(), From 1d5869561ae3a9ab0c7a08361d0e70e1fe439e09 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 30 Nov 2024 20:02:55 +0100 Subject: [PATCH 23/43] [bug] take care of redundant local address when pairing --- src/api.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/api.rs b/src/api.rs index e31a987..49db961 100644 --- a/src/api.rs +++ b/src/api.rs @@ -290,16 +290,18 @@ pub fn is_linking() -> ApiResult { pub fn pair_device(commitment_tx: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; - if local_device.is_linked() { + if local_device.get_pairing_commitment().is_some() { return Err(ApiError::new("Already paired".to_owned())); } - sp_addresses.push( - local_device - .get_wallet() - .get_client() - .get_receiving_address(), - ); + let local_address = local_device + .get_wallet() + .get_client() + .get_receiving_address(); + + if !sp_addresses.iter().any(|a| *a == local_address) { + sp_addresses.push(local_address); + } local_device.pair( OutPoint::from_str(&commitment_tx)?, From bb1e5afb2f335eb4b8f158fbcb20af17091a48b7 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 30 Nov 2024 20:03:20 +0100 Subject: [PATCH 24/43] Replace is_linked() by is_paired() --- src/api.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/api.rs b/src/api.rs index 49db961..ae16a12 100644 --- a/src/api.rs +++ b/src/api.rs @@ -276,14 +276,10 @@ pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult ApiResult { +pub fn is_paired() -> ApiResult { let local_device = lock_local_device()?; - if local_device.is_linked() || local_device.is_linking() { - Ok(true) - } else { - Ok(false) - } + Ok(local_device.get_pairing_commitment().is_some()) } #[wasm_bindgen] From 4362012fcbeb265c3da486e75dadfcadc987d29f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 30 Nov 2024 23:29:48 +0100 Subject: [PATCH 25/43] Add evaluate_state api --- src/api.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/api.rs b/src/api.rs index ae16a12..513f360 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1413,6 +1413,26 @@ pub fn refuse_state(init_commitment: String, merkle_root_hex: String) -> ApiResu add_validation_token(init_commitment, merkle_root_hex, false) } +#[wasm_bindgen] +pub fn evaluate_state(init_commitment: String, previous_state: Option, state: String) -> ApiResult { + let prev_state: Option = if let Some(s) = previous_state { Some(serde_json::from_str(&s)?) } else { None }; + let process_state: ProcessState = serde_json::from_str(&state)?; + + process_state.is_valid(prev_state.as_ref())?; + + let clear_pcd = process_state.decrypt_pcd()?; + let roles = clear_pcd.extract_roles()?; + + // We create a commit msg with the valid state + let outpoint: OutPoint = OutPoint::from_str(&init_commitment)?; + let commit_msg = CommitMessage::new_update_commitment(outpoint, process_state.pcd_commitment, roles); + + Ok(ApiReturn { + commit_to_send: Some(commit_msg), + ..Default::default() + }) +} + fn add_validation_token(init_commitment: String, merkle_root_hex: String, approval: bool) -> ApiResult { let mut merkle_root = [0u8; 32]; let pcd_commitment_vec = Vec::from_hex(&merkle_root_hex)?; @@ -1440,11 +1460,17 @@ fn add_validation_token(init_commitment: String, merkle_root_hex: String, approv let sp_wallet = local_device.get_wallet(); let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); + // we copy the state before modifying it + let previous_state = update_state.clone(); + update_state.validation_tokens.push(proof); + let new_state = update_state.clone(); + let updated_process = UpdatedProcess { commitment_tx: OutPoint::from_str(&init_commitment)?, current_process: process.clone(), + modified_state: Some((previous_state, new_state)), ..Default::default() }; From 8a8eaec4fdfe19e341b93ecd3f9e2bf03bfa2650 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 6 Dec 2024 15:32:04 +0100 Subject: [PATCH 26/43] Update to latest api --- src/api.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/api.rs b/src/api.rs index 513f360..22d958f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -63,7 +63,7 @@ use sdk_common::network::{ NewTxMessage, }; use sdk_common::pcd::{ - compare_maps, AnkPcdHash, AnkPcdTag, Member, Pcd, RoleDefinition, ValidationRule, + AnkPcdHash, AnkPcdTag, Member, Pcd, RoleDefinition, ValidationRule, }; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; @@ -84,7 +84,6 @@ pub struct UpdatedProcess { pub current_process: Process, pub new_state: Option, pub modified_state: Option<(ProcessState, ProcessState)>, // first the previous state, then the current one - pub user_validation_required: bool, } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] @@ -941,7 +940,7 @@ fn handle_prd( 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 // 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 encrypted_pcd_commitments = pcd.hash_all_fields(OutPoint::null())?; let encrypted_pcd_root = ::create_merkle_tree(&Value::Object(encrypted_pcd_commitments))?.root().unwrap().to_lower_hex_string(); let mut processes = lock_processes()?; let updated_prd: Prd; @@ -1142,7 +1141,7 @@ pub fn create_new_process( relay_address: String, fee_rate: u32, ) -> ApiResult { - let pcd = ::from_string(&init_state)?; + let pcd = ::new_from_string(&init_state)?; // check that we have a proper roles map let roles = pcd.extract_roles()?; @@ -1178,7 +1177,7 @@ pub fn create_new_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)?; + let fields_commitment = pcd.hash_all_fields(outpoint)?; let mut process = Process::new(outpoint); @@ -1238,7 +1237,7 @@ pub fn update_process( let new_state_val = Value::from_str(&new_state)?; // We hash all the new values - let pcd_commitment = Value::Object(new_state_val.hash_fields(outpoint)?); + let pcd_commitment = Value::Object(new_state_val.hash_all_fields(outpoint)?); let new_state_merkle_root = ::create_merkle_tree(&pcd_commitment)?.root().unwrap(); // We compare the new state with the previous one @@ -1329,7 +1328,6 @@ pub fn create_update_message( .flat_map(|rule| rule.fields.clone()) .collect(); for member in role.members { - debug!("member: {:?}", member); // Check that we have a shared_secret with all members if let Some(no_secret_address) = member.get_addresses().iter() .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) @@ -1352,7 +1350,7 @@ pub fn create_update_message( // 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_hash = update_state.encrypted_pcd.hash_all_fields(OutPoint::null())?; let encrypted_pcd_merkle_root = ::create_merkle_tree(&Value::Object(encrypted_pcd_hash))?.root().unwrap(); let full_prd = Prd::new_update( @@ -1605,6 +1603,7 @@ struct UserDiff { new_value: Value, notify_user: bool, need_validation: bool, + // validated: bool, proof: Option, // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such } @@ -1615,6 +1614,7 @@ pub struct PcdUpdates { pub previous_pcd: Option, // We don't have a previous state for creation pub decrypted_pcds: HashMap, // Key is the merkle root of the whole state pub modified_values: Vec, + // pub proofs: HashMap>, // key is the merkle root of the whole state, } /// Get active update proposals for a given process outpoint @@ -1635,7 +1635,7 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { let previous_pcd = match relevant_process.get_latest_commited_state() { Some(state) => { let mut decrypted_pcd = Map::new(); - state.encrypted_pcd.decrypt_fields(&state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd); + state.encrypted_pcd.decrypt_all(state.commited_in, &state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd); Some(Value::Object(decrypted_pcd)) } None => None @@ -1655,7 +1655,7 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { let fields_to_validate = state.get_fields_to_validate_for_member(&member)?; let mut decrypted_pcd = Map::new(); - state.encrypted_pcd.decrypt_fields(&state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd)?; + state.encrypted_pcd.decrypt_all(state.commited_in, &state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd)?; let root = state.pcd_commitment.create_merkle_tree()?.root_hex().unwrap(); if let Some(ref previous_state) = previous_pcd { @@ -1663,7 +1663,7 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { let previous_value = previous_state.get(key).or_else(|| Some(&Value::Null)).unwrap(); if previous_value == value { continue; } let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) }; - let notify_user = if need_validation { true } else if !value.is_hex_string() { true } else { false }; + let notify_user = if need_validation { true } else if value.is_hex_string(Some(32)).is_err() { true } else { false }; let diff = UserDiff { new_state_merkle_root: root.clone(), field: key.to_owned(), @@ -1678,7 +1678,7 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult { } else { for (key, value) in &decrypted_pcd { let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) }; - let notify_user = if need_validation { true } else if !value.is_hex_string() { true } else { false }; + let notify_user = if need_validation { true } else if value.is_hex_string(Some(32)).is_err() { true } else { false }; let diff = UserDiff { new_state_merkle_root: root.clone(), field: key.to_owned(), From af51c9308769d1fca29a3d0e8a5e0514a46a73a1 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 6 Dec 2024 15:32:27 +0100 Subject: [PATCH 27/43] Fix pairing test : add storages to RoleDefinition --- tests/pairing.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/pairing.rs b/tests/pairing.rs index 2fc9d31..922a117 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -154,7 +154,8 @@ fn test_pairing() { ], "min_sig_member": 1.0 } - ] + ], + "storages": [] } }, "session_privkey": initial_session_privkey, From 8f089fd3db05d975e6d65f2413f5925911d010dd Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:29:42 +0100 Subject: [PATCH 28/43] Define UserDiff --- src/api.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/api.rs b/src/api.rs index 22d958f..1af5849 100644 --- a/src/api.rs +++ b/src/api.rs @@ -76,6 +76,30 @@ use sdk_common::secrets::SecretsStore; use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub enum DiffStatus { + #[default] + None, + Rejected, + Validated, +} + +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct UserDiff { + pub new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state + pub value_commitment: String, + pub field: String, + pub previous_value: Value, + pub new_value: Value, + pub notify_user: bool, + pub need_validation: bool, + pub validation_status: DiffStatus, +} + #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] From 1bf8d919911fa8276c882c6c5d3660d7e30b2042 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:30:14 +0100 Subject: [PATCH 29/43] Update UpdatedProcess with UserDiff --- src/api.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 1af5849..b844315 100644 --- a/src/api.rs +++ b/src/api.rs @@ -106,8 +106,9 @@ pub struct UserDiff { pub struct UpdatedProcess { pub commitment_tx: OutPoint, pub current_process: Process, - pub new_state: Option, - pub modified_state: Option<(ProcessState, ProcessState)>, // first the previous state, then the current one + pub new_diffs: Vec, // All diffs should have the same new_state_merkle_root + pub modified_state: Option, // basically when we add/receive validation proofs for a state + // I think we should never have both new_state and modified_state } #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] From 3ed94c7538e75e3419217455bb2e214f8c76346a Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:34:00 +0100 Subject: [PATCH 30/43] Update parse_cipher for prd update --- src/api.rs | 184 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 110 insertions(+), 74 deletions(-) diff --git a/src/api.rs b/src/api.rs index b844315..151683d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -893,36 +893,37 @@ fn handle_prd( ..Default::default() }) } - PrdType::Update | PrdType::TxProposal | PrdType::Message => { - // Those all have some new data we don't know about yet - // We send a Confirm to get the pcd - // Add the prd to our list of actions for this process - relevant_process.insert_impending_request(prd.clone()); - let member: Member = serde_json::from_str(&prd.sender)?; - let mut ciphers = vec![]; - for address in member.get_addresses() { - if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) { - let cipher = confirm_prd(&prd, &shared_secret)?; - ciphers.push(cipher); - } else { - // For now we don't fail if we're missing an address for a member but maybe we should - warn!("Failed to find secret for address {}", address); - } + PrdType::Update => { + // Compute the merkle tree root for the proposed new state to see if we already know about it + let update_merkle_root = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?.to_lower_hex_string(); + if relevant_process.get_state_for_commitments_root(&update_merkle_root).is_ok() { + // We already know about that state + return Err(AnyhowError::msg("Received update for a state we already know")); } - // This should never happen - if ciphers.is_empty() { - return Err(anyhow::Error::msg(format!("No available secrets for member {:?}", member))); - } + let new_state = ProcessState { + commited_in: OutPoint::from_str(&prd.root_commitment)?, + pcd_commitment: prd.pcd_commitments, + merkle_root: update_merkle_root.clone(), + keys: prd.keys, + ..Default::default() + }; + + // Compute the diffs + // At this point we don't have the encrypted values + // But it can still be useful to track diffs + let diffs = create_diffs(&relevant_process, &new_state)?; + + relevant_process.insert_concurrent_state(new_state); let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: relevant_process.clone(), + new_diffs: diffs, ..Default::default() }; return Ok(ApiReturn { - ciphers_to_send: ciphers, updated_process: Some(updated_process), ..Default::default() }); @@ -936,9 +937,6 @@ fn handle_prd( }) .ok_or(anyhow::Error::msg("Original request not found"))?; - // Once we found the prd update, we can add the received proofs as validation tokens - let previous_state = to_update.clone(); - to_update .validation_tokens .extend(prd.validation_tokens); @@ -949,7 +947,7 @@ fn handle_prd( let updated_process = UpdatedProcess { commitment_tx: OutPoint::from_str(&prd.root_commitment)?, current_process: relevant_process.clone(), - modified_state: Some((previous_state, updated_state.clone())), + modified_state: Some(updated_state.merkle_root), ..Default::default() }; @@ -962,54 +960,6 @@ fn handle_prd( } } -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 - // We pass an empty Outpoint as salt, as there's no point salting hash of encrypted values - let encrypted_pcd_commitments = pcd.hash_all_fields(OutPoint::null())?; - let encrypted_pcd_root = ::create_merkle_tree(&Value::Object(encrypted_pcd_commitments))?.root().unwrap().to_lower_hex_string(); - let mut processes = lock_processes()?; - let updated_prd: Prd; - 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 == encrypted_pcd_root) - { - // We update the process and return it - prd.payload = pcd.to_string(); - updated_prd = prd.clone(); - // We can now safely mark the prd to be remove from the process - prd.prd_type = PrdType::None; - } else { - continue; - } - let new_state = ProcessState { - commited_in: *outpoint, - pcd_commitment: updated_prd.pcd_commitments, - encrypted_pcd: pcd, - keys: updated_prd.keys, - validation_tokens: vec![] - }; - process.insert_concurrent_state(new_state.clone())?; - process.prune_impending_requests(); - - let udpated_process = UpdatedProcess { - commitment_tx: *outpoint, - current_process: process.clone(), - new_state: Some(new_state), - ..Default::default() - }; - - return Ok(ApiReturn { - updated_process: Some(udpated_process), - ..Default::default() - }); - } - - Err(anyhow::Error::msg("Failed to find matching prd")) -} - fn handle_decrypted_message( secret: AnkSharedSecretHash, plain: Vec, @@ -1017,13 +967,99 @@ fn handle_decrypted_message( let local_address: SilentPaymentAddress = lock_local_device()?.get_wallet().get_client().get_receiving_address().try_into()?; if let Ok(prd) = Prd::extract_from_message(&plain, local_address) { handle_prd(prd, secret) - } else if let Ok(pcd) = Value::from_str(&String::from_utf8(plain)?) { - handle_pcd(pcd) } else { Err(anyhow::Error::msg("Failed to handle decrypted message")) } } +#[wasm_bindgen] +/// Use the provided Map to update a state +/// The map uses hash commitment as keys, as in storage +pub fn update_process_state(init_commitment: String, state_id: String, hash2values: String) -> ApiResult { + let hash2values_map = serde_json::from_str::(&hash2values)?.to_value_object()?; + + // Get the process + let outpoint = OutPoint::from_str(&init_commitment)?; + + let mut processes = lock_processes()?; + { + // First a mutable borrow of the process + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + // Get the state + let state = process.get_latest_concurrent_states_mut()? + .into_iter() + .find(|state| state.merkle_root == state_id) + .ok_or(ApiError::new("Unknown state".to_owned()))?; + + // Update each value + // Check if there's already something + // If we have the key, decrypt and compare to the commitment + if state.encrypted_pcd.as_object().is_some() && !state.encrypted_pcd.as_object().unwrap().is_empty() { + return Err(ApiError::new("State already existing".to_owned())); + } + let state_commitments = state.pcd_commitment.to_value_object()?; + let mut new_encrypted_pcd: Map = Map::with_capacity(hash2values_map.len()); + + for (hash, value) in hash2values_map { + // Check the hash in pcd_commitment, get the corresponding field name + let (field, _) = state_commitments.iter().find(|(field, commitment)| *hash == **commitment) + .ok_or(ApiError::new(format!("Failed to find the commitment {}", hash)))?; + + new_encrypted_pcd.insert(field.clone(), value); + } + + // decrypt all we can and check it matches the commitment + state.encrypted_pcd = Value::Object(new_encrypted_pcd); + let commited_in = serialize(&state.commited_in); + + let clear_pcd = state.decrypt_pcd()?; + + for (i, (key, value)) in clear_pcd.iter().enumerate() { + // hash each value, and check the result against commitments + if let Some(expected) = state_commitments.get(key.as_str()) { + // value can already be the commitment, if we don't have the encryption key + if value.is_hex_string(Some(32)).is_ok() { + // check if the clear value is the commitment + if expected.as_str().unwrap() == value.as_str().unwrap() { continue; } + } + // Otherwise we hash the value whatever it is, it must match the commitment + let mut value_bin = value.to_string().into_bytes(); + value_bin.push(i.try_into().unwrap()); + let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &commited_in); + if tagged_hash.as_byte_array().to_lower_hex_string() != expected.as_str().unwrap() { + // We set the encrypted pcd back to empty + state.encrypted_pcd = Value::Object(Map::new()); + return Err(ApiError::new(format!("Retrieved value for {} doesn't match the commitment", key))); + } + } else { + // This shouldn't be possible + state.encrypted_pcd = Value::Object(Map::new()); + return Err(ApiError::new(format!("Missing commitment for key {}", key))); + } + } + } + + // If every value we can decrypt is valid, then we return the new state and diffs + // We borrow it again immutably + let process = processes.get(&outpoint).unwrap(); + let state = process.get_latest_concurrent_states()?.into_iter().find(|s| s.merkle_root == state_id).unwrap(); + let diffs = create_diffs(&process, &state)?; + + let udpated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: process.clone(), + new_diffs: diffs, + ..Default::default() + }; + + Ok(ApiReturn { + updated_process: Some(udpated_process), + ..Default::default() + }) +} + #[wasm_bindgen] pub fn parse_cipher(cipher_msg: String) -> ApiResult { // Check that the cipher is not empty or too long From 0c702de95372d4816d4df14951d50c250255b3da Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:34:51 +0100 Subject: [PATCH 31/43] Rm handling of prd confirm --- src/api.rs | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/src/api.rs b/src/api.rs index 151683d..f8ae639 100644 --- a/src/api.rs +++ b/src/api.rs @@ -850,49 +850,6 @@ fn handle_prd( }; match prd.prd_type { - PrdType::Confirm => { - // It must match a prd we sent previously - // We send the whole data in a pcd - debug!("Received confirm prd with commitments {:#?}", prd.pcd_commitments); - let relevant_state = relevant_process - .get_latest_concurrent_states()? - .into_iter() - .find(|state| { - state.pcd_commitment == prd.pcd_commitments - }) - .ok_or(anyhow::Error::msg("Original request not found"))?; - - let member: Member = serde_json::from_str(&prd.sender)?; - - // We send the data to all addresses of the member we know a secret for - let mut ciphers = vec![]; - for address in member.get_addresses() { - if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) { - let cipher = encrypt_with_key(shared_secret.as_byte_array(), serde_json::to_string(&relevant_state.encrypted_pcd)?.as_bytes())?; - ciphers.push(cipher.to_lower_hex_string()); - } else { - // For now we don't fail if we're missing an address for a member but maybe we should - warn!("Failed to find secret for address {}", address); - } - } - - // This should never happen since we sent a message to get a confirmation back - if ciphers.is_empty() { - return Err(anyhow::Error::msg(format!("No available secrets for member {:?}", member))); - } - - let clear_pcd = relevant_state.decrypt_pcd()?; - - let roles = clear_pcd.extract_roles()?; - - let commit_msg = CommitMessage::new_update_commitment(outpoint, relevant_state.pcd_commitment.clone(), roles); - - return Ok(ApiReturn { - ciphers_to_send: ciphers, - commit_to_send: Some(commit_msg), - ..Default::default() - }) - } PrdType::Update => { // Compute the merkle tree root for the proposed new state to see if we already know about it let update_merkle_root = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?.to_lower_hex_string(); From 2afc6c9a85a33dd220ce9bb288686380200e2588 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:35:50 +0100 Subject: [PATCH 32/43] Update create_new_process --- src/api.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/api.rs b/src/api.rs index f8ae639..b9f3a1d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1195,19 +1195,24 @@ pub fn create_new_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_all_fields(outpoint)?; + let pcd_commitment = Value::Object(pcd.hash_all_fields(outpoint)?); + + let merkle_root = pcd_commitment.create_merkle_tree()?.root().ok_or(ApiError::new("Invalid merkle tree".to_owned()))?.to_lower_hex_string(); let mut process = Process::new(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()), + pcd_commitment: pcd_commitment.clone(), + merkle_root: merkle_root.clone(), encrypted_pcd: Value::Object(fields2cipher.clone()), keys: fields2keys.clone(), validation_tokens: vec![], }; + let diffs = create_diffs(&process, &process_state)?; + process.insert_concurrent_state(process_state.clone())?; { @@ -1219,12 +1224,12 @@ pub fn create_new_process( processes.insert(outpoint.clone(), process.clone()); } - let commit_msg = CommitMessage::new_first_commitment(transaction, Value::Object(fields_commitment), roles); + let commit_msg = CommitMessage::new_first_commitment(transaction, pcd_commitment, roles); let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process, - new_state: Some(process_state), + new_diffs: diffs, ..Default::default() }; From 166c7c993f9da8ddd49c5d06f101a4ba549e57f6 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:36:35 +0100 Subject: [PATCH 33/43] Add create_diffs --- src/api.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/api.rs b/src/api.rs index b9f3a1d..066ee64 100644 --- a/src/api.rs +++ b/src/api.rs @@ -772,6 +772,76 @@ fn confirm_prd(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult AnyhowResult> { + let new_state_commitments = new_state.pcd_commitment.as_object().ok_or(AnyhowError::msg("new_state commitments is not an object"))?; + + let device = lock_local_device()?; + let our_id = device.to_member(); + let is_pairing = device.get_pairing_commitment().is_none(); + + let fields_to_validate = if new_state.encrypted_pcd != Value::Null { + new_state.get_fields_to_validate_for_member(&our_id)? + } else { + vec![] + }; + + let new_state_root = &new_state.merkle_root; + let new_state_decrypted = match new_state.decrypt_pcd() { + Ok(val) => val, + Err(_) => Map::new() + }; + + let mut diffs = vec![]; + if let Some(prev_state) = process.get_latest_commited_state() { + // We first decrypt as much as we can of the prev_state + let clear_prev_state = prev_state.decrypt_pcd()?; + // We just make a diff for values that are different from previous state + for (field, prev_hash) in prev_state.pcd_commitment.as_object().unwrap() { + if let Some(new_hash) = new_state_commitments.get(field.as_str()) { + if new_hash.as_str() == prev_hash.as_str() { + continue; + } else { + // There's a diff + let previous_value = clear_prev_state.get(field.as_str()).unwrap().clone(); + let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; + let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; + diffs.push(UserDiff { + new_state_merkle_root: new_state_root.to_owned(), + value_commitment: new_hash.as_str().unwrap().to_string(), + field: field.to_owned(), + previous_value, + new_value, + notify_user: false, + need_validation, + validation_status: DiffStatus::None, + }); + } + } else { + // We're missing a hash + return Err(AnyhowError::msg(format!("No commitment for field {} in new state", field))); + } + } + } else { + // All fields need a diff + for (field, hash) in new_state_commitments { + let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; + let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; + diffs.push(UserDiff { + new_state_merkle_root: new_state_root.to_owned(), + value_commitment: hash.as_str().unwrap().to_string(), + field: field.to_owned(), + previous_value: Value::Null, + new_value, + notify_user: false, + need_validation, + validation_status: DiffStatus::None, + }); + } + } + + Ok(diffs) +} + fn handle_prd_connect(prd: Prd, secret: AnkSharedSecretHash) -> AnyhowResult { let local_device = lock_local_device()?; let local_member = local_device.to_member(); From f789b3037281123d2ed31bd51496eb5d5f41240f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:37:59 +0100 Subject: [PATCH 34/43] Update update_process --- src/api.rs | 58 +++++++++++++++--------------------------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/src/api.rs b/src/api.rs index 066ee64..2c8cf32 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1314,7 +1314,7 @@ pub fn create_new_process( #[wasm_bindgen] pub fn update_process( init_commitment: String, - new_state: String, + new_state_str: String, ) -> ApiResult { let outpoint = OutPoint::from_str(&init_commitment)?; @@ -1322,46 +1322,29 @@ pub fn update_process( let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; - let last_state = process.get_latest_commited_state() + let prev_state = process.get_latest_commited_state() .ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?; - let last_state_commitments = &last_state.pcd_commitment; + let last_state_commitments = &prev_state.pcd_commitment; - let new_state_val = Value::from_str(&new_state)?; + let clear_new_state = Value::from_str(&new_state_str)?.to_value_object()?; - // We hash all the new values - let pcd_commitment = Value::Object(new_state_val.hash_all_fields(outpoint)?); - let new_state_merkle_root = ::create_merkle_tree(&pcd_commitment)?.root().unwrap(); + let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.clone())?; // We compare the new state with the previous one - let last_state_merkle_root = ::create_merkle_tree(last_state_commitments)?.root().unwrap(); + let last_state_merkle_root = &prev_state.merkle_root; - if last_state_merkle_root == new_state_merkle_root { + if *last_state_merkle_root == new_state.merkle_root { return Err(ApiError::new("new proposed state is identical to the previous commited state".to_owned())); } - // We create the encrypted pcd - let mut fields2keys = Map::new(); - let mut fields2cipher = Map::new(); - let fields_to_encrypt: Vec = new_state_val - .as_object() - .unwrap() - .keys() - .map(|k| k.clone()) - .collect(); - new_state_val.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); + // We check that we don't have already a similar concurrent state + let concurrent_processes = process.get_latest_concurrent_states()?; + if concurrent_processes.iter().any(|p| p.merkle_root == new_state.merkle_root) { + return Err(ApiError::new("New state already known".to_owned())); + } - let encrypted_pcd = Value::Object(fields2cipher); - - let device = lock_local_device()?; - - let new_state = ProcessState { - commited_in: outpoint, - pcd_commitment: pcd_commitment, - encrypted_pcd: encrypted_pcd, - keys: fields2keys, - validation_tokens: vec![] - }; + let diffs = create_diffs(&process, &new_state)?; // Add the new state to the process process.insert_concurrent_state(new_state.clone())?; @@ -1369,7 +1352,7 @@ pub fn update_process( let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process.clone(), - new_state: Some(new_state), + new_diffs: diffs, ..Default::default() }; @@ -1391,19 +1374,10 @@ pub fn create_update_message( let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; - let mut merkle_root_bin = [0u8; 32]; - let merkle_root_vec = Vec::from_hex(&merkle_root_hex)?; - - if merkle_root_vec.len() != 32 { - return Err(ApiError::new("merkle root must be 32B long".to_owned())); - } - - merkle_root_bin.copy_from_slice(&merkle_root_vec); - - let update_state = process.get_state_for_commitments_root(merkle_root_bin)?; + let update_state = process.get_state_for_commitments_root(&merkle_root_hex)?; // 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()?.to_value_object()?; + let clear_state = update_state.decrypt_pcd()?; let roles = Value::Object(clear_state).extract_roles()?; From 92f4d62c88d71547af06d2720e5be4b6d2c8b68f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:38:36 +0100 Subject: [PATCH 35/43] Update add_validation_token --- src/api.rs | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/api.rs b/src/api.rs index 2c8cf32..12cfef0 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1499,13 +1499,6 @@ pub fn evaluate_state(init_commitment: String, previous_state: Option, s } fn add_validation_token(init_commitment: String, merkle_root_hex: String, approval: bool) -> ApiResult { - let mut merkle_root = [0u8; 32]; - let pcd_commitment_vec = Vec::from_hex(&merkle_root_hex)?; - if pcd_commitment_vec.len() != 32 { - return Err(ApiError::new("pcd_commitment must be 32B long".to_owned())); - } - merkle_root.copy_from_slice(&pcd_commitment_vec); - let mut processes = lock_processes()?; let outpoint = OutPoint::from_str(&init_commitment)?; @@ -1513,29 +1506,30 @@ fn add_validation_token(init_commitment: String, merkle_root_hex: String, approv let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; - let update_state: &mut ProcessState = process.get_state_for_commitments_root(merkle_root)?; + { + let update_state: &mut ProcessState = process.get_state_for_commitments_root_mut(&merkle_root_hex)?; - let message_hash = if approval { - AnkHash::ValidationYes(AnkValidationYesHash::from_byte_array(merkle_root)) - } else { - AnkHash::ValidationNo(AnkValidationNoHash::from_byte_array(merkle_root)) - }; + let mut merkle_root = [0u8; 32]; - let local_device = lock_local_device()?; - let sp_wallet = local_device.get_wallet(); - let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); + merkle_root.copy_from_slice(&Vec::from_hex(&merkle_root_hex)?); - // we copy the state before modifying it - let previous_state = update_state.clone(); + let message_hash = if approval { + AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(merkle_root)) + } else { + AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(merkle_root)) + }; - update_state.validation_tokens.push(proof); + let local_device = lock_local_device()?; + let sp_wallet = local_device.get_wallet(); + let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); - let new_state = update_state.clone(); + update_state.validation_tokens.push(proof); + } let updated_process = UpdatedProcess { commitment_tx: OutPoint::from_str(&init_commitment)?, current_process: process.clone(), - modified_state: Some((previous_state, new_state)), + modified_state: Some(merkle_root_hex), ..Default::default() }; From 1106d25ea1bac7e5045e2a0fadc2dd017879dc29 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:39:09 +0100 Subject: [PATCH 36/43] Update create_response_prd --- src/api.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/api.rs b/src/api.rs index 12cfef0..ae5cce9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1541,13 +1541,6 @@ fn add_validation_token(init_commitment: String, merkle_root_hex: String, approv #[wasm_bindgen] pub fn create_response_prd(init_commitment: String, merkle_root_hex: String) -> ApiResult { - let mut merkle_root = [0u8; 32]; - let pcd_commitment_vec = Vec::from_hex(&merkle_root_hex)?; - if pcd_commitment_vec.len() != 32 { - return Err(ApiError::new("pcd_commitment must be 32B long".to_owned())); - } - merkle_root.copy_from_slice(&pcd_commitment_vec); - let mut processes = lock_processes()?; let outpoint = OutPoint::from_str(&init_commitment)?; @@ -1555,10 +1548,10 @@ pub fn create_response_prd(init_commitment: String, merkle_root_hex: String) -> let process = processes.get_mut(&outpoint) .ok_or(ApiError::new("Unknown process".to_owned()))?; - let update_state: &mut ProcessState = process.get_state_for_commitments_root(merkle_root)?; + let update_state: &mut ProcessState = process.get_state_for_commitments_root_mut(&merkle_root_hex)?; // 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()?.to_value_object()?; + let clear_state = update_state.decrypt_pcd()?; let roles = Value::Object(clear_state).extract_roles()?; From a919c6d83d0894edf78b6711c57d65ae6cf45077 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:39:35 +0100 Subject: [PATCH 37/43] Rm dead code and minor fixes --- src/api.rs | 192 ++--------------------------------------------------- 1 file changed, 5 insertions(+), 187 deletions(-) diff --git a/src/api.rs b/src/api.rs index ae5cce9..8f8bd4a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -670,86 +670,6 @@ pub fn parse_new_tx(new_tx_msg: String, block_height: u32) -> ApiResult 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()))?; - -// 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 = Value::from_str(&prd_ref.payload)?; -// let pcd_hash: AnkPcdHash = AnkPcdHash::from_value(&pcd); - -// 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 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; -// } -// } -// } -// } - -// return Ok(ApiReturn { -// ciphers_to_send: ciphers, -// ..Default::default() -// }); -// } -// _ => unimplemented!(), -// }; -// } - fn confirm_prd(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { match prd.prd_type { PrdType::Confirm | PrdType::Response | PrdType::List => { @@ -1234,7 +1154,7 @@ pub fn create_new_process( // 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 + // 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 @@ -1486,7 +1406,7 @@ pub fn evaluate_state(init_commitment: String, previous_state: Option, s process_state.is_valid(prev_state.as_ref())?; let clear_pcd = process_state.decrypt_pcd()?; - let roles = clear_pcd.extract_roles()?; + let roles = Value::Object(clear_pcd).extract_roles()?; // We create a commit msg with the valid state let outpoint: OutPoint = OutPoint::from_str(&init_commitment)?; @@ -1647,111 +1567,9 @@ pub fn create_faucet_msg() -> ApiResult { Ok(faucet_msg.to_string()) } -#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] -#[tsify(into_wasm_abi)] -#[allow(non_camel_case_types)] -struct UserDiff { - new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state - field: String, - previous_value: Value, - new_value: Value, - notify_user: bool, - need_validation: bool, - // validated: bool, - proof: Option, // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such -} - -#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] -#[tsify(into_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct PcdUpdates { - pub previous_pcd: Option, // We don't have a previous state for creation - pub decrypted_pcds: HashMap, // Key is the merkle root of the whole state - pub modified_values: Vec, - // pub proofs: HashMap>, // key is the merkle root of the whole state, -} - -/// 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_storages(process_outpoint: String) -> ApiResult> { let outpoint = OutPoint::from_str(&process_outpoint)?; - - let mut processes = lock_processes()?; - - let relevant_process = processes - .get(&outpoint) - .ok_or(ApiError::new("process not found".to_owned()))?; - - let mut decrypted_pcds = HashMap::new(); - let mut modified_values = Vec::new(); - - let previous_pcd = match relevant_process.get_latest_commited_state() { - Some(state) => { - let mut decrypted_pcd = Map::new(); - state.encrypted_pcd.decrypt_all(state.commited_in, &state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd); - Some(Value::Object(decrypted_pcd)) - } - None => None - }; - - let device = lock_local_device()?; - let member = device.to_member(); - - let is_pairing = device.get_pairing_commitment().is_none(); - - for state in relevant_process.get_latest_concurrent_states()? { - if state.encrypted_pcd == Value::Null { - // This is the last empty state, ignore it - continue; - } - - let fields_to_validate = state.get_fields_to_validate_for_member(&member)?; - - let mut decrypted_pcd = Map::new(); - state.encrypted_pcd.decrypt_all(state.commited_in, &state.pcd_commitment.to_value_object()?, &state.keys, &mut decrypted_pcd)?; - let root = state.pcd_commitment.create_merkle_tree()?.root_hex().unwrap(); - - if let Some(ref previous_state) = previous_pcd { - for (key, value) in &decrypted_pcd { - let previous_value = previous_state.get(key).or_else(|| Some(&Value::Null)).unwrap(); - if previous_value == value { continue; } - let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) }; - let notify_user = if need_validation { true } else if value.is_hex_string(Some(32)).is_err() { true } else { false }; - let diff = UserDiff { - new_state_merkle_root: root.clone(), - field: key.to_owned(), - previous_value: previous_value.clone(), - new_value: value.clone(), - need_validation, - notify_user, - proof: None - }; - modified_values.push(diff); - } - } else { - for (key, value) in &decrypted_pcd { - let need_validation = if is_pairing && key.as_str() == "roles" { true } else { fields_to_validate.iter().any(|f| *key == **f) }; - let notify_user = if need_validation { true } else if value.is_hex_string(Some(32)).is_err() { true } else { false }; - let diff = UserDiff { - new_state_merkle_root: root.clone(), - field: key.to_owned(), - previous_value: Value::Null, - new_value: value.clone(), - need_validation, - notify_user, - proof: None - }; - modified_values.push(diff); - } - - } - decrypted_pcds.insert(root, Value::Object(decrypted_pcd)); - } - - Ok(PcdUpdates { - previous_pcd, - decrypted_pcds, - modified_values, - }) + + Ok(vec![]) } From 40dbc1b2bafed315bc966262342c70beda62998f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 11 Dec 2024 23:39:42 +0100 Subject: [PATCH 38/43] Update pairing tests --- tests/pairing.rs | 153 +++++++++++++++++++++-------------------------- 1 file changed, 67 insertions(+), 86 deletions(-) diff --git a/tests/pairing.rs b/tests/pairing.rs index 922a117..267b1c7 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -2,15 +2,15 @@ use std::collections::HashMap; use std::str::FromStr; use sdk_client::api::{ - create_device_from_sp_wallet, create_new_process, create_response_prd, create_update_message, dump_device, get_address, get_update_proposals, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup, validate_state + create_device_from_sp_wallet, create_new_process, create_response_prd, create_update_message, dump_device, get_address, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup, update_process_state, validate_state }; use sdk_common::crypto::AnkSharedSecretHash; use sdk_common::log::debug; -use sdk_common::pcd::{Member, Pcd}; -use sdk_common::sp_client::bitcoin::hex::DisplayHex; +use sdk_common::pcd::{Member, Pcd, RoleDefinition}; use sdk_common::secrets::SecretsStore; +use sdk_common::sp_client::bitcoin::hex::FromHex; use sdk_common::sp_client::bitcoin::OutPoint; -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; use wasm_bindgen_test::*; @@ -97,6 +97,8 @@ fn test_pairing() { let mut bob_process_cache = HashMap::new(); let mut alice_secrets_store = SecretsStore::new(); let mut bob_secrets_store = SecretsStore::new(); + let mut alice_diff_cache = Vec::new(); + let mut bob_diff_cache = Vec::new(); debug!("==============================================\nStarting test_pairing\n=============================================="); @@ -163,6 +165,7 @@ fn test_pairing() { "key_parity": true, // This allows us to use a 32 bytes array in serialization }); + debug!("Alice creates the pairing process"); let create_process_return = create_new_process(pairing_init_state.to_string(), RELAY_ADDRESS.to_owned(), 1).unwrap(); let commit_msg = create_process_return.commit_to_send.unwrap(); @@ -184,12 +187,18 @@ fn test_pairing() { let updated_process = create_process_return.updated_process.unwrap(); alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); + // Alice keeps track of the change she needs to validate + let create_process_diffs = updated_process.new_diffs; + + let new_state_id = &create_process_diffs.get(0).unwrap().new_state_merkle_root; + + alice_diff_cache.extend(create_process_diffs.iter()); + // We send the commit_msg to the relay we got the address from // now we create prd update for this new process - debug!("Alice sends an update prd to Bob"); - let root = ::create_merkle_tree(&commit_msg.pcd_commitment).unwrap().root().unwrap(); - let create_update_return = create_update_message(updated_process.commitment_tx.to_string(), root.to_lower_hex_string()).unwrap(); + debug!("Alice creates an update prd to Bob"); + let create_update_return = create_update_message(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); let updated_process = create_update_return.updated_process.unwrap(); alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); @@ -212,11 +221,40 @@ fn test_pairing() { let updated_process = bob_parsed_return.updated_process.unwrap(); + let parsed_prd_diffs = updated_process.new_diffs; + + // debug!("Bob creates process {} with state {}", updated_process.commitment_tx, new_state_id); bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let prd_confirm_cipher = bob_parsed_return.ciphers_to_send.iter().next().unwrap(); + // Bob also keeps track of changes - debug!("Bob sends a Confirm Prd to Alice"); + bob_diff_cache.extend(parsed_prd_diffs.into_iter()); + + debug!("Bob can now fetch the data from storage using the hashes"); + // We have to cheat here and let Bob access Alice process cache + let process = alice_process_cache.get(&updated_process.commitment_tx).unwrap(); + + let state = process.get_state_for_commitments_root(&new_state_id).unwrap(); + + let hash2values: Map = bob_diff_cache.iter() + .filter(|diff| diff.new_state_merkle_root == *new_state_id) + .map(|diff| { + let encrypted_value = state.encrypted_pcd.as_object().unwrap().get(&diff.field).unwrap(); + (diff.value_commitment.clone(), encrypted_value.clone()) + }) + .collect(); + let update_process_res = update_process_state(updated_process.commitment_tx.to_string(), new_state_id.clone(), serde_json::to_string(&Value::Object(hash2values)).unwrap()).unwrap(); + + let updated_process = update_process_res.updated_process.unwrap(); + + let parsed_prd_diffs = updated_process.new_diffs; + + bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); + + bob_diff_cache.extend(parsed_prd_diffs); + + // We can also prune the old diffs from the cache + bob_diff_cache.retain(|diff| diff.new_value != Value::Null); // this is only for testing, as we're playing both parts let bob_device = dump_device().unwrap(); @@ -227,34 +265,23 @@ fn test_pairing() { set_process_cache(serde_json::to_string(&alice_process_cache).unwrap()).unwrap(); set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap(); - debug!("Alice receives the Confirm Prd"); - let alice_parsed_confirm = parse_cipher(prd_confirm_cipher.clone()).unwrap(); + let commitment_outpoint = alice_process_cache.keys().next().unwrap(); - debug!( - "Alice parsed Bob's Confirm Prd: {:#?}", - alice_parsed_confirm - ); + debug!("Alice can validate the new state of the process"); + let relevant_process = alice_process_cache.get(&commitment_outpoint).unwrap(); - // Alice simply shoots back the return value in the ws - let bob_received_pcd = alice_parsed_confirm.ciphers_to_send[0].clone(); - let commit_msg = alice_parsed_confirm.commit_to_send.unwrap(); - - // Take the relevant state out of the process - let relevant_process = alice_process_cache.get(&OutPoint::from_str(&commit_msg.init_tx).unwrap()).unwrap(); - - let concurrent_states = relevant_process.get_latest_concurrent_states().unwrap(); - let relevant_state = concurrent_states.into_iter().find(|s| s.pcd_commitment == commit_msg.pcd_commitment).unwrap(); - - let root = ::create_merkle_tree(&relevant_state.pcd_commitment).unwrap().root().unwrap(); + for diff in alice_diff_cache { + debug!("User validate diff: {:#?}", diff); + } // Alice can also sign her response and send it to Bob - let validate_state_return = validate_state(commit_msg.init_tx, root.to_lower_hex_string()).unwrap(); + let validate_state_return = validate_state(commitment_outpoint.to_string(), new_state_id.clone()).unwrap(); let updated_process = validate_state_return.updated_process.unwrap(); alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let alice_response = create_response_prd(updated_process.commitment_tx.to_string(), root.to_lower_hex_string()).unwrap(); + let alice_response = create_response_prd(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); // ======================= Bob reset_device().unwrap(); @@ -262,77 +289,31 @@ fn test_pairing() { set_process_cache(serde_json::to_string(&bob_process_cache).unwrap()).unwrap(); set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap(); - debug!("Bob parses Alice's pcd"); - let bob_parsed_pcd_return = parse_cipher(bob_received_pcd).unwrap(); - - let updated_process = bob_parsed_pcd_return.updated_process.unwrap(); - - // Here we would update our database - bob_process_cache.insert( - updated_process.commitment_tx, - updated_process.current_process - ); - - // At this point, user must validate the pairing proposal received from Alice - // We decrypt the content of the pcd so that we can display to user what matters - let alice_proposal = get_update_proposals(updated_process.commitment_tx.to_string()).unwrap().decrypted_pcds; - - debug!("Alice proposal: {:#?}", alice_proposal); - - let (pcd_commitment_root, proposal) = alice_proposal.iter().next().unwrap(); - - // debug!("proposal: {:#?}", proposal); - - // get the roles from the proposal - let roles = proposal.extract_roles().unwrap(); - - // we check that the proposal contains only one member - assert!(roles.len() == 1); - assert!(roles["owner"].members.len() == 1); - - // we get all the addresses of the members of the proposal - let proposal_members = roles - .iter() - .flat_map(|(_, members)| members.members.iter().flat_map(|m| m.get_addresses())) - .collect::>(); - - // we can automatically check that a pairing member contains local device address + the one that sent the proposal - assert!(proposal_members.contains(&alice_address)); - assert!(proposal_members.contains(&bob_address)); - assert!(proposal_members.len() == 2); // no free riders - - // We remove the local address, but maybe that's the responsibility of the Member type - let proposal_members = proposal_members - .into_iter() - .filter(|m| m != &bob_address) - .collect::>(); - - debug!("proposal_members: {:?}", proposal_members); - - // we can now show all the addresses to the user on device to prompt confirmation - debug!("Pop-up: User confirmation"); + for diff in &bob_diff_cache { + if diff.need_validation { + debug!("Pop-up: User confirmation"); + debug!("{:#?}", diff); + } + } // If user is ok, we can add our own validation token // Get the whole commitment from the process - let bob_validated_process = validate_state(updated_process.commitment_tx.to_string(), pcd_commitment_root.to_string()).unwrap(); + let bob_validated_process = validate_state(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); let updated_process = bob_validated_process.updated_process.unwrap(); bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let bob_response = create_response_prd(updated_process.commitment_tx.to_string(), pcd_commitment_root.to_string()).unwrap(); + let bob_response = create_response_prd(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); let ciphers = bob_response.ciphers_to_send; // We would send it to Alice to let her know we agree debug!("Bob pairs device with Alice"); - pair_device(updated_process.commitment_tx.to_string(), proposal_members).unwrap(); + let roles: HashMap = serde_json::from_value(bob_diff_cache.iter().find(|diff| diff.field == "roles").unwrap().new_value.clone()).unwrap(); + let owner = roles.get("owner").unwrap(); + let members_to_pair: Vec = owner.members.iter().flat_map(|m| m.get_addresses()).collect(); + pair_device(updated_process.commitment_tx.to_string(), members_to_pair).unwrap(); // We can also check alice response let parsed_alice_response = parse_cipher(alice_response.ciphers_to_send[0].clone()).unwrap(); - - debug!("parsed_alice_response: {:#?}", parsed_alice_response.updated_process.unwrap()); - - // Since we have enough validation we can send it directly to relay for commitment - - // login(); } From f899719cc1fa04959cb87b709fdb4c1d38ee936a Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 12 Dec 2024 16:47:36 +0100 Subject: [PATCH 39/43] Update ProcessState with descriptions --- src/api.rs | 61 +++++++++++++++++++----------------------------- tests/pairing.rs | 2 +- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/api.rs b/src/api.rs index 8f8bd4a..3a33ccf 100644 --- a/src/api.rs +++ b/src/api.rs @@ -90,9 +90,11 @@ pub enum DiffStatus { #[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct UserDiff { + pub process_id: String, pub new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state pub value_commitment: String, pub field: String, + pub description: Option, pub previous_value: Value, pub new_value: Value, pub notify_user: bool, @@ -711,24 +713,30 @@ fn create_diffs(process: &Process, new_state: &ProcessState) -> AnyhowResult Map::new() }; + let new_state_descriptions = &new_state.descriptions; + + let process_id = process.get_process_id()?.to_string(); let mut diffs = vec![]; if let Some(prev_state) = process.get_latest_commited_state() { // We first decrypt as much as we can of the prev_state let clear_prev_state = prev_state.decrypt_pcd()?; // We just make a diff for values that are different from previous state for (field, prev_hash) in prev_state.pcd_commitment.as_object().unwrap() { + let description = new_state_descriptions.get(field).map(|d| d.to_string()); + let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; + let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; if let Some(new_hash) = new_state_commitments.get(field.as_str()) { if new_hash.as_str() == prev_hash.as_str() { continue; } else { // There's a diff let previous_value = clear_prev_state.get(field.as_str()).unwrap().clone(); - let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; - let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; diffs.push(UserDiff { + process_id: process_id.clone(), new_state_merkle_root: new_state_root.to_owned(), value_commitment: new_hash.as_str().unwrap().to_string(), field: field.to_owned(), + description, previous_value, new_value, notify_user: false, @@ -744,12 +752,15 @@ fn create_diffs(process: &Process, new_state: &ProcessState) -> AnyhowResult, fee_rate: u32) -> Ap #[wasm_bindgen] pub fn create_new_process( - init_state: String, + init_state_str: String, + descriptions_str: Option, relay_address: String, fee_rate: u32, ) -> ApiResult { - let pcd = ::new_from_string(&init_state)?; + let init_state = ::new_from_string(&init_state_str)?; + let descriptions = if let Some(d) = descriptions_str { ::new_from_string(&d)? } else { Value::Object(Map::new()) }; // check that we have a proper roles map - let roles = pcd.extract_roles()?; - - // 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() - .unwrap() - .keys() - .map(|k| k.clone()) - .collect(); - - pcd.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); + let roles = init_state.extract_roles()?; // We create a transaction that spends to the relay address let psbt = create_transaction_for_addresses(vec![relay_address.clone()], fee_rate)?; @@ -1184,26 +1185,13 @@ pub fn create_new_process( // 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 pcd_commitment = Value::Object(pcd.hash_all_fields(outpoint)?); - - let merkle_root = pcd_commitment.create_merkle_tree()?.root().ok_or(ApiError::new("Invalid merkle tree".to_owned()))?.to_lower_hex_string(); + let new_state = ProcessState::new(outpoint, init_state.to_value_object()?, descriptions.to_value_object()?)?; let mut process = Process::new(outpoint); - // We now create the first process state with all that data - let process_state = ProcessState { - commited_in: outpoint, - pcd_commitment: pcd_commitment.clone(), - merkle_root: merkle_root.clone(), - encrypted_pcd: Value::Object(fields2cipher.clone()), - keys: fields2keys.clone(), - validation_tokens: vec![], - }; + let diffs = create_diffs(&process, &new_state)?; - let diffs = create_diffs(&process, &process_state)?; - - process.insert_concurrent_state(process_state.clone())?; + process.insert_concurrent_state(new_state.clone())?; { let mut processes = lock_processes()?; @@ -1214,7 +1202,7 @@ pub fn create_new_process( processes.insert(outpoint.clone(), process.clone()); } - let commit_msg = CommitMessage::new_first_commitment(transaction, pcd_commitment, roles); + let commit_msg = CommitMessage::new_first_commitment(transaction, new_state.pcd_commitment, roles); let updated_process = UpdatedProcess { commitment_tx: outpoint, @@ -1246,10 +1234,11 @@ pub fn update_process( .ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?; let last_state_commitments = &prev_state.pcd_commitment; + let last_state_descriptions = &prev_state.descriptions; let clear_new_state = Value::from_str(&new_state_str)?.to_value_object()?; - let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.clone())?; + let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.clone(), last_state_descriptions.clone())?; // We compare the new state with the previous one let last_state_merkle_root = &prev_state.merkle_root; @@ -1373,8 +1362,6 @@ pub fn create_update_message( return Err(ApiError::new("Empty ciphers list".to_owned())); } - process.insert_impending_request(full_prd); - let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process.clone(), diff --git a/tests/pairing.rs b/tests/pairing.rs index 267b1c7..e06a5c3 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -166,7 +166,7 @@ fn test_pairing() { }); debug!("Alice creates the pairing process"); - let create_process_return = create_new_process(pairing_init_state.to_string(), RELAY_ADDRESS.to_owned(), 1).unwrap(); + let create_process_return = create_new_process(pairing_init_state.to_string(), None, RELAY_ADDRESS.to_owned(), 1).unwrap(); let commit_msg = create_process_return.commit_to_send.unwrap(); From cc8d64f6ce39e7781f9638a9e35a9eec2e14fa06 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 17 Dec 2024 23:43:58 +0100 Subject: [PATCH 40/43] Add up_to_date_roles to UpdatedProcess --- src/api.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 3a33ccf..f4e55c6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -108,6 +108,7 @@ pub struct UserDiff { pub struct UpdatedProcess { pub commitment_tx: OutPoint, pub current_process: Process, + pub up_to_date_roles: HashMap, pub new_diffs: Vec, // All diffs should have the same new_state_merkle_root pub modified_state: Option, // basically when we add/receive validation proofs for a state // I think we should never have both new_state and modified_state @@ -878,6 +879,7 @@ fn handle_prd( commitment_tx: outpoint, current_process: relevant_process.clone(), new_diffs: diffs, + up_to_date_roles: roles, ..Default::default() }; @@ -1207,6 +1209,7 @@ pub fn create_new_process( let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process, + up_to_date_roles: roles, new_diffs: diffs, ..Default::default() }; @@ -1236,9 +1239,11 @@ pub fn update_process( let last_state_commitments = &prev_state.pcd_commitment; let last_state_descriptions = &prev_state.descriptions; - let clear_new_state = Value::from_str(&new_state_str)?.to_value_object()?; + let clear_new_state = Value::from_str(&new_state_str)?; - let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.clone(), last_state_descriptions.clone())?; + let roles = clear_new_state.extract_roles()?; + + let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.to_value_object()?, last_state_descriptions.clone())?; // We compare the new state with the previous one let last_state_merkle_root = &prev_state.merkle_root; @@ -1261,6 +1266,7 @@ pub fn update_process( let updated_process = UpdatedProcess { commitment_tx: outpoint, current_process: process.clone(), + up_to_date_roles: roles, new_diffs: diffs, ..Default::default() }; From 76ce2c067247b107ea501d4f61207a22c2db2a12 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 17 Dec 2024 23:53:35 +0100 Subject: [PATCH 41/43] Update create_update_message --- src/api.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/api.rs b/src/api.rs index f4e55c6..b02bbfe 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1303,13 +1303,13 @@ pub fn create_update_message( let mut all_members: HashMap> = HashMap::new(); let shared_secrets = lock_shared_secrets()?; - for (name, role) in roles { + for (name, role) in &roles { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); - for member in role.members { + for member in &role.members { // Check that we have a shared_secret with all members if let Some(no_secret_address) = member.get_addresses().iter() .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) @@ -1330,15 +1330,10 @@ pub fn create_update_message( let sender: Member = local_device .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_all_fields(OutPoint::null())?; - let encrypted_pcd_merkle_root = ::create_merkle_tree(&Value::Object(encrypted_pcd_hash))?.root().unwrap(); - let full_prd = Prd::new_update( outpoint, serde_json::to_string(&sender)?, - serialize(&encrypted_pcd_merkle_root).to_lower_hex_string(), + roles, update_state.keys.clone(), update_state.pcd_commitment.clone(), ); @@ -1368,14 +1363,7 @@ pub fn create_update_message( return Err(ApiError::new("Empty ciphers list".to_owned())); } - let updated_process = UpdatedProcess { - commitment_tx: outpoint, - current_process: process.clone(), - ..Default::default() - }; - Ok(ApiReturn { - updated_process: Some(updated_process), ciphers_to_send: ciphers, ..Default::default() }) From d54981f66ebc8f00020ef0e4bd7983795b26dbeb Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 17 Dec 2024 23:54:57 +0100 Subject: [PATCH 42/43] Minor fix in create_new_process --- src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.rs b/src/api.rs index b02bbfe..6f1832f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1204,7 +1204,7 @@ pub fn create_new_process( processes.insert(outpoint.clone(), process.clone()); } - let commit_msg = CommitMessage::new_first_commitment(transaction, new_state.pcd_commitment, roles); + let commit_msg = CommitMessage::new_first_commitment(transaction, new_state.pcd_commitment, roles.clone()); let updated_process = UpdatedProcess { commitment_tx: outpoint, From ee777bbcbc5707a8295d07ccb729df19058106de Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 17 Dec 2024 23:55:28 +0100 Subject: [PATCH 43/43] Update handle_prd --- src/api.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 6f1832f..5ce65de 100644 --- a/src/api.rs +++ b/src/api.rs @@ -860,8 +860,15 @@ fn handle_prd( return Err(AnyhowError::msg("Received update for a state we already know")); } + let commited_in = OutPoint::from_str(&prd.root_commitment)?; + + // Extract the roles from the payload + let proposal_roles: HashMap = serde_json::from_str(&prd.payload)?; + + // TODO: check that the role in the prd has the right commitment + let new_state = ProcessState { - commited_in: OutPoint::from_str(&prd.root_commitment)?, + commited_in, pcd_commitment: prd.pcd_commitments, merkle_root: update_merkle_root.clone(), keys: prd.keys, @@ -870,9 +877,18 @@ fn handle_prd( // Compute the diffs // At this point we don't have the encrypted values - // But it can still be useful to track diffs let diffs = create_diffs(&relevant_process, &new_state)?; + // Take the roles from the last validated state + let mut roles = HashMap::new(); + if let Some(last_state) = relevant_process.get_latest_commited_state() { + let decrypted_last_state = last_state.decrypt_pcd()?; + roles = Value::Object(decrypted_last_state).extract_roles()?; + } else { + // We don't have commited state yet, let's take the current roles + roles = proposal_roles; + } + relevant_process.insert_concurrent_state(new_state); let updated_process = UpdatedProcess { @@ -903,6 +919,10 @@ fn handle_prd( let updated_state = to_update.clone(); + let clear_state = to_update.decrypt_pcd()?; + + let roles = Value::Object(clear_state).extract_roles()?; + // We must return an update of the process let updated_process = UpdatedProcess { commitment_tx: OutPoint::from_str(&prd.root_commitment)?, @@ -911,8 +931,15 @@ fn handle_prd( ..Default::default() }; + let commit_msg = CommitMessage::new_update_commitment( + OutPoint::from_str(&prd.root_commitment)?, + updated_state.pcd_commitment, + roles + ); + return Ok(ApiReturn { updated_process: Some(updated_process), + commit_to_send: Some(commit_msg), ..Default::default() }); }