Skip to content

Commit b55a7f8

Browse files
committed
Provide a Rustier wrapper for zcash_script
This adds a `Script` trait that exposes slightly Rustier types in order to have a common interface for the existing C++ implementation as well as the upcoming Rust implementation (and a third instance that runs both and checks that the Rust result matches the C++ one). The module structure (interpreter.rs, zcash_script.rs) and locations of definitions are intended to mirror the structure of the C++ code, especially as we get the Rust implementation in place, for easier comparison. That organization is very likely to change once everything has been checked.
1 parent 59642ca commit b55a7f8

File tree

5 files changed

+325
-7
lines changed

5 files changed

+325
-7
lines changed

Cargo.lock

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "zcash_script"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
authors = ["Tamas Blummer <[email protected]>", "Zcash Foundation <[email protected]>"]
55
license = "Apache-2.0"
66
readme = "README.md"
@@ -60,6 +60,7 @@ path = "src/lib.rs"
6060
external-secp = []
6161

6262
[dependencies]
63+
bitflags = "2.5.0"
6364

6465
[build-dependencies]
6566
# The `bindgen` dependency should automatically upgrade to match the version used by zebra-state's `rocksdb` dependency in:

src/interpreter.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
bitflags::bitflags! {
2+
/// The different SigHash types, as defined in <https://zips.z.cash/zip-0143>
3+
///
4+
/// TODO: There are three implementations of this (with three distinct primitive types):
5+
/// - u8 constants in librustzcash,
6+
/// - i32 (well, c_int) bitflags from the C++ constants here, and
7+
/// - u32 bitflags in zebra-chain.
8+
///
9+
/// Ideally we could unify on bitflags in librustzcash.
10+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11+
pub struct HashType: i32 {
12+
/// Sign all the outputs
13+
const All = 1;
14+
/// Sign none of the outputs - anyone can spend
15+
const None = 2;
16+
/// Sign one of the outputs - anyone can spend the rest
17+
const Single = 3;
18+
/// Anyone can add inputs to this transaction
19+
const AnyoneCanPay = 0x80;
20+
}
21+
}
22+
23+
bitflags::bitflags! {
24+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
25+
pub struct VerificationFlags: u32 {
26+
const None = 0;
27+
28+
/// Evaluate P2SH subscripts (softfork safe, BIP16).
29+
const P2SH = 1 << 0;
30+
31+
/// Passing a non-strict-DER signature or one with undefined hashtype to a checksig operation causes script failure.
32+
/// Evaluating a pubkey that is not (0x04 + 64 bytes) or (0x02 or 0x03 + 32 bytes) by checksig causes script failure.
33+
/// (softfork safe, but not used or intended as a consensus rule).
34+
const StrictEnc = 1 << 1;
35+
36+
/// Passing a non-strict-DER signature or one with S > order/2 to a checksig operation causes script failure
37+
/// (softfork safe, BIP62 rule 5).
38+
const LowS = 1 << 3;
39+
40+
/// verify dummy stack item consumed by CHECKMULTISIG is of zero-length (softfork safe, BIP62 rule 7).
41+
const NullDummy = 1 << 4;
42+
43+
/// Using a non-push operator in the scriptSig causes script failure (softfork safe, BIP62 rule 2).
44+
const SigPushOnly = 1 << 5;
45+
46+
/// Require minimal encodings for all push operations (OP_0... OP_16, OP_1NEGATE where possible, direct
47+
/// pushes up to 75 bytes, OP_PUSHDATA up to 255 bytes, OP_PUSHDATA2 for anything larger). Evaluating
48+
/// any other push causes the script to fail (BIP62 rule 3).
49+
/// In addition, whenever a stack element is interpreted as a number, it must be of minimal length (BIP62 rule 4).
50+
/// (softfork safe)
51+
const MinimalData = 1 << 6;
52+
53+
/// Discourage use of NOPs reserved for upgrades (NOP1-10)
54+
///
55+
/// Provided so that nodes can avoid accepting or mining transactions
56+
/// containing executed NOP's whose meaning may change after a soft-fork,
57+
/// thus rendering the script invalid; with this flag set executing
58+
/// discouraged NOPs fails the script. This verification flag will never be
59+
/// a mandatory flag applied to scripts in a block. NOPs that are not
60+
/// executed, e.g. within an unexecuted IF ENDIF block, are *not* rejected.
61+
const DiscourageUpgradableNOPs = 1 << 7;
62+
63+
/// Require that only a single stack element remains after evaluation. This changes the success criterion from
64+
/// "At least one stack element must remain, and when interpreted as a boolean, it must be true" to
65+
/// "Exactly one stack element must remain, and when interpreted as a boolean, it must be true".
66+
/// (softfork safe, BIP62 rule 6)
67+
/// Note: CLEANSTACK should never be used without P2SH.
68+
const CleanStack = 1 << 8;
69+
70+
/// Verify CHECKLOCKTIMEVERIFY
71+
///
72+
/// See BIP65 for details.
73+
const CHECKLOCKTIMEVERIFY = 1 << 9;
74+
}
75+
}

src/lib.rs

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,187 @@
11
#![doc(html_logo_url = "https://www.zfnd.org/images/zebra-icon.png")]
2-
#![doc(html_root_url = "https://docs.rs/zcash_script/0.2.0")]
2+
#![doc(html_root_url = "https://docs.rs/zcash_script/0.3.0")]
3+
#![allow(unsafe_code)]
34

45
mod cxx;
56
pub use cxx::*;
7+
8+
mod interpreter;
9+
pub use interpreter::{HashType, VerificationFlags};
10+
mod zcash_script;
11+
pub use zcash_script::*;
12+
13+
use std::os::raw::{c_int, c_uint, c_void};
14+
15+
pub enum Cxx {}
16+
17+
impl From<zcash_script_error_t> for Error {
18+
#[allow(non_upper_case_globals)]
19+
fn from(err_code: zcash_script_error_t) -> Error {
20+
match err_code {
21+
zcash_script_error_t_zcash_script_ERR_OK => Error::Ok,
22+
zcash_script_error_t_zcash_script_ERR_VERIFY_SCRIPT => Error::VerifyScript,
23+
unknown => Error::Unknown(unknown),
24+
}
25+
}
26+
}
27+
28+
/// The sighash callback to use with zcash_script.
29+
extern "C" fn sighash(
30+
sighash_out: *mut u8,
31+
sighash_out_len: c_uint,
32+
ctx: *const c_void,
33+
script_code: *const u8,
34+
script_code_len: c_uint,
35+
hash_type: c_int,
36+
) {
37+
// SAFETY: `ctx` is a valid SighashCallbackt because it is always passed to
38+
// `verify_callback` which simply forwards it to the callback.
39+
// `script_code` and `sighash_out` are valid buffers since they are always
40+
// specified when the callback is called.
41+
unsafe {
42+
let ctx = ctx as *const &SighashCallback;
43+
let script_code_vec = std::slice::from_raw_parts(script_code, script_code_len as usize);
44+
if let Some(sighash) = (*ctx)(script_code_vec, HashType::from_bits_retain(hash_type)) {
45+
// Sanity check; must always be true.
46+
assert_eq!(sighash_out_len, sighash.len() as c_uint);
47+
std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len());
48+
}
49+
}
50+
}
51+
52+
/// This steals a bit of the wrapper code from zebra_script, to provide the API that they want.
53+
impl ZcashScript for Cxx {
54+
fn verify_callback(
55+
sighash_callback: &SighashCallback,
56+
lock_time: i64,
57+
is_final: bool,
58+
script_pub_key: &[u8],
59+
signature_script: &[u8],
60+
flags: VerificationFlags,
61+
) -> Result<(), Error> {
62+
let mut err = 0;
63+
64+
let flags = flags.bits();
65+
66+
let is_final = if is_final { 1 } else { 0 };
67+
68+
// SAFETY: The `script` fields are created from a valid Rust `slice`.
69+
let ret = unsafe {
70+
zcash_script_verify_callback(
71+
(&sighash_callback as *const &SighashCallback) as *const c_void,
72+
Some(sighash),
73+
lock_time,
74+
is_final,
75+
script_pub_key.as_ptr(),
76+
script_pub_key.len() as u32,
77+
signature_script.as_ptr(),
78+
signature_script.len() as u32,
79+
flags,
80+
&mut err,
81+
)
82+
};
83+
84+
if ret == 1 {
85+
Ok(())
86+
} else {
87+
Err(Error::from(err))
88+
}
89+
}
90+
91+
/// Returns the number of transparent signature operations in the
92+
/// transparent inputs and outputs of this transaction.
93+
fn legacy_sigop_count_script(script: &[u8]) -> u32 {
94+
unsafe { zcash_script_legacy_sigop_count_script(script.as_ptr(), script.len() as u32) }
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
pub use super::*;
101+
use hex::FromHex;
102+
103+
lazy_static::lazy_static! {
104+
pub static ref SCRIPT_PUBKEY: Vec<u8> = <Vec<u8>>::from_hex("a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187").unwrap();
105+
pub static ref SCRIPT_SIG: Vec<u8> = <Vec<u8>>::from_hex("00483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453ae").expect("Block bytes are in valid hex representation");
106+
}
107+
108+
fn sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
109+
hex::decode("e8c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c")
110+
.unwrap()
111+
.as_slice()
112+
.first_chunk::<32>()
113+
.map(|hash| *hash)
114+
}
115+
116+
fn invalid_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
117+
hex::decode("08c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c")
118+
.unwrap()
119+
.as_slice()
120+
.first_chunk::<32>()
121+
.map(|hash| *hash)
122+
}
123+
124+
fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
125+
None
126+
}
127+
128+
#[test]
129+
fn it_works() {
130+
let n_lock_time: i64 = 2410374;
131+
let is_final: bool = true;
132+
let script_pub_key = &SCRIPT_PUBKEY;
133+
let script_sig = &SCRIPT_SIG;
134+
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
135+
136+
let ret = Cxx::verify_callback(
137+
&sighash,
138+
n_lock_time,
139+
is_final,
140+
script_pub_key,
141+
script_sig,
142+
flags,
143+
);
144+
145+
assert!(ret.is_ok());
146+
}
147+
148+
#[test]
149+
fn it_fails_on_invalid_sighash() {
150+
let n_lock_time: i64 = 2410374;
151+
let is_final: bool = true;
152+
let script_pub_key = &SCRIPT_PUBKEY;
153+
let script_sig = &SCRIPT_SIG;
154+
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
155+
156+
let ret = Cxx::verify_callback(
157+
&invalid_sighash,
158+
n_lock_time,
159+
is_final,
160+
script_pub_key,
161+
script_sig,
162+
flags,
163+
);
164+
165+
assert_eq!(ret, Err(Error::Ok));
166+
}
167+
168+
#[test]
169+
fn it_fails_on_missing_sighash() {
170+
let n_lock_time: i64 = 2410374;
171+
let is_final: bool = true;
172+
let script_pub_key = &SCRIPT_PUBKEY;
173+
let script_sig = &SCRIPT_SIG;
174+
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
175+
176+
let ret = Cxx::verify_callback(
177+
&missing_sighash,
178+
n_lock_time,
179+
is_final,
180+
script_pub_key,
181+
script_sig,
182+
flags,
183+
);
184+
185+
assert_eq!(ret, Err(Error::Ok));
186+
}
187+
}

src/zcash_script.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use super::interpreter::*;
2+
3+
/// This maps to `zcash_script_error_t`, but most of those cases aren’t used any more. This only
4+
/// replicates the still-used cases, and then an `Unknown` bucket for anything else that might
5+
/// happen.
6+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
7+
#[repr(u32)]
8+
pub enum Error {
9+
/// Any failure that results in the script being invalid.
10+
Ok = 0,
11+
/// An exception was caught.
12+
VerifyScript = 7,
13+
/// Some other failure value recovered from C++.
14+
Unknown(u32),
15+
}
16+
17+
/// A function which is called to obtain the sighash.
18+
/// - script_code: the scriptCode being validated. Note that this not always
19+
/// matches script_sig, i.e. for P2SH.
20+
/// - hash_type: the hash type being used.
21+
///
22+
/// The underlying C++ callback doesn’t give much opportunity for rich failure reporting, but
23+
/// returning `None` indicates _some_ failure to produce the desired hash.
24+
///
25+
/// TODO: Can we get the “32” from somewhere rather than hardcoding it?
26+
pub type SighashCallback = dyn Fn(&[u8], HashType) -> Option<[u8; 32]>;
27+
28+
/// The external API of zcash_script. This is defined to make it possible to compare the C++ and
29+
/// Rust implementations.
30+
pub trait ZcashScript {
31+
/// Returns `Ok(())` if the a transparent input correctly spends the matching output
32+
/// under the additional constraints specified by `flags`. This function
33+
/// receives only the required information to validate the spend and not
34+
/// the transaction itself. In particular, the sighash for the spend
35+
/// is obtained using a callback function.
36+
///
37+
/// - sighash_callback: a callback function which is called to obtain the sighash.
38+
/// - n_lock_time: the lock time of the transaction being validated.
39+
/// - is_final: a boolean indicating whether the input being validated is final
40+
/// (i.e. its sequence number is 0xFFFFFFFF).
41+
/// - script_pub_key: the scriptPubKey of the output being spent.
42+
/// - script_sig: the scriptSig of the input being validated.
43+
/// - flags: the script verification flags to use.
44+
/// - err: if not NULL, err will contain an error/success code for the operation.
45+
///
46+
/// Note that script verification failure is indicated by `Err(Error::Ok)`.
47+
fn verify_callback(
48+
sighash_callback: &SighashCallback,
49+
n_lock_time: i64,
50+
is_final: bool,
51+
script_pub_key: &[u8],
52+
script_sig: &[u8],
53+
flags: VerificationFlags,
54+
) -> Result<(), Error>;
55+
56+
/// Returns the number of transparent signature operations in the input or
57+
/// output script pointed to by script.
58+
fn legacy_sigop_count_script(script: &[u8]) -> u32;
59+
}

0 commit comments

Comments
 (0)