Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a Rustier wrapper for zcash_script #171

Merged
merged 7 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
71 changes: 71 additions & 0 deletions src/interpreter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
bitflags::bitflags! {
/// The different SigHash types, as defined in <https://zips.z.cash/zip-0143>
///
/// 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;
}
}
Comment on lines +1 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's incorrect to represent HashType as purely bitflags, because the lower bits are not valid to interpret as flags. That is, HashType::All | HashType::None should not be considered equal to HashType::Single, but that is what this type enables.

It's not immediately obvious to me where the boundary between "upper bitflags" and "lower type number" should be, but that should be figured out at some point.


bitflags::bitflags! {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// Script verification flags
pub struct VerificationFlags: u32 {
sellout marked this conversation as resolved.
Show resolved Hide resolved
/// 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;
}
}
198 changes: 197 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,201 @@
//! 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")]
#![doc(html_root_url = "https://docs.rs/zcash_script/0.3.0")]
#![allow(unsafe_code)]

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<zcash_script_error_t> 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be fine for now, but we definitely will want to improve how the callbacks are handled. I don't have a good sense of how the lifetimes are being passed around.

// 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,
sellout marked this conversation as resolved.
Show resolved Hide resolved
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<u32, Error> {
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 {
pub use super::*;
use hex::FromHex;

lazy_static::lazy_static! {
pub static ref SCRIPT_PUBKEY: Vec<u8> = <Vec<u8>>::from_hex("a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187").unwrap();
pub static ref SCRIPT_SIG: Vec<u8> = <Vec<u8>>::from_hex("00483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453ae").expect("Block bytes are in valid hex representation");
}

fn sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
hex::decode("e8c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c")
.unwrap()
.as_slice()
.first_chunk::<32>()
.map(|hash| *hash)
}

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 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(
&sighash,
n_lock_time,
is_final,
script_pub_key,
script_sig,
flags,
);

assert!(ret.is_ok());
}

#[test]
fn it_fails_on_invalid_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(
&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_eq!(ret, Err(Error::Ok));
}
}
68 changes: 68 additions & 0 deletions src/zcash_script.rs
Original file line number Diff line number Diff line change
@@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repr(u32) combined with the explicit enum discriminants and the documentation for this field, indicates that the expectation is that this field contains the C++ enum discriminant if it doesn't match an existing one. But that's not how repr(u32) works. This enum will instead have the following layout:

#[repr(C)]
union ErrorRepr {
    Ok: ErrorVariantOk,
    VerifyScript: ErrorVariantVerifyScript,
    InvalidScriptSize: ErrorVariantInvalidScriptSize,
    Unknown: ErrorVariantUnknown,
}

#[repr(u32)]
enum ErrorTag {
    Ok = 0,
    VerifyScript = 7,
    InvalidScriptSize, // Implicit = 8
    Unknown, // Implicit = 9
}

#[repr(C)]
struct ErrorVariantOk(ErrorTag);

#[repr(C)]
struct ErrorVariantVerifyScript(ErrorTag);

#[repr(C)]
struct ErrorVariantInvalidScriptSize(ErrorTag, TryFromIntError);

#[repr(C)]
struct ErrorVariantUnknown(ErrorTag, i64);

It is therefore unsuitable for passing across an FFI, which is the only reason we'd want to add #[repr(u32)] and the explicit discriminants. In particular, two things are misleading:

  • Error::Unknown will have an implicit discriminant that potentially collides with a real discriminant on the C++ side.
  • Error::Unknown's field is disjoint from the discriminant values.

Remove the #[repr(u32)] and the explicit discriminants.

}

/// All signature hashes are 32 bits, since they are necessarily produced by SHA256.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've not used SHA-256 for sighashes since Overwinter. See ZIPs 143, 243, and 244.

Suggested change
/// All signature hashes are 32 bits, since they are necessarily produced by SHA256.
/// All signature hashes are 32 bytes, since they are either:
/// - a SHA-256 output (for v1 or v2 transactions).
/// - a BLAKE2b-256 output (for v3 and above transactions).

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?
sellout marked this conversation as resolved.
Show resolved Hide resolved
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field doesn't appear anywhere in the trait method. Is it meant to refer to sighash: SighashCalculator?

/// - 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<u32, Error>;
}
Loading