Merge branch 'minimal_prd_pcd' into dev

This commit is contained in:
Sosthene 2024-09-25 10:11:29 +02:00
commit f3ab3525a3
10 changed files with 1042 additions and 686 deletions

View File

@ -10,9 +10,10 @@ crate-type = ["cdylib", "rlib"]
aes-gcm = "0.10.3"
anyhow = "1.0"
js-sys = "0.3.69"
log = "0.4.6"
rand = "0.8.5"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
serde_json = { version = "1.0.108", features = ["preserve_order"]}
# 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" }

View File

@ -1,63 +1,11 @@
use std::collections::HashMap;
use anyhow::{Error, Result};
use serde::{Deserialize, Serialize};
use sp_client::{
bitcoin::{
hex::{DisplayHex, FromHex},
key::constants::SECRET_KEY_SIZE,
Txid,
},
silentpayments::{
bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine},
secp256k1::PublicKey,
utils::SilentPaymentAddress,
},
};
use tsify::Tsify;
use sp_client::silentpayments::{bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine}, secp256k1::PublicKey};
use aes_gcm::aead::{Aead, Payload};
pub use aes_gcm::{AeadCore, Aes256Gcm, KeyInit};
use rand::thread_rng;
const AAD: &[u8] = "4nk".as_bytes();
const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2;
const THIRTYTWO: usize = 32;
#[derive(Debug)]
pub struct SharedPoint([u8; 64]);
impl SharedPoint {
pub fn as_inner(&self) -> &[u8; 64] {
&self.0
}
}
#[derive(Debug, Serialize, Deserialize, Tsify, Clone, Default, PartialEq)]
#[tsify(from_wasm_abi, into_wasm_abi)]
pub struct AnkSharedSecret {
secret: String,
}
impl AnkSharedSecret {
pub fn new(shared_point: PublicKey) -> Self {
let mut shared_point_bin = [0u8; 64];
shared_point_bin.copy_from_slice(&shared_point.serialize_uncompressed()[1..]);
let secret = AnkSharedSecretHash::from_shared_point(shared_point_bin).to_byte_array();
Self {
secret: secret.to_lower_hex_string(),
}
}
pub fn to_byte_array(&self) -> [u8; 32] {
let bytes = Vec::from_hex(&self.secret).unwrap();
let mut buf = [0u8; 32];
buf.copy_from_slice(&bytes);
buf
}
}
pub const AAD: &[u8] = "4nk".as_bytes();
sha256t_hash_newtype! {
pub struct AnkSharedSecretTag = hash_str("4nk/AnkSharedSecret");
@ -67,317 +15,39 @@ sha256t_hash_newtype! {
}
impl AnkSharedSecretHash {
pub fn from_shared_point(shared_point: [u8; 64]) -> Self {
pub fn from_shared_point(shared_point: PublicKey) -> Self {
let mut eng = AnkSharedSecretHash::engine();
eng.input(&shared_point);
eng.input(&shared_point.serialize_uncompressed()[1..]);
AnkSharedSecretHash::from_engine(eng)
}
}
pub struct HalfKey([u8; HALFKEYSIZE]);
pub fn encrypt_with_key(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
let encryption_eng = Aes256Gcm::new(key.into());
let nonce = Aes256Gcm::generate_nonce(&mut thread_rng());
let payload = Payload {
msg: plaintext,
aad: AAD,
};
let ciphertext = encryption_eng.encrypt(&nonce, payload)
.map_err(|e| anyhow::anyhow!(e))?;
impl TryFrom<Vec<u8>> for HalfKey {
type Error = anyhow::Error;
fn try_from(value: Vec<u8>) -> std::prelude::v1::Result<Self, Error> {
if value.len() == HALFKEYSIZE {
let mut buf = [0u8; HALFKEYSIZE];
buf.copy_from_slice(&value);
Ok(HalfKey(buf))
} else {
Err(Error::msg("Invalid length for HalfKey"))
}
}
let mut res: Vec<u8> = Vec::with_capacity(nonce.len() + ciphertext.len());
res.extend_from_slice(&nonce);
res.extend_from_slice(&ciphertext);
Ok(res)
}
impl HalfKey {
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn decrypt_with_key(key: &[u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>> {
let decryption_eng = Aes256Gcm::new(key.into());
let nonce = &ciphertext[..12];
let payload = Payload {
msg: &ciphertext[12..],
aad: AAD,
};
let plaintext = decryption_eng.decrypt(nonce.into(), payload)
.map_err(|e| anyhow::anyhow!(e))?;
pub fn to_inner(&self) -> Vec<u8> {
self.0.to_vec()
}
}
pub enum Purpose {
Login,
ThirtyTwoBytes,
Arbitrary,
}
pub type CipherText = Vec<u8>;
pub type EncryptedKey = Vec<u8>;
pub struct Aes256Decryption {
pub purpose: Purpose,
cipher_text: CipherText,
aes_key: [u8; 32],
nonce: [u8; 12],
}
impl Aes256Decryption {
pub fn new(purpose: Purpose, cipher_text: CipherText, aes_key: [u8; 32]) -> Result<Self> {
if cipher_text.len() <= 12 {
return Err(Error::msg("cipher_text is shorter than nonce length"));
}
let (message_nonce, message_cipher) = cipher_text.split_at(12);
let mut nonce = [0u8; 12];
nonce.copy_from_slice(message_nonce);
Ok(Self {
purpose,
cipher_text: message_cipher.to_vec(),
aes_key,
nonce,
})
}
pub fn decrypt_with_key(&self) -> Result<Vec<u8>> {
match self.purpose {
Purpose::Login => {
let half_key = self.decrypt_login()?;
Ok(half_key.to_inner())
}
Purpose::ThirtyTwoBytes => {
let thirty_two_buf = self.decrypt_thirty_two()?;
Ok(thirty_two_buf.to_vec())
}
Purpose::Arbitrary => {
let arbitrary = self.decrypt_arbitrary()?;
Ok(arbitrary)
}
}
}
fn decrypt_login(&self) -> Result<HalfKey> {
let cipher = Aes256Gcm::new(&self.aes_key.into());
let plain = cipher
.decrypt(&self.nonce.into(), &*self.cipher_text)
.map_err(|e| Error::msg(format!("{}", e)))?;
if plain.len() != SECRET_KEY_SIZE / 2 {
return Err(Error::msg("Plain text of invalid lenght for a login"));
}
let mut key_half = [0u8; SECRET_KEY_SIZE / 2];
key_half.copy_from_slice(&plain);
Ok(HalfKey(key_half))
}
fn decrypt_thirty_two(&self) -> Result<[u8; THIRTYTWO]> {
let cipher = Aes256Gcm::new(&self.aes_key.into());
let plain = cipher
.decrypt(&self.nonce.into(), &*self.cipher_text)
.map_err(|e| Error::msg(format!("{}", e)))?;
if plain.len() != THIRTYTWO {
return Err(Error::msg("Plain text of invalid length, should be 32"));
}
let mut thirty_two = [0u8; THIRTYTWO];
thirty_two.copy_from_slice(&plain);
Ok(thirty_two)
}
fn decrypt_arbitrary(&self) -> Result<Vec<u8>> {
let cipher = Aes256Gcm::new(&self.aes_key.into());
let payload = Payload {
msg: &self.cipher_text,
aad: AAD,
};
let plain = cipher
.decrypt(&self.nonce.into(), payload)
.map_err(|e| Error::msg(format!("{}", e)))?;
Ok(plain)
}
}
pub struct Aes256Encryption {
pub purpose: Purpose,
plaintext: Vec<u8>,
aes_key: [u8; 32],
nonce: [u8; 12],
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, AnkSharedSecret>>,
}
impl Aes256Encryption {
pub fn new(purpose: Purpose, plaintext: Vec<u8>) -> Result<Self> {
let mut rng = thread_rng();
let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into();
let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into();
Self::import_key(purpose, plaintext, aes_key, nonce)
}
pub fn set_shared_secret(
&mut self,
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, AnkSharedSecret>>,
) {
self.shared_secrets = shared_secrets;
}
pub fn encrypt_keys_with_shared_secrets(
&self,
) -> Result<HashMap<SilentPaymentAddress, EncryptedKey>> {
let mut res = HashMap::new();
let mut rng = thread_rng();
for (_, sp_address2shared_secret) in self.shared_secrets.iter() {
for (sp_address, shared_secret) in sp_address2shared_secret {
let cipher = Aes256Gcm::new_from_slice(&shared_secret.to_byte_array())
.map_err(|e| Error::msg(format!("{}", e)))?;
let nonce = Aes256Gcm::generate_nonce(&mut rng);
let encrypted_key = cipher
.encrypt(&nonce, self.aes_key.as_slice())
.map_err(|e| Error::msg(format!("{}", e)))?;
let mut ciphertext = Vec::<u8>::with_capacity(nonce.len() + encrypted_key.len());
ciphertext.extend(nonce);
ciphertext.extend(encrypted_key);
res.insert(sp_address.to_owned(), ciphertext);
}
}
Ok(res)
}
pub fn import_key(
purpose: Purpose,
plaintext: Vec<u8>,
aes_key: [u8; 32],
nonce: [u8; 12],
) -> Result<Self> {
if plaintext.len() == 0 {
return Err(Error::msg("Can't create encryption for an empty message"));
}
Ok(Self {
purpose,
plaintext,
aes_key,
nonce,
shared_secrets: HashMap::new(),
})
}
pub fn export_key(&self) -> [u8; 32] {
self.aes_key
}
pub fn encrypt_with_aes_key(&self) -> Result<CipherText> {
match self.purpose {
Purpose::Login => self.encrypt_login(),
Purpose::ThirtyTwoBytes => self.encrypt_thirty_two(),
Purpose::Arbitrary => self.encrypt_arbitrary(),
}
}
fn encrypt_login(&self) -> Result<CipherText> {
let half_key: HalfKey = self.plaintext.clone().try_into()?;
let cipher = Aes256Gcm::new(&self.aes_key.into());
let cipher_text = cipher
.encrypt(&self.nonce.into(), half_key.as_slice())
.map_err(|e| Error::msg(format!("{}", e)))?;
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
res.extend_from_slice(&self.nonce);
res.extend_from_slice(&cipher_text);
Ok(res)
}
fn encrypt_thirty_two(&self) -> Result<CipherText> {
if self.plaintext.len() != 32 {
return Err(Error::msg("Invalid length, should be 32"));
}
let mut thirty_two = [0u8; 32];
thirty_two.copy_from_slice(&self.plaintext);
let cipher = Aes256Gcm::new(&self.aes_key.into());
let cipher_text = cipher
.encrypt(&self.nonce.into(), thirty_two.as_slice())
.map_err(|e| Error::msg(format!("{}", e)))?;
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
res.extend_from_slice(&self.nonce);
res.extend_from_slice(&cipher_text);
Ok(res)
}
fn encrypt_arbitrary(&self) -> Result<CipherText> {
let cipher = Aes256Gcm::new(&self.aes_key.into());
let payload = Payload {
msg: &self.plaintext,
aad: AAD,
};
let cipher_text = cipher
.encrypt(&self.nonce.into(), payload)
.map_err(|e| Error::msg(format!("{}", e)))?;
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
res.extend_from_slice(&self.nonce);
res.extend_from_slice(&cipher_text);
Ok(res)
}
}
#[cfg(test)]
mod tests {
use std::{io::Read, str::FromStr};
use sp_client::bitcoin::hex::FromHex;
use super::*;
const ALICE_SP_ADDRESS: &str = "tsp1qqw3lqr6xravz9nf8ntazgwwl0fqv47kfjdxsnxs6eutavqfwyv5q6qk97mmyf6dtkdyzqlu2zv6h9j2ggclk7vn705q5u2phglpq7yw3dg5rwpdz";
const BOB_SP_ADDRESS: &str = "tsp1qq2hlsgrj0gz8kcfkf9flqw5llz0u2vr04telqndku9mcqm6dl4fhvq60t8r78srrf56w9yr7w9e9dusc2wjqc30up6fjwnh9mw3e3veqegdmtf08";
const TRANSACTION: &str = "4e6d03dec558e1b6624f813bf2da7cd8d8fb1c2296684c08cf38724dcfd8d10b";
const ALICE_SHARED_SECRET: &str =
"ccf02d364c2641ca129a3fdf49de57b705896e233f7ba6d738991993ea7e2106";
const BOB_SHARED_SECRET: &str =
"15ef3e377fb842e81de52dbaaea8ba30aeb051a81043ee19264afd27353da521";
#[test]
fn new_aes_empty_plaintext() {
let plaintext = Vec::new();
let aes_enc = Aes256Encryption::new(Purpose::Login, plaintext);
assert!(aes_enc.is_err());
}
#[test]
fn aes_encrypt_login_invalid_length() {
let plaintext = "example";
let aes_enc_short = Aes256Encryption::new(Purpose::Login, plaintext.as_bytes().to_vec());
assert!(aes_enc_short.is_ok());
let cipher = aes_enc_short.unwrap().encrypt_with_aes_key();
assert!(cipher.is_err());
let plaintext = [1u8; 64];
let aes_enc_long = Aes256Encryption::new(Purpose::Login, plaintext.to_vec());
assert!(aes_enc_long.is_ok());
let cipher = aes_enc_long.unwrap().encrypt_with_aes_key();
assert!(cipher.is_err());
}
#[test]
fn aes_encrypt_login() {
let plaintext = [1u8; HALFKEYSIZE];
let aes_key = Aes256Gcm::generate_key(&mut thread_rng());
let nonce = Aes256Gcm::generate_nonce(&mut thread_rng());
let aes_enc = Aes256Encryption::import_key(
Purpose::Login,
plaintext.to_vec(),
aes_key.into(),
nonce.into(),
);
assert!(aes_enc.is_ok());
let cipher = aes_enc.unwrap().encrypt_with_aes_key();
assert!(cipher.is_ok());
let mut plain_key = [0u8; 32];
plain_key.copy_from_slice(&aes_key.to_vec());
let aes_dec = Aes256Decryption::new(Purpose::Login, cipher.unwrap(), plain_key);
assert!(aes_dec.is_ok());
}
Ok(plaintext)
}

60
src/device.rs Normal file
View File

@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use sp_client::{bitcoin::{hashes::Hash, OutPoint, Txid}, spclient::SpWallet};
use crate::pcd::Member;
#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Device {
sp_wallet: SpWallet,
pairing_process_commitment: Option<Txid>,
paired_member: Option<Member>,
}
impl Device {
pub fn new(sp_wallet: SpWallet) -> Self {
Self {
sp_wallet,
pairing_process_commitment: None,
paired_member: 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_linking(&self) -> bool {
match self.pairing_process_commitment {
Some(ref value) => value.as_raw_hash().as_byte_array().iter().all(|&b| b == 0),
None => false,
}
}
pub fn is_linked(&self) -> bool {
match self.pairing_process_commitment {
Some(ref value) => !value.as_raw_hash().as_byte_array().iter().all(|&b| b == 0),
None => false,
}
}
pub fn get_process_commitment(&self) -> Option<Txid> {
self.pairing_process_commitment.clone()
}
pub fn pair(&mut self, commitment_tx: Txid, member: Member) {
self.pairing_process_commitment = Some(commitment_tx);
self.paired_member = Some(member);
}
pub fn to_member(&self) -> Option<Member> {
self.paired_member.clone()
}
}

View File

@ -1,6 +1,29 @@
use std::sync::{Mutex, MutexGuard};
use std::fmt::Debug;
pub use sp_client;
pub use log;
pub use aes_gcm;
pub mod crypto;
pub mod device;
pub mod error;
pub mod network;
pub mod pcd;
pub mod prd;
pub mod process;
pub mod silentpayments;
pub mod signature;
pub const MAX_PRD_PAYLOAD_SIZE: usize = u16::MAX as usize; // 64KiB sounds reasonable for now
pub trait MutexExt<T> {
fn lock_anyhow(&self) -> Result<MutexGuard<T>, anyhow::Error>;
}
impl<T: Debug> MutexExt<T> for Mutex<T> {
fn lock_anyhow(&self) -> Result<MutexGuard<T>, anyhow::Error> {
self.lock()
.map_err(|e| anyhow::Error::msg(format!("Failed to lock: {}", e)))
}
}

View File

@ -1,21 +1,17 @@
use std::default;
use std::str::FromStr;
use aes_gcm::{AeadCore, Aes256Gcm};
use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, KeyInit};
use anyhow::{Error, Result};
use js_sys::Date;
use rand::{thread_rng, RngCore};
use serde::{Deserialize, Serialize};
use sp_client::bitcoin::consensus::serialize;
use sp_client::bitcoin::hashes::Hash;
use serde_json::Value;
use sp_client::bitcoin::hex::{DisplayHex, FromHex};
use sp_client::bitcoin::secp256k1::PublicKey;
use sp_client::bitcoin::{BlockHash, OutPoint, Transaction};
use sp_client::silentpayments::utils::SilentPaymentAddress;
use sp_client::bitcoin::OutPoint;
use tsify::Tsify;
use crate::crypto::{Aes256Decryption, Aes256Encryption, Purpose};
use crate::crypto::AAD;
use crate::error::AnkError;
use crate::pcd::Member;
#[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
@ -109,36 +105,13 @@ impl NewTxMessage {
}
}
#[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct CipherMessage {
pub sender: String,
pub message: String,
pub error: Option<AnkError>,
}
impl CipherMessage {
pub fn new(sender: String, message: String) -> Self {
Self {
sender,
message,
error: None,
}
}
pub fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
#[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct AnkNetworkMsg {
#[derive(Debug, Serialize, Deserialize)]
pub struct Envelope {
pub flag: AnkFlag,
pub content: String,
}
impl AnkNetworkMsg {
impl Envelope {
pub fn new(flag: AnkFlag, raw: &str) -> Self {
Self {
flag,
@ -160,48 +133,28 @@ impl AnkNetworkMsg {
pub enum CachedMessageStatus {
#[default]
NoStatus,
FaucetWaiting,
Pairing,
Login,
CipherWaitingTx,
TxWaitingCipher,
SentWaitingConfirmation, // we're sender and wait for commited_in to be spent
ReceivedMustConfirm, // we're receiver and we spend commited_in to sender
MustSpendConfirmation, // we're sender and we must spend confirmed_by
Trusted, // Can receive more messages
Closed, // No more messages will be trusted from this handshake
TxWaitingPrd,
Opened, // Can receive more messages
}
/// Unique struct for both 4nk messages and notification/key exchange, both rust and ts
/// 0. Faucet: commited_in with nothing else, status is NoStatus
/// 1. notification:
/// 0. sender: ciphertext, plaintext, commited_in, sender, recipient, shared_secret, key
/// 1. receiver (without tx): ciphertext
/// 2. receiver (tx without msg): commited_in, commitment, recipient, shared_secret
/// 3. receiver (receive tx after msg): plaintext, key, sender, commited_in, commitment, recipient, shared_secret
/// 4. receiver (msg after tx): ciphertext, key, plaintext, sender
/// 2. confirmation:
/// 0. receiver (spend the smallest vout that pays him in the first tx): confirmed_by
/// 1. sender (detect a transaction that pays him and spend commited_by): confirmed_by
/// 2. sender toggle status to complete when it spent confirmed_by, receiver when it detects the confirmed_by is spent
#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone, PartialEq)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct CachedMessage {
pub id: u32,
pub status: CachedMessageStatus,
pub ciphertext: Option<String>, // Needed when we are waiting for a key in transaction, discarded after
pub plaintext: Vec<String>, // Append new messages received while in Trusted state
pub commited_in: Option<OutPoint>,
pub tied_by: Option<u32>, // index of the output that ties the proposal
pub transaction: Option<String>,
pub commitment: Option<String>, // content of the op_return
pub sender: Option<String>, // Never None when message sent
pub recipient: Option<String>, // Never None when message sent
pub shared_secret: Option<String>, // Never None when message sent
pub key: Option<String>, // Never None when message sent
pub sender: Option<Member>, // Never None when message sent
pub recipient: Option<Member>, // Never None when message sent
pub shared_secrets: Vec<String>, // Max 2 secrets in case we send to both address of the recipient
pub cipher: Vec<String>, // Max 2 ciphers in case we send to both address of the recipient
pub prd: Option<String>, // Never None when message sent
pub pcd: Option<Value>, // Value is here an alias for impl Pcd
pub confirmed_by: Option<OutPoint>, // If this None, Sender keeps sending
pub timestamp: u64,
pub error: Option<AnkError>,
}
impl CachedMessage {
@ -223,93 +176,54 @@ impl CachedMessage {
serde_json::to_string(self).unwrap()
}
pub fn try_decrypt_cipher(&self, cipher: Vec<u8>) -> Result<Vec<u8>> {
if self.shared_secret.is_none() {
pub fn try_decrypt_message(&self, cipher: Vec<u8>) -> Result<Vec<u8>> {
if self.shared_secrets.is_empty() {
return Err(Error::msg(
"Can't try decrypt this message, there's no shared secret",
));
}
let mut shared_secret = [0u8; 32];
shared_secret.copy_from_slice(&Vec::from_hex(self.shared_secret.as_ref().unwrap())?);
let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, shared_secret)?;
for shared_secret in &self.shared_secrets {
let mut key = [0u8; 32];
let mut nonce = [0u8; 12];
key.copy_from_slice(&Vec::from_hex(shared_secret)?);
nonce.copy_from_slice(&cipher[..12]);
aes_decrypt.decrypt_with_key()
let engine = Aes256Gcm::new(&key.into());
let payload = Payload {
msg: &cipher[12..],
aad: AAD,
};
match engine.decrypt(&nonce.into(), payload) {
Ok(plain) => return Ok(plain),
Err(_) => continue
}
}
Err(Error::msg("Failed to decrypt message"))
}
pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Result<Vec<u8>> {
if self.ciphertext.is_none() || self.shared_secret.is_some() {
if self.cipher.is_empty() || !self.shared_secrets.is_empty() {
return Err(Error::msg(
"Can't try decrypt this message, ciphertext is none or shared_secret already found",
));
}
let cipher_bin = Vec::from_hex(self.ciphertext.as_ref().unwrap())?;
let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher_bin, shared_secret)?;
for prd_cipher in &self.cipher {
let cipher = Vec::from_hex(prd_cipher)?;
let mut nonce = [0u8; 12];
nonce.copy_from_slice(&cipher[..12]);
aes_decrypt.decrypt_with_key()
}
}
#[derive(Debug, Serialize, Deserialize, Tsify, Default)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct TrustedChannel {
id: u32, // Just take the id of the cached message?
revokation_outpoint: OutPoint, // revokation output is locked by the shared_secret
shared_secret: [u8; 32],
revoked_in_block: [u8; 32],
with: String, // Silent payment address
}
impl TrustedChannel {
pub fn new(with: String) -> Result<Self> {
// check that with is valid silent payment address
SilentPaymentAddress::try_from(with.as_str())?;
// Generating random id
let mut new = Self::default();
let mut buf = [0u8; 4];
thread_rng().fill_bytes(&mut buf);
new.id = u32::from_be_bytes(buf);
new.with = with;
Ok(new)
}
pub fn set_revokation(
&mut self,
revokation_outpoint: String,
shared_secret: String,
) -> Result<()> {
let mut buf = [0u8; 32];
buf.copy_from_slice(&Vec::from_hex(&shared_secret)?);
self.revokation_outpoint = OutPoint::from_str(&revokation_outpoint)?;
self.shared_secret.copy_from_slice(&buf);
Ok(())
}
pub fn revoke(&mut self, revoked_in_block: String) -> Result<()> {
let block_hash = BlockHash::from_str(&revoked_in_block)?;
let mut buf = [0u8; 32];
buf.copy_from_slice(&serialize::<BlockHash>(&block_hash));
self.revoked_in_block = buf;
Ok(())
}
pub fn is_set(&self) -> bool {
self.revokation_outpoint == OutPoint::default()
}
pub fn is_revoked(&self) -> bool {
self.revoked_in_block != [0u8; 32]
}
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 cipher = aes256_encryption.encrypt_with_aes_key()?;
Ok(cipher.to_lower_hex_string())
let engine = Aes256Gcm::new(&shared_secret.into());
let payload = Payload {
msg: &cipher[12..],
aad: AAD,
};
match engine.decrypt(&nonce.into(), payload) {
Ok(plain) => return Ok(plain),
Err(_) => continue
}
}
Err(Error::msg("Failed to decrypt message"))
}
}

233
src/pcd.rs Normal file
View File

@ -0,0 +1,233 @@
use std::{collections::HashSet, str::FromStr};
use anyhow::{Result, Error};
use aes_gcm::{aead::{Aead, Payload}, AeadCore, Aes256Gcm, KeyInit};
use log::debug;
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sp_client::{bitcoin::{hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, Txid}, silentpayments::utils::SilentPaymentAddress};
use tsify::Tsify;
use crate::crypto::AAD;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Member {
sp_addresses: Vec<String>
}
impl Member {
pub fn new(
sp_addresses: Vec<SilentPaymentAddress>,
) -> Result<Self> {
if sp_addresses.is_empty() {
return Err(Error::msg("empty address set"));
}
let mut seen = HashSet::new();
for s in sp_addresses.iter() {
if !seen.insert(s.clone()) {
return Err(Error::msg("Duplicate addresses found"));
}
}
let res: Vec<String> = sp_addresses.iter()
.map(|a| Into::<String>::into(*a))
.collect();
Ok(Self {
sp_addresses: res
})
}
pub fn get_addresses(&self) -> Vec<String> {
self.sp_addresses.clone()
}
}
sha256t_hash_newtype! {
pub struct AnkPcdTag = hash_str("4nk/Pcd");
#[hash_newtype(forward)]
pub struct AnkPcdHash(_);
}
impl AnkPcdHash {
pub fn from_value(value: &Value) -> Self {
let mut eng = AnkPcdHash::engine();
eng.input(value.to_string().as_bytes());
AnkPcdHash::from_engine(eng)
}
pub fn from_map(map: &Map<String, Value>) -> Self {
let value = Value::Object(map.clone());
let mut eng = AnkPcdHash::engine();
eng.input(value.to_string().as_bytes());
AnkPcdHash::from_engine(eng)
}
}
pub trait Pcd<'a>: Serialize + Deserialize<'a> {
fn tagged_hash(&self) -> AnkPcdHash {
AnkPcdHash::from_value(&self.to_value())
}
fn encrypt_fields(&self, fields2keys: &mut Map<String, Value>, fields2cipher: &mut Map<String, Value>) -> Result<()> {
let as_value = self.to_value();
let as_map = as_value.as_object().ok_or_else(|| Error::msg("Expected object"))?;
let mut rng = thread_rng();
for (field, value) in as_map {
let aes_key = Aes256Gcm::generate_key(&mut rng);
let nonce = Aes256Gcm::generate_nonce(&mut rng);
fields2keys.insert(field.to_owned(), Value::String(aes_key.to_lower_hex_string()));
let encrypt_eng = Aes256Gcm::new(&aes_key);
let value_string = value.to_string();
let payload = Payload {
msg: value_string.as_bytes(),
aad: AAD,
};
let cipher = encrypt_eng.encrypt(&nonce, payload)
.map_err(|e| Error::msg(format!("Encryption failed for field {}: {}", field, e)))?;
let mut res = Vec::with_capacity(nonce.len() + cipher.len());
res.extend_from_slice(&nonce);
res.extend_from_slice(&cipher);
fields2cipher.insert(field.to_owned(), Value::String(res.to_lower_hex_string()));
}
Ok(())
}
fn decrypt_fields(&self, fields2keys: &Map<String, Value>, fields2plain: &mut Map<String, Value>) -> Result<()> {
let value = self.to_value();
let map = value.as_object().unwrap();
for (field, encrypted_value) in map.iter() {
if let Some(aes_key) = fields2keys.get(field) {
let key_buf = Vec::from_hex(&aes_key.to_string().trim_matches('\"'))?;
let decrypt_eng = Aes256Gcm::new(key_buf.as_slice().into());
let raw_cipher = Vec::from_hex(&encrypted_value.as_str().ok_or_else(|| Error::msg("Expected string"))?.trim_matches('\"'))?;
if raw_cipher.len() < 28 {
return Err(Error::msg(format!("Invalid ciphertext length for field {}", field)));
}
let payload = Payload {
msg: &raw_cipher[12..],
aad: AAD,
};
let plain = decrypt_eng.decrypt(raw_cipher[..12].into(), payload)
.map_err(|_| Error::msg(format!("Failed to decrypt field {}", field)))?;
let decrypted_value: String = String::from_utf8(plain)?;
fields2plain.insert(field.to_owned(), Value::String(decrypted_value));
} else {
fields2plain.insert(field.to_owned(), Value::Null);
}
}
Ok(())
}
fn to_value(&self) -> Value {
Value::from_str(&serde_json::to_string(&self).unwrap()).unwrap()
}
}
impl Pcd<'_> for Value {}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct ValidationRule {
quorum: f32, // Must be >= 0.0, <= 1.0, 0.0 means reading right
pub fields: Vec<String>, // Which fields are concerned by this rule
min_sig_member: f32, // Must be >= 0.0, <= 1.0, does each member need to sign with all it's devices?
}
impl ValidationRule {
pub fn new(quorum: f32, fields: Vec<String>, min_sig_member: f32) -> Result<Self> {
if quorum < 0.0 || quorum > 1.0 {
return Err(Error::msg("quorum must be 0.0 < quorum <= 1.0"));
}
if min_sig_member < 0.0 || min_sig_member > 1.0 {
return Err(Error::msg("min_signatures_member must be 0.0 < min_signatures_member <= 1.0"));
}
if fields.is_empty() {
return Err(Error::msg("Fields can't be empty"));
}
let res = Self {
quorum,
fields,
min_sig_member,
};
Ok(res)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct RoleDefinition {
pub members: Vec<Member>,
pub validation_rules: Vec<ValidationRule>,
}
pub fn compare_maps(map1: &Map<String, Value>, map2: &Map<String, Value>) -> bool {
// First, check if both maps have the same keys
if map1.keys().collect::<Vec<&String>>() != map2.keys().collect::<Vec<&String>>() {
return false;
}
// Then, check if the corresponding values have the same type
for key in map1.keys() {
let value1 = map1.get(key).unwrap();
let value2 = map2.get(key).unwrap();
if !compare_values(value1, value2) {
return false;
}
}
true
}
fn compare_values(value1: &Value, value2: &Value) -> bool {
if value1.is_null() && value2.is_null() {
return true;
} else if value1.is_boolean() && value2.is_boolean() {
return true;
} else if value1.is_number() && value2.is_number() {
return true;
} else if value1.is_string() && value2.is_string() {
return true;
} else if value1.is_array() && value2.is_array() {
return compare_arrays(value1.as_array().unwrap(), value2.as_array().unwrap());
} else if value1.is_object() && value2.is_object() {
// Recursive comparison for nested objects
return compare_maps(value1.as_object().unwrap(), value2.as_object().unwrap());
} else {
return false;
}
}
fn compare_arrays(array1: &Vec<Value>, array2: &Vec<Value>) -> bool {
// Compare the type of each element in the arrays
for (elem1, elem2) in array1.iter().zip(array2.iter()) {
if !compare_values(elem1, elem2) {
return false;
}
}
true
}

209
src/prd.rs Normal file
View File

@ -0,0 +1,209 @@
use std::collections::HashSet;
use std::str::FromStr;
use anyhow::{Result, Error};
use serde::{Serialize, Deserialize};
use serde_json::{Map, Value};
use sp_client::bitcoin::hex::FromHex;
use sp_client::bitcoin::secp256k1::SecretKey;
use sp_client::bitcoin::{OutPoint, XOnlyPublicKey};
use sp_client::silentpayments::utils::SilentPaymentAddress;
use sp_client::spclient::SpWallet;
use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine};
use tsify::Tsify;
use crate::pcd::{AnkPcdHash, Member, Pcd};
use crate::signature::{AnkHash, AnkMessageHash, Proof};
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub enum PrdType {
#[default]
None,
Message,
Update, // Update an existing process
List, // request a list of items
Response,
Confirm,
TxProposal, // Send a psbt asking for recipient signature, used for login not sure about other use cases
}
sha256t_hash_newtype! {
pub struct AnkPrdTag = hash_str("4nk/Prd");
#[hash_newtype(forward)]
pub struct AnkPrdHash(_);
}
impl AnkPrdHash {
pub fn from_value(value: &Value) -> Self {
let mut eng = AnkPrdHash::engine();
eng.input(value.to_string().as_bytes());
AnkPrdHash::from_engine(eng)
}
pub fn from_map(map: &Map<String, Value>) -> Self {
let value = Value::Object(map.clone());
let mut eng = AnkPrdHash::engine();
eng.input(value.to_string().as_bytes());
AnkPrdHash::from_engine(eng)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[allow(non_camel_case_types)]
pub struct Prd {
pub prd_type: PrdType,
pub root_commitment: String,
pub sender: String,
pub keys: Map<String, Value>, // key is a key in pcd, value is the key to decrypt it
pub validation_tokens: Vec<Proof>,
pub payload: String, // Payload depends on the actual type
pub proof: Option<Proof>, // This must be None up to the creation of the network message
}
impl Prd {
pub fn new_update(
root_commitment: OutPoint,
sender: String, // Should take Member as argument
encrypted_pcd: Map<String, Value>,
keys: Map<String, Value>
) -> Self {
Self {
prd_type: PrdType::Update,
root_commitment: root_commitment.to_string(),
sender,
validation_tokens: vec![],
keys,
payload: Value::Object(encrypted_pcd).to_string(),
proof: None,
}
}
pub fn new_response(
root_commitment: OutPoint,
sender: String,
validation_token: Proof,
pcd_commitment: AnkPcdHash,
) -> Self {
Self {
prd_type: PrdType::Response,
root_commitment: root_commitment.to_string(),
sender,
validation_tokens: vec![validation_token],
keys: Map::new(),
payload: pcd_commitment.to_string(),
proof: None,
}
}
pub fn new_confirm(
root_commitment: OutPoint,
sender: Member,
pcd_commitment: AnkPcdHash,
) -> Self {
Self {
prd_type: PrdType::Confirm,
root_commitment: root_commitment.to_string(),
sender: serde_json::to_string(&sender).unwrap(),
validation_tokens: vec![],
keys: Map::new(),
payload: pcd_commitment.to_string(),
proof: None,
}
}
fn _extract_from_message(plain: &[u8], commitment: Option<&AnkPrdHash>) -> Result<Self> {
let prd: Prd = serde_json::from_slice(plain)?;
if let Some(commitment) = commitment {
// check that the hash of the prd is consistent with what's commited in the op_return
if prd.create_commitment() != *commitment {
return Err(anyhow::Error::msg("Received prd is not what was commited in the transaction"));
}
}
// check that the proof is consistent
let sender: Member = serde_json::from_str(&prd.sender)?;
if let Some(proof) = prd.proof {
// take the spending keys in sender
let addresses = sender.get_addresses();
let mut spend_keys: Vec<XOnlyPublicKey> = vec![];
for address in addresses {
spend_keys.push(<SilentPaymentAddress>::try_from(address)?.get_spend_key().x_only_public_key().0);
}
// The key in proof must be one of the sender keys
let proof_key = proof.get_key();
let mut known_key = false;
for key in spend_keys {
if key == proof_key {
known_key = true;
break;
}
}
if !known_key {
return Err(anyhow::Error::msg("Proof signed with an unknown key"));
}
proof.verify()?;
}
// check that the commitment outpoint is valid, just in case
OutPoint::from_str(&prd.root_commitment)?;
Ok(prd)
}
pub fn extract_from_message(plain: &[u8]) -> Result<Self> {
Self::_extract_from_message(plain, None)
}
pub fn extract_from_message_with_commitment(plain: &[u8], commitment: &AnkPrdHash) -> Result<Self> {
Self::_extract_from_message(plain, Some(commitment))
}
pub fn filter_keys(&mut self, to_keep: HashSet<String>) {
let current_keys = self.keys.clone();
let filtered_keys: Map<String, Value> = current_keys.into_iter()
.filter(|(field, _)| to_keep.contains(field))
.collect();
self.keys = filtered_keys;
}
/// We commit to everything except the keys and the proof
/// Because 1) we need one commitment to common data for all recipients of the transaction
/// 2) we already commit to the keys in the sender proof anyway
pub fn create_commitment(&self) -> AnkPrdHash {
let mut to_commit = self.clone();
to_commit.keys = Map::new();
to_commit.proof = None;
if to_commit.payload.len() != 64 && Vec::from_hex(&to_commit.payload).is_err() {
to_commit.payload = Value::from_str(&to_commit.payload).unwrap().tagged_hash().to_string();
}
AnkPrdHash::from_value(&to_commit.to_value())
}
/// Generate the signed proof and serialize to send over the network
pub fn to_network_msg(&self, sp_wallet: &SpWallet) -> Result<String> {
let spend_sk: SecretKey = sp_wallet.get_client().get_spend_key().try_into()?;
let to_sign = self.clone(); // we sign the whole prd, incl the keys, for each recipient
let message_hash = AnkHash::Message(AnkMessageHash::from_message(to_sign.to_string().as_bytes()));
let proof = Proof::new(message_hash, spend_sk);
let mut res = self.clone();
res.proof = Some(proof);
Ok(res.to_string())
}
pub fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
pub fn to_value(&self) -> Value {
Value::from_str(&self.to_string()).unwrap()
}
}

86
src/process.rs Normal file
View File

@ -0,0 +1,86 @@
use std::{collections::HashMap, sync::{Mutex, MutexGuard, OnceLock}};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress};
use crate::{crypto::AnkSharedSecretHash, prd::Prd, signature::Proof, MutexExt};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ProcessState {
pub commited_in: OutPoint,
pub encrypted_pcd: Value, // Some fields may be clear, if the owner of the process decides so
pub keys: Map<String, Value>, // We may not always have all the keys
pub validation_token: Vec<Proof>, // This signs the encrypted pcd
}
/// A process is basically a succession of states
/// If a process has nothing to do with us, shared_secrets and impending_requests will be empty
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct Process {
states: Vec<ProcessState>,
shared_secrets: HashMap<String, AnkSharedSecretHash>,
impending_requests: Vec<Prd>,
}
impl Process {
pub fn new(states: Vec<ProcessState>, shared_secrets: HashMap<SilentPaymentAddress, AnkSharedSecretHash>, impending_requests: Vec<Prd>) -> Self {
Self {
states,
shared_secrets: shared_secrets.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
impending_requests,
}
}
pub fn insert_shared_secret(&mut self, address: SilentPaymentAddress, secret: AnkSharedSecretHash) {
self.shared_secrets.insert(address.to_string(), secret);
}
pub fn get_shared_secret_for_address(&self, address: &SilentPaymentAddress) -> Option<AnkSharedSecretHash> {
self.shared_secrets.get(&address.to_string()).cloned()
}
pub fn get_all_secrets(&self) -> HashMap<SilentPaymentAddress, AnkSharedSecretHash> {
self.shared_secrets.clone().into_iter().map(|(k, v)| (SilentPaymentAddress::try_from(k.as_str()).unwrap(), v)).collect()
}
pub fn insert_state(&mut self, state: ProcessState) {
self.states.push(state);
}
pub fn get_status_at(&self, index: usize) -> Option<&ProcessState> {
self.states.get(index)
}
pub fn get_status_at_mut(&mut self, index: usize) -> Option<&mut ProcessState> {
self.states.get_mut(index)
}
pub fn get_latest_state(&self) -> Option<&ProcessState> {
self.states.last()
}
pub fn get_latest_state_mut(&mut self) -> Option<&mut ProcessState> {
self.states.last_mut()
}
pub fn insert_impending_request(&mut self, request: Prd) {
self.impending_requests.push(request);
}
pub fn get_impending_requests(&self) -> Vec<&Prd> {
self.impending_requests.iter().collect()
}
pub fn get_impending_requests_mut(&mut self) -> Vec<&mut Prd> {
self.impending_requests.iter_mut().collect()
}
}
pub static CACHEDPROCESSES: OnceLock<Mutex<HashMap<OutPoint, Process>>> = OnceLock::new();
pub fn lock_processes() -> Result<MutexGuard<'static, HashMap<OutPoint, Process>>, anyhow::Error> {
CACHEDPROCESSES
.get_or_init(|| Mutex::new(HashMap::new()))
.lock_anyhow()
}

111
src/signature.rs Normal file
View File

@ -0,0 +1,111 @@
use anyhow::Result;
use rand::{thread_rng, RngCore};
use serde::{Serialize, Deserialize};
use sp_client::bitcoin::key::Secp256k1;
use sp_client::bitcoin::secp256k1::schnorr::Signature;
use sp_client::bitcoin::secp256k1::{Keypair, Message, SecretKey, XOnlyPublicKey};
use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine};
use crate::pcd::AnkPcdHash;
sha256t_hash_newtype! {
pub struct AnkMessageTag = hash_str("4nk/Message");
#[hash_newtype(forward)]
pub struct AnkMessageHash(_);
pub struct AnkValidationYesTag = hash_str("4nk/yes");
#[hash_newtype(forward)]
pub struct AnkValidationYesHash(_);
pub struct AnkValidationNoTag = hash_str("4nk/no");
#[hash_newtype(forward)]
pub struct AnkValidationNoHash(_);
}
impl AnkMessageHash {
pub fn from_message(message: &[u8]) -> Self {
let mut eng = AnkMessageHash::engine();
eng.input(&message);
AnkMessageHash::from_engine(eng)
}
}
impl AnkValidationYesHash {
pub fn from_commitment(commitment: AnkPcdHash) -> Self {
let mut eng = AnkValidationYesHash::engine();
eng.input(&commitment.to_byte_array());
AnkValidationYesHash::from_engine(eng)
}
}
impl AnkValidationNoHash {
pub fn from_commitment(commitment: AnkPcdHash) -> Self {
let mut eng = AnkValidationNoHash::engine();
eng.input(&commitment.to_byte_array());
AnkValidationNoHash::from_engine(eng)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum AnkHash {
Message(AnkMessageHash),
ValidationYes(AnkValidationYesHash),
ValidationNo(AnkValidationNoHash),
}
impl AnkHash {
pub fn to_byte_array(&self) -> [u8; 32] {
match self {
AnkHash::Message(hash) => hash.to_byte_array(),
AnkHash::ValidationYes(hash) => hash.to_byte_array(),
AnkHash::ValidationNo(hash) => hash.to_byte_array(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Proof {
signature: Signature,
message: AnkHash,
key: XOnlyPublicKey
}
impl Proof {
pub fn new(message_hash: AnkHash, signing_key: SecretKey) -> Self {
let secp = Secp256k1::signing_only();
let keypair = Keypair::from_secret_key(&secp, &signing_key);
let mut aux_rand = [0u8; 32];
thread_rng().fill_bytes(&mut aux_rand);
let sig = secp.sign_schnorr_with_aux_rand(&Message::from_digest(message_hash.to_byte_array()), &keypair, &aux_rand);
Self {
signature: sig,
message: message_hash,
key: keypair.x_only_public_key().0
}
}
pub fn get_key(&self) -> XOnlyPublicKey {
self.key
}
pub fn verify(&self) -> Result<()> {
let secp = Secp256k1::verification_only();
secp.verify_schnorr(&self.signature, &Message::from_digest(self.message.to_byte_array()), &self.key)?;
Ok(())
}
pub fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}

View File

@ -5,10 +5,8 @@ use anyhow::{Error, Result};
use rand::{thread_rng, Rng};
use sp_client::bitcoin::consensus::deserialize;
use sp_client::bitcoin::key::{Keypair, Secp256k1, TapTweak};
use sp_client::bitcoin::psbt::{raw, Output};
use sp_client::bitcoin::{Address, Psbt, ScriptBuf, Transaction, Txid};
use sp_client::bitcoin::{Amount, OutPoint, TxOut};
use sp_client::bitcoin::psbt::raw;
use sp_client::bitcoin::{Amount, OutPoint, Psbt};
use sp_client::constants::{
self, DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE,
};
@ -29,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
@ -39,20 +35,24 @@ 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);
for outpoint in mandatory_inputs {
let (must_outpoint, must_output) = available_outpoints
.remove_entry(&outpoint)
.ok_or_else(|| Error::msg("Mandatory outpoint unknown"))?;
.ok_or_else(|| Error::msg(format!("Mandatory outpoint unknown: {}", outpoint)))?;
total_available += must_output.amount;
inputs.insert(must_outpoint, must_output);
}
@ -91,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 {
@ -114,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"));
}
@ -170,199 +171,247 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result<HashMap<String, Vec<u
Ok(res)
}
#[cfg(test)]
mod tests {
use std::io::Write;
// #[cfg(test)]
// mod tests {
// use std::io::Write;
use crate::network::CipherMessage;
// use crate::pcd::Pcd;
// use crate::prd::{Prd, PrdType};
// use crate::process::{Member, Process, Role, ValidationRules};
use super::*;
use sp_client::bitcoin::hashes::{sha256, Hash};
use sp_client::bitcoin::hex::{DisplayHex, FromHex};
use sp_client::bitcoin::secp256k1::PublicKey;
use sp_client::bitcoin::Transaction;
use sp_client::silentpayments::utils::receiving::{
calculate_tweak_data, get_pubkey_from_input,
};
// use super::*;
// use serde_json::Value;
// 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, Txid};
// use sp_client::silentpayments::utils::receiving::{
// calculate_tweak_data, get_pubkey_from_input,
// };
const ALICE_WALLET: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"e3d8922a41a7cb1a84a90f4334e987bb5ea2df6a1fdf44f789b5302de119f9e2\",\"spend_key\":{\"Secret\":\"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,104,242,105,185,6,124,208,34,44,149,52,163,38,63,221,150,12,198,24,95,143,126,235,37,149,233,88,118,32,86,233,152],\"spend_pubkey\":[3,198,82,196,243,12,59,126,109,143,144,157,128,176,168,94,54,134,232,139,115,102,11,178,128,244,239,251,40,228,67,153,72],\"change_label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"labels\":[[\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",[2,244,223,255,57,50,216,27,133,112,138,69,120,126,85,110,6,242,141,33,136,191,82,164,241,54,179,115,84,161,145,174,154]]]}},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"}}}}";
const BOB_WALLET: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"0de90b7195c1380d5fde13de3f1d66d53423a9896314839e36ba672653af60b4\",\"spend_key\":{\"Secret\":\"affe686075ecbe17b8ce7de45ec31314804259d0a4bc1c863de21ffd6dc598f8\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,85,96,92,243,247,237,192,205,9,178,146,101,237,132,232,15,2,69,138,31,118,76,140,142,207,90,13,192,94,254,150,133],\"spend_pubkey\":[3,5,157,91,250,169,41,61,190,37,30,98,152,253,180,138,250,86,162,102,82,148,130,220,44,153,127,83,43,246,93,17,232],\"change_label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"labels\":[[\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",[2,237,237,247,213,154,87,34,239,168,235,87,122,152,94,41,35,101,184,201,58,201,6,185,58,157,52,208,129,167,2,224,198]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":2146,\"outputs\":{\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"}}}}";
const ALICE_WALLET_CONFIRMATION: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"e3d8922a41a7cb1a84a90f4334e987bb5ea2df6a1fdf44f789b5302de119f9e2\",\"spend_key\":{\"Secret\":\"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,104,242,105,185,6,124,208,34,44,149,52,163,38,63,221,150,12,198,24,95,143,126,235,37,149,233,88,118,32,86,233,152],\"spend_pubkey\":[3,198,82,196,243,12,59,126,109,143,144,157,128,176,168,94,54,134,232,139,115,102,11,178,128,244,239,251,40,228,67,153,72],\"change_label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"labels\":[[\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",[2,244,223,255,57,50,216,27,133,112,138,69,120,126,85,110,6,242,141,33,136,191,82,164,241,54,179,115,84,161,145,174,154]]]}},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d\"}},\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:1\":{\"blockheight\":0,\"tweak\":\"28994b2f2ee8e5f35d6d2dcdee1580d0455fe3dc37f81e0a36864473ee86f5c4\",\"amount\":3937246,\"script\":\"51207d06144e982b6fd38a85d6152f1f95746b059553258a31e04df97fe6b5f19ea1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"}}}}";
const BOB_WALLET_CONFIRMATION: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"0de90b7195c1380d5fde13de3f1d66d53423a9896314839e36ba672653af60b4\",\"spend_key\":{\"Secret\":\"affe686075ecbe17b8ce7de45ec31314804259d0a4bc1c863de21ffd6dc598f8\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,85,96,92,243,247,237,192,205,9,178,146,101,237,132,232,15,2,69,138,31,118,76,140,142,207,90,13,192,94,254,150,133],\"spend_pubkey\":[3,5,157,91,250,169,41,61,190,37,30,98,152,253,180,138,250,86,162,102,82,148,130,220,44,153,127,83,43,246,93,17,232],\"change_label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"labels\":[[\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",[2,237,237,247,213,154,87,34,239,168,235,87,122,152,94,41,35,101,184,201,58,201,6,185,58,157,52,208,129,167,2,224,198]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":2146,\"outputs\":{\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0\":{\"blockheight\":0,\"tweak\":\"0e3395ff27bde9187ffaeeb2521f6277d3b83911f16ccbaf59a1a68d99a0ab93\",\"amount\":1200,\"script\":\"512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c\",\"label\":null,\"spend_status\":\"Unspent\"},\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"}}}}";
const ALICE_WALLET_ANSWER: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"e3d8922a41a7cb1a84a90f4334e987bb5ea2df6a1fdf44f789b5302de119f9e2\",\"spend_key\":{\"Secret\":\"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,104,242,105,185,6,124,208,34,44,149,52,163,38,63,221,150,12,198,24,95,143,126,235,37,149,233,88,118,32,86,233,152],\"spend_pubkey\":[3,198,82,196,243,12,59,126,109,143,144,157,128,176,168,94,54,134,232,139,115,102,11,178,128,244,239,251,40,228,67,153,72],\"change_label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"labels\":[[\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",[2,244,223,255,57,50,216,27,133,112,138,69,120,126,85,110,6,242,141,33,136,191,82,164,241,54,179,115,84,161,145,174,154]]]}},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:1\":{\"blockheight\":0,\"tweak\":\"28994b2f2ee8e5f35d6d2dcdee1580d0455fe3dc37f81e0a36864473ee86f5c4\",\"amount\":3937246,\"script\":\"51207d06144e982b6fd38a85d6152f1f95746b059553258a31e04df97fe6b5f19ea1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"},\"bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0\":{\"blockheight\":0,\"tweak\":\"3bf77beab9c053e1ed974288d5d246962376d2badddc623af1f2ef7af57067b7\",\"amount\":1046,\"script\":\"5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa\",\"label\":null,\"spend_status\":\"Unspent\"},\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d\"}}}}}";
const BOB_WALLET_ANSWER: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"0de90b7195c1380d5fde13de3f1d66d53423a9896314839e36ba672653af60b4\",\"spend_key\":{\"Secret\":\"affe686075ecbe17b8ce7de45ec31314804259d0a4bc1c863de21ffd6dc598f8\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,85,96,92,243,247,237,192,205,9,178,146,101,237,132,232,15,2,69,138,31,118,76,140,142,207,90,13,192,94,254,150,133],\"spend_pubkey\":[3,5,157,91,250,169,41,61,190,37,30,98,152,253,180,138,250,86,162,102,82,148,130,220,44,153,127,83,43,246,93,17,232],\"change_label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"labels\":[[\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",[2,237,237,247,213,154,87,34,239,168,235,87,122,152,94,41,35,101,184,201,58,201,6,185,58,157,52,208,129,167,2,224,198]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":2146,\"outputs\":{\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"},\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0\":{\"blockheight\":0,\"tweak\":\"0e3395ff27bde9187ffaeeb2521f6277d3b83911f16ccbaf59a1a68d99a0ab93\",\"amount\":1200,\"script\":\"512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c\",\"label\":null,\"spend_status\":{\"Spent\":\"bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9\"}}}}}";
const ALICE_ADDRESS: &str = "sprt1qqf50y6deqe7dqg3vj562xf3lmktqe3sct78ha6e9jh54sa3q2m5esq7x2tz0xrpm0ekclyyaszc2sh3ksm5gkumxpwegpa80lv5wgsuefqaufx8q";
const BOB_ADDRESS: &str = "sprt1qqf2kqh8n7lkupngfk2fxtmvyaq8sy3v2ramyeryweadqmsz7l6tg2qc9n4dl42ff8klz28nznr7mfzh6263xv555stwzextl2v4lvhg3aqq7ru8u";
const COMMITMENT: &str = "e4395114bdb1276bbcf3b0b6ef1c970a213f689b2bf8524e08599a1a65c146e7";
const FEE_RATE: Amount = Amount::from_sat(1);
// const ALICE_WALLET: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"e3d8922a41a7cb1a84a90f4334e987bb5ea2df6a1fdf44f789b5302de119f9e2\",\"spend_key\":{\"Secret\":\"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,104,242,105,185,6,124,208,34,44,149,52,163,38,63,221,150,12,198,24,95,143,126,235,37,149,233,88,118,32,86,233,152],\"spend_pubkey\":[3,198,82,196,243,12,59,126,109,143,144,157,128,176,168,94,54,134,232,139,115,102,11,178,128,244,239,251,40,228,67,153,72],\"change_label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"labels\":[[\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",[2,244,223,255,57,50,216,27,133,112,138,69,120,126,85,110,6,242,141,33,136,191,82,164,241,54,179,115,84,161,145,174,154]]]}},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"}}}}";
// const BOB_WALLET: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"0de90b7195c1380d5fde13de3f1d66d53423a9896314839e36ba672653af60b4\",\"spend_key\":{\"Secret\":\"affe686075ecbe17b8ce7de45ec31314804259d0a4bc1c863de21ffd6dc598f8\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,85,96,92,243,247,237,192,205,9,178,146,101,237,132,232,15,2,69,138,31,118,76,140,142,207,90,13,192,94,254,150,133],\"spend_pubkey\":[3,5,157,91,250,169,41,61,190,37,30,98,152,253,180,138,250,86,162,102,82,148,130,220,44,153,127,83,43,246,93,17,232],\"change_label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"labels\":[[\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",[2,237,237,247,213,154,87,34,239,168,235,87,122,152,94,41,35,101,184,201,58,201,6,185,58,157,52,208,129,167,2,224,198]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":2146,\"outputs\":{\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"}}}}";
// const ALICE_WALLET_CONFIRMATION: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"e3d8922a41a7cb1a84a90f4334e987bb5ea2df6a1fdf44f789b5302de119f9e2\",\"spend_key\":{\"Secret\":\"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,104,242,105,185,6,124,208,34,44,149,52,163,38,63,221,150,12,198,24,95,143,126,235,37,149,233,88,118,32,86,233,152],\"spend_pubkey\":[3,198,82,196,243,12,59,126,109,143,144,157,128,176,168,94,54,134,232,139,115,102,11,178,128,244,239,251,40,228,67,153,72],\"change_label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"labels\":[[\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",[2,244,223,255,57,50,216,27,133,112,138,69,120,126,85,110,6,242,141,33,136,191,82,164,241,54,179,115,84,161,145,174,154]]]}},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d\"}},\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:1\":{\"blockheight\":0,\"tweak\":\"28994b2f2ee8e5f35d6d2dcdee1580d0455fe3dc37f81e0a36864473ee86f5c4\",\"amount\":3937246,\"script\":\"51207d06144e982b6fd38a85d6152f1f95746b059553258a31e04df97fe6b5f19ea1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"}}}}";
// const BOB_WALLET_CONFIRMATION: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"0de90b7195c1380d5fde13de3f1d66d53423a9896314839e36ba672653af60b4\",\"spend_key\":{\"Secret\":\"affe686075ecbe17b8ce7de45ec31314804259d0a4bc1c863de21ffd6dc598f8\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,85,96,92,243,247,237,192,205,9,178,146,101,237,132,232,15,2,69,138,31,118,76,140,142,207,90,13,192,94,254,150,133],\"spend_pubkey\":[3,5,157,91,250,169,41,61,190,37,30,98,152,253,180,138,250,86,162,102,82,148,130,220,44,153,127,83,43,246,93,17,232],\"change_label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"labels\":[[\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",[2,237,237,247,213,154,87,34,239,168,235,87,122,152,94,41,35,101,184,201,58,201,6,185,58,157,52,208,129,167,2,224,198]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":2146,\"outputs\":{\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0\":{\"blockheight\":0,\"tweak\":\"0e3395ff27bde9187ffaeeb2521f6277d3b83911f16ccbaf59a1a68d99a0ab93\",\"amount\":1200,\"script\":\"512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c\",\"label\":null,\"spend_status\":\"Unspent\"},\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"}}}}";
// const ALICE_WALLET_ANSWER: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"e3d8922a41a7cb1a84a90f4334e987bb5ea2df6a1fdf44f789b5302de119f9e2\",\"spend_key\":{\"Secret\":\"93292e5b21042c6cfc742ba30e9d2a1e01609b12d154a1825184ed12c7b9631b\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,104,242,105,185,6,124,208,34,44,149,52,163,38,63,221,150,12,198,24,95,143,126,235,37,149,233,88,118,32,86,233,152],\"spend_pubkey\":[3,198,82,196,243,12,59,126,109,143,144,157,128,176,168,94,54,134,232,139,115,102,11,178,128,244,239,251,40,228,67,153,72],\"change_label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"labels\":[[\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",[2,244,223,255,57,50,216,27,133,112,138,69,120,126,85,110,6,242,141,33,136,191,82,164,241,54,179,115,84,161,145,174,154]]]}},\"outputs\":{\"wallet_fingerprint\":[187,119,108,230,171,125,106,11],\"birthday\":1620,\"last_scan\":2146,\"outputs\":{\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:1\":{\"blockheight\":0,\"tweak\":\"28994b2f2ee8e5f35d6d2dcdee1580d0455fe3dc37f81e0a36864473ee86f5c4\",\"amount\":3937246,\"script\":\"51207d06144e982b6fd38a85d6152f1f95746b059553258a31e04df97fe6b5f19ea1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":\"Unspent\"},\"bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0\":{\"blockheight\":0,\"tweak\":\"3bf77beab9c053e1ed974288d5d246962376d2badddc623af1f2ef7af57067b7\",\"amount\":1046,\"script\":\"5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa\",\"label\":null,\"spend_status\":\"Unspent\"},\"9a4a67cc5a40bf882d8b300d91024d7c97024b3b68b2df7745a5b9ea1df1888c:1\":{\"blockheight\":1620,\"tweak\":\"b8b63b3ed97d297b744135cfac2fb4a344c881a77543b71f1fcd16bc67514f26\",\"amount\":3938643,\"script\":\"51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1\",\"label\":\"ac14a827e2d023b8f7804303a47259366117d99ed932b641d4a8eaf1b82cc992\",\"spend_status\":{\"Spent\":\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d\"}}}}}";
// const BOB_WALLET_ANSWER: &str = "{\"tx_history\": [],\"client\":{\"network\":\"regtest\",\"label\":\"default\",\"scan_sk\":\"0de90b7195c1380d5fde13de3f1d66d53423a9896314839e36ba672653af60b4\",\"spend_key\":{\"Secret\":\"affe686075ecbe17b8ce7de45ec31314804259d0a4bc1c863de21ffd6dc598f8\"},\"mnemonic\":null,\"sp_receiver\":{\"version\":0,\"network\":\"Regtest\",\"scan_pubkey\":[2,85,96,92,243,247,237,192,205,9,178,146,101,237,132,232,15,2,69,138,31,118,76,140,142,207,90,13,192,94,254,150,133],\"spend_pubkey\":[3,5,157,91,250,169,41,61,190,37,30,98,152,253,180,138,250,86,162,102,82,148,130,220,44,153,127,83,43,246,93,17,232],\"change_label\":\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",\"labels\":[[\"56572fc770b52096879662f97f98263d3e126f5a4a38f00f2895a9dde4c47c1c\",[2,237,237,247,213,154,87,34,239,168,235,87,122,152,94,41,35,101,184,201,58,201,6,185,58,157,52,208,129,167,2,224,198]]]}},\"outputs\":{\"wallet_fingerprint\":[203,200,4,248,139,36,241,232],\"birthday\":2146,\"last_scan\":2146,\"outputs\":{\"fbd9c63e0dd08c2569b51a0d6095a79ee2acfcac66acdb594328a095f1fadb63:1\":{\"blockheight\":2146,\"tweak\":\"678dbcbdb40cd3733c8dbbd508761a0937009cf75a9f466e3c98877e79037cbc\",\"amount\":99896595,\"script\":\"5120deab0c5a3d23de30477b0b5a95a261c96e29afdd9813c665d2bf025ad2b3f919\",\"label\":null,\"spend_status\":\"Unspent\"},\"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0\":{\"blockheight\":0,\"tweak\":\"0e3395ff27bde9187ffaeeb2521f6277d3b83911f16ccbaf59a1a68d99a0ab93\",\"amount\":1200,\"script\":\"512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c\",\"label\":null,\"spend_status\":{\"Spent\":\"bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9\"}}}}}";
// const ALICE_ADDRESS: &str = "sprt1qqf50y6deqe7dqg3vj562xf3lmktqe3sct78ha6e9jh54sa3q2m5esq7x2tz0xrpm0ekclyyaszc2sh3ksm5gkumxpwegpa80lv5wgsuefqaufx8q";
// 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();
let outpoint_data = (
prevout.previous_output.txid.to_string(),
prevout.previous_output.vout,
);
let input_pubkey =
get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap();
let tweak_data =
calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap();
tweak_data
}
// fn helper_get_tweak_data(tx: &Transaction, spk: ScriptBuf) -> PublicKey {
// let prevout = tx.input.get(0).unwrap().to_owned();
// let outpoint_data = (
// prevout.previous_output.txid.to_string(),
// prevout.previous_output.vout,
// );
// let input_pubkey =
// get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap();
// let tweak_data =
// calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap();
// tweak_data
// }
fn helper_create_commitment(payload_to_hash: String) -> String {
let mut engine = sha256::HashEngine::default();
engine.write_all(&payload_to_hash.as_bytes());
let hash = sha256::Hash::from_engine(engine);
hash.to_byte_array().to_lower_hex_string()
}
// fn helper_create_commitment(payload_to_hash: String) -> sha256::Hash {
// let mut engine = sha256::HashEngine::default();
// engine.write_all(&payload_to_hash.as_bytes()).unwrap();
// let hash = sha256::Hash::from_engine(engine);
// hash
// }
#[test]
fn it_creates_notification_transaction() {
let recipient = Recipient {
address: BOB_ADDRESS.to_owned(),
amount: Amount::from_sat(1200),
nb_outputs: 1,
};
let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap();
let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap();
let message: CipherMessage = CipherMessage::new(ALICE_ADDRESS.to_owned(), "TEST".to_owned());
let commitment = helper_create_commitment(serde_json::to_string(&message).unwrap());
// #[test]
// fn it_creates_notification_transaction() {
// let recipient = Recipient {
// address: BOB_ADDRESS.to_owned(),
// amount: Amount::from_sat(1200),
// nb_outputs: 1,
// };
// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap();
// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap();
// let pcd = Pcd::new(Value::String("TEST".to_owned()));
// let pcd_hash = helper_create_commitment(pcd.to_string());
// let mut key = [0u8; 32];
// key.copy_from_slice(&Vec::from_hex(KEY).unwrap());
assert!(commitment == "d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99");
// 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 psbt = create_transaction(
&vec![],
&HashSet::new(),
&alice_wallet,
vec![recipient],
Some(Vec::from_hex(COMMITMENT).unwrap()),
FEE_RATE,
None,
)
.unwrap();
// let validation_rules = ValidationRules::new(0.5, Role::User);
let final_tx = psbt.extract_tx().unwrap();
let spk = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1";
// let pcd_template = serde_json::json!({
// "int": 0,
// "string": "exemple_data",
// "array": [
// "element1",
// "element2"
// ]
// });
let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap());
// 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,
// );
// Check that Alice and Bob are both able to find that transaction
let alice_update = alice_wallet
.update_wallet_with_transaction(&final_tx, 0, tweak_data)
.unwrap();
assert!(alice_update.len() > 0);
let bob_update = bob_wallet
.update_wallet_with_transaction(&final_tx, 0, tweak_data)
.unwrap();
assert!(bob_update.len() > 0);
println!("{:?}", alice_wallet.get_outputs().to_outpoints_list());
println!("{:?}", bob_wallet.get_outputs().to_outpoints_list());
}
// 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());
#[test]
fn it_creates_confirmation_transaction() {
let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_CONFIRMATION).unwrap();
let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_CONFIRMATION).unwrap();
// let psbt = create_transaction(
// &vec![],
// &HashSet::new(),
// &alice_wallet,
// vec![recipient],
// Some(commitment.as_byte_array().to_vec()),
// FEE_RATE,
// None,
// )
// .unwrap();
// Bob must spend notification output
let (confirmation_outpoint, _) = bob_wallet
.get_outputs()
.get_outpoint(
OutPoint::from_str(
"148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0",
)
.unwrap(),
)
.unwrap();
// let final_tx = psbt.extract_tx().unwrap();
// let spk = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1";
let recipient = Recipient {
address: ALICE_ADDRESS.to_owned(),
amount: Amount::from_sat(0),
nb_outputs: 1,
};
// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap());
let psbt = create_transaction(
&vec![&confirmation_outpoint],
&HashSet::new(),
&bob_wallet,
vec![recipient],
None,
FEE_RATE,
Some(ALICE_ADDRESS.to_owned()),
)
.unwrap();
// // Check that Alice and Bob are both able to find that transaction
// let alice_update = alice_wallet
// .update_wallet_with_transaction(&final_tx, 0, tweak_data)
// .unwrap();
// assert!(alice_update.len() > 0);
// let bob_update = bob_wallet
// .update_wallet_with_transaction(&final_tx, 0, tweak_data)
// .unwrap();
// assert!(bob_update.len() > 0);
// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list());
// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list());
// }
let final_tx = psbt.extract_tx().unwrap();
// println!(
// "{}",
// serialize::<Transaction>(&final_tx).to_lower_hex_string()
// );
let spk = "512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c";
// #[test]
// fn it_creates_confirmation_transaction() {
// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_CONFIRMATION).unwrap();
// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_CONFIRMATION).unwrap();
let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap());
// // Bob must spend notification output
// let (confirmation_outpoint, _) = bob_wallet
// .get_outputs()
// .get_outpoint(
// OutPoint::from_str(
// "148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0",
// )
// .unwrap(),
// )
// .unwrap();
// Check that Alice and Bob are both able to find that transaction
let alice_update = alice_wallet
.update_wallet_with_transaction(&final_tx, 0, tweak_data)
.unwrap();
assert!(alice_update.len() > 0);
let bob_update = bob_wallet
.update_wallet_with_transaction(&final_tx, 0, tweak_data)
.unwrap();
assert!(bob_update.len() > 0);
println!("{:?}", alice_wallet.get_outputs().to_outpoints_list());
println!("{:?}", bob_wallet.get_outputs().to_outpoints_list());
}
// let recipient = Recipient {
// address: ALICE_ADDRESS.to_owned(),
// amount: Amount::from_sat(0),
// nb_outputs: 1,
// };
#[test]
fn it_creates_answer_transaction() {
let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_ANSWER).unwrap();
let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_ANSWER).unwrap();
// let psbt = create_transaction(
// &vec![&confirmation_outpoint],
// &HashSet::new(),
// &bob_wallet,
// vec![recipient],
// None,
// FEE_RATE,
// Some(ALICE_ADDRESS.to_owned()),
// )
// .unwrap();
// Bob must spend notification output
let (confirmation_outpoint, _) = alice_wallet
.get_outputs()
.get_outpoint(
OutPoint::from_str(
"bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0",
)
.unwrap(),
)
.unwrap();
// let final_tx = psbt.extract_tx().unwrap();
// // println!(
// // "{}",
// // serialize::<Transaction>(&final_tx).to_lower_hex_string()
// // );
// let spk = "512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c";
let recipient = Recipient {
address: BOB_ADDRESS.to_owned(),
amount: Amount::from_sat(0),
nb_outputs: 1,
};
// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap());
let psbt = create_transaction(
&vec![&confirmation_outpoint],
&HashSet::new(),
&alice_wallet,
vec![recipient],
None,
FEE_RATE,
Some(BOB_ADDRESS.to_owned()),
)
.unwrap();
// // Check that Alice and Bob are both able to find that transaction
// let alice_update = alice_wallet
// .update_wallet_with_transaction(&final_tx, 0, tweak_data)
// .unwrap();
// assert!(alice_update.len() > 0);
// let bob_update = bob_wallet
// .update_wallet_with_transaction(&final_tx, 0, tweak_data)
// .unwrap();
// assert!(bob_update.len() > 0);
// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list());
// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list());
// }
let final_tx = psbt.extract_tx().unwrap();
// println!("{}", serialize::<Transaction>(&final_tx).to_lower_hex_string());
let spk = "5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa";
// #[test]
// fn it_creates_answer_transaction() {
// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_ANSWER).unwrap();
// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_ANSWER).unwrap();
let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap());
// // Bob must spend notification output
// let (confirmation_outpoint, _) = alice_wallet
// .get_outputs()
// .get_outpoint(
// OutPoint::from_str(
// "bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0",
// )
// .unwrap(),
// )
// .unwrap();
// Check that Alice and Bob are both able to find that transaction
let alice_update = alice_wallet
.update_wallet_with_transaction(&final_tx, 0, tweak_data)
.unwrap();
assert!(alice_update.len() > 0);
let bob_update = bob_wallet
.update_wallet_with_transaction(&final_tx, 0, tweak_data)
.unwrap();
assert!(bob_update.len() > 0);
println!("{:?}", alice_wallet.get_outputs().to_outpoints_list());
println!("{:?}", bob_wallet.get_outputs().to_outpoints_list());
}
}
// let recipient = Recipient {
// address: BOB_ADDRESS.to_owned(),
// amount: Amount::from_sat(0),
// nb_outputs: 1,
// };
// let psbt = create_transaction(
// &vec![&confirmation_outpoint],
// &HashSet::new(),
// &alice_wallet,
// vec![recipient],
// None,
// FEE_RATE,
// Some(BOB_ADDRESS.to_owned()),
// )
// .unwrap();
// let final_tx = psbt.extract_tx().unwrap();
// // println!("{}", serialize::<Transaction>(&final_tx).to_lower_hex_string());
// let spk = "5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa";
// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap());
// // Check that Alice and Bob are both able to find that transaction
// let alice_update = alice_wallet
// .update_wallet_with_transaction(&final_tx, 0, tweak_data)
// .unwrap();
// assert!(alice_update.len() > 0);
// let bob_update = bob_wallet
// .update_wallet_with_transaction(&final_tx, 0, tweak_data)
// .unwrap();
// assert!(bob_update.len() > 0);
// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list());
// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list());
// }
// }