Refactoring to update to latest common

This commit is contained in:
NicolasCantu 2025-04-08 16:03:12 +02:00 committed by Nicolas Cantu
parent 193d49caa7
commit 29961af392
4 changed files with 206 additions and 189 deletions

View File

@ -6,10 +6,10 @@ use std::{
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use bitcoincore_rpc::bitcoin::hex::DisplayHex; use bitcoincore_rpc::bitcoin::hex::DisplayHex;
use sdk_common::pcd::{Member, Roles}; use sdk_common::{pcd::Member, silentpayments::sign_transaction, sp_client::{silentpayments::SilentPaymentAddress, RecipientAddress}};
use sdk_common::serialization::{OutPointMemberMap, OutPointProcessMap}; use sdk_common::serialization::{OutPointMemberMap, OutPointProcessMap};
use sdk_common::silentpayments::create_transaction; use sdk_common::silentpayments::create_transaction;
use sdk_common::sp_client::spclient::Recipient; use sdk_common::sp_client::{FeeRate, Recipient};
use sdk_common::network::{AnkFlag, CommitMessage, HandshakeMessage}; use sdk_common::network::{AnkFlag, CommitMessage, HandshakeMessage};
use sdk_common::sp_client::bitcoin::{Amount, OutPoint}; use sdk_common::sp_client::bitcoin::{Amount, OutPoint};
use sdk_common::process::{lock_processes, Process, ProcessState}; use sdk_common::process::{lock_processes, Process, ProcessState};
@ -41,14 +41,14 @@ pub(crate) fn handle_commit_request(commit_msg: CommitMessage) -> Result<OutPoin
let our_sp_address = WALLET let our_sp_address = WALLET
.get() .get()
.ok_or(Error::msg("Wallet not initialized"))? .ok_or(Error::msg("Wallet not initialized"))?
.get_wallet()? .lock_anyhow()?
.get_client() .get_sp_client()
.get_receiving_address(); .get_receiving_address();
let mut new_process_map = HashMap::new(); let mut new_process_map = HashMap::new();
let new_process = processes.get(&commit_msg.process_id).unwrap().clone(); let new_process = processes.get(&commit_msg.process_id).unwrap().clone();
new_process_map.insert(commit_msg.process_id, new_process); new_process_map.insert(commit_msg.process_id, new_process);
let init_msg = HandshakeMessage::new( let init_msg = HandshakeMessage::new(
our_sp_address, our_sp_address.to_string(),
OutPointMemberMap(HashMap::new()), OutPointMemberMap(HashMap::new()),
OutPointProcessMap(new_process_map), OutPointProcessMap(new_process_map),
); );
@ -70,20 +70,20 @@ fn handle_new_process(commit_msg: &CommitMessage) -> Result<Process> {
let merkle_root_bin = pcd_commitment.create_merkle_tree()?.root().unwrap(); let merkle_root_bin = pcd_commitment.create_merkle_tree()?.root().unwrap();
if let Ok(pairing_process_id) = handle_member_list(&commit_msg.roles, commit_msg.process_id) { if let Ok(pairing_process_id) = handle_member_list(&commit_msg) {
dump_cached_members()?; dump_cached_members()?;
// Send a handshake message to every connected client // Send a handshake message to every connected client
if let Some(new_member) = lock_members().unwrap().get(&pairing_process_id) { if let Some(new_member) = lock_members().unwrap().get(&pairing_process_id) {
let our_sp_address = WALLET let our_sp_address = WALLET
.get() .get()
.ok_or(Error::msg("Wallet not initialized"))? .ok_or(Error::msg("Wallet not initialized"))?
.get_wallet()? .lock_anyhow()?
.get_client() .get_sp_client()
.get_receiving_address(); .get_receiving_address();
let mut new_member_map = HashMap::new(); let mut new_member_map = HashMap::new();
new_member_map.insert(pairing_process_id, new_member.clone()); new_member_map.insert(pairing_process_id, new_member.clone());
let init_msg = HandshakeMessage::new( let init_msg = HandshakeMessage::new(
our_sp_address, our_sp_address.into(),
OutPointMemberMap(new_member_map), OutPointMemberMap(new_member_map),
OutPointProcessMap(HashMap::new()), OutPointProcessMap(HashMap::new()),
); );
@ -124,22 +124,28 @@ pub fn lock_members() -> Result<MutexGuard<'static, HashMap<OutPoint, Member>>,
.lock_anyhow() .lock_anyhow()
} }
fn handle_member_list(roles: &Roles, process_id: OutPoint) -> Result<OutPoint> { fn handle_member_list(commit_msg: &CommitMessage) -> Result<OutPoint> {
//Check if there is one role with one member //Check if there is one role with one member
if roles.len() != 1 { if commit_msg.roles.len() != 1 {
return Err(Error::msg("Process is not a pairing process")); return Err(Error::msg("Process is not a pairing process"));
} }
if let Some(pairing_role) = roles.get("pairing") { if let Some(pairing_role) = commit_msg.roles.get("pairing") {
if pairing_role.members.len() == 1 { if !pairing_role.members.is_empty() {
if let Some(member) = pairing_role.members.get(0) { return Err(Error::msg("Process is not a pairing process"));
let mut memberlist = lock_members()?;
memberlist.insert(
process_id,
member.to_owned());
return Ok(process_id);
}
} }
} else {
return Err(Error::msg("Process is not a pairing process"));
}
if let Some(paired_addresses) = commit_msg.public_data.get("pairedAddresses") {
let paired_addresses: Vec<String> = serde_json::from_value(paired_addresses.clone())?;
let mut memberlist = lock_members()?;
memberlist.insert(
commit_msg.process_id,
Member::new(paired_addresses.iter().map(|a| a.as_str().try_into().unwrap()).collect())
);
return Ok(commit_msg.process_id);
} }
Err(Error::msg("Process is not a pairing process")) Err(Error::msg("Process is not a pairing process"))
@ -253,7 +259,8 @@ fn process_validation(updated_process: &mut Process, commit_msg: &CommitMessage)
let state_to_validate = updated_process let state_to_validate = updated_process
.get_state_for_id(&new_state_id)?; .get_state_for_id(&new_state_id)?;
state_to_validate.is_valid(updated_process.get_latest_commited_state())?; let members = lock_members()?.clone();
state_to_validate.is_valid(updated_process.get_latest_commited_state(), &OutPointMemberMap(members))?;
let commited_in = commit_new_transaction(updated_process, state_to_validate.clone())?; let commited_in = commit_new_transaction(updated_process, state_to_validate.clone())?;
@ -268,26 +275,26 @@ fn commit_new_transaction(
let sp_wallet = WALLET let sp_wallet = WALLET
.get() .get()
.ok_or(Error::msg("Wallet not initialized"))? .ok_or(Error::msg("Wallet not initialized"))?
.get_wallet()?; .lock_anyhow()?;
let commitment_payload = Vec::from(state_to_commit.state_id); let commitment_payload = Vec::from(state_to_commit.state_id);
let mut recipients = vec![]; let mut recipients = vec![];
recipients.push(Recipient { recipients.push(Recipient {
address: sp_wallet.get_client().get_receiving_address(), address: RecipientAddress::SpAddress(sp_wallet.get_sp_client().get_receiving_address()),
amount: Amount::from_sat(1000), amount: Amount::from_sat(1000),
nb_outputs: 1,
}); });
// TODO not sure if this is still used
// If the process is a pairing, we add another output that directly pays the owner of the process // If the process is a pairing, we add another output that directly pays the owner of the process
// We can find out simply by looking at the members list // We can find out simply by looking at the members list
if let Some(member) = lock_members()?.get(&updated_process.get_process_id().unwrap()) { if let Some(member) = lock_members()?.get(&updated_process.get_process_id().unwrap()) {
// We just pick one of the devices of the member at random en pay to it, member can then share the private key between all devices // We just pick one of the devices of the member at random en pay to it, member can then share the private key between all devices
// For now we take the first address // For now we take the first address
let address: SilentPaymentAddress = member.get_addresses().get(0).unwrap().as_str().try_into()?;
recipients.push(Recipient { recipients.push(Recipient {
address: member.get_addresses().iter().next().unwrap().to_string(), address: RecipientAddress::SpAddress(address),
amount: Amount::from_sat(1000), amount: Amount::from_sat(1000),
nb_outputs: 1
}); });
} }
// This output is used to generate publicly available public keys without having to go through too many loops // This output is used to generate publicly available public keys without having to go through too many loops
@ -305,19 +312,32 @@ fn commit_new_transaction(
return Err(Error::msg(format!("Missing next commitment outpoint for process {}", updated_process.get_process_id()?))); return Err(Error::msg(format!("Missing next commitment outpoint for process {}", updated_process.get_process_id()?)));
}; };
let psbt = create_transaction( let unspent_outputs = sp_wallet.get_unspent_outputs();
vec![next_commited_in], let mut available_outpoints = vec![];
&freezed_utxos, // We push the next_commited_in at the top of the available outpoints
&sp_wallet, if let Some(output) = unspent_outputs.get(&next_commited_in) {
available_outpoints.push((next_commited_in, output.clone()));
}
// We filter out freezed utxos
for (outpoint, output) in unspent_outputs {
if !freezed_utxos.contains(&outpoint) {
available_outpoints.push((outpoint, output));
}
}
let unsigned_transaction = create_transaction(
available_outpoints,
sp_wallet.get_sp_client(),
recipients, recipients,
Some(commitment_payload), Some(commitment_payload),
fee_rate, FeeRate::from_sat_per_vb(fee_rate.to_sat() as f32),
None,
)?; )?;
let new_tx = psbt.extract_tx()?; let final_tx = sign_transaction(sp_wallet.get_sp_client(), unsigned_transaction)?;
let txid = daemon.broadcast(&new_tx)?; daemon.test_mempool_accept(&final_tx)?;
let txid = daemon.broadcast(&final_tx)?;
let commited_in = OutPoint::new(txid, 0); let commited_in = OutPoint::new(txid, 0);
freezed_utxos.insert(commited_in); freezed_utxos.insert(commited_in);
@ -337,23 +357,34 @@ mod tests {
use bitcoincore_rpc::bitcoin::consensus::deserialize; use bitcoincore_rpc::bitcoin::consensus::deserialize;
use bitcoincore_rpc::bitcoin::hex::DisplayHex; use bitcoincore_rpc::bitcoin::hex::DisplayHex;
use sdk_common::pcd::Member; use sdk_common::pcd::Member;
use sdk_common::pcd::Pcd;
use sdk_common::pcd::PcdCommitments;
use sdk_common::pcd::RoleDefinition; use sdk_common::pcd::RoleDefinition;
use sdk_common::pcd::Roles;
use sdk_common::pcd::ValidationRule; use sdk_common::pcd::ValidationRule;
use sdk_common::process::CACHEDPROCESSES; use sdk_common::process::CACHEDPROCESSES;
use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; use sdk_common::sp_client::silentpayments::SilentPaymentAddress;
use serde_json::json;
use mockall::predicate::*; use mockall::predicate::*;
use mockall::mock; use mockall::mock;
use std::collections::BTreeMap;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Mutex; use std::sync::Mutex;
use bitcoincore_rpc::bitcoin::*; use bitcoincore_rpc::bitcoin::*;
use crate::daemon::RpcCall; use crate::daemon::RpcCall;
use crate::DiskStorage;
use crate::StateFile;
use std::sync::OnceLock; use std::sync::OnceLock;
use sdk_common::sp_client::bitcoin::consensus::serialize; use sdk_common::sp_client::bitcoin::consensus::serialize;
use sdk_common::sp_client::bitcoin::hex::FromHex;
use serde_json::{Map, Value}; use serde_json::{Map, Value};
const LOCAL_ADDRESS: &str = "sprt1qq222dhaxlzmjft2pa7qtspw2aw55vwfmtnjyllv5qrsqwm3nufxs6q7t88jf9asvd7rxhczt87de68du3jhem54xvqxy80wc6ep7lauxacsrq79v"; const LOCAL_ADDRESS: &str = "sprt1qq222dhaxlzmjft2pa7qtspw2aw55vwfmtnjyllv5qrsqwm3nufxs6q7t88jf9asvd7rxhczt87de68du3jhem54xvqxy80wc6ep7lauxacsrq79v";
const INIT_TRANSACTION: &str = "02000000000102b01b832bf34cf87583c628839c5316546646dcd4939e339c1d83e693216cdfa00100000000fdffffffdd1ca865b199accd4801634488fca87e0cf81b36ee7e9bec526a8f922539b8670000000000fdffffff0200e1f505000000001600140798fac9f310cefad436ea928f0bdacf03a11be544e0f5050000000016001468a66f38e7c2c9e367577d6fad8532ae2c728ed2014043764b77de5041f80d19e3d872f205635f87486af015c00d2a3b205c694a0ae1cbc60e70b18bcd4470abbd777de63ae52600aba8f5ad1334cdaa6bcd931ab78b0140b56dd8e7ac310d6dcbc3eff37f111ced470990d911b55cd6ff84b74b579c17d0bba051ec23b738eeeedba405a626d95f6bdccb94c626db74c57792254bfc5a7c00000000"; const INIT_TRANSACTION: &str = "02000000000102b01b832bf34cf87583c628839c5316546646dcd4939e339c1d83e693216cdfa00100000000fdffffffdd1ca865b199accd4801634488fca87e0cf81b36ee7e9bec526a8f922539b8670000000000fdffffff0200e1f505000000001600140798fac9f310cefad436ea928f0bdacf03a11be544e0f5050000000016001468a66f38e7c2c9e367577d6fad8532ae2c728ed2014043764b77de5041f80d19e3d872f205635f87486af015c00d2a3b205c694a0ae1cbc60e70b18bcd4470abbd777de63ae52600aba8f5ad1334cdaa6bcd931ab78b0140b56dd8e7ac310d6dcbc3eff37f111ced470990d911b55cd6ff84b74b579c17d0bba051ec23b738eeeedba405a626d95f6bdccb94c626db74c57792254bfc5a7c00000000";
const TMP_WALLET: &str = "/tmp/.4nk/wallet";
const TMP_PROCESSES: &str = "/tmp/.4nk/processes";
const TMP_MEMBERS: &str = "/tmp/.4nk/members";
// Define the mock for Daemon with the required methods // Define the mock for Daemon with the required methods
mock! { mock! {
@ -476,27 +507,51 @@ mod tests {
println!("Initialized CACHEDPROCESSES"); println!("Initialized CACHEDPROCESSES");
} }
if STORAGE.get().is_none() {
let wallet_file = StateFile::new(PathBuf::from_str(TMP_WALLET).unwrap());
let processes_file = StateFile::new(PathBuf::from_str(TMP_PROCESSES).unwrap());
let members_file = StateFile::new(PathBuf::from_str(TMP_MEMBERS).unwrap());
wallet_file.create().unwrap();
processes_file.create().unwrap();
members_file.create().unwrap();
let disk_storage = DiskStorage {
wallet_file,
processes_file,
members_file
};
STORAGE
.set(Mutex::new(disk_storage))
.expect("STORAGE should initialize only once");
println!("Initialized STORAGE");
}
} }
fn mock_commit_msg(process_id: OutPoint) -> CommitMessage { fn mock_commit_msg(process_id: OutPoint) -> CommitMessage {
let field_name = "roles".to_owned(); let field_names = ["a".to_owned(), "b".to_owned(), "pub_a".to_owned(), "roles".to_owned()];
let member = Member::new(vec![SilentPaymentAddress::try_from(LOCAL_ADDRESS).unwrap()]).unwrap(); let pairing_id = OutPoint::from_str("b0c8378ee68e9a73836b04423ddb6de9fc0e2e715e04ffe6aa34117bb1025f01:0").unwrap();
let validation_rule = ValidationRule::new(1.0, vec![field_name.clone()], 1.0).unwrap(); let member = Member::new(vec![SilentPaymentAddress::try_from(LOCAL_ADDRESS).unwrap()]);
let validation_rule = ValidationRule::new(1.0, Vec::from(field_names), 1.0).unwrap();
let role_def = RoleDefinition { let role_def = RoleDefinition {
members: vec![member], members: vec![pairing_id],
validation_rules: vec![validation_rule], validation_rules: vec![validation_rule],
storages: vec![], storages: vec![],
}; };
let roles = BTreeMap::from([(String::from("role_name"), role_def)]); let roles = Roles::new(BTreeMap::from([(String::from("role_name"), role_def)]));
let pcd_commitment = json!({field_name: "b30212b9649054b71f938fbe0d1c08e72de95bdb12b8008082795c6e9c4ad26a"}); let public_data = Pcd::new(BTreeMap::from([("pub_a".to_owned(), Value::Null)]));
let clear_state = Pcd::new(BTreeMap::from([("a".to_owned(), Value::Null), ("b".to_owned(), Value::Null)]));
let pcd_commitments = PcdCommitments::new(&process_id, &Pcd::new(public_data.clone().into_iter().chain(clear_state).collect()), &roles).unwrap();
let commit_msg = CommitMessage { let commit_msg = CommitMessage {
process_id, process_id,
roles: roles.clone(), roles,
public_data: BTreeMap::new(), public_data,
validation_tokens: vec![], validation_tokens: vec![],
pcd_commitment: pcd_commitment.clone(), pcd_commitment: pcd_commitments,
error: None, error: None,
}; };
@ -537,7 +592,6 @@ mod tests {
let new_state = ProcessState { let new_state = ProcessState {
commited_in: process_id, commited_in: process_id,
pcd_commitment, pcd_commitment,
encrypted_pcd: Value::Object(roles_map),
..Default::default() ..Default::default()
}; };
let target = vec![&empty_state, &new_state]; let target = vec![&empty_state, &new_state];
@ -576,7 +630,6 @@ mod tests {
let new_state = ProcessState { let new_state = ProcessState {
commited_in: process_id, commited_in: process_id,
pcd_commitment, pcd_commitment,
encrypted_pcd: Value::Object(roles_map),
..Default::default() ..Default::default()
}; };
let empty_state = ProcessState { let empty_state = ProcessState {

View File

@ -2,6 +2,7 @@ use std::{collections::HashMap, str::FromStr};
use bitcoincore_rpc::bitcoin::secp256k1::PublicKey; use bitcoincore_rpc::bitcoin::secp256k1::PublicKey;
use bitcoincore_rpc::json::{self as bitcoin_json}; use bitcoincore_rpc::json::{self as bitcoin_json};
use sdk_common::silentpayments::sign_transaction;
use sdk_common::sp_client::bitcoin::secp256k1::{ use sdk_common::sp_client::bitcoin::secp256k1::{
rand::thread_rng, Keypair, Message as Secp256k1Message, Secp256k1, ThirtyTwoByteHash, rand::thread_rng, Keypair, Message as Secp256k1Message, Secp256k1, ThirtyTwoByteHash,
}; };
@ -24,14 +25,13 @@ use sdk_common::{
use sdk_common::sp_client::silentpayments::sending::generate_recipient_pubkeys; use sdk_common::sp_client::silentpayments::sending::generate_recipient_pubkeys;
use sdk_common::sp_client::silentpayments::utils::sending::calculate_partial_secret; use sdk_common::sp_client::silentpayments::utils::sending::calculate_partial_secret;
use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; use sdk_common::sp_client::{FeeRate, OwnedOutput, Recipient, RecipientAddress};
use sdk_common::sp_client::spclient::Recipient;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use crate::lock_freezed_utxos; use crate::lock_freezed_utxos;
use crate::scan::check_transaction_alone; use crate::scan::check_transaction_alone;
use crate::{scan::compute_partial_tweak_to_transaction, MutexExt, DAEMON, FAUCET_AMT, WALLET}; use crate::{scan::compute_partial_tweak_to_transaction, MutexExt, DAEMON, FAUCET_AMT, WALLET, SilentPaymentAddress};
fn spend_from_core(dest: XOnlyPublicKey) -> Result<(Transaction, Amount)> { fn spend_from_core(dest: XOnlyPublicKey) -> Result<(Transaction, Amount)> {
let core = DAEMON let core = DAEMON
@ -64,56 +64,51 @@ fn spend_from_core(dest: XOnlyPublicKey) -> Result<(Transaction, Amount)> {
} }
fn faucet_send(sp_address: SilentPaymentAddress, commitment: &str) -> Result<(Transaction, PublicKey)> { fn faucet_send(sp_address: SilentPaymentAddress, commitment: &str) -> Result<(Transaction, PublicKey)> {
let sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?.get_wallet()?; let sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?.lock_anyhow()?;
// do we have a sp output available ?
let available_outpoints = sp_wallet.get_outputs().to_spendable_list();
let available_amt = available_outpoints let fee_estimate = DAEMON
.get()
.ok_or(Error::msg("DAEMON not initialized"))?
.lock_anyhow()?
.estimate_fee(6)
.unwrap_or(Amount::from_sat(1000))
.checked_div(1000)
.unwrap();
log::debug!("fee estimate for 6 blocks: {}", fee_estimate);
let recipient = Recipient {
address: RecipientAddress::SpAddress(sp_address),
amount: FAUCET_AMT,
};
let freezed_utxos = lock_freezed_utxos()?;
// We filter out the freezed utxos from available list
let available_outpoints: Vec<(OutPoint, OwnedOutput)> = sp_wallet.get_outputs()
.iter() .iter()
.fold(Amount::from_sat(0), |acc, (_, x)| acc + x.amount); .filter_map(|(outpoint, output)| {
if !freezed_utxos.contains(&outpoint) {
// If we don't have at least 4 times the amount we need to send, we take some reserves out Some((*outpoint, output.clone()))
if available_amt > FAUCET_AMT.checked_mul(4).unwrap() { } else {
let mut total_amt = Amount::from_sat(0); None
let mut inputs = HashMap::new();
for (outpoint, output) in available_outpoints {
total_amt += output.amount;
inputs.insert(outpoint, output);
if total_amt >= FAUCET_AMT {
break;
} }
} })
.collect();
let recipient = Recipient { // If we had mandatory inputs, we would make sure to put them at the top of the list
address: sp_address.into(), // We don't care for faucet though
amount: FAUCET_AMT,
nb_outputs: 1,
};
let fee_estimate = DAEMON // We try to pay the faucet amount
.get() if let Ok(unsigned_transaction) = create_transaction(
.ok_or(Error::msg("DAEMON not initialized"))? available_outpoints,
.lock_anyhow()? sp_wallet.get_sp_client(),
.estimate_fee(6)
.unwrap_or(Amount::from_sat(1000))
.checked_div(1000)
.unwrap();
log::debug!("fee estimate for 6 blocks: {}", fee_estimate);
let freezed_utxos = lock_freezed_utxos()?;
let signed_psbt = create_transaction(
vec![],
&freezed_utxos,
&sp_wallet,
vec![recipient], vec![recipient],
Some(Vec::from_hex(commitment).unwrap()), Some(Vec::from_hex(commitment).unwrap()),
fee_estimate, FeeRate::from_sat_per_vb(fee_estimate.to_sat() as f32),
None )
)?; {
let final_tx = sign_transaction(sp_wallet.get_sp_client(), unsigned_transaction)?;
let final_tx = signed_psbt.extract_tx()?;
let partial_tweak = compute_partial_tweak_to_transaction(&final_tx)?; let partial_tweak = compute_partial_tweak_to_transaction(&final_tx)?;
@ -121,13 +116,15 @@ fn faucet_send(sp_address: SilentPaymentAddress, commitment: &str) -> Result<(Tr
.get() .get()
.ok_or(Error::msg("DAEMON not initialized"))? .ok_or(Error::msg("DAEMON not initialized"))?
.lock_anyhow()?; .lock_anyhow()?;
// First check that mempool accept it
daemon.test_mempool_accept(&final_tx)?;
let txid = daemon.broadcast(&final_tx)?; let txid = daemon.broadcast(&final_tx)?;
log::debug!("Sent tx {}", txid); log::debug!("Sent tx {}", txid);
// We immediately add the new tx to our wallet to prevent accidental double spend // We immediately add the new tx to our wallet to prevent accidental double spend
check_transaction_alone(sp_wallet, &final_tx, &partial_tweak)?; check_transaction_alone(sp_wallet, &final_tx, &partial_tweak)?;
Ok((final_tx, partial_tweak)) Ok((final_tx, partial_tweak))
} else { } else {
// let's try to spend directly from the mining address // let's try to spend directly from the mining address
let secp = Secp256k1::signing_only(); let secp = Secp256k1::signing_only();
@ -170,7 +167,7 @@ fn faucet_send(sp_address: SilentPaymentAddress, commitment: &str) -> Result<(Tr
.get(0) .get(0)
.expect("Failed to generate keys") .expect("Failed to generate keys")
.to_owned(); .to_owned();
let change_sp_address = sp_wallet.get_client().get_receiving_address(); let change_sp_address = sp_wallet.get_sp_client().get_receiving_address();
let change_output_key: XOnlyPublicKey = let change_output_key: XOnlyPublicKey =
generate_recipient_pubkeys(vec![change_sp_address], partial_secret)? generate_recipient_pubkeys(vec![change_sp_address], partial_secret)?
.into_values() .into_values()
@ -240,7 +237,9 @@ fn faucet_send(sp_address: SilentPaymentAddress, commitment: &str) -> Result<(Tr
.get() .get()
.ok_or(Error::msg("DAEMON not initialized"))? .ok_or(Error::msg("DAEMON not initialized"))?
.lock_anyhow()?; .lock_anyhow()?;
// We don't worry about core_tx being refused by core
daemon.broadcast(&core_tx)?; daemon.broadcast(&core_tx)?;
daemon.test_mempool_accept(&faucet_tx)?;
let txid = daemon.broadcast(&faucet_tx)?; let txid = daemon.broadcast(&faucet_tx)?;
log::debug!("Sent tx {}", txid); log::debug!("Sent tx {}", txid);
} }

View File

@ -10,28 +10,27 @@ use std::{
sync::{Mutex, MutexGuard, OnceLock}, sync::{Mutex, MutexGuard, OnceLock},
}; };
use bitcoincore_rpc::json::{self as bitcoin_json}; use bitcoincore_rpc::{bitcoin::secp256k1::SecretKey, json::{self as bitcoin_json}};
use commit::{lock_members, MEMBERLIST}; use commit::{lock_members, MEMBERLIST};
use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt};
use log::{debug, error, warn}; use log::{debug, error, warn};
use message::{broadcast_message, process_message, BroadcastType, MessageCache, MESSAGECACHE}; use message::{broadcast_message, process_message, BroadcastType, MessageCache, MESSAGECACHE};
use scan::{check_transaction_alone, compute_partial_tweak_to_transaction}; use scan::{check_transaction_alone, compute_partial_tweak_to_transaction};
use sdk_common::{network::HandshakeMessage, pcd::Member, process::{lock_processes, Process, CACHEDPROCESSES}, serialization::{OutPointMemberMap, OutPointProcessMap}, sp_client::{bitcoin::{ use sdk_common::{network::HandshakeMessage, pcd::Member, process::{lock_processes, Process, CACHEDPROCESSES}, serialization::{OutPointMemberMap, OutPointProcessMap}, silentpayments::SpWallet, sp_client::{bitcoin::{
consensus::deserialize, consensus::deserialize,
hex::{DisplayHex, FromHex}, hex::{DisplayHex, FromHex},
Amount, Network, Transaction, Amount, Network, Transaction,
}, silentpayments::utils::SilentPaymentAddress}, MutexExt}; }, silentpayments::SilentPaymentAddress, OwnedOutput}, MutexExt};
use sdk_common::sp_client::{ use sdk_common::sp_client::{
bitcoin::OutPoint, bitcoin::OutPoint,
bitcoin::secp256k1::rand::{thread_rng, Rng}, bitcoin::secp256k1::rand::{thread_rng, Rng},
spclient::SpWallet, SpClient, SpendKey
}; };
use sdk_common::{ use sdk_common::{
error::AnkError, error::AnkError,
network::{AnkFlag, NewTxMessage}, network::{AnkFlag, NewTxMessage},
}; };
use sdk_common::sp_client::spclient::{derive_keys_from_seed, SpClient, SpendKey};
use serde_json::Value; use serde_json::Value;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
@ -127,7 +126,7 @@ impl StateFile {
#[derive(Debug)] #[derive(Debug)]
pub struct DiskStorage { pub struct DiskStorage {
// wallet_file: StateFile, pub wallet_file: StateFile,
pub processes_file: StateFile, pub processes_file: StateFile,
pub members_file: StateFile, pub members_file: StateFile,
} }
@ -136,30 +135,14 @@ pub static STORAGE: OnceLock<Mutex<DiskStorage>> = OnceLock::new();
const FAUCET_AMT: Amount = Amount::from_sat(10_000); const FAUCET_AMT: Amount = Amount::from_sat(10_000);
#[derive(Debug)] pub(crate) static WALLET: OnceLock<Mutex<SpWallet>> = OnceLock::new();
struct SilentPaymentWallet {
sp_wallet: Mutex<SpWallet>,
storage: Mutex<StateFile>,
}
impl SilentPaymentWallet {
pub fn get_wallet(&self) -> Result<MutexGuard<SpWallet>> {
self.sp_wallet.lock_anyhow()
}
// This is terrible design
pub fn save(&self, unlocked_wallet: MutexGuard<SpWallet>) -> Result<()> {
let value = serde_json::from_str(&serde_json::to_string(&unlocked_wallet.clone())?)?;
self.storage.lock_anyhow()?.save(&value)
}
}
pub(crate) static WALLET: OnceLock<SilentPaymentWallet> = OnceLock::new();
fn handle_new_tx_request(new_tx_msg: &NewTxMessage) -> Result<()> { fn handle_new_tx_request(new_tx_msg: &NewTxMessage) -> Result<()> {
let tx = deserialize::<Transaction>(&Vec::from_hex(&new_tx_msg.transaction)?)?; let tx = deserialize::<Transaction>(&Vec::from_hex(&new_tx_msg.transaction)?)?;
// we try to broadcast it
DAEMON.get().unwrap().lock_anyhow()?.broadcast(&tx).map_err(|e| AnkError::NewTxError(e.to_string()))?; let daemon = DAEMON.get().unwrap().lock_anyhow()?;
daemon.test_mempool_accept(&tx)?;
daemon.broadcast(&tx)?;
Ok(()) Ok(())
} }
@ -247,8 +230,8 @@ fn create_new_tx_message(transaction: Vec<u8>) -> Result<NewTxMessage> {
let partial_tweak = compute_partial_tweak_to_transaction(&tx)?; let partial_tweak = compute_partial_tweak_to_transaction(&tx)?;
let sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?; let sp_wallet = WALLET.get().ok_or_else(|| Error::msg("Wallet not initialized"))?.lock_anyhow()?;
check_transaction_alone(sp_wallet.get_wallet()?, &tx, &partial_tweak)?; check_transaction_alone(sp_wallet, &tx, &partial_tweak)?;
Ok(NewTxMessage::new( Ok(NewTxMessage::new(
transaction.to_lower_hex_string(), transaction.to_lower_hex_string(),
@ -368,35 +351,26 @@ async fn main() -> Result<()> {
// Create a new wallet file if it doesn't exist or fails to load // Create a new wallet file if it doesn't exist or fails to load
wallet_file.create()?; wallet_file.create()?;
// Generate a seed and derive keys let mut rng = thread_rng();
let mut seed = [0u8; 64];
thread_rng().fill(&mut seed);
let (scan_sk, spend_sk) = derive_keys_from_seed(&seed, config.network)
.expect("Couldn't generate a new sp_wallet");
let new_client = SpClient::new( let new_client = SpClient::new(
config.wallet_name.clone(), SecretKey::new(&mut rng),
scan_sk, SpendKey::Secret(SecretKey::new(&mut rng)),
SpendKey::Secret(spend_sk),
None,
config.network, config.network,
) )
.expect("Failed to create a new SpClient"); .expect("Failed to create a new SpClient");
// Initialize a new wallet with the generated client let mut sp_wallet = SpWallet::new(new_client);
let mut wallet = SpWallet::new(new_client, None, vec![])?;
// Set birthday and update scan information // Set birthday and update scan information
let outputs = wallet.get_mut_outputs(); sp_wallet.set_birthday(current_tip);
outputs.set_birthday(current_tip); sp_wallet.set_last_scan(current_tip);
outputs.update_last_scan(current_tip);
// Save the newly created wallet to disk // Save the newly created wallet to disk
let json = serde_json::to_value(wallet.clone())?; let json = serde_json::to_value(sp_wallet.clone())?;
wallet_file.save(&json)?; wallet_file.save(&json)?;
wallet sp_wallet
} }
}; };
@ -437,31 +411,25 @@ async fn main() -> Result<()> {
*freezed_utxos = utxo_to_freeze; *freezed_utxos = utxo_to_freeze;
} }
let our_sp_address = sp_wallet.get_client().get_receiving_address(); let our_sp_address = sp_wallet.get_sp_client().get_receiving_address();
log::info!( log::info!(
"Using wallet {} with address {}", "Using wallet with address {}",
sp_wallet.get_client().label,
our_sp_address, our_sp_address,
); );
log::info!( log::info!(
"Found {} outputs for a total balance of {}", "Found {} outputs for a total balance of {}",
sp_wallet.get_outputs().to_spendable_list().len(), sp_wallet.get_outputs().len(),
sp_wallet.get_outputs().get_balance() sp_wallet.get_balance()
); );
let last_scan = sp_wallet.get_outputs().get_last_scan(); let last_scan = sp_wallet.get_last_scan();
WALLET WALLET
.set(SilentPaymentWallet { .set(Mutex::new(sp_wallet))
sp_wallet: Mutex::new(sp_wallet),
storage: Mutex::new(wallet_file),
})
.expect("Failed to initialize WALLET"); .expect("Failed to initialize WALLET");
WALLET.get().unwrap().save(WALLET.get().unwrap().get_wallet()?).unwrap();
CACHEDPROCESSES CACHEDPROCESSES
.set(Mutex::new(cached_processes)) .set(Mutex::new(cached_processes))
.expect("Failed to initialize CACHEDPROCESSES"); .expect("Failed to initialize CACHEDPROCESSES");
@ -471,6 +439,7 @@ async fn main() -> Result<()> {
.expect("Failed to initialize MEMBERLIST"); .expect("Failed to initialize MEMBERLIST");
let storage = DiskStorage { let storage = DiskStorage {
wallet_file,
processes_file, processes_file,
members_file, members_file,
}; };
@ -495,7 +464,7 @@ async fn main() -> Result<()> {
// Let's spawn the handling of each connection in a separate task. // Let's spawn the handling of each connection in a separate task.
while let Ok((stream, addr)) = listener.accept().await { while let Ok((stream, addr)) = listener.accept().await {
tokio::spawn(handle_connection(stream, addr, our_sp_address.as_str().try_into().unwrap())); tokio::spawn(handle_connection(stream, addr, our_sp_address));
} }
Ok(()) Ok(())

View File

@ -3,8 +3,9 @@ use std::str::FromStr;
use std::sync::MutexGuard; use std::sync::MutexGuard;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use bitcoincore_rpc::bitcoin::absolute::Height;
use electrum_client::ElectrumApi; use electrum_client::ElectrumApi;
use hex::FromHex; use sdk_common::silentpayments::SpWallet;
use sdk_common::sp_client::bitcoin::bip158::BlockFilter; use sdk_common::sp_client::bitcoin::bip158::BlockFilter;
use sdk_common::sp_client::bitcoin::hex::DisplayHex; use sdk_common::sp_client::bitcoin::hex::DisplayHex;
use sdk_common::sp_client::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; use sdk_common::sp_client::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
@ -13,10 +14,10 @@ use sdk_common::sp_client::silentpayments::receiving::Receiver;
use sdk_common::sp_client::silentpayments::utils::receiving::{ use sdk_common::sp_client::silentpayments::utils::receiving::{
calculate_tweak_data, get_pubkey_from_input, calculate_tweak_data, get_pubkey_from_input,
}; };
use sdk_common::sp_client::spclient::{OutputSpendStatus, OwnedOutput, SpWallet}; use sdk_common::sp_client::{OutputSpendStatus, OwnedOutput};
use tokio::time::Instant; use tokio::time::Instant;
use crate::{electrumclient, MutexExt, DAEMON, WALLET}; use crate::{electrumclient, MutexExt, DAEMON, STORAGE, WALLET};
pub fn compute_partial_tweak_to_transaction(tx: &Transaction) -> Result<PublicKey> { pub fn compute_partial_tweak_to_transaction(tx: &Transaction) -> Result<PublicKey> {
let daemon = DAEMON.get().ok_or(Error::msg("DAEMON not initialized"))?; let daemon = DAEMON.get().ok_or(Error::msg("DAEMON not initialized"))?;
@ -85,7 +86,7 @@ fn get_script_to_secret_map(
} }
pub fn check_transaction_alone(mut wallet: MutexGuard<SpWallet>, tx: &Transaction, tweak_data: &PublicKey) -> Result<HashMap<OutPoint, OwnedOutput>> { pub fn check_transaction_alone(mut wallet: MutexGuard<SpWallet>, tx: &Transaction, tweak_data: &PublicKey) -> Result<HashMap<OutPoint, OwnedOutput>> {
let updates = match wallet.update_wallet_with_transaction(tx, 0, *tweak_data) { let updates = match wallet.update_with_transaction(tx, tweak_data, 0) {
Ok(updates) => updates, Ok(updates) => updates,
Err(e) => { Err(e) => {
log::debug!("Error while checking transaction: {}", e); log::debug!("Error while checking transaction: {}", e);
@ -94,7 +95,8 @@ pub fn check_transaction_alone(mut wallet: MutexGuard<SpWallet>, tx: &Transactio
}; };
if updates.len() > 0 { if updates.len() > 0 {
WALLET.get().unwrap().save(wallet)?; let storage = STORAGE.get().ok_or_else(|| Error::msg("Failed to get STORAGE"))?;
storage.lock_anyhow()?.wallet_file.save(&serde_json::to_value(wallet.clone())?)?;
} }
Ok(updates) Ok(updates)
@ -167,6 +169,7 @@ fn scan_block_outputs(
.collect(); .collect();
let ours = sp_receiver.scan_transaction(&secret.unwrap(), xonlykeys?)?; let ours = sp_receiver.scan_transaction(&secret.unwrap(), xonlykeys?)?;
let height = Height::from_consensus(blkheight as u32)?;
for (label, map) in ours { for (label, map) in ours {
res.extend(p2tr_outs.iter().filter_map(|(i, o)| { res.extend(p2tr_outs.iter().filter_map(|(i, o)| {
match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) { match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) {
@ -188,10 +191,10 @@ fn scan_block_outputs(
return Some(( return Some((
outpoint, outpoint,
OwnedOutput { OwnedOutput {
blockheight: blkheight as u32, blockheight: height,
tweak: hex::encode(tweak.secret_bytes()), tweak: tweak.secret_bytes(),
amount: o.value, amount: o.value,
script: hex::encode(o.script_pubkey.as_bytes()), script: o.script_pubkey.clone(),
label: label_str, label: label_str,
spend_status: OutputSpendStatus::Unspent, spend_status: OutputSpendStatus::Unspent,
}, },
@ -213,7 +216,7 @@ fn scan_block_outputs(
} }
fn scan_block_inputs( fn scan_block_inputs(
our_outputs: HashMap<OutPoint, OwnedOutput>, our_outputs: &HashMap<OutPoint, OwnedOutput>,
txdata: Vec<Transaction>, txdata: Vec<Transaction>,
) -> Result<Vec<OutPoint>> { ) -> Result<Vec<OutPoint>> {
let mut found = vec![]; let mut found = vec![];
@ -234,8 +237,7 @@ pub fn scan_blocks(mut n_blocks_to_scan: u32, electrum_url: &str) -> anyhow::Res
log::info!("Starting a rescan"); log::info!("Starting a rescan");
let electrum_client = electrumclient::create_electrum_client(electrum_url)?; let electrum_client = electrumclient::create_electrum_client(electrum_url)?;
let sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?; let mut sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?.lock_anyhow()?;
let mut wallet = sp_wallet.get_wallet()?;
let core = DAEMON let core = DAEMON
.get() .get()
@ -243,7 +245,7 @@ pub fn scan_blocks(mut n_blocks_to_scan: u32, electrum_url: &str) -> anyhow::Res
.lock_anyhow()?; .lock_anyhow()?;
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let scan_height = wallet.get_outputs().get_last_scan(); let scan_height = sp_wallet.get_last_scan();
let tip_height: u32 = core.get_current_height()?.try_into()?; let tip_height: u32 = core.get_current_height()?.try_into()?;
// 0 means scan to tip // 0 means scan to tip
@ -270,15 +272,14 @@ pub fn scan_blocks(mut n_blocks_to_scan: u32, electrum_url: &str) -> anyhow::Res
let mut tweak_data_map = electrum_client.sp_tweaks(start as usize)?; let mut tweak_data_map = electrum_client.sp_tweaks(start as usize)?;
let scan_sk = wallet.get_client().get_scan_key(); let scan_sk = sp_wallet.get_sp_client().get_scan_key();
let sp_receiver = wallet.get_client().sp_receiver.clone();
let start_time = Instant::now(); let start_time = Instant::now();
for (blkheight, blkhash, blkfilter) in filters { for (blkheight, blkhash, blkfilter) in filters {
let spk2secret = match tweak_data_map.remove(&(&blkheight)) { let spk2secret = match tweak_data_map.remove(&blkheight) {
Some(tweak_data_vec) => { Some(tweak_data_vec) => {
get_script_to_secret_map(&sp_receiver, tweak_data_vec, scan_sk.into(), &secp)? get_script_to_secret_map(&sp_wallet.get_sp_client().sp_receiver, tweak_data_vec, scan_sk.into(), &secp)?
} }
None => HashMap::new(), None => HashMap::new(),
}; };
@ -287,42 +288,38 @@ pub fn scan_blocks(mut n_blocks_to_scan: u32, electrum_url: &str) -> anyhow::Res
let candidate_spks: Vec<&[u8; 34]> = spk2secret.keys().collect(); let candidate_spks: Vec<&[u8; 34]> = spk2secret.keys().collect();
// check if owned inputs are spent // check if owned inputs are spent
let our_outputs: HashMap<OutPoint, OwnedOutput> = let owned_spks: Vec<Vec<u8>> = sp_wallet.get_outputs()
wallet.get_outputs().to_outpoints_list();
let owned_spks: Result<Vec<Vec<u8>>> = our_outputs
.iter() .iter()
.map(|(_, output)| { .map(|(_, output)| {
let script = Vec::from_hex(&output.script).map_err(|e| Error::new(e)); let script = output.script.to_bytes();
script script
}) })
.collect(); .collect();
let matched = check_block(blkfilter, blkhash, candidate_spks, owned_spks?)?; let matched = check_block(blkfilter, blkhash, candidate_spks, owned_spks)?;
if matched { if matched {
let blk = core.get_block(blkhash)?; let blk = core.get_block(blkhash)?;
// scan block for new outputs, and add them to our list // scan block for new outputs, and add them to our list
let utxo_created_in_block = let utxo_created_in_block =
scan_block_outputs(&sp_receiver, &blk.txdata, blkheight.into(), spk2secret)?; scan_block_outputs(&sp_wallet.get_sp_client().sp_receiver, &blk.txdata, blkheight.into(), spk2secret)?;
if !utxo_created_in_block.is_empty() { if !utxo_created_in_block.is_empty() {
wallet sp_wallet
.get_mut_outputs() .get_mut_outputs()
.extend_from(utxo_created_in_block); .extend(utxo_created_in_block);
} }
// update the list of outputs just in case // update the list of outputs just in case
// utxos may be created and destroyed in the same block // utxos may be created and destroyed in the same block
let updated_outputs: HashMap<OutPoint, OwnedOutput> =
wallet.get_outputs().to_outpoints_list();
// search inputs and mark as mined // search inputs and mark as mined
let utxo_destroyed_in_block = scan_block_inputs(updated_outputs, blk.txdata)?; let utxo_destroyed_in_block = scan_block_inputs(sp_wallet.get_outputs(), blk.txdata)?;
if !utxo_destroyed_in_block.is_empty() { if !utxo_destroyed_in_block.is_empty() {
let outputs = wallet.get_mut_outputs(); let outputs = sp_wallet.get_mut_outputs();
for outpoint in utxo_destroyed_in_block { for outpoint in utxo_destroyed_in_block {
outputs.mark_mined(outpoint, blkhash)?; if let Some(output) = outputs.get_mut(&outpoint) {
output.spend_status = OutputSpendStatus::Mined(blkhash.to_string());
}
} }
} }
} }
@ -335,10 +332,9 @@ pub fn scan_blocks(mut n_blocks_to_scan: u32, electrum_url: &str) -> anyhow::Res
); );
// update last_scan height // update last_scan height
wallet sp_wallet
.get_mut_outputs() .set_last_scan(end);
.update_last_scan(end); STORAGE.get().unwrap().lock_anyhow()?.wallet_file.save(&serde_json::to_value(sp_wallet.clone())?)?;
WALLET.get().unwrap().save(wallet)?;
Ok(()) Ok(())
} }