805 lines
28 KiB
Rust
805 lines
28 KiB
Rust
use std::{
|
|
any::Any, collections::HashMap, env, fmt::Debug, net::SocketAddr, str::FromStr, sync::{Arc, Mutex, MutexGuard}
|
|
};
|
|
|
|
use bitcoincore_rpc::json::{self as bitcoin_json};
|
|
use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt};
|
|
use log::{debug, error};
|
|
use sdk_common::network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage};
|
|
use sp_backend::bitcoin::{
|
|
absolute::LockTime,
|
|
consensus::{deserialize, serialize},
|
|
hex::DisplayHex,
|
|
key::TapTweak,
|
|
sighash::{Prevouts, SighashCache},
|
|
taproot::Signature,
|
|
transaction::Version,
|
|
Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness,
|
|
XOnlyPublicKey,
|
|
};
|
|
use sp_backend::{
|
|
bitcoin::secp256k1::{
|
|
rand::{thread_rng, Rng},
|
|
Error as Secp256k1Error, Keypair, Message as Secp256k1Message, PublicKey, Scalar,
|
|
Secp256k1, SecretKey, ThirtyTwoByteHash,
|
|
},
|
|
spclient::SpWallet,
|
|
};
|
|
|
|
use sp_backend::db::{JsonFile, Storage};
|
|
use sp_backend::silentpayments::receiving::Label;
|
|
use sp_backend::silentpayments::sending::{generate_recipient_pubkeys, SilentPaymentAddress};
|
|
use sp_backend::silentpayments::utils::receiving::{
|
|
calculate_shared_secret, calculate_tweak_data, get_pubkey_from_input,
|
|
};
|
|
use sp_backend::silentpayments::utils::sending::calculate_partial_secret;
|
|
use sp_backend::spclient::{
|
|
derive_keys_from_seed, OutputSpendStatus, OwnedOutput, Recipient, SpClient, SpendKey,
|
|
};
|
|
use tokio::net::{TcpListener, TcpStream};
|
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
|
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
|
use tokio_tungstenite::tungstenite::Message;
|
|
|
|
use anyhow::{Error, Result};
|
|
use zeromq::{Socket, SocketRecv};
|
|
|
|
mod daemon;
|
|
mod electrumclient;
|
|
mod scan;
|
|
|
|
use crate::{daemon::Daemon, scan::scan_blocks};
|
|
|
|
type Tx = UnboundedSender<Message>;
|
|
|
|
type PeerMap = Arc<Mutex<HashMap<SocketAddr, Tx>>>;
|
|
|
|
type SharedDaemon = Arc<Mutex<Daemon>>;
|
|
|
|
const FAUCET_AMT: Amount = Amount::from_sat(1000);
|
|
|
|
pub(crate) trait MutexExt<T> {
|
|
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error>;
|
|
}
|
|
|
|
impl<T: Debug> MutexExt<T> for Mutex<T> {
|
|
fn lock_anyhow(&self) -> Result<MutexGuard<T>, Error> {
|
|
self.lock()
|
|
.map_err(|e| Error::msg(format!("Failed to lock: {}", e)))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SilentPaymentWallet {
|
|
sp_wallet: Mutex<SpWallet>,
|
|
storage: Mutex<JsonFile>,
|
|
}
|
|
|
|
impl SilentPaymentWallet {
|
|
pub fn get_wallet(&self) -> Result<MutexGuard<SpWallet>> {
|
|
self.sp_wallet.lock_anyhow()
|
|
}
|
|
|
|
pub fn save(&self) -> Result<()> {
|
|
self.storage.lock_anyhow()?.save(&self.sp_wallet)
|
|
}
|
|
}
|
|
|
|
enum BroadcastType {
|
|
Sender(SocketAddr),
|
|
ExcludeSender(SocketAddr),
|
|
#[allow(dead_code)]
|
|
ToAll,
|
|
}
|
|
|
|
fn broadcast_message(
|
|
peers: PeerMap,
|
|
flag: AnkFlag,
|
|
payload: String,
|
|
broadcast: BroadcastType,
|
|
) -> Result<()> {
|
|
// log::debug!("Broadcasting message: {}", msg);
|
|
let ank_msg = AnkNetworkMsg {
|
|
flag,
|
|
content: payload,
|
|
};
|
|
let msg = Message::Text(serde_json::to_string(&ank_msg)?);
|
|
match broadcast {
|
|
BroadcastType::Sender(addr) => {
|
|
peers
|
|
.lock()
|
|
.map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))?
|
|
.iter()
|
|
.find(|(peer_addr, _)| peer_addr == &&addr)
|
|
.ok_or(Error::msg("Failed to find the sender in the peer_map"))?
|
|
.1
|
|
.send(msg.clone())?;
|
|
}
|
|
BroadcastType::ExcludeSender(addr) => {
|
|
peers
|
|
.lock()
|
|
.map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))?
|
|
.iter()
|
|
.filter(|(peer_addr, _)| peer_addr != &&addr)
|
|
.for_each(|(_, peer_tx)| {
|
|
let _ = peer_tx.send(msg.clone());
|
|
});
|
|
}
|
|
BroadcastType::ToAll => {
|
|
peers
|
|
.lock()
|
|
.map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))?
|
|
.iter()
|
|
.for_each(|(_, peer_tx)| {
|
|
let _ = peer_tx.send(msg.clone());
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn spend_from_core(
|
|
dest: XOnlyPublicKey,
|
|
daemon: Arc<Mutex<Daemon>>,
|
|
) -> Result<(Transaction, Amount)> {
|
|
let core = daemon
|
|
.lock()
|
|
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?;
|
|
let unspent_list: Vec<bitcoin_json::ListUnspentResultEntry> =
|
|
core.list_unspent_from_to(None)?;
|
|
|
|
if !unspent_list.is_empty() {
|
|
let network = core.get_network()?;
|
|
|
|
let spk = ScriptBuf::new_p2tr_tweaked(dest.dangerous_assume_tweaked());
|
|
|
|
let new_psbt = core.create_psbt(&unspent_list, spk, network)?;
|
|
let processed_psbt = core.process_psbt(new_psbt)?;
|
|
let finalize_psbt_result = core.finalize_psbt(processed_psbt)?;
|
|
let final_psbt = Psbt::from_str(&finalize_psbt_result)?;
|
|
let total_fee = final_psbt.fee()?;
|
|
let final_tx = final_psbt.extract_tx()?;
|
|
let fee_rate = total_fee
|
|
.checked_div(final_tx.weight().to_vbytes_ceil())
|
|
.unwrap();
|
|
|
|
Ok((final_tx, fee_rate))
|
|
} else {
|
|
// we don't have enough available coins to pay for this faucet request
|
|
Err(Error::msg("No spendable outputs"))
|
|
}
|
|
}
|
|
|
|
fn find_owned_outputs(
|
|
tx: &Transaction,
|
|
ours: HashMap<Option<Label>, HashMap<XOnlyPublicKey, Scalar>>,
|
|
) -> Result<HashMap<OutPoint, OwnedOutput>> {
|
|
let mut res: HashMap<OutPoint, OwnedOutput> = HashMap::new();
|
|
for (label, map) in ours {
|
|
res.extend(tx.output.iter().enumerate().filter_map(
|
|
|(i, o)| match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) {
|
|
Ok(key) => {
|
|
if let Some(scalar) = map.get(&key) {
|
|
match SecretKey::from_slice(&scalar.to_be_bytes()) {
|
|
Ok(tweak) => {
|
|
let outpoint = OutPoint {
|
|
txid: tx.txid(),
|
|
vout: i as u32,
|
|
};
|
|
let label_str: Option<String>;
|
|
if let Some(l) = &label {
|
|
label_str =
|
|
Some(l.as_inner().to_be_bytes().to_lower_hex_string());
|
|
} else {
|
|
label_str = None;
|
|
}
|
|
return Some((
|
|
outpoint,
|
|
OwnedOutput {
|
|
blockheight: 0,
|
|
tweak: tweak.secret_bytes().to_lower_hex_string(),
|
|
amount: o.value,
|
|
script: o.script_pubkey.as_bytes().to_lower_hex_string(),
|
|
label: label_str,
|
|
spend_status: OutputSpendStatus::Unspent,
|
|
},
|
|
));
|
|
}
|
|
Err(_) => {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
Err(_) => None,
|
|
},
|
|
));
|
|
}
|
|
Ok(res)
|
|
}
|
|
|
|
fn faucet_send(
|
|
sp_address: SilentPaymentAddress,
|
|
sp_wallet: Arc<SilentPaymentWallet>,
|
|
shared_daemon: SharedDaemon,
|
|
) -> Result<Txid> {
|
|
let mut first_tx: Option<Transaction> = None;
|
|
let final_tx: Transaction;
|
|
let mut new_outpoints: HashMap<OutPoint, OwnedOutput>;
|
|
|
|
// do we have a sp output available ?
|
|
let available_outpoints = sp_wallet.get_wallet()?.get_outputs().to_spendable_list();
|
|
|
|
let available_amt = available_outpoints
|
|
.iter()
|
|
.fold(Amount::from_sat(0), |acc, (_, x)| acc + x.amount);
|
|
|
|
// If we don't have at least 4 times the amount we need to send, we take some reserves out
|
|
if available_amt > FAUCET_AMT.checked_mul(4).unwrap() {
|
|
let mut total_amt = Amount::from_sat(0);
|
|
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;
|
|
}
|
|
}
|
|
|
|
let recipient = Recipient {
|
|
address: sp_address.into(),
|
|
amount: FAUCET_AMT,
|
|
nb_outputs: 1,
|
|
};
|
|
|
|
let fee_estimate = shared_daemon
|
|
.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 wallet = sp_wallet.get_wallet()?;
|
|
|
|
let mut new_psbt =
|
|
wallet
|
|
.get_client()
|
|
.create_new_psbt(inputs.clone(), vec![recipient], None)?;
|
|
log::debug!("Created psbt: {}", new_psbt);
|
|
SpClient::set_fees(&mut new_psbt, fee_estimate, sp_address.into())?;
|
|
wallet.get_client().fill_sp_outputs(&mut new_psbt)?;
|
|
log::debug!("Definitive psbt: {}", new_psbt);
|
|
let mut aux_rand = [0u8; 32];
|
|
thread_rng().fill(&mut aux_rand);
|
|
let mut signed = wallet.get_client().sign_psbt(new_psbt, &aux_rand)?;
|
|
log::debug!("signed psbt: {}", signed);
|
|
SpClient::finalize_psbt(&mut signed)?;
|
|
|
|
final_tx = signed.extract_tx()?;
|
|
|
|
// take all we need to register the new sp output
|
|
let outpoints: Vec<(String, u32)> = final_tx
|
|
.input
|
|
.iter()
|
|
.map(|i| (i.previous_output.txid.to_string(), i.previous_output.vout))
|
|
.collect();
|
|
|
|
let our_sp_address: SilentPaymentAddress = wallet
|
|
.get_client()
|
|
.sp_receiver
|
|
.get_receiving_address()
|
|
.try_into()?;
|
|
let our_spend_pubkey = our_sp_address.get_spend_key();
|
|
let secp = Secp256k1::verification_only();
|
|
let input_pubkeys: Result<Vec<PublicKey>, Secp256k1Error> = inputs
|
|
.iter()
|
|
.map(|(_, o)| {
|
|
let tweak = SecretKey::from_str(&o.tweak)?;
|
|
our_spend_pubkey.mul_tweak(&secp, &tweak.into())
|
|
})
|
|
.collect();
|
|
let input_pubkeys = input_pubkeys?;
|
|
let input_pubkeys: Vec<&PublicKey> = input_pubkeys.iter().collect();
|
|
let partial_tweak = calculate_tweak_data(&input_pubkeys, &outpoints)?;
|
|
let ecdh_shared_secret =
|
|
calculate_shared_secret(partial_tweak, wallet.get_client().get_scan_key())?;
|
|
|
|
let outputs_to_check: Result<Vec<XOnlyPublicKey>, Secp256k1Error> = final_tx
|
|
.output
|
|
.iter()
|
|
.map(|o| XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]))
|
|
.collect();
|
|
|
|
let ours = wallet
|
|
.get_client()
|
|
.sp_receiver
|
|
.scan_transaction(&ecdh_shared_secret, outputs_to_check?)?;
|
|
|
|
new_outpoints = find_owned_outputs(&final_tx, ours)?;
|
|
} else {
|
|
// let's try to spend directly from the mining address
|
|
let secp = Secp256k1::signing_only();
|
|
let keypair = Keypair::new(&secp, &mut thread_rng());
|
|
|
|
// we first spend from core to the pubkey we just created
|
|
let (core_tx, fee_rate) =
|
|
spend_from_core(keypair.x_only_public_key().0, shared_daemon.clone())?;
|
|
|
|
// check that the first output of the transaction pays to the key we just created
|
|
assert!(
|
|
core_tx.output[0].script_pubkey
|
|
== ScriptBuf::new_p2tr_tweaked(
|
|
keypair.x_only_public_key().0.dangerous_assume_tweaked()
|
|
)
|
|
);
|
|
|
|
// create a new transaction that spends the newly created UTXO to the sp_address
|
|
let mut faucet_tx = Transaction {
|
|
input: vec![TxIn {
|
|
previous_output: OutPoint::new(core_tx.txid(), 0),
|
|
..Default::default()
|
|
}],
|
|
output: vec![],
|
|
version: Version::TWO,
|
|
lock_time: LockTime::ZERO,
|
|
};
|
|
|
|
// now do the silent payment operations with the final recipient address
|
|
let partial_secret = calculate_partial_secret(
|
|
&[(keypair.secret_key(), true)],
|
|
&[(core_tx.txid().to_string(), 0)],
|
|
)?;
|
|
|
|
let ext_output_key: XOnlyPublicKey =
|
|
generate_recipient_pubkeys(vec![sp_address.into()], partial_secret)?
|
|
.into_values()
|
|
.flatten()
|
|
.collect::<Vec<XOnlyPublicKey>>()
|
|
.get(0)
|
|
.expect("Failed to generate keys")
|
|
.to_owned();
|
|
let change_sp_address = sp_wallet.get_wallet()?.get_client().get_receiving_address();
|
|
let change_output_key: XOnlyPublicKey =
|
|
generate_recipient_pubkeys(vec![change_sp_address], partial_secret)?
|
|
.into_values()
|
|
.flatten()
|
|
.collect::<Vec<XOnlyPublicKey>>()
|
|
.get(0)
|
|
.expect("Failed to generate keys")
|
|
.to_owned();
|
|
|
|
let ext_spk = ScriptBuf::new_p2tr_tweaked(ext_output_key.dangerous_assume_tweaked());
|
|
let change_spk = ScriptBuf::new_p2tr_tweaked(change_output_key.dangerous_assume_tweaked());
|
|
|
|
faucet_tx.output.push(TxOut {
|
|
value: FAUCET_AMT,
|
|
script_pubkey: ext_spk,
|
|
});
|
|
faucet_tx.output.push(TxOut {
|
|
value: core_tx.output[0].value - FAUCET_AMT,
|
|
script_pubkey: change_spk,
|
|
});
|
|
|
|
// dummy signature only used for fee estimation
|
|
faucet_tx.input[0].witness.push([1; 64].to_vec());
|
|
|
|
let abs_fee = fee_rate
|
|
.checked_mul(faucet_tx.weight().to_vbytes_ceil())
|
|
.ok_or_else(|| Error::msg("Fee rate multiplication overflowed"))?;
|
|
|
|
// reset the witness to empty
|
|
faucet_tx.input[0].witness = Witness::new();
|
|
|
|
faucet_tx.output[1].value -= abs_fee;
|
|
|
|
let first_tx_outputs = vec![core_tx.output[0].clone()];
|
|
let prevouts = Prevouts::All(&first_tx_outputs);
|
|
|
|
let hash_ty = TapSighashType::Default;
|
|
|
|
let mut cache = SighashCache::new(&faucet_tx);
|
|
|
|
let sighash = cache.taproot_key_spend_signature_hash(0, &prevouts, hash_ty)?;
|
|
|
|
let msg = Secp256k1Message::from_digest(sighash.into_32());
|
|
|
|
let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng());
|
|
let final_sig = Signature { sig, hash_ty };
|
|
|
|
faucet_tx.input[0].witness.push(final_sig.to_vec());
|
|
|
|
// take all we need to register the new sp output
|
|
let outpoints: Vec<(String, u32)> = vec![(core_tx.txid().to_string(), 0)];
|
|
|
|
first_tx = Some(core_tx);
|
|
|
|
let client = sp_wallet.get_wallet()?;
|
|
|
|
let input_pubkey = &keypair.public_key();
|
|
|
|
let input_pub_keys: Vec<&PublicKey> = vec![input_pubkey];
|
|
let partial_tweak = calculate_tweak_data(&input_pub_keys, &outpoints)?;
|
|
let ecdh_shared_secret =
|
|
calculate_shared_secret(partial_tweak, client.get_client().get_scan_key())?;
|
|
|
|
let p2tr_outs = vec![ext_output_key, change_output_key];
|
|
|
|
let ours = client
|
|
.get_client()
|
|
.sp_receiver
|
|
.scan_transaction(&ecdh_shared_secret, p2tr_outs)?;
|
|
|
|
final_tx = faucet_tx;
|
|
|
|
new_outpoints = find_owned_outputs(&final_tx, ours)?;
|
|
}
|
|
|
|
if let Ok(daemon) = shared_daemon.lock() {
|
|
// get current blockheight
|
|
let blkheight: u32 = daemon.get_current_height()?.try_into()?;
|
|
// update the new outpoints
|
|
for o in new_outpoints.iter_mut() {
|
|
o.1.blockheight = blkheight;
|
|
}
|
|
|
|
// broadcast one or two transactions
|
|
if first_tx.is_some() {
|
|
daemon.broadcast(&first_tx.unwrap())?;
|
|
}
|
|
daemon.broadcast(&final_tx)?;
|
|
} else {
|
|
return Err(Error::msg("Failed to lock daemon"));
|
|
}
|
|
|
|
// update our sp_client with the change output(s)
|
|
sp_wallet
|
|
.get_wallet()?
|
|
.get_mut_outputs()
|
|
.extend_from(new_outpoints);
|
|
|
|
// save to disk
|
|
sp_wallet.save()?;
|
|
|
|
Ok(final_tx.txid())
|
|
}
|
|
|
|
fn handle_faucet_request(
|
|
msg: &str,
|
|
sp_wallet: Arc<SilentPaymentWallet>,
|
|
shared_daemon: SharedDaemon,
|
|
) -> Result<Txid> {
|
|
if let Ok(sp_address) = SilentPaymentAddress::try_from(&msg["faucet".len()..]) {
|
|
// send bootstrap coins to this sp_address
|
|
faucet_send(sp_address, sp_wallet, shared_daemon)
|
|
} else {
|
|
Err(Error::msg(format!(
|
|
"faucet message with unparsable sp_address"
|
|
)))
|
|
}
|
|
}
|
|
|
|
async fn handle_connection(
|
|
peers: PeerMap,
|
|
shared_daemon: SharedDaemon,
|
|
sp_wallet: Arc<SilentPaymentWallet>,
|
|
raw_stream: TcpStream,
|
|
addr: SocketAddr,
|
|
) {
|
|
debug!("Incoming TCP connection from: {}", addr);
|
|
|
|
let ws_stream = tokio_tungstenite::accept_async(raw_stream)
|
|
.await
|
|
.expect("Error during the websocket handshake occurred");
|
|
debug!("WebSocket connection established");
|
|
|
|
// Insert the write part of this peer to the peer map.
|
|
let (tx, rx) = unbounded_channel();
|
|
match peers.lock_anyhow() {
|
|
Ok(mut peer_map) => peer_map.insert(addr, tx),
|
|
Err(e) => {
|
|
log::error!("{}", e);
|
|
panic!();
|
|
}
|
|
};
|
|
|
|
let (outgoing, incoming) = ws_stream.split();
|
|
|
|
let broadcast_incoming = incoming.try_for_each({
|
|
let peers = peers.clone();
|
|
move |msg| {
|
|
if let Ok(raw_msg) = msg.to_text() {
|
|
debug!("Received msg: {}", raw_msg);
|
|
let parsed = serde_json::from_str::<AnkNetworkMsg>(raw_msg);
|
|
match parsed {
|
|
Ok(ank_msg) => match ank_msg.flag {
|
|
AnkFlag::Faucet => {
|
|
debug!("Received a faucet message");
|
|
if let Ok(content) =
|
|
serde_json::from_str::<FaucetMessage>(&ank_msg.content)
|
|
{
|
|
match handle_faucet_request(
|
|
&content.sp_address,
|
|
sp_wallet.clone(),
|
|
shared_daemon.clone(),
|
|
) {
|
|
Ok(new_tx_msg) => {
|
|
if let Err(e) = broadcast_message(
|
|
peers.clone(),
|
|
AnkFlag::NewTx,
|
|
serde_json::to_string(&new_tx_msg)
|
|
.expect("This should not fail"),
|
|
BroadcastType::Sender(addr),
|
|
) {
|
|
log::error!(
|
|
"Failed to broadcast message: {}",
|
|
e.to_string()
|
|
);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
if let Err(e) = broadcast_message(
|
|
peers.clone(),
|
|
AnkFlag::Error,
|
|
e.to_string(),
|
|
BroadcastType::Sender(addr),
|
|
) {
|
|
log::error!("Failed to broadcast message: {}", e);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log::error!("Invalid content for faucet message");
|
|
}
|
|
}
|
|
AnkFlag::NewTx => unimplemented!(),
|
|
AnkFlag::Error => unimplemented!(),
|
|
AnkFlag::Unknown => unimplemented!(),
|
|
},
|
|
Err(_) => log::error!("Failed to parse network message"),
|
|
}
|
|
} else {
|
|
// we don't care
|
|
log::debug!("Received non-text message {} from peer {}", msg, addr);
|
|
}
|
|
future::ok(())
|
|
}
|
|
});
|
|
|
|
let receive_from_others = UnboundedReceiverStream::new(rx)
|
|
.map(Ok)
|
|
.forward(outgoing)
|
|
.map(|result| {
|
|
if let Err(e) = result {
|
|
debug!("Error sending message: {}", e);
|
|
}
|
|
});
|
|
|
|
pin_mut!(broadcast_incoming, receive_from_others);
|
|
future::select(broadcast_incoming, receive_from_others).await;
|
|
|
|
debug!("{} disconnected", &addr);
|
|
peers.lock().unwrap().remove(&addr);
|
|
}
|
|
|
|
fn compute_partial_tweak_to_transaction(
|
|
tx: Transaction,
|
|
daemon: Arc<Mutex<Daemon>>,
|
|
) -> Result<PublicKey> {
|
|
let mut outpoints: Vec<(String, u32)> = Vec::with_capacity(tx.input.len());
|
|
let mut pubkeys: Vec<PublicKey> = Vec::with_capacity(tx.input.len());
|
|
for input in tx.input {
|
|
outpoints.push((
|
|
input.previous_output.txid.to_string(),
|
|
input.previous_output.vout,
|
|
));
|
|
let prev_tx = daemon
|
|
.lock()
|
|
.map_err(|e| Error::msg(format!("Failed to lock the daemon: {}", e)))?
|
|
.get_transaction(&input.previous_output.txid, None)
|
|
.map_err(|e| Error::msg(format!("Failed to find previous transaction: {}", e)))?;
|
|
|
|
if let Some(output) = prev_tx.output.get(input.previous_output.vout as usize) {
|
|
match get_pubkey_from_input(
|
|
&input.script_sig.to_bytes(),
|
|
&input.witness.to_vec(),
|
|
&output.script_pubkey.to_bytes(),
|
|
) {
|
|
Ok(Some(pubkey)) => pubkeys.push(pubkey),
|
|
Ok(None) => continue,
|
|
Err(e) => {
|
|
return Err(Error::msg(format!(
|
|
"Can't extract pubkey from input: {}",
|
|
e
|
|
)))
|
|
}
|
|
}
|
|
} else {
|
|
return Err(Error::msg("Transaction with a non-existing input"));
|
|
}
|
|
}
|
|
|
|
let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect();
|
|
let partial_tweak = calculate_tweak_data(&input_pub_keys, &outpoints)?;
|
|
Ok(partial_tweak)
|
|
}
|
|
|
|
fn create_new_tx_message(transaction: Vec<u8>, daemon: Arc<Mutex<Daemon>>) -> Result<NewTxMessage> {
|
|
let tx: Transaction = deserialize(&transaction)?;
|
|
|
|
if tx.is_coinbase() {
|
|
return Err(Error::msg("Can't process coinbase transaction"));
|
|
}
|
|
|
|
let partial_tweak = compute_partial_tweak_to_transaction(tx, daemon)?;
|
|
Ok(NewTxMessage::new(
|
|
transaction.to_lower_hex_string(),
|
|
Some(partial_tweak.to_string()),
|
|
))
|
|
}
|
|
|
|
async fn handle_zmq(peers: PeerMap, shared_daemon: SharedDaemon) {
|
|
debug!("Starting listening on Core");
|
|
let mut socket = zeromq::SubSocket::new();
|
|
socket.connect("tcp://127.0.0.1:29100").await.unwrap();
|
|
socket.subscribe("rawtx").await.unwrap();
|
|
// socket.subscribe("hashblock");
|
|
debug!("{:?}", socket.type_id());
|
|
loop {
|
|
let core_msg = match socket.recv().await {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
error!("Zmq error: {}", e);
|
|
continue;
|
|
}
|
|
};
|
|
debug!("Received a message");
|
|
|
|
let payload: String =
|
|
if let (Some(topic), Some(data)) = (core_msg.get(0), core_msg.get(1)) {
|
|
match std::str::from_utf8(&topic) {
|
|
Ok("rawtx") => {
|
|
match create_new_tx_message(data.to_vec(), shared_daemon.clone()) {
|
|
Ok(m) => serde_json::to_string(&m).expect("This shouldn't fail"),
|
|
Err(e) => {
|
|
error!("{}", e);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
Ok("hashblock") => todo!(),
|
|
_ => {
|
|
error!("Unexpected message in zmq");
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
error!("Empty message");
|
|
continue;
|
|
};
|
|
|
|
if let Err(e) = broadcast_message(
|
|
peers.clone(),
|
|
AnkFlag::NewTx,
|
|
payload,
|
|
BroadcastType::ToAll,
|
|
) {
|
|
log::error!("{}", e.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::main(flavor = "multi_thread")]
|
|
async fn main() -> Result<()> {
|
|
env_logger::init();
|
|
|
|
let addr = env::args()
|
|
.nth(1)
|
|
.unwrap_or_else(|| "127.0.0.1:8090".to_string());
|
|
let wallet_name = env::args().nth(2).unwrap_or_else(|| "default".to_owned());
|
|
let is_testnet: bool = env::args()
|
|
.nth(3)
|
|
.unwrap_or_else(|| "true".to_owned())
|
|
.parse()
|
|
.expect("Please provide either \"true\" or \"false\"");
|
|
let core_wallet: Option<String> = env::args().nth(4);
|
|
|
|
let peers = PeerMap::new(Mutex::new(HashMap::new()));
|
|
|
|
// Connect the rpc daemon
|
|
let shared_daemon = Arc::new(Mutex::new(Daemon::connect(core_wallet)?));
|
|
|
|
let current_tip: u32 = shared_daemon
|
|
.lock_anyhow()?
|
|
.get_current_height()?
|
|
.try_into()?;
|
|
|
|
let mut config_dir = env::var("HOME")?;
|
|
config_dir.push_str("/.4nk");
|
|
let sp_wallet_file = JsonFile::new(&config_dir, &wallet_name);
|
|
|
|
// load an existing sp_wallet, or create a new one
|
|
let sp_wallet = match <JsonFile as Storage<SpWallet>>::load(&sp_wallet_file) {
|
|
Err(_) => {
|
|
let mut seed = [0u8; 64];
|
|
thread_rng().fill(&mut seed);
|
|
let (scan_sk, spend_sk) = derive_keys_from_seed(&seed, is_testnet)
|
|
.expect("Couldn't generate a new sp_wallet");
|
|
let new_client = SpClient::new(
|
|
wallet_name,
|
|
scan_sk,
|
|
SpendKey::Secret(spend_sk),
|
|
None,
|
|
is_testnet,
|
|
)
|
|
.expect("Failed to create a new SpClient");
|
|
|
|
let mut wallet = SpWallet::new(new_client, None)?;
|
|
|
|
// set birthday to avoid unnecessary scanning
|
|
let outputs = wallet.get_mut_outputs();
|
|
outputs.set_birthday(current_tip);
|
|
outputs.update_last_scan(current_tip);
|
|
|
|
wallet
|
|
}
|
|
Ok(wallet) => wallet,
|
|
};
|
|
|
|
log::info!(
|
|
"Using wallet {} with address {}",
|
|
sp_wallet.get_client().label,
|
|
sp_wallet.get_client().get_receiving_address()
|
|
);
|
|
|
|
log::info!(
|
|
"Found {} outputs for a total balance of {}",
|
|
sp_wallet.get_outputs().to_spendable_list().len(),
|
|
sp_wallet.get_outputs().get_balance()
|
|
);
|
|
|
|
let last_scan = sp_wallet.get_outputs().get_last_scan();
|
|
|
|
let shared_sp_wallet = Mutex::new(sp_wallet);
|
|
let shared_wallet_storage = Mutex::new(sp_wallet_file);
|
|
|
|
let sp_wallet = Arc::new(SilentPaymentWallet {
|
|
sp_wallet: shared_sp_wallet,
|
|
storage: shared_wallet_storage,
|
|
});
|
|
|
|
sp_wallet.save()?;
|
|
|
|
if last_scan < current_tip {
|
|
log::info!("Scanning for our outputs");
|
|
scan_blocks(
|
|
shared_daemon.clone(),
|
|
sp_wallet.clone(),
|
|
current_tip - last_scan,
|
|
)?;
|
|
}
|
|
|
|
// Subscribe to Bitcoin Core
|
|
tokio::spawn(handle_zmq(peers.clone(), shared_daemon.clone()));
|
|
|
|
// Create the event loop and TCP listener we'll accept connections on.
|
|
let try_socket = TcpListener::bind(&addr).await;
|
|
let listener = try_socket.expect("Failed to bind");
|
|
debug!("Listening on: {}", addr);
|
|
|
|
// Let's spawn the handling of each connection in a separate task.
|
|
while let Ok((stream, addr)) = listener.accept().await {
|
|
tokio::spawn(handle_connection(
|
|
peers.clone(),
|
|
shared_daemon.clone(),
|
|
sp_wallet.clone(),
|
|
stream,
|
|
addr,
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|