sdk_relay/src/spclient.rs
2024-03-08 20:40:25 +01:00

788 lines
26 KiB
Rust

use std::{
collections::{BTreeMap, HashMap},
str::FromStr,
};
use bitcoin::psbt::{raw, Input, Output};
use bitcoin::{
bip32::{DerivationPath, Xpriv},
consensus::{deserialize, serialize},
hashes::hex::FromHex,
key::TapTweak,
psbt::PsbtSighashType,
secp256k1::{
constants::SECRET_KEY_SIZE, Keypair, Message, PublicKey, Scalar, Secp256k1, SecretKey,
ThirtyTwoByteHash,
},
sighash::{Prevouts, SighashCache},
taproot::Signature,
Address, Amount, BlockHash, Network, OutPoint, ScriptBuf, TapLeafHash, Transaction, Txid, TxIn, TxOut, Witness,
};
use log::info;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use serde_with::DisplayFromStr;
use silentpayments::sending::SilentPaymentAddress;
use silentpayments::utils as sp_utils;
use silentpayments::{receiving::Receiver, utils::LabelHash};
use silentpayments::secp256k1::rand::{thread_rng, prelude::SliceRandom};
use anyhow::{Error, Result};
use crate::db::FileWriter;
use crate::constants::{NUMS, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, PSBT_SP_TWEAK_KEY};
pub use bitcoin::psbt::Psbt;
pub struct ScanProgress {
pub start: u32,
pub current: u32,
pub end: u32,
}
type SpendingTxId = String;
type MinedInBlock = String;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum OutputSpendStatus {
Unspent,
Spent(SpendingTxId),
Mined(MinedInBlock),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct OwnedOutput {
pub txoutpoint: String,
pub blockheight: u32,
pub tweak: String,
pub amount: u64,
pub script: String,
pub label: Option<String>,
pub spend_status: OutputSpendStatus,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Recipient {
pub address: String, // either old school or silent payment
pub amount: u64,
pub nb_outputs: u32, // if address is not SP, only 1 is valid
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub enum SpendKey {
Secret(SecretKey),
Public(PublicKey),
}
#[serde_as]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct SpClient {
pub label: String,
scan_sk: SecretKey,
spend_key: SpendKey,
pub mnemonic: Option<String>,
pub sp_receiver: Receiver,
pub birthday: u32,
pub last_scan: u32,
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
owned: HashMap<OutPoint, OwnedOutput>,
writer: FileWriter,
}
impl SpClient {
pub fn new(
label: String,
scan_sk: SecretKey,
spend_key: SpendKey,
mnemonic: Option<String>,
birthday: u32,
is_testnet: bool,
) -> Result<Self> {
let secp = Secp256k1::signing_only();
let scan_pubkey = scan_sk.public_key(&secp);
let sp_receiver: Receiver;
let change_label = LabelHash::from_b_scan_and_m(scan_sk, 0).to_scalar();
match spend_key {
SpendKey::Public(key) => {
sp_receiver = Receiver::new(0, scan_pubkey, key, change_label.into(), is_testnet)?;
}
SpendKey::Secret(key) => {
let spend_pubkey = key.public_key(&secp);
sp_receiver = Receiver::new(
0,
scan_pubkey,
spend_pubkey,
change_label.into(),
is_testnet,
)?;
}
}
let writer = FileWriter::new(&label)?;
Ok(Self {
label,
scan_sk,
spend_key,
mnemonic,
sp_receiver,
birthday,
last_scan: if birthday == 0 { 0 } else { birthday - 1 },
owned: HashMap::new(),
writer,
})
}
pub fn try_init_from_disk(label: String) -> Result<SpClient> {
let empty = SpClient::new(
label,
SecretKey::from_slice(&[1u8; SECRET_KEY_SIZE]).unwrap(),
SpendKey::Secret(SecretKey::from_slice(&[1u8; SECRET_KEY_SIZE]).unwrap()),
None,
0,
false,
)?;
empty.retrieve_from_disk()
}
pub fn update_last_scan(&mut self, scan_height: u32) {
self.last_scan = scan_height;
}
pub fn get_spendable_amt(&self) -> u64 {
self.owned
.values()
.filter(|x| x.spend_status == OutputSpendStatus::Unspent)
.fold(0, |acc, x| acc + x.amount)
}
#[allow(dead_code)]
pub fn get_unconfirmed_amt(&self) -> u64 {
self.owned
.values()
.filter(|x| match x.spend_status {
OutputSpendStatus::Spent(_) => true,
_ => false,
})
.fold(0, |acc, x| acc + x.amount)
}
pub fn extend_owned(&mut self, owned: Vec<(OutPoint, OwnedOutput)>) {
self.owned.extend(owned);
}
pub fn check_outpoint_owned(&self, outpoint: OutPoint) -> bool {
self.owned.contains_key(&outpoint)
}
// pub fn mark_transaction_inputs_as_spent(
// &mut self,
// tx: nakamoto::chain::Transaction,
// ) -> Result<()> {
// let txid = tx.txid();
// // note: this currently fails for collaborative transactions
// for input in tx.input {
// self.mark_outpoint_spent(input.previous_output, txid)?;
// }
// send_amount_update(self.get_spendable_amt());
// self.save_to_disk()
// }
pub fn mark_outpoint_spent(&mut self, outpoint: OutPoint, txid: Txid) -> Result<()> {
if let Some(owned) = self.owned.get_mut(&outpoint) {
match owned.spend_status {
OutputSpendStatus::Unspent => {
info!("marking {} as spent by tx {}", owned.txoutpoint, txid);
owned.spend_status = OutputSpendStatus::Spent(txid.to_string());
}
_ => return Err(Error::msg("owned outpoint is already spent")),
}
Ok(())
} else {
Err(anyhow::anyhow!("owned outpoint not found"))
}
}
pub fn mark_outpoint_mined(&mut self, outpoint: OutPoint, blkhash: BlockHash) -> Result<()> {
if let Some(owned) = self.owned.get_mut(&outpoint) {
match owned.spend_status {
OutputSpendStatus::Mined(_) => {
return Err(Error::msg("owned outpoint is already mined"))
}
_ => {
info!("marking {} as mined in block {}", owned.txoutpoint, blkhash);
owned.spend_status = OutputSpendStatus::Mined(blkhash.to_string());
}
}
Ok(())
} else {
Err(anyhow::anyhow!("owned outpoint not found"))
}
}
pub fn list_outpoints(&self) -> Vec<OwnedOutput> {
self.owned.values().cloned().collect()
}
pub fn reset_from_blockheight(self, blockheight: u32) -> Self {
let mut new = self.clone();
new.owned = HashMap::new();
new.owned = self
.owned
.into_iter()
.filter(|o| o.1.blockheight <= blockheight)
.collect();
new.last_scan = blockheight;
new
}
pub fn save_to_disk(&self) -> Result<()> {
self.writer.write_to_file(self)
}
pub fn retrieve_from_disk(self) -> Result<Self> {
self.writer.read_from_file()
}
pub fn delete_from_disk(self) -> Result<()> {
self.writer.delete()
}
pub fn get_receiving_address(&self) -> String {
self.sp_receiver.get_receiving_address()
}
pub fn get_scan_key(&self) -> SecretKey {
self.scan_sk
}
pub fn fill_sp_outputs(&self, psbt: &mut Psbt) -> Result<()> {
let b_spend = match self.spend_key {
SpendKey::Secret(key) => key,
SpendKey::Public(_) => return Err(Error::msg("Watch-only wallet, can't spend")),
};
let mut input_privkeys: Vec<SecretKey> = vec![];
for (i, input) in psbt.inputs.iter().enumerate() {
if let Some(tweak) = input.proprietary.get(&raw::ProprietaryKey {
prefix: PSBT_SP_PREFIX.as_bytes().to_vec(),
subtype: PSBT_SP_SUBTYPE,
key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(),
}) {
let mut buffer = [0u8; 32];
if tweak.len() != 32 {
return Err(Error::msg(format!("Invalid tweak at input {}", i)));
}
buffer.copy_from_slice(tweak.as_slice());
let scalar = Scalar::from_be_bytes(buffer)?;
input_privkeys.push(b_spend.add_tweak(&scalar)?);
} else {
// For now all inputs belong to us
return Err(Error::msg(format!("Missing tweak at input {}", i)));
}
}
let a_sum = Self::get_a_sum_secret_keys(&input_privkeys);
let outpoints: Vec<(String, u32)> = psbt
.unsigned_tx
.input
.iter()
.map(|i| {
let prev_out = i.previous_output;
(prev_out.txid.to_string(), prev_out.vout)
})
.collect();
let outpoints_hash: Scalar =
sp_utils::hash_outpoints(&outpoints, a_sum.public_key(&Secp256k1::signing_only()))?;
let partial_secret =
sp_utils::sending::sender_calculate_partial_secret(a_sum, outpoints_hash)?;
// get all the silent addresses
let mut sp_addresses: Vec<String> = Vec::with_capacity(psbt.outputs.len());
for output in psbt.outputs.iter() {
// get the sp address from psbt
if let Some(value) = output.proprietary.get(&raw::ProprietaryKey {
prefix: PSBT_SP_PREFIX.as_bytes().to_vec(),
subtype: PSBT_SP_SUBTYPE,
key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(),
}) {
let sp_address = SilentPaymentAddress::try_from(deserialize::<String>(value)?)?;
sp_addresses.push(sp_address.into());
} else {
// Not a sp output
continue;
}
}
let mut sp_address2xonlypubkeys =
silentpayments::sending::generate_multiple_recipient_pubkeys(
sp_addresses,
partial_secret,
)?;
for (i, output) in psbt.unsigned_tx.output.iter_mut().enumerate() {
// get the sp address from psbt
let output_data = &psbt.outputs[i];
if let Some(value) = output_data.proprietary.get(&raw::ProprietaryKey {
prefix: PSBT_SP_PREFIX.as_bytes().to_vec(),
subtype: PSBT_SP_SUBTYPE,
key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(),
}) {
let sp_address = SilentPaymentAddress::try_from(deserialize::<String>(value)?)?;
if let Some(xonlypubkeys) = sp_address2xonlypubkeys.get_mut(&sp_address.to_string())
{
if !xonlypubkeys.is_empty() {
let output_key = xonlypubkeys.remove(0); // actually we could randomize it
// update the script pubkey
output.script_pubkey =
ScriptBuf::new_p2tr_tweaked(output_key.dangerous_assume_tweaked());
} else {
return Err(Error::msg(format!(
"We're missing a key for address {}",
sp_address
)));
}
} else {
return Err(Error::msg(format!("Can't find address {}", sp_address)));
}
} else {
// Not a sp output
continue;
}
}
Ok(())
}
pub fn set_fees(psbt: &mut Psbt, fee_rate: u32, payer: String) -> Result<()> {
let payer_vouts: Vec<u32> = match SilentPaymentAddress::try_from(payer.clone()) {
Ok(sp_address) => psbt
.outputs
.iter()
.enumerate()
.filter_map(|(i, o)| {
if let Some(value) = o.proprietary.get(&raw::ProprietaryKey {
prefix: PSBT_SP_PREFIX.as_bytes().to_vec(),
subtype: PSBT_SP_SUBTYPE,
key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(),
}) {
let candidate =
SilentPaymentAddress::try_from(deserialize::<String>(value).unwrap())
.unwrap();
if sp_address == candidate {
Some(i as u32)
} else {
None
}
} else {
None
}
})
.collect(),
Err(_) => {
let address = Address::from_str(&payer)?;
let spk = address.assume_checked().script_pubkey();
psbt.unsigned_tx
.output
.iter()
.enumerate()
.filter_map(|(i, o)| {
if o.script_pubkey == spk {
Some(i as u32)
} else {
None
}
})
.collect() // Actually we should have only one output for normal address
}
};
if payer_vouts.is_empty() {
return Err(Error::msg("Payer is not part of this transaction"));
}
// check against the total amt in inputs
let total_input_amt: u64 = psbt
.iter_funding_utxos()
.try_fold(0u64, |sum, utxo_result| {
utxo_result.map(|utxo| sum + utxo.value.to_sat())
})?;
// total amt in outputs should be equal
let total_output_amt: u64 = psbt
.unsigned_tx
.output
.iter()
.fold(0, |sum, add| sum + add.value.to_sat());
let dust = total_input_amt - total_output_amt;
// now compute the size of the tx
let fake = Self::sign_psbt_fake(psbt);
let vsize = fake.vsize();
// absolut amount of fees
let fee_amt: u64 = (fee_rate * vsize as u32).into();
// now deduce the fees from one of the payer outputs
// TODO deduce fee from the change address
if fee_amt > dust {
let mut rng = thread_rng();
if let Some(deduce_from) = payer_vouts.choose(&mut rng) {
let output = &mut psbt.unsigned_tx.output[*deduce_from as usize];
let old_value = output.value;
output.value = old_value - Amount::from_sat(fee_amt - dust); // account for eventual dust
} else {
return Err(Error::msg("no payer vout"));
}
}
Ok(())
}
pub fn create_new_psbt(
&self,
inputs: Vec<OwnedOutput>,
mut recipients: Vec<Recipient>,
payload: Option<&[u8]>
) -> Result<Psbt> {
let mut tx_in: Vec<bitcoin::TxIn> = vec![];
let mut inputs_data: Vec<(ScriptBuf, u64, Scalar)> = vec![];
let mut total_input_amount = 0u64;
let mut total_output_amount = 0u64;
for i in inputs {
tx_in.push(TxIn {
previous_output: bitcoin::OutPoint::from_str(&i.txoutpoint)?,
script_sig: ScriptBuf::new(),
sequence: bitcoin::Sequence::MAX,
witness: bitcoin::Witness::new(),
});
let scalar = Scalar::from_be_bytes(FromHex::from_hex(&i.tweak)?)?;
total_input_amount += i.amount;
inputs_data.push((ScriptBuf::from_hex(&i.script)?, i.amount, scalar));
}
// We could compute the outputs key right away,
// but keeping things separated may be interesting,
// for example creating transactions in a watch-only wallet
// and using another signer
let placeholder_spk = ScriptBuf::new_p2tr_tweaked(
bitcoin::XOnlyPublicKey::from_str(NUMS)?.dangerous_assume_tweaked(),
);
let _outputs: Result<Vec<bitcoin::TxOut>> = recipients
.iter()
.map(|o| {
let script_pubkey: ScriptBuf;
match SilentPaymentAddress::try_from(o.address.as_str()) {
Ok(sp_address) => {
if self.sp_receiver.is_testnet != sp_address.is_testnet() {
return Err(Error::msg(format!(
"Wrong network for address {}",
sp_address
)));
}
script_pubkey = placeholder_spk.clone();
}
Err(_) => {
let unchecked_address = Address::from_str(&o.address)?; // TODO: handle better garbage string
let address_is_testnet = match *unchecked_address.network() {
Network::Bitcoin => false,
_ => true,
};
if self.sp_receiver.is_testnet != address_is_testnet {
return Err(Error::msg(format!(
"Wrong network for address {}",
unchecked_address.assume_checked()
)));
}
script_pubkey = ScriptBuf::from_bytes(
unchecked_address
.assume_checked()
.script_pubkey()
.to_bytes(),
);
}
}
total_output_amount += o.amount;
Ok(TxOut {
value: Amount::from_sat(o.amount),
script_pubkey,
})
})
.collect();
let mut outputs = _outputs?;
let change_amt = total_input_amount - total_output_amount;
// Add change output
let change_address = self.sp_receiver.get_change_address();
outputs.push(TxOut {
value: Amount::from_sat(change_amt),
script_pubkey: placeholder_spk,
});
recipients.push(Recipient {
address: change_address,
amount: change_amt,
nb_outputs: 1,
});
if let Some(data) = payload {
if data.len() > 40 {
return Err(Error::msg("Payload must be max 40B"));
}
let mut op_return = bitcoin::script::PushBytesBuf::new();
op_return.extend_from_slice(data);
outputs.push(TxOut {
value: Amount::from_sat(0),
script_pubkey: ScriptBuf::new_op_return(op_return),
});
}
let tx = bitcoin::Transaction {
version: bitcoin::transaction::Version(2),
lock_time: bitcoin::absolute::LockTime::ZERO,
input: tx_in,
output: outputs,
};
let mut psbt = Psbt::from_unsigned_tx(tx)?;
// Add the witness utxo to the input in psbt
for (i, input_data) in inputs_data.iter().enumerate() {
let (script_pubkey, value, tweak) = input_data;
let witness_txout = TxOut {
value: Amount::from_sat(*value),
script_pubkey: script_pubkey.clone(),
};
let mut psbt_input = Input {
witness_utxo: Some(witness_txout),
..Default::default()
};
psbt_input.proprietary.insert(
raw::ProprietaryKey {
prefix: PSBT_SP_PREFIX.as_bytes().to_vec(),
subtype: PSBT_SP_SUBTYPE,
key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(),
},
tweak.to_be_bytes().to_vec(),
);
psbt.inputs[i] = psbt_input;
}
for (i, recipient) in recipients.iter().enumerate() {
if let Ok(sp_address) = SilentPaymentAddress::try_from(recipient.address.as_str()) {
// Add silentpayment address to the output
let mut psbt_output = Output {
..Default::default()
};
psbt_output.proprietary.insert(
raw::ProprietaryKey {
prefix: PSBT_SP_PREFIX.as_bytes().to_vec(),
subtype: PSBT_SP_SUBTYPE,
key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(),
},
serialize(&sp_address.to_string()),
);
psbt.outputs[i] = psbt_output;
} else {
// Regular address, we don't need to add more data
continue;
}
}
Ok(psbt)
}
pub fn get_a_sum_secret_keys(input: &[SecretKey]) -> SecretKey {
let secp = Secp256k1::new();
let mut negated_keys: Vec<SecretKey> = vec![];
for key in input {
let (_, parity) = key.x_only_public_key(&secp);
if parity == bitcoin::secp256k1::Parity::Odd {
negated_keys.push(key.negate());
} else {
negated_keys.push(*key);
}
}
let (head, tail) = negated_keys.split_first().unwrap();
let result: SecretKey = tail
.iter()
.fold(*head, |acc, &item| acc.add_tweak(&item.into()).unwrap());
result
}
pub(crate) fn taproot_sighash<
T: std::ops::Deref<Target = Transaction> + std::borrow::Borrow<Transaction>,
>(
input: &Input,
prevouts: &Vec<&TxOut>,
input_index: usize,
cache: &mut SighashCache<T>,
tapleaf_hash: Option<TapLeafHash>,
) -> Result<(Message, PsbtSighashType), Error> {
let prevouts = Prevouts::All(prevouts);
let hash_ty = input
.sighash_type
.map(|ty| ty.taproot_hash_ty())
.unwrap_or(Ok(bitcoin::TapSighashType::Default))?;
let sighash = match tapleaf_hash {
Some(leaf_hash) => cache.taproot_script_spend_signature_hash(
input_index,
&prevouts,
leaf_hash,
hash_ty,
)?,
None => cache.taproot_key_spend_signature_hash(input_index, &prevouts, hash_ty)?,
};
let msg = Message::from_digest(sighash.into_32());
Ok((msg, hash_ty.into()))
}
// Sign a transaction with garbage, used for easier fee estimation
fn sign_psbt_fake(psbt: &Psbt) -> Transaction {
let mut fake_psbt = psbt.clone();
let fake_sig = [1u8; 64];
for i in fake_psbt.inputs.iter_mut() {
i.tap_key_sig = Some(Signature::from_slice(&fake_sig).unwrap());
}
Self::finalize_psbt(&mut fake_psbt).unwrap();
fake_psbt.extract_tx().expect("Invalid fake tx")
}
pub fn sign_psbt(&self, psbt: Psbt) -> Result<Psbt> {
let b_spend = match self.spend_key {
SpendKey::Secret(key) => key,
SpendKey::Public(_) => return Err(Error::msg("Watch-only wallet, can't spend")),
};
let mut cache = SighashCache::new(&psbt.unsigned_tx);
let mut prevouts: Vec<&TxOut> = vec![];
for input in &psbt.inputs {
if let Some(witness_utxo) = &input.witness_utxo {
prevouts.push(witness_utxo);
}
}
let mut signed_psbt = psbt.clone();
let secp = Secp256k1::signing_only();
for (i, input) in psbt.inputs.iter().enumerate() {
let tap_leaf_hash: Option<TapLeafHash> = None;
let (msg, sighash_ty) =
Self::taproot_sighash(input, &prevouts, i, &mut cache, tap_leaf_hash)?;
// Construct the signing key
let tweak = input.proprietary.get(&raw::ProprietaryKey {
prefix: PSBT_SP_PREFIX.as_bytes().to_vec(),
subtype: PSBT_SP_SUBTYPE,
key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(),
});
if tweak.is_none() {
panic!("Missing tweak")
};
let tweak = SecretKey::from_slice(tweak.unwrap().as_slice()).unwrap();
let sk = b_spend.add_tweak(&tweak.into())?;
let keypair = Keypair::from_secret_key(&secp, &sk);
let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng());
signed_psbt.inputs[i].tap_key_sig = Some(Signature {
sig,
hash_ty: sighash_ty.taproot_hash_ty()?,
});
}
Ok(signed_psbt)
}
pub(crate) fn finalize_psbt(psbt: &mut Psbt) -> Result<()> {
psbt.inputs.iter_mut().for_each(|i| {
let mut script_witness = Witness::new();
if let Some(sig) = i.tap_key_sig {
script_witness.push(sig.to_vec());
} else {
panic!("Missing signature");
}
i.final_script_witness = Some(script_witness);
// Clear all the data fields as per the spec.
i.tap_key_sig = None;
i.partial_sigs = BTreeMap::new();
i.sighash_type = None;
i.redeem_script = None;
i.witness_script = None;
i.bip32_derivation = BTreeMap::new();
});
Ok(())
}
}
pub fn derive_keys_from_seed(seed: &[u8], is_testnet: bool) -> Result<(SecretKey, SecretKey)> {
let network = if is_testnet {
Network::Testnet
} else {
Network::Bitcoin
};
let xprv = Xpriv::new_master(network, seed)?;
let (scan_privkey, spend_privkey) = derive_keys_from_xprv(xprv)?;
Ok((scan_privkey, spend_privkey))
}
fn derive_keys_from_xprv(xprv: Xpriv) -> Result<(SecretKey, SecretKey)> {
let (scan_path, spend_path) = match xprv.network {
bitcoin::Network::Bitcoin => ("m/352h/0h/0h/1h/0", "m/352h/0h/0h/0h/0"),
_ => ("m/352h/1h/0h/1h/0", "m/352h/1h/0h/0h/0"),
};
let secp = Secp256k1::signing_only();
let scan_path = DerivationPath::from_str(scan_path)?;
let spend_path = DerivationPath::from_str(spend_path)?;
let scan_privkey = xprv.derive_priv(&secp, &scan_path)?.private_key;
let spend_privkey = xprv.derive_priv(&secp, &spend_path)?.private_key;
Ok((scan_privkey, spend_privkey))
}