Skip to main content

smplx_sdk/signer/
core.rs

1use std::collections::{HashMap, HashSet};
2use std::str::FromStr;
3
4use elements_miniscript::Descriptor;
5use elements_miniscript::bitcoin::PublicKey;
6use elements_miniscript::descriptor::Wpkh;
7
8use simplicityhl::Value;
9use simplicityhl::WitnessValues;
10use simplicityhl::elements::pset::PartiallySignedTransaction;
11use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr};
12use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Transaction, TxOut, Txid};
13use simplicityhl::simplicity::bitcoin::XOnlyPublicKey;
14use simplicityhl::simplicity::hashes::Hash;
15use simplicityhl::str::WitnessName;
16use simplicityhl::value::ValueConstructible;
17
18use bip39::Mnemonic;
19
20use elements_miniscript::{
21    DescriptorPublicKey,
22    bitcoin::{NetworkKind, PrivateKey, bip32::DerivationPath},
23    elements::{
24        EcdsaSighashType,
25        bitcoin::bip32::{Fingerprint, Xpriv, Xpub},
26        sighash::SighashCache,
27    },
28    elementssig_to_rawsig,
29    psbt::PsbtExt,
30};
31
32use super::error::SignerError;
33use crate::constants::MIN_FEE;
34use crate::program::ProgramTrait;
35use crate::provider::ProviderTrait;
36use crate::provider::SimplicityNetwork;
37use crate::transaction::FinalTransaction;
38use crate::transaction::PartialInput;
39use crate::transaction::PartialOutput;
40use crate::transaction::RequiredSignature;
41
42pub const PLACEHOLDER_FEE: u64 = 1;
43
44pub trait SignerTrait {
45    fn sign_program(
46        &self,
47        pst: &PartiallySignedTransaction,
48        program: &dyn ProgramTrait,
49        input_index: usize,
50        network: &SimplicityNetwork,
51    ) -> Result<schnorr::Signature, SignerError>;
52
53    fn sign_input(
54        &self,
55        pst: &PartiallySignedTransaction,
56        input_index: usize,
57    ) -> Result<(PublicKey, ecdsa::Signature), SignerError>;
58}
59
60pub struct Signer {
61    xprv: Xpriv,
62    provider: Box<dyn ProviderTrait>,
63    network: SimplicityNetwork,
64    secp: Secp256k1<All>,
65}
66
67impl SignerTrait for Signer {
68    fn sign_program(
69        &self,
70        pst: &PartiallySignedTransaction,
71        program: &dyn ProgramTrait,
72        input_index: usize,
73        network: &SimplicityNetwork,
74    ) -> Result<schnorr::Signature, SignerError> {
75        let env = program.get_env(pst, input_index, network)?;
76        let msg = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array());
77
78        let private_key = self.get_private_key()?;
79        let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner);
80
81        Ok(self.secp.sign_schnorr(&msg, &keypair))
82    }
83
84    fn sign_input(
85        &self,
86        pst: &PartiallySignedTransaction,
87        input_index: usize,
88    ) -> Result<(PublicKey, ecdsa::Signature), SignerError> {
89        let tx = pst.extract_tx()?;
90
91        let mut sighash_cache = SighashCache::new(&tx);
92        let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros();
93
94        let message = pst
95            .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)?
96            .to_secp_msg();
97
98        let private_key = self.get_private_key()?;
99        let public_key = private_key.public_key(&self.secp);
100
101        let signature = self.secp.sign_ecdsa_low_r(&message, &private_key.inner);
102
103        Ok((public_key, signature))
104    }
105}
106
107enum Estimate {
108    Success(Transaction, u64),
109    Failure(u64),
110}
111
112impl Signer {
113    pub fn new(mnemonic: &str, provider: Box<dyn ProviderTrait>) -> Result<Self, SignerError> {
114        let secp = Secp256k1::new();
115        let mnemonic: Mnemonic = mnemonic
116            .parse()
117            .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?;
118        let seed = mnemonic.to_seed("");
119        let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?;
120
121        let network = *provider.get_network();
122
123        Ok(Self {
124            xprv,
125            provider,
126            network,
127            secp,
128        })
129    }
130
131    // TODO: add an ability to send arbitrary assets
132    pub fn send(&self, to: Script, amount: u64) -> Result<(Transaction, u64), SignerError> {
133        let mut ft = FinalTransaction::new(self.network);
134
135        ft.add_output(PartialOutput::new(to, amount, self.network.policy_asset()));
136
137        self.finalize(&ft)
138    }
139
140    pub fn finalize(&self, tx: &FinalTransaction) -> Result<(Transaction, u64), SignerError> {
141        let mut signer_utxos = self.get_wpkh_utxos_asset(self.network.policy_asset())?;
142        let mut set = HashSet::new();
143
144        for input in tx.inputs() {
145            set.insert(OutPoint {
146                txid: input.partial_input.witness_txid,
147                vout: input.partial_input.witness_output_index,
148            });
149        }
150
151        signer_utxos.retain(|(outpoint, _)| !set.contains(outpoint));
152        signer_utxos.sort_by(|a, b| b.1.value.cmp(&a.1.value));
153
154        let mut fee_tx = tx.clone();
155        let mut curr_fee = MIN_FEE;
156        let fee_rate = self.provider.fetch_fee_rate(1)?;
157
158        for utxo in signer_utxos {
159            let policy_amount_delta = fee_tx.calculate_fee_delta();
160
161            if policy_amount_delta >= curr_fee as i64 {
162                match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? {
163                    Estimate::Success(tx, fee) => return Ok((tx, fee)),
164                    Estimate::Failure(required_fee) => curr_fee = required_fee,
165                }
166            }
167
168            fee_tx.add_input(PartialInput::new(utxo.0, utxo.1), RequiredSignature::NativeEcdsa)?;
169        }
170
171        // need to try one more time after the loop
172        let policy_amount_delta = fee_tx.calculate_fee_delta();
173
174        if policy_amount_delta >= curr_fee as i64 {
175            match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? {
176                Estimate::Success(tx, fee) => return Ok((tx, fee)),
177                Estimate::Failure(required_fee) => curr_fee = required_fee,
178            }
179        }
180
181        Err(SignerError::NotEnoughFunds(curr_fee))
182    }
183
184    pub fn finalize_strict(
185        &self,
186        tx: &FinalTransaction,
187        target_blocks: u32,
188    ) -> Result<(Transaction, u64), SignerError> {
189        let policy_amount_delta = tx.calculate_fee_delta();
190
191        if policy_amount_delta < MIN_FEE as i64 {
192            return Err(SignerError::DustAmount(policy_amount_delta));
193        }
194
195        let fee_rate = self.provider.fetch_fee_rate(target_blocks)?;
196
197        // policy_amount_delta will be > 0
198        match self.estimate_tx(tx.clone(), fee_rate, policy_amount_delta as u64)? {
199            Estimate::Success(tx, fee) => Ok((tx, fee)),
200            Estimate::Failure(required_fee) => Err(SignerError::NotEnoughFeeAmount(policy_amount_delta, required_fee)),
201        }
202    }
203
204    pub fn get_provider(&self) -> &dyn ProviderTrait {
205        self.provider.as_ref()
206    }
207
208    pub fn get_wpkh_address(&self) -> Result<Address, SignerError> {
209        let fingerprint = self.fingerprint()?;
210        let path = self.get_derivation_path()?;
211        let xpub = self.derive_xpub(&path)?;
212
213        let desc = format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)");
214
215        let descriptor: Descriptor<DescriptorPublicKey> =
216            Descriptor::Wpkh(Wpkh::from_str(&desc).map_err(|e| SignerError::WpkhDescriptor(e.to_string()))?);
217
218        Ok(descriptor.clone().into_single_descriptors()?[0]
219            .at_derivation_index(1)?
220            .address(self.network.address_params())?)
221    }
222
223    pub fn get_wpkh_utxos(&self) -> Result<Vec<(OutPoint, TxOut)>, SignerError> {
224        self.get_wpkh_utxos_filter(|_| true)
225    }
226
227    pub fn get_wpkh_utxos_asset(&self, asset: AssetId) -> Result<Vec<(OutPoint, TxOut)>, SignerError> {
228        self.get_wpkh_utxos_filter(|(_, txout)| txout.asset.explicit().unwrap() == asset)
229    }
230
231    // TODO: can this be optimized to not populate TxOuts that are filtered out?
232    pub fn get_wpkh_utxos_txid(&self, txid: Txid) -> Result<Vec<(OutPoint, TxOut)>, SignerError> {
233        self.get_wpkh_utxos_filter(|(outpoint, _)| outpoint.txid == txid)
234    }
235
236    pub fn get_wpkh_utxos_filter<F>(&self, filter: F) -> Result<Vec<(OutPoint, TxOut)>, SignerError>
237    where
238        F: FnMut(&(OutPoint, TxOut)) -> bool,
239    {
240        let mut utxos = self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?;
241
242        utxos.retain(filter);
243
244        Ok(utxos)
245    }
246
247    pub fn get_schnorr_public_key(&self) -> Result<XOnlyPublicKey, SignerError> {
248        let private_key = self.get_private_key()?;
249        let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner);
250
251        Ok(keypair.x_only_public_key().0)
252    }
253
254    pub fn get_ecdsa_public_key(&self) -> Result<PublicKey, SignerError> {
255        Ok(self.get_private_key()?.public_key(&self.secp))
256    }
257
258    pub fn get_private_key(&self) -> Result<PrivateKey, SignerError> {
259        let master_xprv = self.master_xpriv()?;
260        let full_path = self.get_derivation_path()?;
261
262        let derived =
263            full_path.extend(DerivationPath::from_str("0/1").map_err(|e| SignerError::DerivationPath(e.to_string()))?);
264
265        let ext_derived = master_xprv.derive_priv(&self.secp, &derived)?;
266
267        Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test))
268    }
269
270    fn estimate_tx(
271        &self,
272        mut fee_tx: FinalTransaction,
273        fee_rate: f32,
274        available_delta: u64,
275    ) -> Result<Estimate, SignerError> {
276        // estimate the tx fee with the change
277        // use this wpkh address as a change script
278        fee_tx.add_output(PartialOutput::new(
279            self.get_wpkh_address()?.script_pubkey(),
280            PLACEHOLDER_FEE,
281            self.network.policy_asset(),
282        ));
283
284        fee_tx.add_output(PartialOutput::new(
285            Script::new(),
286            PLACEHOLDER_FEE,
287            self.network.policy_asset(),
288        ));
289
290        let final_tx = self.sign_tx(&fee_tx)?;
291        let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate);
292
293        if available_delta > fee && available_delta - fee >= MIN_FEE {
294            // we have enough funds to cover the change UTXO
295            let outputs = fee_tx.outputs_mut();
296
297            outputs[outputs.len() - 2].amount = available_delta - fee;
298            outputs[outputs.len() - 1].amount = fee;
299
300            let final_tx = self.sign_tx(&fee_tx)?;
301
302            return Ok(Estimate::Success(final_tx, fee));
303        }
304
305        // not enough funds, so we need to estimate without the change
306        fee_tx.remove_output(fee_tx.n_outputs() - 2);
307
308        let final_tx = self.sign_tx(&fee_tx)?;
309        let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate);
310
311        if available_delta < fee {
312            return Ok(Estimate::Failure(fee));
313        }
314
315        let outputs = fee_tx.outputs_mut();
316
317        // change the fee output amount
318        outputs[outputs.len() - 1].amount = available_delta;
319
320        // finalize the tx with fee and without the change
321        let final_tx = self.sign_tx(&fee_tx)?;
322
323        Ok(Estimate::Success(final_tx, fee))
324    }
325
326    fn sign_tx(&self, tx: &FinalTransaction) -> Result<Transaction, SignerError> {
327        let mut pst = tx.extract_pst();
328        let inputs = tx.inputs();
329
330        for (index, input_i) in inputs.iter().enumerate() {
331            // we need to prune the program
332            if let Some(program_input) = &input_i.program_input {
333                let signed_witness: Result<WitnessValues, SignerError> = match &input_i.required_sig {
334                    // sign the program and insert the signature into the witness
335                    RequiredSignature::Witness(witness_name) => Ok(self.get_signed_program_witness(
336                        &pst,
337                        program_input.program.as_ref(),
338                        &program_input.witness.build_witness(),
339                        witness_name,
340                        index,
341                    )?),
342                    // just build the passed witness
343                    _ => Ok(program_input.witness.build_witness()),
344                };
345                let pruned_witness = program_input
346                    .program
347                    .finalize(&pst, &signed_witness.unwrap(), index, &self.network)
348                    .unwrap();
349
350                pst.inputs_mut()[index].final_script_witness = Some(pruned_witness);
351            } else {
352                // we need to sign the UTXO as is
353                // TODO: do we always sign?
354                let signed_witness = self.sign_input(&pst, index)?;
355                let raw_sig = elementssig_to_rawsig(&(signed_witness.1, EcdsaSighashType::All));
356
357                pst.inputs_mut()[index].final_script_witness = Some(vec![raw_sig, signed_witness.0.to_bytes()]);
358            }
359        }
360
361        Ok(pst.extract_tx()?)
362    }
363
364    fn get_signed_program_witness(
365        &self,
366        pst: &PartiallySignedTransaction,
367        program: &dyn ProgramTrait,
368        witness: &WitnessValues,
369        witness_name: &str,
370        index: usize,
371    ) -> Result<WitnessValues, SignerError> {
372        let signature = self.sign_program(pst, program, index, &self.network)?;
373
374        let mut hm = HashMap::new();
375
376        witness.iter().for_each(|el| {
377            hm.insert(el.0.clone(), el.1.clone());
378        });
379
380        hm.insert(
381            WitnessName::from_str_unchecked(witness_name),
382            Value::byte_array(signature.serialize()),
383        );
384
385        Ok(WitnessValues::from(hm))
386    }
387
388    fn derive_xpriv(&self, path: &DerivationPath) -> Result<Xpriv, SignerError> {
389        Ok(self.xprv.derive_priv(&self.secp, &path)?)
390    }
391
392    fn master_xpriv(&self) -> Result<Xpriv, SignerError> {
393        self.derive_xpriv(&DerivationPath::master())
394    }
395
396    fn derive_xpub(&self, path: &DerivationPath) -> Result<Xpub, SignerError> {
397        let derived = self.derive_xpriv(path)?;
398
399        Ok(Xpub::from_priv(&self.secp, &derived))
400    }
401
402    fn master_xpub(&self) -> Result<Xpub, SignerError> {
403        self.derive_xpub(&DerivationPath::master())
404    }
405
406    fn fingerprint(&self) -> Result<Fingerprint, SignerError> {
407        Ok(self.master_xpub()?.fingerprint())
408    }
409
410    fn get_derivation_path(&self) -> Result<DerivationPath, SignerError> {
411        let coin_type = if self.network.is_mainnet() { 1776 } else { 1 };
412        let path = format!("84h/{coin_type}h/0h");
413
414        DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string()))
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use crate::provider::EsploraProvider;
421
422    use super::*;
423
424    #[test]
425    fn keys_correspond_to_address() {
426        let url = "https://blockstream.info/liquidtestnet/api".to_string();
427        let network = SimplicityNetwork::LiquidTestnet;
428
429        let signer = Signer::new(
430            "exist carry drive collect lend cereal occur much tiger just involve mean",
431            Box::new(EsploraProvider::new(url, network)),
432        )
433        .unwrap();
434
435        let address = signer.get_wpkh_address().unwrap();
436        let pubkey = signer.get_ecdsa_public_key().unwrap();
437
438        let derived_addr = Address::p2wpkh(&pubkey, None, network.address_params());
439
440        assert_eq!(derived_addr.to_string(), address.to_string());
441    }
442}