Update pairing test
This commit is contained in:
parent
2463b8f05b
commit
93ae65ece7
296
src/api.rs
296
src/api.rs
@ -85,7 +85,7 @@ pub struct ApiReturn {
|
|||||||
pub new_tx_to_send: Option<NewTxMessage>,
|
pub new_tx_to_send: Option<NewTxMessage>,
|
||||||
pub ciphers_to_send: Vec<String>,
|
pub ciphers_to_send: Vec<String>,
|
||||||
pub commit_to_send: Option<CommitMessage>,
|
pub commit_to_send: Option<CommitMessage>,
|
||||||
pub decrypted_pcds: Vec<Value>,
|
pub decrypted_pcds: HashMap<String, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ApiResult<T: FromWasmAbi> = Result<T, ApiError>;
|
pub type ApiResult<T: FromWasmAbi> = Result<T, ApiError>;
|
||||||
@ -779,7 +779,11 @@ fn handle_prd(
|
|||||||
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
|
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
|
||||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||||
debug!("Creating new process for outpoint: {}", outpoint);
|
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
|
// It must match a prd we sent previously
|
||||||
// We send the whole data in a pcd
|
// We send the whole data in a pcd
|
||||||
debug!("Received confirm prd {:#?}", prd);
|
debug!("Received confirm prd {:#?}", prd);
|
||||||
let original_request = relevant_process
|
let relevant_state = relevant_process
|
||||||
.get_impending_requests()
|
.get_latest_concurrent_states()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|r| {
|
.find(|state| {
|
||||||
if r.prd_type != PrdType::Update {
|
state.pcd_commitment == prd.pcd_commitments
|
||||||
return false;
|
|
||||||
}
|
|
||||||
r.pcd_commitments == prd.pcd_commitments
|
|
||||||
})
|
})
|
||||||
.ok_or(anyhow::Error::msg("Original request not found"))?;
|
.ok_or(anyhow::Error::msg("Original request not found"))?;
|
||||||
|
|
||||||
let member: Member = serde_json::from_str(&prd.sender)?;
|
let member: Member = serde_json::from_str(&prd.sender)?;
|
||||||
|
|
||||||
// We send the data to all addresses of the member we know a secret for
|
// We send the data to all addresses of the member we know a secret for
|
||||||
let mut ciphers = vec![];
|
let mut ciphers = vec![];
|
||||||
for address in member.get_addresses() {
|
for address in member.get_addresses() {
|
||||||
if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) {
|
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 {
|
} else {
|
||||||
// For now we don't fail if we're missing an address for a member but maybe we should
|
// 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);
|
warn!("Failed to find secret for address {}", address);
|
||||||
@ -831,6 +834,7 @@ fn handle_prd(
|
|||||||
for address in member.get_addresses() {
|
for address in member.get_addresses() {
|
||||||
if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) {
|
if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) {
|
||||||
let cipher = confirm_prd(&prd, &shared_secret)?;
|
let cipher = confirm_prd(&prd, &shared_secret)?;
|
||||||
|
ciphers.push(cipher);
|
||||||
} else {
|
} else {
|
||||||
// For now we don't fail if we're missing an address for a member but maybe we should
|
// 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);
|
warn!("Failed to find secret for address {}", address);
|
||||||
@ -880,26 +884,33 @@ fn handle_prd(
|
|||||||
|
|
||||||
fn handle_pcd(pcd: Value) -> AnyhowResult<ApiReturn> {
|
fn handle_pcd(pcd: Value) -> AnyhowResult<ApiReturn> {
|
||||||
// We received an encrypted pcd, so we can compute the merkle root of a tree where all the encrypted values are the leaves
|
// 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
|
// 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_fields(OutPoint::null())?;
|
||||||
|
let encrypted_pcd_root = <Value as Pcd>::create_merkle_tree(&Value::Object(encrypted_pcd_commitments))?.root().unwrap().to_lower_hex_string();
|
||||||
let mut processes = lock_processes()?;
|
let mut processes = lock_processes()?;
|
||||||
|
let updated_prd: Prd;
|
||||||
for (outpoint, process) in processes.iter_mut() {
|
for (outpoint, process) in processes.iter_mut() {
|
||||||
// We check all pending requests and match the payload with the hash of this pcd
|
// We check all pending requests and match the payload with the hash of this pcd
|
||||||
if let Some(prd) = process
|
if let Some(prd) = process
|
||||||
.get_impending_requests_mut()
|
.get_impending_requests_mut()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|r| *r.payload == pcd.to_string())
|
.find(|r| *r.payload == encrypted_pcd_root)
|
||||||
{
|
{
|
||||||
// We update the process and return it
|
// We update the process and return it
|
||||||
prd.payload = pcd.to_string();
|
prd.payload = pcd.to_string();
|
||||||
return Ok(ApiReturn {
|
updated_prd = prd.clone();
|
||||||
updated_process: Some((outpoint.to_string(), process.clone())),
|
// We can now safely mark the prd to be remove from the process
|
||||||
..Default::default()
|
prd.prd_type = PrdType::None;
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
continue;
|
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"))
|
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()
|
let last_state = process.get_latest_commited_state()
|
||||||
.ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?;
|
.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)?;
|
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 = <Value as Pcd>::create_merkle_tree(&Value::Object(pcd_commitment))?.root().unwrap();
|
||||||
|
|
||||||
|
// We compare the new state with the previous one
|
||||||
|
let last_state_merkle_root = <Value as Pcd>::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 fields2keys = Map::new();
|
||||||
let mut fields2cipher = Map::new();
|
let mut fields2cipher = Map::new();
|
||||||
let fields_to_encrypt: Vec<String> = new_state_val
|
let fields_to_encrypt: Vec<String> = new_state_val
|
||||||
@ -1149,18 +1172,25 @@ pub fn update_process(
|
|||||||
.collect();
|
.collect();
|
||||||
new_state_val.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher);
|
new_state_val.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher);
|
||||||
|
|
||||||
// TODO what are the actual differences?
|
// We create an encrypted values merkle root
|
||||||
if *last_state_encrypted_val == new_state_val {
|
let new_state_encrypted_commitments = Value::Object(fields2cipher).hash_fields(OutPoint::null())?;
|
||||||
return Err(ApiError::new("New state is identical to last state".to_owned()));
|
let new_state_encrypted_root = <Value as Pcd>::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);
|
let device = lock_local_device()?;
|
||||||
to_update.keys = fields2keys;
|
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
|
// Add the new state to the process
|
||||||
process.insert_state(to_update);
|
process.insert_state(&update_prd);
|
||||||
|
|
||||||
Ok(ApiReturn {
|
Ok(ApiReturn {
|
||||||
updated_process: Some((init_commitment, process.clone())),
|
updated_process: Some((init_commitment, process.clone())),
|
||||||
@ -1185,7 +1215,7 @@ pub fn create_update_message(
|
|||||||
let new_state_commitments = <Value as Pcd>::from_string(&pcd_commitment)?;
|
let new_state_commitments = <Value as Pcd>::from_string(&pcd_commitment)?;
|
||||||
|
|
||||||
let update_state: &ProcessState;
|
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;
|
update_state = state;
|
||||||
} else {
|
} 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
|
// 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().as_object().unwrap().clone();
|
||||||
|
|
||||||
// debug!("clear_state: {:#?}", clear_state);
|
|
||||||
let roles = Value::Object(clear_state).extract_roles()?;
|
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<Member, HashSet<String>> = HashMap::new();
|
let mut all_members: HashMap<Member, HashSet<String>> = HashMap::new();
|
||||||
let shared_secrets = lock_shared_secrets()?;
|
let shared_secrets = lock_shared_secrets()?;
|
||||||
for (name, role) in roles {
|
for (name, role) in roles {
|
||||||
@ -1207,12 +1241,16 @@ pub fn create_update_message(
|
|||||||
.flat_map(|rule| rule.fields.clone())
|
.flat_map(|rule| rule.fields.clone())
|
||||||
.collect();
|
.collect();
|
||||||
for member in role.members {
|
for member in role.members {
|
||||||
|
debug!("member: {:?}", member);
|
||||||
// Check that we have a shared_secret with all members
|
// Check that we have a shared_secret with all members
|
||||||
if member.get_addresses().iter()
|
if let Some(no_secret_address) = member.get_addresses().iter()
|
||||||
.any(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none())
|
.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
|
// We ignore it if we don't have a secret with ourselves
|
||||||
return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member)));
|
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) {
|
if !all_members.contains_key(&member) {
|
||||||
all_members.insert(member.clone(), HashSet::new());
|
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
|
let sender: Member = local_device
|
||||||
.to_member();
|
.to_member();
|
||||||
|
|
||||||
// To allow the recipient to identify the pcd that contains only encrypted values, we compute the merkle tree of the encrypted pcd
|
// 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
|
// 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_fields(OutPoint::null())?;
|
||||||
let encrypted_pcd_merkle_root = <Value as Pcd>::create_merkle_tree(Value::Object(encrypted_pcd_hash))?.root().unwrap();
|
let encrypted_pcd_merkle_root = <Value as Pcd>::create_merkle_tree(&Value::Object(encrypted_pcd_hash))?.root().unwrap();
|
||||||
|
|
||||||
let full_prd = Prd::new_update(
|
let full_prd = Prd::new_update(
|
||||||
outpoint,
|
outpoint,
|
||||||
@ -1262,6 +1295,11 @@ pub fn create_update_message(
|
|||||||
ciphers.push(cipher.to_lower_hex_string());
|
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);
|
process.insert_impending_request(full_prd);
|
||||||
|
|
||||||
Ok(ApiReturn {
|
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<ApiReturn> {
|
||||||
|
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 = <Value as Pcd>::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<Member, HashSet<String>> = HashMap::new();
|
||||||
|
let shared_secrets = lock_shared_secrets()?;
|
||||||
|
for (name, role) in roles {
|
||||||
|
let fields: Vec<String> = 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 = <Value as Pcd>::create_merkle_tree(&Value::Object(encrypted_pcd_hash))?.root().unwrap();
|
||||||
|
|
||||||
|
let root = <Value as Pcd>::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)]
|
#[derive(Tsify, Serialize, Deserialize)]
|
||||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
@ -1304,77 +1452,33 @@ pub fn get_update_proposals(process_outpoint: String) -> ApiResult<ApiReturn> {
|
|||||||
.get(&outpoint)
|
.get(&outpoint)
|
||||||
.ok_or(ApiError::new("process not found".to_owned()))?;
|
.ok_or(ApiError::new("process not found".to_owned()))?;
|
||||||
|
|
||||||
let mut updated_process = relevant_process.clone();
|
let mut decrypted_pcds = HashMap::new();
|
||||||
|
|
||||||
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![];
|
|
||||||
|
|
||||||
// We first push the last commited state, if any
|
// We first push the last commited state, if any
|
||||||
match relevant_process.get_latest_commited_state() {
|
match relevant_process.get_latest_commited_state() {
|
||||||
Some(state) => {
|
Some(state) => {
|
||||||
let mut decrypted_pcd = Map::new();
|
let mut decrypted_pcd = Map::new();
|
||||||
state.encrypted_pcd.decrypt_fields(&state.keys, &mut decrypted_pcd);
|
state.encrypted_pcd.decrypt_fields(&state.keys, &mut decrypted_pcd);
|
||||||
decrypted_pcds.push(Value::Object(decrypted_pcd));
|
let root = <Value as Pcd>::create_merkle_tree(&state.pcd_commitment)?.root().unwrap();
|
||||||
|
decrypted_pcds.insert(root.to_lower_hex_string(), Value::Object(decrypted_pcd));
|
||||||
}
|
}
|
||||||
None => ()
|
None => ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maybe that's the right place for adding a new state with what we've got from the prd update
|
for state in relevant_process.get_latest_concurrent_states()? {
|
||||||
// We should probably iterate on every update proposals and see which one don't have a state yet
|
if state.encrypted_pcd == Value::Null {
|
||||||
let mut update_states = false;
|
// This is the last empty state, ignore it
|
||||||
for proposal in update_proposals {
|
continue;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
// We add the decrypted state to our return variable
|
|
||||||
let mut decrypted_pcd = Map::new();
|
let mut decrypted_pcd = Map::new();
|
||||||
pcd.decrypt_fields(&proposal.keys, &mut decrypted_pcd)?;
|
state.encrypted_pcd.decrypt_fields(&state.keys, &mut decrypted_pcd)?;
|
||||||
decrypted_pcds.push(Value::Object(decrypted_pcd));
|
let root = <Value as Pcd>::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();
|
Ok(ApiReturn {
|
||||||
if update_states {
|
decrypted_pcds,
|
||||||
// We replace the process
|
..Default::default()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
177
tests/pairing.rs
177
tests/pairing.rs
@ -2,19 +2,15 @@ use std::collections::HashMap;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use sdk_client::api::{
|
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::crypto::AnkSharedSecretHash;
|
||||||
use sdk_common::pcd::{Member, RoleDefinition};
|
use sdk_common::log::debug;
|
||||||
use sdk_common::sp_client::bitcoin::consensus::deserialize;
|
use sdk_common::pcd::{Member, Pcd};
|
||||||
use sdk_common::sp_client::bitcoin::hex::FromHex;
|
use sdk_common::sp_client::bitcoin::hex::DisplayHex;
|
||||||
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 sdk_common::secrets::SecretsStore;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use tsify::JsValueSerdeExt;
|
|
||||||
use wasm_bindgen_test::*;
|
use wasm_bindgen_test::*;
|
||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
@ -93,6 +89,8 @@ wasm_bindgen_test_configure!(run_in_browser);
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn test_pairing() {
|
fn test_pairing() {
|
||||||
|
const RELAY_ADDRESS: &str = "tsp1qqvfm6wvd55r68ltysdhmagg7qavxrzlmm9a7tujsp8qqy6x2vr0muqajt5p2jdxfw450wyeygevypxte29sxlxzgprmh2gwnutnt09slrcqqy5h4";
|
||||||
|
|
||||||
setup();
|
setup();
|
||||||
let mut alice_process_cache = HashMap::new();
|
let mut alice_process_cache = HashMap::new();
|
||||||
let mut bob_process_cache = HashMap::new();
|
let mut bob_process_cache = HashMap::new();
|
||||||
@ -107,14 +105,18 @@ fn test_pairing() {
|
|||||||
|
|
||||||
// we get our own address
|
// we get our own address
|
||||||
let alice_address = get_address().unwrap();
|
let alice_address = get_address().unwrap();
|
||||||
|
debug!("alice address: {}", alice_address);
|
||||||
|
|
||||||
// we scan the qr code or get the address by any other means
|
// we scan the qr code or get the address by any other means
|
||||||
let bob_address = helper_get_bob_address();
|
let bob_address = helper_get_bob_address();
|
||||||
|
debug!("bob_address: {}", bob_address);
|
||||||
|
|
||||||
// we add some shared_secret in both secrets_store
|
// we add some shared_secret in both secrets_store
|
||||||
let shared_secret = "c3f1a64e15d2e8d50f852c20b7f0b47cbe002d9ef80bc79582d09d6f38612d45";
|
let shared_secret = AnkSharedSecretHash::from_str("c3f1a64e15d2e8d50f852c20b7f0b47cbe002d9ef80bc79582d09d6f38612d45").unwrap();
|
||||||
alice_secrets_store.confirm_secret_for_address(shared_secret, bob_address.try_into().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.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
|
// Alice creates the new member with Bob address
|
||||||
let new_member = Member::new(vec![
|
let new_member = Member::new(vec![
|
||||||
@ -127,6 +129,10 @@ fn test_pairing() {
|
|||||||
let initial_session_pubkey = [0u8; 32];
|
let initial_session_pubkey = [0u8; 32];
|
||||||
|
|
||||||
let pairing_init_state = json!({
|
let pairing_init_state = json!({
|
||||||
|
"html": "",
|
||||||
|
"js": "",
|
||||||
|
"style": "",
|
||||||
|
"zones": [],
|
||||||
"description": "AliceBob",
|
"description": "AliceBob",
|
||||||
"roles": {
|
"roles": {
|
||||||
"owner": {
|
"owner": {
|
||||||
@ -155,48 +161,63 @@ fn test_pairing() {
|
|||||||
"key_parity": true, // This allows us to use a 32 bytes array in serialization
|
"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");
|
debug!("Alice pairs her device");
|
||||||
// we can update our local device now, first with an empty txid
|
// 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");
|
debug!("Alice sends an update prd to Bob");
|
||||||
let alice_pairing_return =
|
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();
|
let (root_outpoint, alice_init_process) = alice_pairing_return.updated_process.unwrap();
|
||||||
alice_process_cache.insert(root_outpoint.clone(), alice_init_process.clone());
|
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
|
// this is only for testing, as we're playing both parts
|
||||||
let alice_device = dump_device().unwrap();
|
let alice_device = dump_device().unwrap();
|
||||||
let alice_processes = dump_process_cache().unwrap();
|
|
||||||
|
|
||||||
// ======================= Bob
|
// ======================= Bob
|
||||||
reset_device().unwrap();
|
reset_device().unwrap();
|
||||||
create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).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");
|
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_parsed_return.updated_process.unwrap();
|
||||||
|
|
||||||
let (root_commitment, relevant_process) = bob_retrieved_prd.updated_process.unwrap();
|
|
||||||
|
|
||||||
bob_process_cache.insert(root_commitment.clone(), relevant_process);
|
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");
|
debug!("Bob sends a Confirm Prd to Alice");
|
||||||
|
|
||||||
// this is only for testing, as we're playing both parts
|
// this is only for testing, as we're playing both parts
|
||||||
let bob_device = dump_device().unwrap();
|
let bob_device = dump_device().unwrap();
|
||||||
let bob_processes = dump_process_cache().unwrap();
|
|
||||||
|
|
||||||
// ======================= Alice
|
// ======================= Alice
|
||||||
reset_device().unwrap();
|
reset_device().unwrap();
|
||||||
restore_device(alice_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");
|
debug!("Alice receives the Confirm Prd");
|
||||||
let alice_parsed_confirm = parse_cipher(prd_confirm_cipher.clone()).unwrap();
|
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
|
// Alice simply shoots back the return value in the ws
|
||||||
let bob_received_pcd = alice_parsed_confirm.ciphers_to_send[0].clone();
|
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
|
// ======================= Bob
|
||||||
reset_device().unwrap();
|
reset_device().unwrap();
|
||||||
restore_device(bob_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");
|
debug!("Bob parses Alice's pcd");
|
||||||
let bob_parsed_pcd_return = parse_cipher(bob_received_pcd).unwrap();
|
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
|
// Here we would update our database
|
||||||
bob_process_cache.insert(
|
bob_process_cache.insert(
|
||||||
root_commitment.clone(),
|
root_outpoint.clone(),
|
||||||
bob_parsed_pcd_return.updated_process.unwrap().1,
|
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
|
// 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
|
// 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);
|
debug!("Alice proposal: {:#?}", alice_proposal);
|
||||||
|
|
||||||
let proposal = Value::from_str(&alice_proposal.get(0).unwrap()).unwrap();
|
let (pcd_commitment_root, proposal) = alice_proposal.iter().next().unwrap();
|
||||||
debug!("proposal: {:#?}", proposal);
|
|
||||||
|
// debug!("proposal: {:#?}", proposal);
|
||||||
|
|
||||||
// get the roles from the proposal
|
// get the roles from the proposal
|
||||||
let roles = proposal
|
let roles = proposal.extract_roles().unwrap();
|
||||||
.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
|
// we check that the proposal contains only one member
|
||||||
assert!(roles.len() == 1);
|
assert!(roles.len() == 1);
|
||||||
@ -309,37 +284,33 @@ fn test_pairing() {
|
|||||||
debug!("proposal_members: {:?}", proposal_members);
|
debug!("proposal_members: {:?}", proposal_members);
|
||||||
|
|
||||||
// we can now show all the addresses to the user on device to prompt confirmation
|
// 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
|
// If user is ok, we can add our own validation token
|
||||||
let prd_to_respond = bob_process_cache
|
// Get the whole commitment from the process
|
||||||
.get(&root_commitment)
|
let process = alice_process_cache.get(&root_outpoint).unwrap();
|
||||||
.unwrap()
|
let mut pcd_commitment = String::default();
|
||||||
.get_impending_requests()
|
for p in process.get_latest_concurrent_states().unwrap() {
|
||||||
.get(0)
|
let root = <Value as Pcd>::create_merkle_tree(&p.pcd_commitment).unwrap().root().unwrap();
|
||||||
.unwrap()
|
if *pcd_commitment_root == root.to_lower_hex_string() {
|
||||||
.to_owned();
|
pcd_commitment = p.pcd_commitment.to_string();
|
||||||
let bob_added_validation =
|
break;
|
||||||
add_validation_token_to_prd(root_commitment.clone(), prd_to_respond.create_commitment().to_string(), true).unwrap();
|
}
|
||||||
|
}
|
||||||
|
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(
|
bob_process_cache.insert(
|
||||||
root_commitment.clone(),
|
root_outpoint.clone(),
|
||||||
bob_added_validation.updated_process.unwrap().1,
|
updated_process,
|
||||||
);
|
);
|
||||||
|
|
||||||
// We create the commit msg for the relay that includes Alice and Bob's proofs
|
// We also send an update to the other members
|
||||||
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");
|
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
|
// To make the pairing effective, alice and bob must now creates a new transaction where they both control one output
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user