zebra_script/
lib.rs

1//! Zebra script verification wrapping zcashd's zcash_script library
2#![doc(html_favicon_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-favicon-128.png")]
3#![doc(html_logo_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-icon.png")]
4#![doc(html_root_url = "https://docs.rs/zebra_script")]
5// We allow unsafe code, so we can call zcash_script
6#![allow(unsafe_code)]
7
8use core::fmt;
9use std::{
10    ffi::{c_int, c_uint, c_void},
11    sync::Arc,
12};
13
14use thiserror::Error;
15
16use zcash_script::{
17    zcash_script_error_t, zcash_script_error_t_zcash_script_ERR_OK,
18    zcash_script_error_t_zcash_script_ERR_TX_DESERIALIZE,
19    zcash_script_error_t_zcash_script_ERR_TX_INDEX,
20    zcash_script_error_t_zcash_script_ERR_TX_SIZE_MISMATCH,
21};
22
23use zebra_chain::{
24    parameters::NetworkUpgrade,
25    transaction::{HashType, SigHasher, Transaction},
26    transparent,
27};
28
29/// An Error type representing the error codes returned from zcash_script.
30#[derive(Copy, Clone, Debug, Error, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Error {
33    /// script verification failed
34    ScriptInvalid,
35    /// could not deserialize tx
36    TxDeserialize,
37    /// input index out of bounds
38    TxIndex,
39    /// tx has an invalid size
40    TxSizeMismatch,
41    /// tx is a coinbase transaction and should not be verified
42    TxCoinbase,
43    /// unknown error from zcash_script: {0}
44    Unknown(zcash_script_error_t),
45}
46
47impl fmt::Display for Error {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.write_str(&match self {
50            Error::ScriptInvalid => "script verification failed".to_owned(),
51            Error::TxDeserialize => "could not deserialize tx".to_owned(),
52            Error::TxIndex => "input index out of bounds".to_owned(),
53            Error::TxSizeMismatch => "tx has an invalid size".to_owned(),
54            Error::TxCoinbase => {
55                "tx is a coinbase transaction and should not be verified".to_owned()
56            }
57            Error::Unknown(e) => format!("unknown error from zcash_script: {e}"),
58        })
59    }
60}
61
62impl From<zcash_script_error_t> for Error {
63    #[allow(non_upper_case_globals)]
64    fn from(err_code: zcash_script_error_t) -> Error {
65        match err_code {
66            zcash_script_error_t_zcash_script_ERR_OK => Error::ScriptInvalid,
67            zcash_script_error_t_zcash_script_ERR_TX_DESERIALIZE => Error::TxDeserialize,
68            zcash_script_error_t_zcash_script_ERR_TX_INDEX => Error::TxIndex,
69            zcash_script_error_t_zcash_script_ERR_TX_SIZE_MISMATCH => Error::TxSizeMismatch,
70            unknown => Error::Unknown(unknown),
71        }
72    }
73}
74
75/// A preprocessed Transaction which can be used to verify scripts within said
76/// Transaction.
77#[derive(Debug)]
78pub struct CachedFfiTransaction {
79    /// The deserialized Zebra transaction.
80    ///
81    /// This field is private so that `transaction`, and `all_previous_outputs` always match.
82    transaction: Arc<Transaction>,
83
84    /// The outputs from previous transactions that match each input in the transaction
85    /// being verified.
86    all_previous_outputs: Vec<transparent::Output>,
87}
88
89/// A sighash context used for the zcash_script sighash callback.
90struct SigHashContext<'a> {
91    /// The index of the input being verified.
92    input_index: usize,
93    /// The SigHasher for the transaction being verified.
94    sighasher: SigHasher<'a>,
95}
96
97/// The sighash callback to use with zcash_script.
98extern "C" fn sighash(
99    sighash_out: *mut u8,
100    sighash_out_len: c_uint,
101    ctx: *const c_void,
102    script_code: *const u8,
103    script_code_len: c_uint,
104    hash_type: c_int,
105) {
106    // SAFETY: `ctx` is a valid SigHashContext because it is always passed to
107    // `zcash_script_verify_callback` which simply forwards it to the callback.
108    // `script_code` and `sighash_out` are valid buffers since they are always
109    //  specified when the callback is called.
110    unsafe {
111        let ctx = ctx as *const SigHashContext;
112        let script_code_vec =
113            std::slice::from_raw_parts(script_code, script_code_len as usize).to_vec();
114        let sighash = (*ctx).sighasher.sighash(
115            HashType::from_bits_truncate(hash_type as u32),
116            Some(((*ctx).input_index, script_code_vec)),
117        );
118        // Sanity check; must always be true.
119        assert_eq!(sighash_out_len, sighash.0.len() as c_uint);
120        std::ptr::copy_nonoverlapping(sighash.0.as_ptr(), sighash_out, sighash.0.len());
121    }
122}
123
124impl CachedFfiTransaction {
125    /// Construct a `PrecomputedTransaction` from a `Transaction` and the outputs
126    /// from previous transactions that match each input in the transaction
127    /// being verified.
128    pub fn new(
129        transaction: Arc<Transaction>,
130        all_previous_outputs: Vec<transparent::Output>,
131    ) -> Self {
132        Self {
133            transaction,
134            all_previous_outputs,
135        }
136    }
137
138    /// Returns the transparent inputs for this transaction.
139    pub fn inputs(&self) -> &[transparent::Input] {
140        self.transaction.inputs()
141    }
142
143    /// Returns the outputs from previous transactions that match each input in the transaction
144    /// being verified.
145    pub fn all_previous_outputs(&self) -> &Vec<transparent::Output> {
146        &self.all_previous_outputs
147    }
148
149    /// Verify if the script in the input at `input_index` of a transaction correctly spends the
150    /// matching [`transparent::Output`] it refers to.
151    #[allow(clippy::unwrap_in_result)]
152    pub fn is_valid(&self, nu: NetworkUpgrade, input_index: usize) -> Result<(), Error> {
153        let previous_output = self
154            .all_previous_outputs
155            .get(input_index)
156            .ok_or(Error::TxIndex)?
157            .clone();
158        let transparent::Output {
159            value: _,
160            lock_script,
161        } = previous_output;
162        let script_pub_key: &[u8] = lock_script.as_raw_bytes();
163
164        // This conversion is useful on some platforms, but not others.
165        #[allow(clippy::useless_conversion)]
166        let n_in = input_index
167            .try_into()
168            .expect("transaction indexes are much less than c_uint::MAX");
169
170        let flags = zcash_script::zcash_script_SCRIPT_FLAGS_VERIFY_P2SH
171            | zcash_script::zcash_script_SCRIPT_FLAGS_VERIFY_CHECKLOCKTIMEVERIFY;
172        // This conversion is useful on some platforms, but not others.
173        #[allow(clippy::useless_conversion)]
174        let flags = flags
175            .try_into()
176            .expect("zcash_script_SCRIPT_FLAGS_VERIFY_* enum values fit in a c_uint");
177
178        let mut err = 0;
179        let lock_time = self.transaction.raw_lock_time() as i64;
180        let is_final = if self.transaction.inputs()[input_index].sequence() == u32::MAX {
181            1
182        } else {
183            0
184        };
185        let signature_script = match &self.transaction.inputs()[input_index] {
186            transparent::Input::PrevOut {
187                outpoint: _,
188                unlock_script,
189                sequence: _,
190            } => unlock_script.as_raw_bytes(),
191            transparent::Input::Coinbase { .. } => Err(Error::TxCoinbase)?,
192        };
193
194        let ctx = Box::new(SigHashContext {
195            input_index: n_in,
196            sighasher: SigHasher::new(&self.transaction, nu, &self.all_previous_outputs),
197        });
198        // SAFETY: The `script_*` fields are created from a valid Rust `slice`.
199        let ret = unsafe {
200            zcash_script::zcash_script_verify_callback(
201                (&*ctx as *const SigHashContext) as *const c_void,
202                Some(sighash),
203                lock_time,
204                is_final,
205                script_pub_key.as_ptr(),
206                script_pub_key.len() as u32,
207                signature_script.as_ptr(),
208                signature_script.len() as u32,
209                flags,
210                &mut err,
211            )
212        };
213
214        if ret == 1 {
215            Ok(())
216        } else {
217            Err(Error::from(err))
218        }
219    }
220
221    /// Returns the number of transparent signature operations in the
222    /// transparent inputs and outputs of this transaction.
223    #[allow(clippy::unwrap_in_result)]
224    pub fn legacy_sigop_count(&self) -> Result<u64, Error> {
225        let mut count: u64 = 0;
226
227        for input in self.transaction.inputs() {
228            count += match input {
229                transparent::Input::PrevOut {
230                    outpoint: _,
231                    unlock_script,
232                    sequence: _,
233                } => {
234                    let script = unlock_script.as_raw_bytes();
235                    // SAFETY: `script` is created from a valid Rust `slice`.
236                    unsafe {
237                        zcash_script::zcash_script_legacy_sigop_count_script(
238                            script.as_ptr(),
239                            script.len() as u32,
240                        )
241                    }
242                }
243                transparent::Input::Coinbase { .. } => 0,
244            } as u64;
245        }
246
247        for output in self.transaction.outputs() {
248            let script = output.lock_script.as_raw_bytes();
249            // SAFETY: `script` is created from a valid Rust `slice`.
250            let ret = unsafe {
251                zcash_script::zcash_script_legacy_sigop_count_script(
252                    script.as_ptr(),
253                    script.len() as u32,
254                )
255            };
256            count += ret as u64;
257        }
258        Ok(count)
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use hex::FromHex;
265    use std::sync::Arc;
266    use zebra_chain::{
267        parameters::NetworkUpgrade,
268        serialization::{ZcashDeserialize, ZcashDeserializeInto},
269        transaction::Transaction,
270        transparent::{self, Output},
271    };
272    use zebra_test::prelude::*;
273
274    lazy_static::lazy_static! {
275        pub static ref SCRIPT_PUBKEY: Vec<u8> = <Vec<u8>>::from_hex("76a914f47cac1e6fec195c055994e8064ffccce0044dd788ac")
276            .unwrap();
277        pub static ref SCRIPT_TX: Vec<u8> = <Vec<u8>>::from_hex("0400008085202f8901fcaf44919d4a17f6181a02a7ebe0420be6f7dad1ef86755b81d5a9567456653c010000006a473044022035224ed7276e61affd53315eca059c92876bc2df61d84277cafd7af61d4dbf4002203ed72ea497a9f6b38eb29df08e830d99e32377edb8a574b8a289024f0241d7c40121031f54b095eae066d96b2557c1f99e40e967978a5fd117465dbec0986ca74201a6feffffff020050d6dc0100000017a9141b8a9bda4b62cd0d0582b55455d0778c86f8628f870d03c812030000001976a914e4ff5512ffafe9287992a1cd177ca6e408e0300388ac62070d0095070d000000000000000000000000")
278            .expect("Block bytes are in valid hex representation");
279    }
280
281    fn verify_valid_script(
282        nu: NetworkUpgrade,
283        tx: &[u8],
284        amount: u64,
285        pubkey: &[u8],
286    ) -> Result<()> {
287        let transaction =
288            tx.zcash_deserialize_into::<Arc<zebra_chain::transaction::Transaction>>()?;
289        let output = transparent::Output {
290            value: amount.try_into()?,
291            lock_script: transparent::Script::new(pubkey),
292        };
293        let input_index = 0;
294
295        let previous_output = vec![output];
296        let verifier = super::CachedFfiTransaction::new(transaction, previous_output);
297        verifier.is_valid(nu, input_index)?;
298
299        Ok(())
300    }
301
302    #[test]
303    fn verify_valid_script_v4() -> Result<()> {
304        let _init_guard = zebra_test::init();
305
306        verify_valid_script(
307            NetworkUpgrade::Blossom,
308            &SCRIPT_TX,
309            212 * u64::pow(10, 8),
310            &SCRIPT_PUBKEY,
311        )
312    }
313
314    #[test]
315    fn count_legacy_sigops() -> Result<()> {
316        let _init_guard = zebra_test::init();
317
318        let transaction =
319            SCRIPT_TX.zcash_deserialize_into::<Arc<zebra_chain::transaction::Transaction>>()?;
320
321        let cached_tx = super::CachedFfiTransaction::new(transaction, Vec::new());
322        assert_eq!(cached_tx.legacy_sigop_count()?, 1);
323
324        Ok(())
325    }
326
327    #[test]
328    fn fail_invalid_script() -> Result<()> {
329        let _init_guard = zebra_test::init();
330
331        let transaction =
332            SCRIPT_TX.zcash_deserialize_into::<Arc<zebra_chain::transaction::Transaction>>()?;
333        let coin = u64::pow(10, 8);
334        let amount = 211 * coin;
335        let output = transparent::Output {
336            value: amount.try_into()?,
337            lock_script: transparent::Script::new(&SCRIPT_PUBKEY.clone()[..]),
338        };
339        let input_index = 0;
340        let verifier = super::CachedFfiTransaction::new(transaction, vec![output]);
341        verifier
342            .is_valid(NetworkUpgrade::Blossom, input_index)
343            .expect_err("verification should fail");
344
345        Ok(())
346    }
347
348    #[test]
349    fn reuse_script_verifier_pass_pass() -> Result<()> {
350        let _init_guard = zebra_test::init();
351
352        let coin = u64::pow(10, 8);
353        let transaction =
354            SCRIPT_TX.zcash_deserialize_into::<Arc<zebra_chain::transaction::Transaction>>()?;
355        let amount = 212 * coin;
356        let output = transparent::Output {
357            value: amount.try_into()?,
358            lock_script: transparent::Script::new(&SCRIPT_PUBKEY.clone()),
359        };
360
361        let verifier = super::CachedFfiTransaction::new(transaction, vec![output]);
362
363        let input_index = 0;
364
365        verifier.is_valid(NetworkUpgrade::Blossom, input_index)?;
366        verifier.is_valid(NetworkUpgrade::Blossom, input_index)?;
367
368        Ok(())
369    }
370
371    #[test]
372    fn reuse_script_verifier_pass_fail() -> Result<()> {
373        let _init_guard = zebra_test::init();
374
375        let coin = u64::pow(10, 8);
376        let amount = 212 * coin;
377        let output = transparent::Output {
378            value: amount.try_into()?,
379            lock_script: transparent::Script::new(&SCRIPT_PUBKEY.clone()),
380        };
381        let transaction =
382            SCRIPT_TX.zcash_deserialize_into::<Arc<zebra_chain::transaction::Transaction>>()?;
383
384        let verifier = super::CachedFfiTransaction::new(transaction, vec![output]);
385
386        let input_index = 0;
387
388        verifier.is_valid(NetworkUpgrade::Blossom, input_index)?;
389        verifier
390            .is_valid(NetworkUpgrade::Blossom, input_index + 1)
391            .expect_err("verification should fail");
392
393        Ok(())
394    }
395
396    #[test]
397    fn reuse_script_verifier_fail_pass() -> Result<()> {
398        let _init_guard = zebra_test::init();
399
400        let coin = u64::pow(10, 8);
401        let amount = 212 * coin;
402        let output = transparent::Output {
403            value: amount.try_into()?,
404            lock_script: transparent::Script::new(&SCRIPT_PUBKEY.clone()),
405        };
406        let transaction =
407            SCRIPT_TX.zcash_deserialize_into::<Arc<zebra_chain::transaction::Transaction>>()?;
408
409        let verifier = super::CachedFfiTransaction::new(transaction, vec![output]);
410
411        let input_index = 0;
412
413        verifier
414            .is_valid(NetworkUpgrade::Blossom, input_index + 1)
415            .expect_err("verification should fail");
416        verifier.is_valid(NetworkUpgrade::Blossom, input_index)?;
417
418        Ok(())
419    }
420
421    #[test]
422    fn reuse_script_verifier_fail_fail() -> Result<()> {
423        let _init_guard = zebra_test::init();
424
425        let coin = u64::pow(10, 8);
426        let amount = 212 * coin;
427        let output = transparent::Output {
428            value: amount.try_into()?,
429            lock_script: transparent::Script::new(&SCRIPT_PUBKEY.clone()),
430        };
431        let transaction =
432            SCRIPT_TX.zcash_deserialize_into::<Arc<zebra_chain::transaction::Transaction>>()?;
433
434        let verifier = super::CachedFfiTransaction::new(transaction, vec![output]);
435
436        let input_index = 0;
437
438        verifier
439            .is_valid(NetworkUpgrade::Blossom, input_index + 1)
440            .expect_err("verification should fail");
441
442        verifier
443            .is_valid(NetworkUpgrade::Blossom, input_index + 1)
444            .expect_err("verification should fail");
445
446        Ok(())
447    }
448
449    #[test]
450    fn p2sh() -> Result<()> {
451        let _init_guard = zebra_test::init();
452
453        // real tx with txid 51ded0b026f1ff56639447760bcd673b9f4e44a8afbf3af1dbaa6ca1fd241bea
454        let serialized_tx = "0400008085202f8901c21354bf2305e474ad695382e68efc06e2f8b83c512496f615d153c2e00e688b00000000fdfd0000483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453aeffffffff0250954903000000001976a914a5a4e1797dac40e8ce66045d1a44c4a63d12142988acccf41c590000000017a9141c973c68b2acc6d6688eff9c7a9dd122ac1346ab8786c72400000000000000000000000000000000";
455        let serialized_output = "4065675c0000000017a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187";
456        let tx = Transaction::zcash_deserialize(&hex::decode(serialized_tx).unwrap().to_vec()[..])
457            .unwrap();
458
459        let previous_output =
460            Output::zcash_deserialize(&hex::decode(serialized_output).unwrap().to_vec()[..])
461                .unwrap();
462
463        let verifier = super::CachedFfiTransaction::new(Arc::new(tx), vec![previous_output]);
464
465        verifier.is_valid(NetworkUpgrade::Nu5, 0)?;
466
467        Ok(())
468    }
469}