diff --git a/Cargo.lock b/Cargo.lock index 3a94d6378..4040876ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,9 +36,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "errno" @@ -415,6 +415,7 @@ name = "zcash_script" version = "0.2.0" dependencies = [ "bindgen", + "bitflags", "cc", "hex", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 3bf442428..295afbd29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ path = "src/lib.rs" external-secp = [] [dependencies] +bitflags = "2.5" [build-dependencies] # The `bindgen` dependency should automatically upgrade to match the version used by zebra-state's `rocksdb` dependency in: diff --git a/src/cxx.rs b/src/cxx.rs new file mode 100644 index 000000000..973b9a396 --- /dev/null +++ b/src/cxx.rs @@ -0,0 +1,115 @@ +//! Rust bindings for Zcash transparent scripts. + +#![allow(missing_docs)] +#![allow(clippy::needless_lifetimes)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unsafe_code)] +#![allow(unused_imports)] +#![allow(clippy::unwrap_or_default)] + +// Use the generated C++ bindings +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +#[cfg(test)] +mod tests { + use std::ffi::{c_int, c_uint, c_void}; + + pub use super::zcash_script_error_t; + use hex::FromHex; + + lazy_static::lazy_static! { + pub static ref SCRIPT_PUBKEY: Vec = >::from_hex("a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187").unwrap(); + pub static ref SCRIPT_SIG: Vec = >::from_hex("00483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453ae").expect("Block bytes are in valid hex representation"); + } + + extern "C" fn sighash( + sighash_out: *mut u8, + sighash_out_len: c_uint, + ctx: *const c_void, + _script_code: *const u8, + _script_code_len: c_uint, + _hash_type: c_int, + ) { + unsafe { + assert!(ctx.is_null()); + let sighash = + hex::decode("e8c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") + .unwrap(); + assert!(sighash_out_len == sighash.len() as c_uint); + std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len()); + } + } + + extern "C" fn invalid_sighash( + sighash_out: *mut u8, + sighash_out_len: c_uint, + ctx: *const c_void, + _script_code: *const u8, + _script_code_len: c_uint, + _hash_type: c_int, + ) { + unsafe { + assert!(ctx.is_null()); + let sighash = + hex::decode("08c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") + .unwrap(); + assert!(sighash_out_len == sighash.len() as c_uint); + std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len()); + } + } + + #[test] + fn it_works() { + let nLockTime: i64 = 2410374; + let isFinal: u8 = 1; + let script_pub_key = &*SCRIPT_PUBKEY; + let script_sig = &*SCRIPT_SIG; + let flags: c_uint = 513; + let mut err = 0; + + let ret = unsafe { + super::zcash_script_verify_callback( + std::ptr::null(), + Some(sighash), + nLockTime, + isFinal, + script_pub_key.as_ptr(), + script_pub_key.len() as c_uint, + script_sig.as_ptr(), + script_sig.len() as c_uint, + flags, + &mut err, + ) + }; + + assert!(ret == 1); + } + + #[test] + fn it_fails_on_invalid_sighash() { + let nLockTime: i64 = 2410374; + let isFinal: u8 = 1; + let script_pub_key = &*SCRIPT_PUBKEY; + let script_sig = &*SCRIPT_SIG; + let flags: c_uint = 513; + let mut err = 0; + + let ret = unsafe { + super::zcash_script_verify_callback( + std::ptr::null(), + Some(invalid_sighash), + nLockTime, + isFinal, + script_pub_key.as_ptr(), + script_pub_key.len() as c_uint, + script_sig.as_ptr(), + script_sig.len() as c_uint, + flags, + &mut err, + ) + }; + + assert!(ret != 1); + } +} diff --git a/src/interpreter.rs b/src/interpreter.rs new file mode 100644 index 000000000..a9851f83d --- /dev/null +++ b/src/interpreter.rs @@ -0,0 +1,71 @@ +bitflags::bitflags! { + /// The different SigHash types, as defined in + /// + /// TODO: This is currently defined as `i32` to match the `c_int` constants in this package, but + /// should use librustzcash’s `u8` constants once we’ve removed the C++. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct HashType: i32 { + /// Sign all the outputs + const All = 1; + /// Sign none of the outputs - anyone can spend + const None = 2; + /// Sign one of the outputs - anyone can spend the rest + const Single = 3; + /// Anyone can add inputs to this transaction + const AnyoneCanPay = 0x80; + } +} + +bitflags::bitflags! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + /// Script verification flags + pub struct VerificationFlags: u32 { + /// Evaluate P2SH subscripts (softfork safe, + /// [BIP16](https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki). + const P2SH = 1 << 0; + + /// Passing a non-strict-DER signature or one with undefined hashtype to a checksig operation causes script failure. + /// Evaluating a pubkey that is not (0x04 + 64 bytes) or (0x02 or 0x03 + 32 bytes) by checksig causes script failure. + /// (softfork safe, but not used or intended as a consensus rule). + const StrictEnc = 1 << 1; + + /// Passing a non-strict-DER signature or one with S > order/2 to a checksig operation causes script failure + /// (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 5). + const LowS = 1 << 3; + + /// verify dummy stack item consumed by CHECKMULTISIG is of zero-length (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 7). + const NullDummy = 1 << 4; + + /// Using a non-push operator in the scriptSig causes script failure (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 2). + const SigPushOnly = 1 << 5; + + /// Require minimal encodings for all push operations (OP_0... OP_16, OP_1NEGATE where possible, direct + /// pushes up to 75 bytes, OP_PUSHDATA up to 255 bytes, OP_PUSHDATA2 for anything larger). Evaluating + /// any other push causes the script to fail ([BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 3). + /// In addition, whenever a stack element is interpreted as a number, it must be of minimal length ([BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 4). + /// (softfork safe) + const MinimalData = 1 << 6; + + /// Discourage use of NOPs reserved for upgrades (NOP1-10) + /// + /// Provided so that nodes can avoid accepting or mining transactions + /// containing executed NOP's whose meaning may change after a soft-fork, + /// thus rendering the script invalid; with this flag set executing + /// discouraged NOPs fails the script. This verification flag will never be + /// a mandatory flag applied to scripts in a block. NOPs that are not + /// executed, e.g. within an unexecuted IF ENDIF block, are *not* rejected. + const DiscourageUpgradableNOPs = 1 << 7; + + /// Require that only a single stack element remains after evaluation. This changes the success criterion from + /// "At least one stack element must remain, and when interpreted as a boolean, it must be true" to + /// "Exactly one stack element must remain, and when interpreted as a boolean, it must be true". + /// (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 6) + /// Note: CLEANSTACK should never be used without P2SH. + const CleanStack = 1 << 8; + + /// Verify CHECKLOCKTIMEVERIFY + /// + /// See [BIP65](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki) for details. + const CHECKLOCKTIMEVERIFY = 1 << 9; + } +} diff --git a/src/lib.rs b/src/lib.rs index bcb36f928..e05cfb127 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,117 @@ -//! Rust bindings for Zcash transparent scripts. +//! Zcash transparent script implementations. #![doc(html_logo_url = "https://www.zfnd.org/images/zebra-icon.png")] -#![doc(html_root_url = "https://docs.rs/zcash_script/0.2.0")] -#![allow(missing_docs)] -#![allow(clippy::needless_lifetimes)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] +#![doc(html_root_url = "https://docs.rs/zcash_script/0.3.0")] #![allow(unsafe_code)] -#![allow(unused_imports)] -#![allow(clippy::unwrap_or_default)] -// Use the generated C++ bindings -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +mod cxx; +pub use cxx::*; + +mod interpreter; +pub use interpreter::{HashType, VerificationFlags}; +mod zcash_script; +pub use zcash_script::*; + +use std::os::raw::{c_int, c_uint, c_void}; + +/// A tag to indicate that the C++ implementation of zcash_script should be used. +pub enum Cxx {} + +impl From for Error { + #[allow(non_upper_case_globals)] + fn from(err_code: zcash_script_error_t) -> Error { + match err_code { + zcash_script_error_t_zcash_script_ERR_OK => Error::Ok, + zcash_script_error_t_zcash_script_ERR_VERIFY_SCRIPT => Error::VerifyScript, + unknown => Error::Unknown(unknown.into()), + } + } +} + +/// The sighash callback to use with zcash_script. +extern "C" fn sighash_callback( + sighash_out: *mut u8, + sighash_out_len: c_uint, + ctx: *const c_void, + script_code: *const u8, + script_code_len: c_uint, + hash_type: c_int, +) { + let checked_script_code_len = usize::try_from(script_code_len) + .expect("This was converted from a `usize` in the first place"); + // SAFETY: `script_code` is created from a Rust slice in `verify_callback`, passed through the + // C++ code, eventually to `CallbackTransactionSignatureChecker::CheckSig`, which calls this + // function. + let script_code_vec = + unsafe { std::slice::from_raw_parts(script_code, checked_script_code_len) }; + let ctx = ctx as *const SighashCalculator; + // SAFETY: `ctx` is a valid `SighashCalculator` passed to `verify_callback` which forwards it to + // the `CallbackTransactionSignatureChecker`. + if let Some(sighash) = unsafe { *ctx }(script_code_vec, HashType::from_bits_retain(hash_type)) { + assert_eq!(sighash_out_len, sighash.len().try_into().unwrap()); + // SAFETY: `sighash_out` is a valid buffer created in + // `CallbackTransactionSignatureChecker::CheckSig`. + unsafe { std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len()) }; + } +} + +/// This steals a bit of the wrapper code from zebra_script, to provide the API that they want. +impl ZcashScript for Cxx { + fn verify_callback( + sighash: SighashCalculator, + lock_time: i64, + is_final: bool, + script_pub_key: &[u8], + signature_script: &[u8], + flags: VerificationFlags, + ) -> Result<(), Error> { + let mut err = 0; + + // SAFETY: The `script` fields are created from a valid Rust `slice`. + let ret = unsafe { + zcash_script_verify_callback( + (&sighash as *const SighashCalculator) as *const c_void, + Some(sighash_callback), + lock_time, + if is_final { 1 } else { 0 }, + script_pub_key.as_ptr(), + script_pub_key + .len() + .try_into() + .map_err(Error::InvalidScriptSize)?, + signature_script.as_ptr(), + signature_script + .len() + .try_into() + .map_err(Error::InvalidScriptSize)?, + flags.bits(), + &mut err, + ) + }; + + if ret == 1 { + Ok(()) + } else { + Err(Error::from(err)) + } + } + + /// Returns the number of transparent signature operations in the + /// transparent inputs and outputs of this transaction. + fn legacy_sigop_count_script(script: &[u8]) -> Result { + script + .len() + .try_into() + .map_err(Error::InvalidScriptSize) + .map(|script_len| unsafe { + zcash_script_legacy_sigop_count_script(script.as_ptr(), script_len) + }) + } +} + #[cfg(test)] mod tests { - use std::ffi::{c_int, c_uint, c_void}; - - pub use super::zcash_script_error_t; + pub use super::*; use hex::FromHex; lazy_static::lazy_static! { @@ -25,93 +119,83 @@ mod tests { pub static ref SCRIPT_SIG: Vec = >::from_hex("00483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453ae").expect("Block bytes are in valid hex representation"); } - extern "C" fn sighash( - sighash_out: *mut u8, - sighash_out_len: c_uint, - ctx: *const c_void, - _script_code: *const u8, - _script_code_len: c_uint, - _hash_type: c_int, - ) { - unsafe { - assert!(ctx.is_null()); - let sighash = - hex::decode("e8c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") - .unwrap(); - assert!(sighash_out_len == sighash.len() as c_uint); - std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len()); - } + fn sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> { + hex::decode("e8c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") + .unwrap() + .as_slice() + .first_chunk::<32>() + .map(|hash| *hash) } - extern "C" fn invalid_sighash( - sighash_out: *mut u8, - sighash_out_len: c_uint, - ctx: *const c_void, - _script_code: *const u8, - _script_code_len: c_uint, - _hash_type: c_int, - ) { - unsafe { - assert!(ctx.is_null()); - let sighash = - hex::decode("08c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") - .unwrap(); - assert!(sighash_out_len == sighash.len() as c_uint); - std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len()); - } + fn invalid_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> { + hex::decode("08c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") + .unwrap() + .as_slice() + .first_chunk::<32>() + .map(|hash| *hash) + } + + fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> { + None } #[test] fn it_works() { - let nLockTime: i64 = 2410374; - let isFinal: u8 = 1; - let script_pub_key = &*SCRIPT_PUBKEY; - let script_sig = &*SCRIPT_SIG; - let flags: c_uint = 513; - let mut err = 0; + let n_lock_time: i64 = 2410374; + let is_final: bool = true; + let script_pub_key = &SCRIPT_PUBKEY; + let script_sig = &SCRIPT_SIG; + let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; - let ret = unsafe { - super::zcash_script_verify_callback( - std::ptr::null(), - Some(sighash), - nLockTime, - isFinal, - script_pub_key.as_ptr(), - script_pub_key.len() as c_uint, - script_sig.as_ptr(), - script_sig.len() as c_uint, - flags, - &mut err, - ) - }; + let ret = Cxx::verify_callback( + &sighash, + n_lock_time, + is_final, + script_pub_key, + script_sig, + flags, + ); - assert!(ret == 1); + assert!(ret.is_ok()); } #[test] fn it_fails_on_invalid_sighash() { - let nLockTime: i64 = 2410374; - let isFinal: u8 = 1; - let script_pub_key = &*SCRIPT_PUBKEY; - let script_sig = &*SCRIPT_SIG; - let flags: c_uint = 513; - let mut err = 0; + let n_lock_time: i64 = 2410374; + let is_final: bool = true; + let script_pub_key = &SCRIPT_PUBKEY; + let script_sig = &SCRIPT_SIG; + let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; - let ret = unsafe { - super::zcash_script_verify_callback( - std::ptr::null(), - Some(invalid_sighash), - nLockTime, - isFinal, - script_pub_key.as_ptr(), - script_pub_key.len() as c_uint, - script_sig.as_ptr(), - script_sig.len() as c_uint, - flags, - &mut err, - ) - }; + let ret = Cxx::verify_callback( + &invalid_sighash, + n_lock_time, + is_final, + script_pub_key, + script_sig, + flags, + ); + + assert_eq!(ret, Err(Error::Ok)); + } + + #[test] + fn it_fails_on_missing_sighash() { + let n_lock_time: i64 = 2410374; + let is_final: bool = true; + let script_pub_key = &SCRIPT_PUBKEY; + let script_sig = &SCRIPT_SIG; + let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; + + let ret = Cxx::verify_callback( + &missing_sighash, + n_lock_time, + is_final, + script_pub_key, + script_sig, + flags, + ); - assert!(ret != 1); + assert_eq!(ret, Err(Error::Ok)); } } diff --git a/src/zcash_script.rs b/src/zcash_script.rs new file mode 100644 index 000000000..fa7d330ac --- /dev/null +++ b/src/zcash_script.rs @@ -0,0 +1,68 @@ +use std::num::TryFromIntError; + +use super::interpreter::*; + +/// This maps to `zcash_script_error_t`, but most of those cases aren’t used any more. This only +/// replicates the still-used cases, and then an `Unknown` bucket for anything else that might +/// happen. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum Error { + /// Any failure that results in the script being invalid. + Ok = 0, + /// An exception was caught. + VerifyScript = 7, + /// The script size can’t fit in a `u32`, as required by the C++ code. + InvalidScriptSize(TryFromIntError), + /// Some other failure value recovered from C++. + /// + /// __NB__: Linux uses `u32` for the underlying C++ enum while Windows uses `i32`, so `i64` can + /// hold either. + Unknown(i64), +} + +/// All signature hashes are 32 bits, since they are necessarily produced by SHA256. +pub const SIGHASH_SIZE: usize = 32; + +/// A function which is called to obtain the sighash. +/// - script_code: the scriptCode being validated. Note that this not always +/// matches script_sig, i.e. for P2SH. +/// - hash_type: the hash type being used. +/// +/// The `extern "C"` function that calls this doesn’t give much opportunity for rich failure +/// reporting, but returning `None` indicates _some_ failure to produce the desired hash. +/// +/// TODO: Can we get the “32” from somewhere rather than hardcoding it? +pub type SighashCalculator<'a> = &'a dyn Fn(&[u8], HashType) -> Option<[u8; SIGHASH_SIZE]>; + +/// The external API of zcash_script. This is defined to make it possible to compare the C++ and +/// Rust implementations. +pub trait ZcashScript { + /// Returns `Ok(())` if the a transparent input correctly spends the matching output + /// under the additional constraints specified by `flags`. This function + /// receives only the required information to validate the spend and not + /// the transaction itself. In particular, the sighash for the spend + /// is obtained using a callback function. + /// + /// - sighash_callback: a callback function which is called to obtain the sighash. + /// - n_lock_time: the lock time of the transaction being validated. + /// - is_final: a boolean indicating whether the input being validated is final + /// (i.e. its sequence number is 0xFFFFFFFF). + /// - script_pub_key: the scriptPubKey of the output being spent. + /// - script_sig: the scriptSig of the input being validated. + /// - flags: the script verification flags to use. + /// + /// Note that script verification failure is indicated by `Err(Error::Ok)`. + fn verify_callback( + sighash: SighashCalculator, + n_lock_time: i64, + is_final: bool, + script_pub_key: &[u8], + script_sig: &[u8], + flags: VerificationFlags, + ) -> Result<(), Error>; + + /// Returns the number of transparent signature operations in the input or + /// output script pointed to by script. + fn legacy_sigop_count_script(script: &[u8]) -> Result; +}