282 lines
9.6 KiB
Rust
282 lines
9.6 KiB
Rust
use std::{collections::HashMap, str::FromStr};
|
|
|
|
use bitcoincore_rpc::json::{self as bitcoin_json};
|
|
use sdk_common::sp_client::bitcoin::secp256k1::{
|
|
rand::thread_rng, Keypair, Message as Secp256k1Message, Secp256k1, ThirtyTwoByteHash,
|
|
};
|
|
use sdk_common::sp_client::bitcoin::{
|
|
absolute::LockTime,
|
|
consensus::serialize,
|
|
hex::{DisplayHex, FromHex},
|
|
key::TapTweak,
|
|
script::PushBytesBuf,
|
|
sighash::{Prevouts, SighashCache},
|
|
taproot::Signature,
|
|
transaction::Version,
|
|
Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness,
|
|
XOnlyPublicKey,
|
|
};
|
|
use sdk_common::{
|
|
network::{FaucetMessage, NewTxMessage},
|
|
silentpayments::create_transaction,
|
|
};
|
|
|
|
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::SilentPaymentAddress;
|
|
use sdk_common::sp_client::spclient::Recipient;
|
|
|
|
use anyhow::{Error, Result};
|
|
|
|
use crate::lock_freezed_utxos;
|
|
use crate::{scan::compute_partial_tweak_to_transaction, MutexExt, DAEMON, FAUCET_AMT, WALLET};
|
|
|
|
fn spend_from_core(dest: XOnlyPublicKey) -> Result<(Transaction, Amount)> {
|
|
let core = DAEMON
|
|
.get()
|
|
.ok_or(Error::msg("DAEMON not initialized"))?
|
|
.lock_anyhow()?;
|
|
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 faucet_send(sp_address: SilentPaymentAddress, commitment: &str) -> Result<Transaction> {
|
|
let mut first_tx: Option<Transaction> = None;
|
|
let final_tx: Transaction;
|
|
|
|
let sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?;
|
|
// 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 = 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 wallet = sp_wallet.get_wallet()?;
|
|
|
|
let freezed_utxos = lock_freezed_utxos()?;
|
|
|
|
let signed_psbt = create_transaction(
|
|
vec![],
|
|
&freezed_utxos,
|
|
&wallet,
|
|
vec![recipient],
|
|
Some(Vec::from_hex(commitment).unwrap()),
|
|
fee_estimate,
|
|
None
|
|
)?;
|
|
|
|
final_tx = signed_psbt.extract_tx()?;
|
|
} 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)?;
|
|
|
|
// check that the first output of the transaction pays to the key we just created
|
|
debug_assert!(
|
|
core_tx.output[0].script_pubkey
|
|
== ScriptBuf::new_p2tr_tweaked(
|
|
keypair.x_only_public_key().0.dangerous_assume_tweaked()
|
|
)
|
|
);
|
|
|
|
// This is ugly and can be streamlined
|
|
// 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());
|
|
|
|
let mut op_return = PushBytesBuf::new();
|
|
op_return.extend_from_slice(&Vec::from_hex(commitment)?)?;
|
|
let data_spk = ScriptBuf::new_op_return(op_return);
|
|
|
|
// Take some margin to pay for the fees
|
|
if core_tx.output[0].value < FAUCET_AMT * 4 {
|
|
return Err(Error::msg("Not enough funds"));
|
|
}
|
|
|
|
let change_amt = core_tx.output[0].value.checked_sub(FAUCET_AMT).unwrap();
|
|
|
|
faucet_tx.output.push(TxOut {
|
|
value: FAUCET_AMT,
|
|
script_pubkey: ext_spk,
|
|
});
|
|
faucet_tx.output.push(TxOut {
|
|
value: change_amt,
|
|
script_pubkey: change_spk,
|
|
});
|
|
faucet_tx.output.push(TxOut {
|
|
value: Amount::from_sat(0),
|
|
script_pubkey: data_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());
|
|
|
|
first_tx = Some(core_tx);
|
|
|
|
final_tx = faucet_tx;
|
|
}
|
|
|
|
{
|
|
let daemon = DAEMON
|
|
.get()
|
|
.ok_or(Error::msg("DAEMON not initialized"))?
|
|
.lock_anyhow()?;
|
|
// broadcast one or two transactions
|
|
if first_tx.is_some() {
|
|
daemon.broadcast(&first_tx.unwrap())?;
|
|
}
|
|
let txid = daemon.broadcast(&final_tx)?;
|
|
log::debug!("Sent tx {}", txid);
|
|
}
|
|
|
|
Ok(final_tx)
|
|
}
|
|
|
|
pub fn handle_faucet_request(msg: &FaucetMessage) -> Result<NewTxMessage> {
|
|
let sp_address = SilentPaymentAddress::try_from(msg.sp_address.as_str())?;
|
|
log::debug!("Sending bootstrap coins to {}", sp_address);
|
|
// send bootstrap coins to this sp_address
|
|
let tx = faucet_send(sp_address, &msg.commitment)?;
|
|
|
|
// get the tweak
|
|
let partial_tweak = compute_partial_tweak_to_transaction(&tx)?;
|
|
|
|
// get current blockheight
|
|
let blkheight: u32 = DAEMON
|
|
.get()
|
|
.unwrap()
|
|
.lock_anyhow()?
|
|
.get_current_height()?
|
|
.try_into()?;
|
|
|
|
let sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?;
|
|
|
|
// update our sp_client with the change output(s)
|
|
sp_wallet
|
|
.get_wallet()?
|
|
.update_wallet_with_transaction(&tx, blkheight, partial_tweak)?;
|
|
|
|
log::debug!("updated the wallet");
|
|
// save to disk
|
|
sp_wallet.save()?;
|
|
|
|
log::debug!("saved the wallet");
|
|
Ok(NewTxMessage::new(
|
|
serialize(&tx).to_lower_hex_string(),
|
|
Some(partial_tweak.to_string()),
|
|
))
|
|
}
|