Skip to main content

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