Implement Process

This commit is contained in:
Sosthene 2024-08-20 12:21:15 +02:00 committed by Nicolas Cantu
parent 81e01d01b5
commit c9f62dc3de
6 changed files with 457 additions and 28 deletions

View File

@ -16,4 +16,5 @@ serde_json = "1.0.108"
# sp_client = { path = "../sp-client" }
sp_client = { git = "https://github.com/Sosthene00/sp-client.git", branch = "master" }
tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" }
uuid = { version = "1.10.0", features = ["v4"] }
wasm-bindgen = "0.2.91"

158
src/device.rs Normal file
View File

@ -0,0 +1,158 @@
use std::collections::HashMap;
use anyhow::{Error, Result};
use serde_json::{Map, Value};
use sp_client::bitcoin::consensus::serialize;
use sp_client::bitcoin::hashes::Hash;
use sp_client::bitcoin::secp256k1::SecretKey;
use sp_client::bitcoin::{
Network, OutPoint, ScriptBuf, Transaction, Txid, XOnlyPublicKey,
};
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use sp_client::silentpayments::utils::SilentPaymentAddress;
use sp_client::spclient::{OutputList, SpWallet, SpendKey};
pub const SESSION_INDEX: u32 = 0;
pub const REVOKATION_INDEX: u32 = 1;
#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct PairedDevice {
pub address: String,
pub outgoing_pairing_transaction: [u8; 32],
pub revokation_index: u32,
pub incoming_pairing_transaction: [u8; 32],
pub current_remote_key: [u8; 32],
pub current_session_outpoint: OutPoint, // This will be spend by remote device to notify us of next login
pub current_session_revokation_outpoint: OutPoint, // remote device can revoke current session by spending this
}
impl PairedDevice {
pub fn new(address: SilentPaymentAddress, pairing_txid: Txid, revokation_index: u32) -> Self {
let mut pairing_transaction_buf = [0u8; 32];
pairing_transaction_buf.copy_from_slice(&serialize(&pairing_txid));
Self {
address: address.into(),
outgoing_pairing_transaction: pairing_transaction_buf,
revokation_index,
incoming_pairing_transaction: [0u8; 32],
current_session_revokation_outpoint: OutPoint::default(),
current_session_outpoint: OutPoint::default(),
current_remote_key: [0u8; 32],
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Device {
sp_wallet: SpWallet,
current_session_outpoint: OutPoint, // This is the notification output of incoming login tx
current_session_key: [u8; 32],
current_session_revokation_outpoint: OutPoint, // This is the revokation outpoint of outgoing login tx
paired_device: Option<PairedDevice>,
}
impl Device {
pub fn new(sp_wallet: SpWallet) -> Self {
Self {
sp_wallet,
current_session_outpoint: OutPoint::default(),
current_session_key: [0u8; 32],
current_session_revokation_outpoint: OutPoint::default(),
paired_device: None,
}
}
pub fn get_wallet(&self) -> &SpWallet {
&self.sp_wallet
}
pub fn get_mut_wallet(&mut self) -> &mut SpWallet {
&mut self.sp_wallet
}
pub fn is_linked(&self) -> bool {
self.paired_device.is_some()
}
pub fn is_pairing(&self) -> bool {
self.current_session_key == [0u8; 32]
}
pub fn get_paired_device_info(&self) -> Option<PairedDevice> {
self.paired_device.clone()
}
pub fn get_next_output_to_spend(&self) -> OutPoint {
self.current_session_outpoint
}
pub fn get_session_revokation_outpoint(&self) -> OutPoint {
self.current_session_revokation_outpoint
}
pub fn sign_with_current_session_key(&self) -> Result<()> {
unimplemented!();
}
pub fn encrypt_with_current_session_key(&self) -> Result<()> {
unimplemented!();
}
pub fn new_link(
&mut self,
link_with: SilentPaymentAddress,
outgoing_pairing_tx: Txid,
revokation_output: u32,
incoming_pairing_tx: Txid,
) -> Result<()> {
// let address_looked_for: String = link_with.into();
if let Some(paired_device) = self.paired_device.as_ref() {
return Err(Error::msg(format!(
"Found an already paired device with address {} and revokable by {}:{}",
paired_device.address,
Txid::from_byte_array(paired_device.outgoing_pairing_transaction),
paired_device.revokation_index
)));
} else {
let mut new_device =
PairedDevice::new(link_with, outgoing_pairing_tx, revokation_output);
new_device.incoming_pairing_transaction = incoming_pairing_tx.to_byte_array();
self.paired_device = Some(new_device);
}
Ok(())
}
// We call that when we spent to the remote device and it similarly spent to us
pub fn update_session(
&mut self,
new_session_key: SecretKey,
new_session_outpoint: OutPoint,
new_revokation_outpoint: OutPoint,
new_remote_key: XOnlyPublicKey,
new_remote_session_outpoint: OutPoint,
new_remote_revokation_outpoint: OutPoint,
) -> Result<()> {
if !self.is_linked() {
return Err(Error::msg("Can't update an unpaired device"));
}
self.paired_device
.as_mut()
.map(|d| {
d.current_remote_key = new_remote_key.serialize();
d.current_session_outpoint = new_remote_session_outpoint;
d.current_session_revokation_outpoint = new_remote_revokation_outpoint;
});
self.current_session_key = new_session_key.secret_bytes();
self.current_session_outpoint = new_session_outpoint;
self.current_session_revokation_outpoint = new_revokation_outpoint;
Ok(())
}
}

View File

@ -1,6 +1,8 @@
pub use sp_client;
pub mod crypto;
pub mod device;
pub mod error;
pub mod network;
pub mod process;
pub mod silentpayments;

View File

@ -1,5 +1,5 @@
use std::{default, fmt};
use std::str::FromStr;
use std::{default, fmt};
use aes_gcm::{AeadCore, Aes256Gcm, KeyInit};
use anyhow::{Error, Result};
@ -12,6 +12,7 @@ use sp_client::bitcoin::consensus::serialize;
use sp_client::bitcoin::hashes::sha256::Hash;
// use sp_client::bitcoin::hashes::Hash;
use sp_client::bitcoin::hex::{DisplayHex, FromHex};
use sp_client::bitcoin::secp256k1::schnorr::Signature;
use sp_client::bitcoin::secp256k1::PublicKey;
use sp_client::bitcoin::{BlockHash, OutPoint, Transaction};
use sp_client::silentpayments::utils::SilentPaymentAddress;
@ -19,6 +20,7 @@ use tsify::Tsify;
use crate::crypto::{Aes256Decryption, Aes256Encryption, Purpose};
use crate::error::AnkError;
use crate::process::{Member, Process};
#[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
@ -112,24 +114,67 @@ impl NewTxMessage {
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub enum PrdType {
#[default]
None,
Message,
Update,
List,
Response,
Confirm,
}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct ValidationToken {
member: Member,
message: Hash, // Hash of Pcd | {"yes/no/blank"}
sig: Signature,
sig_alt: Signature, // User must sign with it's 2 paired devices
}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct Prd {
pub prd_type: PrdType,
pub process: Process,
pub sender: String,
pub key: [u8;32],
pub key: [u8; 32],
pub validation_tokens: Vec<ValidationToken>,
pub pcd_commitment: Hash, // hash of the pcd
pub error: Option<AnkError>,
}
impl Prd {
pub fn new(sender: SilentPaymentAddress, key: [u8; 32], pcd_commitment: Hash) -> Self {
Self {
pub fn new(
prd_type: PrdType,
process: Process,
sender: SilentPaymentAddress,
key: [u8; 32],
pcd_commitment: Hash,
) -> Result<Self> {
let mut res = Self {
prd_type,
process,
sender: sender.into(),
validation_tokens: vec![],
key,
pcd_commitment,
error: None,
};
Ok(res)
}
pub fn add_validation_token(&mut self, validation_token: ValidationToken) -> Result<()> {
match self.prd_type {
PrdType::Confirm => self.validation_tokens.push(validation_token),
_ => return Err(Error::msg("This Prd type doesn't allow validation tokens"))
}
Ok(())
}
pub fn encrypt_pcd(&self, pcd: &Pcd) -> Result<Vec<u8>> {
@ -225,6 +270,7 @@ pub enum CachedMessageStatus {
pub struct CachedMessage {
pub id: u32,
pub status: CachedMessageStatus,
pub prd_type: PrdType,
pub commited_in: Option<OutPoint>,
pub tied_by: Option<u32>, // index of the output that ties the proposal
pub commitment: Option<String>, // content of the op_return
@ -232,9 +278,9 @@ pub struct CachedMessage {
pub recipient: Option<String>, // Never None when message sent
pub shared_secret: Option<String>, // Never None when message sent
pub prd_cipher: Option<String>, // When we receive message we can't decrypt we only have this and commited_in_tx
pub prd: Option<Prd>, // Never None when message sent
pub pcd_cipher: Option<String>,
pub pcd: Option<Pcd>, // Never None when message sent
pub prd: Option<Prd>, // Never None when message sent
pub pcd_cipher: Option<String>,
pub pcd: Option<Pcd>, // Never None when message sent
pub pcd_commitment: Option<String>,
pub confirmed_by: Option<OutPoint>, // If this None, Sender keeps sending
pub timestamp: u64,
@ -275,11 +321,10 @@ impl CachedMessage {
pub fn try_decrypt_pcd(&self, cipher: Vec<u8>) -> Result<Vec<u8>> {
if self.prd.is_none() {
return Err(Error::msg(
"Can't try decrypt this message, there's no prd",
));
return Err(Error::msg("Can't try decrypt this message, there's no prd"));
}
let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?;
let aes_decrypt =
Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?;
aes_decrypt.decrypt_with_key()
}
@ -293,7 +338,8 @@ impl CachedMessage {
let mut key = [0u8; 32];
key.copy_from_slice(&shared_secret);
let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap()).expect("Shouldn't keep an invalid hex as cipher");
let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap())
.expect("Shouldn't keep an invalid hex as cipher");
let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, key)?;
aes_decrypt.decrypt_with_key()
@ -356,10 +402,15 @@ impl TrustedChannel {
self.revoked_in_block != [0u8; 32]
}
pub fn encrypt_msg_for(&self, msg: String) -> Result<String> {
pub fn encrypt_msg_for(&self, msg: String) -> Result<String> {
let mut rng = thread_rng();
let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into();
let aes256_encryption = Aes256Encryption::import_key(Purpose::Arbitrary, msg.into_bytes(), self.shared_secret, nonce)?;
let aes256_encryption = Aes256Encryption::import_key(
Purpose::Arbitrary,
msg.into_bytes(),
self.shared_secret,
nonce,
)?;
let cipher = aes256_encryption.encrypt_with_aes_key()?;
Ok(cipher.to_lower_hex_string())
}

170
src/process.rs Normal file
View File

@ -0,0 +1,170 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sp_client::bitcoin::hashes::Hash;
use sp_client::bitcoin::secp256k1::schnorr::Signature;
use sp_client::bitcoin::{OutPoint, Txid};
use sp_client::silentpayments::utils::SilentPaymentAddress;
use tsify::Tsify;
use uuid::Uuid;
use wasm_bindgen::prelude::*;
use crate::device::Device;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Tsify, PartialEq, PartialOrd)]
pub enum Role {
User,
Manager,
Admin,
}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct Member {
nym: String,
sp_address: String,
sp_address_alt: String,
role: Role,
}
impl Member {
pub fn new(
nym: String,
sp_address: SilentPaymentAddress,
sp_address_alt: SilentPaymentAddress,
role: Role,
) -> Self {
Self {
nym,
sp_address: sp_address.into(),
sp_address_alt: sp_address_alt.into(),
role,
}
}
pub fn get_nym(&self) -> String {
self.nym.clone()
}
pub fn get_addresses(&self) -> (String, String) {
(self.sp_address.clone(), self.sp_address_alt.clone())
}
pub fn get_role(&self) -> Role {
self.role
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct ValidationRules {
quorum: f32, // Must be > 0.0, <= 1.0
min_permission: Role, // Only users with at least that Role can send a token
}
impl ValidationRules {
pub fn new(quorum: f32, min_permission: Role) -> Self {
Self {
quorum,
min_permission,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Process {
pub uuid: String,
pub name: String,
pub version: u32,
pub roles: Vec<Member>,
pub validation_rules: ValidationRules,
pub initial_commit_tx: Txid,
pub latest_commit_tx: Txid,
pub html: String,
pub style: String,
pub script: String,
pub pcd_template: Value
}
impl Process {
pub fn new(
name: String,
roles: Vec<Member>,
validation_rules: ValidationRules,
initial_commit_tx: Txid,
html: String,
style: String,
script: String,
pcd_template: Value,
) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
name,
version: 1,
roles,
validation_rules,
initial_commit_tx,
latest_commit_tx: initial_commit_tx,
html,
style,
script,
pcd_template
}
}
pub fn new_pairing_process(
pcd: PairingPcd,
initial_commit_tx: Txid,
) -> Self {
let member = Member::new(
pcd.nym.clone(),
pcd.addresses[0].clone().try_into().unwrap(),
pcd.addresses[1].clone().try_into().unwrap(),
Role::Admin,
);
let validation_rules = ValidationRules::new(1.0, Role::Admin);
Self::new(
"pairing".to_owned(),
vec![member],
validation_rules,
initial_commit_tx,
"".to_owned(),
"".to_owned(),
"".to_owned(),
Value::from_str(&serde_json::to_string(&pcd).unwrap()).unwrap()
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairingPcd {
nym: String,
addresses: [String; 2],
session_index: u32,
revokation_index: u32,
pairing_txs: [Txid; 2],
current_session_txs: [Txid; 2]
}
impl PairingPcd {
pub fn new(
nym: String,
local_address: SilentPaymentAddress,
remote_address: SilentPaymentAddress,
session_index: u32,
revokation_index: u32,
incoming_pairing_tx: Txid,
outgoing_pairing_tx: Txid,
) -> Self {
let empty_txid = Txid::from_byte_array([0u8; Txid::LEN]);
Self {
nym,
addresses: [local_address.into(), remote_address.into()],
session_index,
revokation_index,
pairing_txs: [incoming_pairing_tx, outgoing_pairing_tx],
current_session_txs: [empty_txid, empty_txid]
}
}
}

View File

@ -6,7 +6,7 @@ use anyhow::{Error, Result};
use rand::{thread_rng, Rng};
use sp_client::bitcoin::consensus::deserialize;
use sp_client::bitcoin::psbt::raw;
use sp_client::bitcoin::{Psbt, Amount, OutPoint};
use sp_client::bitcoin::{Amount, OutPoint, Psbt};
use sp_client::constants::{
self, DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE,
};
@ -27,9 +27,7 @@ pub fn create_transaction(
.to_spendable_list()
// filter out freezed utxos
.into_iter()
.filter(|(outpoint, _)| {
!freezed_utxos.contains(outpoint)
})
.filter(|(outpoint, _)| !freezed_utxos.contains(outpoint))
.collect();
// if we have a payload, it means we are notifying, so let's add a revokation output
@ -37,13 +35,17 @@ pub fn create_transaction(
recipients.push(Recipient {
address: sp_wallet.get_client().get_receiving_address(),
amount: DUST_THRESHOLD,
nb_outputs: 1
nb_outputs: 1,
})
}
let sum_outputs = recipients.iter().fold(Amount::from_sat(0), |acc, x| acc + x.amount);
let sum_outputs = recipients
.iter()
.fold(Amount::from_sat(0), |acc, x| acc + x.amount);
let zero_value_recipient = recipients.iter_mut().find(|r| r.amount == Amount::from_sat(0));
let zero_value_recipient = recipients
.iter_mut()
.find(|r| r.amount == Amount::from_sat(0));
let mut inputs: HashMap<OutPoint, OwnedOutput> = HashMap::new();
let mut total_available = Amount::from_sat(0);
@ -89,7 +91,8 @@ pub fn create_transaction(
if let Some(address) = fee_payer {
SpClient::set_fees(&mut new_psbt, fee_rate, address)?;
} else {
let candidates: Vec<Option<String>> = new_psbt.outputs
let candidates: Vec<Option<String>> = new_psbt
.outputs
.iter()
.map(|o| {
if let Some(value) = o.proprietary.get(&raw::ProprietaryKey {
@ -112,17 +115,17 @@ pub fn create_transaction(
for candidate in candidates {
if let Some(c) = candidate {
if c == change_address {
SpClient::set_fees(&mut new_psbt, fee_rate, change_address.clone())?;
SpClient::set_fees(&mut new_psbt, fee_rate, change_address.clone())?;
fee_set = true;
break;
} else if c == sender_address {
SpClient::set_fees(&mut new_psbt, fee_rate, sender_address.clone())?;
SpClient::set_fees(&mut new_psbt, fee_rate, sender_address.clone())?;
fee_set = true;
break;
}
}
}
if !fee_set {
return Err(Error::msg("Must specify payer for fee"));
}
@ -172,13 +175,14 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result<HashMap<String, Vec<u
mod tests {
use std::io::Write;
use crate::network::{Pcd, Prd};
use crate::network::{Pcd, Prd, PrdType};
use crate::process::{Member, Process, Role, ValidationRules};
use super::*;
use sp_client::bitcoin::hashes::{sha256, Hash};
use sp_client::bitcoin::hex::FromHex;
use sp_client::bitcoin::secp256k1::PublicKey;
use sp_client::bitcoin::{ScriptBuf, Transaction};
use sp_client::bitcoin::{ScriptBuf, Transaction, Txid};
use sp_client::silentpayments::utils::receiving::{
calculate_tweak_data, get_pubkey_from_input,
};
@ -193,6 +197,7 @@ mod tests {
const BOB_ADDRESS: &str = "sprt1qqf2kqh8n7lkupngfk2fxtmvyaq8sy3v2ramyeryweadqmsz7l6tg2qc9n4dl42ff8klz28nznr7mfzh6263xv555stwzextl2v4lvhg3aqq7ru8u";
const FEE_RATE: Amount = Amount::from_sat(1);
const KEY: &str = "442a5ea418921c4aa8ce3f7a95427d9450059a3ac11db3dced9abb709b2d9f42";
const INITIAL_COMMIT_TX: &str = "bc7d2ef1820cf67edb900e4c59cae6c692663cd690e691a55df919f002ede841";
fn helper_get_tweak_data(tx: &Transaction, spk: ScriptBuf) -> PublicKey {
let prevout = tx.input.get(0).unwrap().to_owned();
@ -227,7 +232,49 @@ mod tests {
let pcd_hash = helper_create_commitment(pcd.to_string());
let mut key = [0u8; 32];
key.copy_from_slice(&Vec::from_hex(KEY).unwrap());
let prd = Prd::new(ALICE_ADDRESS.try_into().unwrap(), key, pcd_hash);
let alice_member = Member::new(
"alice".to_owned(),
alice_wallet.get_client().get_receiving_address().try_into().unwrap(),
alice_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(),
Role::Admin,
);
let bob_member = Member::new(
"bob".to_owned(),
bob_wallet.get_client().get_receiving_address().try_into().unwrap(),
bob_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(),
Role::User,
);
let validation_rules = ValidationRules::new(0.5, Role::User);
let pcd_template = serde_json::json!({
"int": 0,
"string": "exemple_data",
"array": [
"element1",
"element2"
]
});
let process = Process::new(
"default".to_owned(),
vec![alice_member, bob_member],
validation_rules,
Txid::from_str(INITIAL_COMMIT_TX).unwrap(),
"".to_owned(),
"".to_owned(),
"".to_owned(),
pcd_template,
);
let prd = Prd::new(
PrdType::Update,
process,
ALICE_ADDRESS.try_into().unwrap(),
key,
pcd_hash,
).unwrap();
let commitment = helper_create_commitment(serde_json::to_string(&prd).unwrap());
let psbt = create_transaction(