340 lines
14 KiB
Rust
340 lines
14 KiB
Rust
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 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();
|
||
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();
|
||
}
|