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
8#[cfg(test)]
9mod tests;
10
11use core::fmt;
12use std::sync::Arc;
13
14use thiserror::Error;
15
16use zcash_script::ZcashScript;
17
18use zebra_chain::{
19    parameters::NetworkUpgrade,
20    transaction::{HashType, SigHasher},
21    transparent,
22};
23
24/// An Error type representing the error codes returned from zcash_script.
25#[derive(Clone, Debug, Error, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum Error {
28    /// script verification failed
29    ScriptInvalid,
30    /// input index out of bounds
31    TxIndex,
32    /// tx is a coinbase transaction and should not be verified
33    TxCoinbase,
34    /// unknown error from zcash_script: {0}
35    Unknown(zcash_script::Error),
36    /// transaction is invalid according to zebra_chain (not a zcash_script error)
37    TxInvalid(#[from] zebra_chain::Error),
38}
39
40impl fmt::Display for Error {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.write_str(&match self {
43            Error::ScriptInvalid => "script verification failed".to_owned(),
44            Error::TxIndex => "input index out of bounds".to_owned(),
45            Error::TxCoinbase => {
46                "tx is a coinbase transaction and should not be verified".to_owned()
47            }
48            Error::Unknown(e) => format!("unknown error from zcash_script: {e:?}"),
49            Error::TxInvalid(e) => format!("tx is invalid: {e}"),
50        })
51    }
52}
53
54impl From<zcash_script::Error> for Error {
55    #[allow(non_upper_case_globals)]
56    fn from(err_code: zcash_script::Error) -> Error {
57        match err_code {
58            zcash_script::Error::Ok(_) => Error::ScriptInvalid,
59            unknown => Error::Unknown(unknown),
60        }
61    }
62}
63
64/// Get the interpreter according to the feature flag
65fn get_interpreter(
66    sighash: zcash_script::SighashCalculator<'_>,
67    lock_time: u32,
68    is_final: bool,
69    #[allow(unused)] flags: zcash_script::VerificationFlags,
70) -> impl ZcashScript + use<'_> {
71    #[cfg(feature = "comparison-interpreter")]
72    return zcash_script::cxx_rust_comparison_interpreter(sighash, lock_time, is_final, flags);
73    #[cfg(not(feature = "comparison-interpreter"))]
74    zcash_script::CxxInterpreter {
75        sighash,
76        lock_time,
77        is_final,
78    }
79}
80
81/// A preprocessed Transaction which can be used to verify scripts within said
82/// Transaction.
83#[derive(Debug)]
84pub struct CachedFfiTransaction {
85    /// The deserialized Zebra transaction.
86    ///
87    /// This field is private so that `transaction`, and `all_previous_outputs` always match.
88    transaction: Arc<zebra_chain::transaction::Transaction>,
89
90    /// The outputs from previous transactions that match each input in the transaction
91    /// being verified.
92    all_previous_outputs: Arc<Vec<transparent::Output>>,
93
94    /// The sighasher context to use to compute sighashes.
95    sighasher: SigHasher,
96}
97
98impl CachedFfiTransaction {
99    /// Construct a `PrecomputedTransaction` from a `Transaction` and the outputs
100    /// from previous transactions that match each input in the transaction
101    /// being verified.
102    pub fn new(
103        transaction: Arc<zebra_chain::transaction::Transaction>,
104        all_previous_outputs: Arc<Vec<transparent::Output>>,
105        nu: NetworkUpgrade,
106    ) -> Result<Self, Error> {
107        let sighasher = transaction.sighasher(nu, all_previous_outputs.clone())?;
108        Ok(Self {
109            transaction,
110            all_previous_outputs,
111            sighasher,
112        })
113    }
114
115    /// Returns the transparent inputs for this transaction.
116    pub fn inputs(&self) -> &[transparent::Input] {
117        self.transaction.inputs()
118    }
119
120    /// Returns the outputs from previous transactions that match each input in the transaction
121    /// being verified.
122    pub fn all_previous_outputs(&self) -> &Vec<transparent::Output> {
123        &self.all_previous_outputs
124    }
125
126    /// Return the sighasher being used for this transaction.
127    pub fn sighasher(&self) -> &SigHasher {
128        &self.sighasher
129    }
130
131    /// Verify if the script in the input at `input_index` of a transaction correctly spends the
132    /// matching [`transparent::Output`] it refers to.
133    #[allow(clippy::unwrap_in_result)]
134    pub fn is_valid(&self, input_index: usize) -> Result<(), Error> {
135        let previous_output = self
136            .all_previous_outputs
137            .get(input_index)
138            .ok_or(Error::TxIndex)?
139            .clone();
140        let transparent::Output {
141            value: _,
142            lock_script,
143        } = previous_output;
144        let script_pub_key: &[u8] = lock_script.as_raw_bytes();
145
146        let flags = zcash_script::VerificationFlags::P2SH
147            | zcash_script::VerificationFlags::CHECKLOCKTIMEVERIFY;
148
149        let lock_time = self.transaction.raw_lock_time();
150        let is_final = self.transaction.inputs()[input_index].sequence() == u32::MAX;
151        let signature_script = match &self.transaction.inputs()[input_index] {
152            transparent::Input::PrevOut {
153                outpoint: _,
154                unlock_script,
155                sequence: _,
156            } => unlock_script.as_raw_bytes(),
157            transparent::Input::Coinbase { .. } => Err(Error::TxCoinbase)?,
158        };
159
160        let calculate_sighash = |script_code: &[u8], hash_type: zcash_script::HashType| {
161            let script_code_vec = script_code.to_vec();
162            let mut our_hash_type = match hash_type.signed_outputs {
163                zcash_script::SignedOutputs::All => HashType::ALL,
164                zcash_script::SignedOutputs::Single => HashType::SINGLE,
165                zcash_script::SignedOutputs::None => HashType::NONE,
166            };
167            if hash_type.anyone_can_pay {
168                our_hash_type |= HashType::ANYONECANPAY;
169            }
170            Some(
171                self.sighasher()
172                    .sighash(our_hash_type, Some((input_index, script_code_vec)))
173                    .0,
174            )
175        };
176        let interpreter = get_interpreter(&calculate_sighash, lock_time, is_final, flags);
177        interpreter
178            .verify_callback(script_pub_key, signature_script, flags)
179            .map_err(Error::from)
180    }
181}
182
183/// Trait for counting the number of transparent signature operations
184/// in the transparent inputs and outputs of a transaction.
185pub trait Sigops {
186    /// Returns the number of transparent signature operations in the
187    /// transparent inputs and outputs of the given transaction.
188    fn sigops(&self) -> Result<u32, zcash_script::Error> {
189        // We use dummy values for args that don't influence sigops counting.
190        let interpreter = get_interpreter(
191            &|_, _| None,
192            0,
193            true,
194            zcash_script::VerificationFlags::P2SH
195                | zcash_script::VerificationFlags::CHECKLOCKTIMEVERIFY,
196        );
197
198        self.scripts().try_fold(0, |acc, s| {
199            interpreter.legacy_sigop_count_script(s).map(|n| acc + n)
200        })
201    }
202
203    /// Returns an iterator over the input and output scripts in the transaction.
204    ///
205    /// The number of input scripts in a coinbase tx is zero.
206    fn scripts(&self) -> impl Iterator<Item = &[u8]>;
207}
208
209impl Sigops for zebra_chain::transaction::Transaction {
210    fn scripts(&self) -> impl Iterator<Item = &[u8]> {
211        self.inputs()
212            .iter()
213            .filter_map(|input| match input {
214                transparent::Input::PrevOut { unlock_script, .. } => {
215                    Some(unlock_script.as_raw_bytes())
216                }
217                transparent::Input::Coinbase { .. } => None,
218            })
219            .chain(self.outputs().iter().map(|o| o.lock_script.as_raw_bytes()))
220    }
221}
222
223impl Sigops for zebra_chain::transaction::UnminedTx {
224    fn scripts(&self) -> impl Iterator<Item = &[u8]> {
225        self.transaction.scripts()
226    }
227}
228
229impl Sigops for CachedFfiTransaction {
230    fn scripts(&self) -> impl Iterator<Item = &[u8]> {
231        self.transaction.scripts()
232    }
233}
234
235impl Sigops for zcash_primitives::transaction::Transaction {
236    fn scripts(&self) -> impl Iterator<Item = &[u8]> {
237        self.transparent_bundle().into_iter().flat_map(|bundle| {
238            (!bundle.is_coinbase())
239                .then(|| bundle.vin.iter().map(|i| i.script_sig.0.as_slice()))
240                .into_iter()
241                .flatten()
242                .chain(bundle.vout.iter().map(|o| o.script_pubkey.0.as_slice()))
243        })
244    }
245}