sdk_client/tests/pairing.rs
2024-10-31 14:58:30 +01:00

340 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
};
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::secrets::SecretsStore;
use serde_json::{json, Value};
use tsify::JsValueSerdeExt;
use wasm_bindgen_test::*;
mod utils;
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 Bobs address to her own, setting the base for creating
/// a new `Member` object.
/// 4. **Initiating Pairing**: Alice initializes pairing using the transactions `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 Alices `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 Bobs
/// 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();
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==============================================");
// ========================= Alice
reset_device().unwrap();
create_device_from_sp_wallet(ALICE_LOGIN_WALLET.to_owned()).unwrap();
// we get our own address
let alice_address = get_address().unwrap();
// 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(),
bob_address.as_str().try_into().unwrap(),
])
.unwrap();
let initial_session_privkey = [0u8; 32];
let initial_session_pubkey = [0u8; 32];
let pairing_init_state = json!({
"description": "AliceBob",
"roles": {
"owner": {
"members":
[
new_member
],
"validation_rules":
[
{
"quorum": 1.0,
"fields": [
"description",
"roles",
"session_privkey",
"session_pubkey",
"key_parity"
],
"min_sig_member": 1.0
}
]
}
},
"session_privkey": initial_session_privkey,
"session_pubkey": initial_session_pubkey,
"key_parity": true, // This allows us to use a 32 bytes array in serialization
});
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();
debug!("Alice sends an update prd to Bob");
let alice_pairing_return =
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 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();
debug!("Bob receives the update prd");
let bob_parsed_return = parse_cipher(alice_to_bob_cipher).unwrap();
debug!("Bob retrieved prd: {:#?}", bob_retrieved_prd);
let (root_commitment, relevant_process) = bob_retrieved_prd.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();
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();
debug!("Alice receives the Confirm Prd");
let alice_parsed_confirm = parse_cipher(prd_confirm_cipher.clone()).unwrap();
debug!(
"Alice parsed Bob's Confirm Prd: {:#?}",
alice_parsed_confirm
);
// 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();
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);
// Here we would update our database
bob_process_cache.insert(
root_commitment.clone(),
bob_parsed_pcd_return.updated_process.unwrap().1,
);
// 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();
debug!("Alice proposal: {:#?}", alice_proposal);
let proposal = Value::from_str(&alice_proposal.get(0).unwrap()).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::<Result<HashMap<String, RoleDefinition>, anyhow::Error>>();
let roles = 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::<Vec<String>>();
// 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::<Vec<String>>();
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");
// 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();
bob_process_cache.insert(
root_commitment.clone(),
bob_added_validation.updated_process.unwrap().1,
);
// 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);
debug!("Bob pairs device with Alice");
pair_device(commitment_outpoint.to_string(), proposal_members).unwrap();
// To make the pairing effective, alice and bob must now creates a new transaction where they both control one output
// login();
}