Skip to main content

smplx_sdk/program/
core.rs

1use std::iter;
2use std::sync::Arc;
3
4use dyn_clone::DynClone;
5
6use simplicityhl::CompiledProgram;
7use simplicityhl::elements::pset::PartiallySignedTransaction;
8use simplicityhl::elements::{Address, Script, Transaction, TxOut, taproot};
9use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1};
10use simplicityhl::simplicity::jet::Elements;
11use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo};
12use simplicityhl::simplicity::{BitMachine, RedeemNode, Value, leaf_version};
13use simplicityhl::tracker::DefaultTracker;
14use simplicityhl::{Parameters, WitnessTypes, WitnessValues};
15
16use crate::global::get_log_level;
17
18use super::arguments::ArgumentsTrait;
19use super::error::ProgramError;
20
21use crate::provider::SimplicityNetwork;
22use crate::utils::{hash_script, tap_data_hash, tr_unspendable_key};
23
24/// Executes `simplicity` programs at runtime.
25///
26/// This trait defines a core behavior related to testing and execution.
27pub trait ProgramTrait: DynClone {
28    /// Retrieves the types of arguments required by a `simplicity` program.
29    ///
30    /// # Errors
31    /// Returns a `ProgramError` if parsing or generating ABI metadata fails.
32    fn get_argument_types(&self) -> Result<Parameters, ProgramError>;
33
34    /// Retrieves the witness types required by a `simplicity` program.
35    ///
36    /// # Errors
37    /// Returns a `ProgramError` if parsing or generating ABI metadata fails.
38    fn get_witness_types(&self) -> Result<WitnessTypes, ProgramError>;
39
40    /// Constructs the Elements environment for a specified input index, PST, and network for further program execution.
41    ///
42    /// # Errors
43    /// Returns a `ProgramError` if the input index is out of bounds or if the script pubkey of the UTXO mismatches the expected program script.
44    fn get_env(
45        &self,
46        pst: &PartiallySignedTransaction,
47        input_index: usize,
48        network: &SimplicityNetwork,
49    ) -> Result<ElementsEnv<Arc<Transaction>>, ProgramError>;
50
51    /// Executes a Simplicity program for the given input index of a partially signed transaction.
52    ///
53    /// This function evaluates a Simplicity script associated with a specific transaction input
54    /// in a given network, producing the result of the computation along with the redeem node
55    /// used during execution.
56    ///
57    /// # Errors
58    /// Returns a `ProgramError` if loading the program, satisfying the witness, retrieving the environment, or executing the `BitMachine` fails.
59    fn execute(
60        &self,
61        pst: &PartiallySignedTransaction,
62        witness: &WitnessValues,
63        input_index: usize,
64        network: &SimplicityNetwork,
65    ) -> Result<(Arc<RedeemNode<Elements>>, Value), ProgramError>;
66
67    /// Finalizes and returns `pruned_witness` as output after executing the program on certain parameters.
68    ///
69    /// # Errors
70    /// Returns a `ProgramError` if program execution or constructing the control block fails.
71    fn finalize(
72        &self,
73        pst: &PartiallySignedTransaction,
74        witness: &WitnessValues,
75        input_index: usize,
76        network: &SimplicityNetwork,
77    ) -> Result<Vec<Vec<u8>>, ProgramError>;
78}
79
80/// Represents a program structure containing its source, a public key, arguments, and associated storage.
81///
82/// Abstraction giving the power to execute Simplicity contracts without specifying any additional parameters.
83#[derive(Clone)]
84pub struct Program {
85    source: &'static str,
86    pub_key: XOnlyPublicKey,
87    arguments: Box<dyn ArgumentsTrait>,
88    storage: Vec<[u8; 32]>,
89}
90
91dyn_clone::clone_trait_object!(ProgramTrait);
92
93impl ProgramTrait for Program {
94    fn get_argument_types(&self) -> Result<Parameters, ProgramError> {
95        self.get_argument_types()
96    }
97
98    fn get_witness_types(&self) -> Result<WitnessTypes, ProgramError> {
99        self.get_witness_types()
100    }
101
102    fn get_env(
103        &self,
104        pst: &PartiallySignedTransaction,
105        input_index: usize,
106        network: &SimplicityNetwork,
107    ) -> Result<ElementsEnv<Arc<Transaction>>, ProgramError> {
108        let genesis_hash = network.genesis_block_hash();
109        let cmr = self.load()?.commit().cmr();
110        let utxos: Vec<TxOut> = pst.inputs().iter().filter_map(|x| x.witness_utxo.clone()).collect();
111
112        if utxos.len() <= input_index {
113            return Err(ProgramError::UtxoIndexOutOfBounds {
114                input_index,
115                utxo_count: utxos.len(),
116            });
117        }
118
119        let target_utxo = &utxos[input_index];
120        let script_pubkey = self.get_tr_address(network).script_pubkey();
121
122        if target_utxo.script_pubkey != script_pubkey {
123            return Err(ProgramError::ScriptPubkeyMismatch {
124                expected_hash: script_pubkey.script_hash().to_string(),
125                actual_hash: target_utxo.script_pubkey.script_hash().to_string(),
126            });
127        }
128
129        Ok(ElementsEnv::new(
130            Arc::new(pst.extract_tx()?),
131            utxos
132                .iter()
133                .map(|utxo| ElementsUtxo {
134                    script_pubkey: utxo.script_pubkey.clone(),
135                    asset: utxo.asset,
136                    value: utxo.value,
137                })
138                .collect(),
139            u32::try_from(input_index)?,
140            cmr,
141            self.control_block()?,
142            None,
143            genesis_hash,
144        ))
145    }
146
147    fn execute(
148        &self,
149        pst: &PartiallySignedTransaction,
150        witness: &WitnessValues,
151        input_index: usize,
152        network: &SimplicityNetwork,
153    ) -> Result<(Arc<RedeemNode<Elements>>, Value), ProgramError> {
154        let satisfied = self
155            .load()?
156            .satisfy(witness.clone())
157            .map_err(ProgramError::WitnessSatisfaction)?;
158
159        let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(get_log_level());
160
161        let env = self.get_env(pst, input_index, network)?;
162
163        let pruned = satisfied.redeem().prune_with_tracker(&env, &mut tracker)?;
164        let mut mac = BitMachine::for_program(&pruned)?;
165
166        let result = mac.exec(&pruned, &env)?;
167
168        Ok((pruned, result))
169    }
170
171    fn finalize(
172        &self,
173        pst: &PartiallySignedTransaction,
174        witness: &WitnessValues,
175        input_index: usize,
176        network: &SimplicityNetwork,
177    ) -> Result<Vec<Vec<u8>>, ProgramError> {
178        let pruned = self.execute(pst, witness, input_index, network)?.0;
179
180        let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness();
181        let cmr = pruned.cmr();
182
183        Ok(vec![
184            simplicity_witness_bytes,
185            simplicity_program_bytes,
186            cmr.as_ref().to_vec(),
187            self.control_block()?.serialize(),
188        ])
189    }
190}
191
192impl Program {
193    /// Creates a new instance of the struct with the provided source string and arguments.
194    #[must_use]
195    pub fn new(source: &'static str, arguments: Box<dyn ArgumentsTrait>) -> Self {
196        Self {
197            source,
198            pub_key: tr_unspendable_key(),
199            arguments,
200            storage: Vec::new(),
201        }
202    }
203
204    /// Sets the `pub_key` field of the struct to the provided `XOnlyPublicKey` value and returns the updated builder instance.
205    /// This is used to set the taproot public key for the program.
206    #[must_use]
207    pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self {
208        self.pub_key = pub_key;
209
210        self
211    }
212
213    /// Sets storage capacity for further usage.
214    #[must_use]
215    pub fn with_storage_capacity(mut self, capacity: usize) -> Self {
216        self.storage = vec![[0u8; 32]; capacity];
217
218        self
219    }
220
221    /// Sets a 32-byte value at the specified index in the storage.
222    ///
223    /// # Panics
224    /// Panics if the `index` is out of bounds for the initiasized storage.
225    pub fn set_storage_at(&mut self, index: usize, new_value: [u8; 32]) {
226        let slot = self.storage.get_mut(index).expect("Index out of bounds");
227
228        *slot = new_value;
229    }
230
231    /// Returns the number of storage chunks for a program.
232    #[must_use]
233    pub fn get_storage_len(&self) -> usize {
234        self.storage.len()
235    }
236
237    /// Returns storage as a whole array of 32-byte chunks.
238    #[must_use]
239    pub fn get_storage(&self) -> &[[u8; 32]] {
240        &self.storage
241    }
242
243    /// Returns storage value at a certain index.
244    ///
245    /// # Panics
246    /// Panics if the `index` is out of bounds for the initiated storage.
247    #[must_use]
248    pub fn get_storage_at(&self, index: usize) -> [u8; 32] {
249        self.storage[index]
250    }
251
252    /// Returns a taproot address for a defined `SimplicityNetwork`.
253    ///
254    /// # Panics
255    /// Panics if generating the taproot spending information fails.
256    #[must_use]
257    pub fn get_tr_address(&self, network: &SimplicityNetwork) -> Address {
258        let spend_info = self.taproot_spending_info().unwrap();
259
260        Address::p2tr(
261            secp256k1::SECP256K1,
262            spend_info.internal_key(),
263            spend_info.merkle_root(),
264            None,
265            network.address_params(),
266        )
267    }
268
269    /// Retrieves the `ScriptPubKey` associated with the Simplicity address for the specified network.
270    #[must_use]
271    pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script {
272        self.get_tr_address(network).script_pubkey()
273    }
274
275    /// Retrieves the 32-byte `ScriptPubKey` hash associated with the Simplicity address for the specified network.
276    #[must_use]
277    pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] {
278        hash_script(&self.get_script_pubkey(network))
279    }
280
281    /// Retrieves program ABI metadata for argument types.
282    ///
283    /// # Errors
284    /// Returns a `ProgramError` if compilation fails or generating ABI metadata fails.
285    pub fn get_argument_types(&self) -> Result<Parameters, ProgramError> {
286        let compiled = self.load()?;
287        let abi_meta = compiled.generate_abi_meta().map_err(ProgramError::ProgramGenAbiMeta)?;
288
289        Ok(abi_meta.param_types)
290    }
291
292    /// Retrieves the witness types from the compiled program's ABI metadata.
293    ///
294    /// # Errors
295    /// Returns a `ProgramError` if compilation fails or generating ABI metadata fails.
296    pub fn get_witness_types(&self) -> Result<WitnessTypes, ProgramError> {
297        let compiled = self.load()?;
298        let abi_meta = compiled.generate_abi_meta().map_err(ProgramError::ProgramGenAbiMeta)?;
299
300        Ok(abi_meta.witness_types)
301    }
302
303    fn load(&self) -> Result<CompiledProgram, ProgramError> {
304        let compiled = CompiledProgram::new(self.source, self.arguments.build_arguments(), true)
305            .map_err(ProgramError::Compilation)?;
306        Ok(compiled)
307    }
308
309    fn script_version(&self) -> Result<(Script, taproot::LeafVersion), ProgramError> {
310        let cmr = self.load()?.commit().cmr();
311        let script = Script::from(cmr.as_ref().to_vec());
312
313        Ok((script, leaf_version()))
314    }
315
316    fn taproot_leaf_depths(total_leaves: usize) -> Vec<usize> {
317        assert!(total_leaves > 0, "Taproot tree must contain at least one leaf");
318
319        let next_pow2 = total_leaves.next_power_of_two();
320        let depth = next_pow2.ilog2() as usize;
321
322        let shallow_count = next_pow2 - total_leaves;
323        let deep_count = total_leaves - shallow_count;
324
325        let mut depths = Vec::with_capacity(total_leaves);
326        depths.extend(iter::repeat_n(depth, deep_count));
327
328        if depth > 0 {
329            depths.extend(iter::repeat_n(depth - 1, shallow_count));
330        }
331
332        depths
333    }
334
335    fn taproot_spending_info(&self) -> Result<taproot::TaprootSpendInfo, ProgramError> {
336        let mut builder = taproot::TaprootBuilder::new();
337        let (script, version) = self.script_version()?;
338        let depths = Self::taproot_leaf_depths(1 + self.get_storage_len());
339
340        builder = builder
341            .add_leaf_with_ver(depths[0], script, version)
342            .expect("tap tree should be valid");
343
344        for (slot, depth) in self.get_storage().iter().zip(depths.into_iter().skip(1)) {
345            builder = builder
346                .add_hidden(depth, tap_data_hash(slot))
347                .expect("tap tree should be valid");
348        }
349
350        Ok(builder
351            .finalize(secp256k1::SECP256K1, self.pub_key)
352            .expect("tap tree should be valid"))
353    }
354
355    fn control_block(&self) -> Result<taproot::ControlBlock, ProgramError> {
356        let info = self.taproot_spending_info()?;
357        let script_ver = self.script_version()?;
358
359        Ok(info.control_block(&script_ver).expect("control block should exist"))
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use simplicityhl::{
366        Arguments,
367        elements::{AssetId, confidential, pset::Input},
368    };
369
370    use super::*;
371
372    // simplicityhl/examples/cat.simf
373    const DUMMY_PROGRAM: &str = r"
374        fn main() {
375            let ab: u16 = <(u8, u8)>::into((0x10, 0x01));
376            let c: u16 = 0x1001;
377            assert!(jet::eq_16(ab, c));
378            let ab: u8 = <(u4, u4)>::into((0b1011, 0b1101));
379            let c: u8 = 0b10111101;
380            assert!(jet::eq_8(ab, c));
381        }
382    ";
383
384    #[derive(Clone)]
385    struct EmptyArguments;
386
387    impl ArgumentsTrait for EmptyArguments {
388        fn build_arguments(&self) -> Arguments {
389            Arguments::default()
390        }
391    }
392
393    fn dummy_asset_id(byte: u8) -> AssetId {
394        AssetId::from_slice(&[byte; 32]).unwrap()
395    }
396
397    fn dummy_program() -> Program {
398        Program::new(DUMMY_PROGRAM, Box::new(EmptyArguments))
399    }
400
401    fn dummy_network() -> SimplicityNetwork {
402        SimplicityNetwork::default_regtest()
403    }
404
405    fn make_pst_with_script(script: Script) -> PartiallySignedTransaction {
406        let txout = TxOut {
407            asset: confidential::Asset::Explicit(dummy_asset_id(0xAA)),
408            value: confidential::Value::Explicit(1000),
409            script_pubkey: script,
410            ..Default::default()
411        };
412        let input = Input {
413            witness_utxo: Some(txout),
414            ..Default::default()
415        };
416
417        let mut pst = PartiallySignedTransaction::new_v2();
418
419        pst.add_input(input);
420
421        pst
422    }
423
424    #[test]
425    fn test_get_env_idx() {
426        let program = dummy_program();
427        let network = dummy_network();
428
429        let correct_script = program.get_script_pubkey(&network);
430        let wrong_script = Script::new();
431
432        let mut pst = make_pst_with_script(wrong_script);
433
434        let correct_txout = TxOut {
435            asset: confidential::Asset::Explicit(dummy_asset_id(0xAA)),
436            value: confidential::Value::Explicit(1000),
437            script_pubkey: correct_script,
438            ..Default::default()
439        };
440
441        pst.add_input(Input {
442            witness_utxo: Some(correct_txout),
443            ..Default::default()
444        });
445
446        // take a script with a wrong pubkey
447        assert!(matches!(
448            program.get_env(&pst, 0, &network).unwrap_err(),
449            ProgramError::ScriptPubkeyMismatch { .. }
450        ));
451
452        assert!(program.get_env(&pst, 1, &network).is_ok());
453    }
454
455    #[test]
456    fn test_taproot_leaf_depths_known_values() {
457        let cases = [
458            (1, vec![0]),
459            (2, vec![1, 1]),
460            (3, vec![2, 2, 1]),
461            (4, vec![2, 2, 2, 2]),
462            (5, vec![3, 3, 2, 2, 2]),
463            (6, vec![3, 3, 3, 3, 2, 2]),
464            (8, vec![3, 3, 3, 3, 3, 3, 3, 3]),
465        ];
466
467        for (n, expected) in cases {
468            assert_eq!(Program::taproot_leaf_depths(n), expected, "n={n}");
469        }
470    }
471}