sdk_relay/src/faucet.rs

275 lines
9.6 KiB
Rust

use std::{collections::HashMap, str::FromStr};
use bitcoincore_rpc::bitcoin::secp256k1::PublicKey;
use bitcoincore_rpc::json::{self as bitcoin_json};
use sdk_common::silentpayments::sign_transaction;
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::{FeeRate, OwnedOutput, Recipient, RecipientAddress};
use anyhow::{Error, Result};
use crate::lock_freezed_utxos;
use crate::scan::check_transaction_alone;
use crate::{
scan::compute_partial_tweak_to_transaction, MutexExt, SilentPaymentAddress, 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, PublicKey)> {
let sp_wallet = WALLET
.get()
.ok_or(Error::msg("Wallet not initialized"))?
.lock_anyhow()?;
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_unspent_outputs()
.iter()
.filter_map(|(outpoint, output)| {
if !freezed_utxos.contains(&outpoint) {
Some((*outpoint, output.clone()))
} else {
None
}
})
.collect();
// If we had mandatory inputs, we would make sure to put them at the top of the list
// We don't care for faucet though
// We try to pay the faucet amount
if let Ok(unsigned_transaction) = create_transaction(
available_outpoints,
sp_wallet.get_sp_client(),
vec![recipient],
Some(Vec::from_hex(commitment).unwrap()),
FeeRate::from_sat_per_vb(fee_estimate.to_sat() as f32),
) {
let final_tx = sign_transaction(sp_wallet.get_sp_client(), unsigned_transaction)?;
let partial_tweak = compute_partial_tweak_to_transaction(&final_tx)?;
let daemon = DAEMON
.get()
.ok_or(Error::msg("DAEMON not initialized"))?
.lock_anyhow()?;
// First check that mempool accept it
daemon.test_mempool_accept(&final_tx)?;
let txid = daemon.broadcast(&final_tx)?;
log::debug!("Sent tx {}", txid);
// We immediately add the new tx to our wallet to prevent accidental double spend
check_transaction_alone(sp_wallet, &final_tx, &partial_tweak)?;
Ok((final_tx, partial_tweak))
} 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_sp_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());
{
let daemon = DAEMON
.get()
.ok_or(Error::msg("DAEMON not initialized"))?
.lock_anyhow()?;
// We don't worry about core_tx being refused by core
daemon.broadcast(&core_tx)?;
daemon.test_mempool_accept(&faucet_tx)?;
let txid = daemon.broadcast(&faucet_tx)?;
log::debug!("Sent tx {}", txid);
}
let partial_tweak = compute_partial_tweak_to_transaction(&faucet_tx)?;
check_transaction_alone(sp_wallet, &faucet_tx, &partial_tweak)?;
Ok((faucet_tx, partial_tweak))
}
}
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, partial_tweak) = faucet_send(sp_address, &msg.commitment)?;
Ok(NewTxMessage::new(
serialize(&tx).to_lower_hex_string(),
Some(partial_tweak.to_string()),
))
}