Add various stepwise functionality (#204)

* Move some constants in preparation

This makes some minor changes to constants to facilitate splitting out a
stepwise interpreter.

* Extract a step function from the interpreter

This is generally useful for testing, but specifically, we want to be
able to run this side-by-side with the C++ interpreter, and check that
every bit of our state matches along the way.

This change is as minimal as possible, to avoid divergence until after
it can be compared against C++. E.g., the massive `match opcode {…}`
block that has been moved should only change the dereferencing of
`op_count`.

* Add a `State` struct to make stepping easier

* Expose step interpreter

* Add a stepwise comparison interpreter

The C++ changes aren’t in place yet, so this is currently just an A/A test.

This changes our closures into structs containing a function, because
that’s how we can pass around functions with universally-quantified
lifetimes.

* Make interpreters more flexible

Previously the `ZcashScript` impls didn’t use `self`, so the types were
just tags. However, with the new `StepwiseInterpreter`, they need
`self`.

This also removes `RustInterpreter` in favor of a `rust_interpreter`
function that instantiates an appropriate `StepwiseInterpreter`.

* Add a function for C++/Rust comparison interpreter

* Fix fuzzer

* Clean up `use` in lib.rs

* Fix weird indentation

* Make various fields non-`pub`

* Add a `new` constructor for `Stack`

* Remove incorrect comment

* Appease Clippy

Adds `Default` impls for `Stack` and `State`.

* Rename `State::manual` to `State::from_parts`
This commit is contained in:
Greg Pfeil 2025-04-10 15:03:29 -06:00 committed by GitHub
parent 2afc474338
commit cc157ffdce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1494 additions and 935 deletions

View File

@ -3,6 +3,7 @@
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
extern crate zcash_script; extern crate zcash_script;
use zcash_script::interpreter::CallbackTransactionSignatureChecker;
use zcash_script::*; use zcash_script::*;
fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> { fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
@ -11,14 +12,25 @@ fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]
fuzz_target!(|tup: (u32, bool, &[u8], &[u8], u32)| { fuzz_target!(|tup: (u32, bool, &[u8], &[u8], u32)| {
// `fuzz_target!` doesnt support pattern matching in the parameter list. // `fuzz_target!` doesnt support pattern matching in the parameter list.
let (lock_time, is_final, pub_key, sig, flags) = tup; let (lock_time, is_final, pub_key, sig, flag_bits) = tup;
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>( let flags = testing::repair_flags(VerificationFlags::from_bits_truncate(flag_bits));
&missing_sighash, let ret = check_verify_callback(
lock_time, &CxxInterpreter {
is_final, sighash: &missing_sighash,
lock_time,
is_final,
},
&rust_interpreter(
flags,
CallbackTransactionSignatureChecker {
sighash: &missing_sighash,
lock_time: lock_time.into(),
is_final,
},
),
pub_key, pub_key,
sig, sig,
testing::repair_flags(VerificationFlags::from_bits_truncate(flags)), flags,
); );
assert_eq!( assert_eq!(
ret.0, ret.0,

File diff suppressed because it is too large Load Diff

View File

@ -7,9 +7,9 @@
#[macro_use] #[macro_use]
extern crate enum_primitive; extern crate enum_primitive;
mod cxx; pub mod cxx;
mod external; mod external;
mod interpreter; pub mod interpreter;
mod script; mod script;
pub mod script_error; pub mod script_error;
mod zcash_script; mod zcash_script;
@ -18,92 +18,76 @@ use std::os::raw::{c_int, c_uint, c_void};
use tracing::warn; use tracing::warn;
pub use cxx::*; pub use interpreter::{
pub use interpreter::{HashType, SighashCalculator, SignedOutputs, VerificationFlags}; CallbackTransactionSignatureChecker, DefaultStepEvaluator, HashType, SighashCalculator,
SignedOutputs, VerificationFlags,
};
use script_error::ScriptError;
pub use zcash_script::*; pub use zcash_script::*;
/// A tag to indicate that the C++ implementation of zcash_script should be used. pub struct CxxInterpreter<'a> {
pub enum CxxInterpreter {} pub sighash: SighashCalculator<'a>,
pub lock_time: u32,
pub is_final: bool,
}
impl From<cxx::ScriptError> for Error { impl From<cxx::ScriptError> for Error {
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
fn from(err_code: cxx::ScriptError) -> Self { fn from(err_code: cxx::ScriptError) -> Self {
match err_code { match err_code {
ScriptError_t_SCRIPT_ERR_OK => Error::Ok(script_error::ScriptError::Ok), cxx::ScriptError_t_SCRIPT_ERR_OK => Error::Ok(ScriptError::Ok),
ScriptError_t_SCRIPT_ERR_UNKNOWN_ERROR => { cxx::ScriptError_t_SCRIPT_ERR_UNKNOWN_ERROR => Error::Ok(ScriptError::UnknownError),
Error::Ok(script_error::ScriptError::UnknownError) cxx::ScriptError_t_SCRIPT_ERR_EVAL_FALSE => Error::Ok(ScriptError::EvalFalse),
} cxx::ScriptError_t_SCRIPT_ERR_OP_RETURN => Error::Ok(ScriptError::OpReturn),
ScriptError_t_SCRIPT_ERR_EVAL_FALSE => Error::Ok(script_error::ScriptError::EvalFalse),
ScriptError_t_SCRIPT_ERR_OP_RETURN => Error::Ok(script_error::ScriptError::OpReturn),
ScriptError_t_SCRIPT_ERR_SCRIPT_SIZE => { cxx::ScriptError_t_SCRIPT_ERR_SCRIPT_SIZE => Error::Ok(ScriptError::ScriptSize),
Error::Ok(script_error::ScriptError::ScriptSize) cxx::ScriptError_t_SCRIPT_ERR_PUSH_SIZE => Error::Ok(ScriptError::PushSize),
cxx::ScriptError_t_SCRIPT_ERR_OP_COUNT => Error::Ok(ScriptError::OpCount),
cxx::ScriptError_t_SCRIPT_ERR_STACK_SIZE => Error::Ok(ScriptError::StackSize),
cxx::ScriptError_t_SCRIPT_ERR_SIG_COUNT => Error::Ok(ScriptError::SigCount),
cxx::ScriptError_t_SCRIPT_ERR_PUBKEY_COUNT => Error::Ok(ScriptError::PubKeyCount),
cxx::ScriptError_t_SCRIPT_ERR_VERIFY => Error::Ok(ScriptError::Verify),
cxx::ScriptError_t_SCRIPT_ERR_EQUALVERIFY => Error::Ok(ScriptError::EqualVerify),
cxx::ScriptError_t_SCRIPT_ERR_CHECKMULTISIGVERIFY => {
Error::Ok(ScriptError::CheckMultisigVerify)
} }
ScriptError_t_SCRIPT_ERR_PUSH_SIZE => Error::Ok(script_error::ScriptError::PushSize), cxx::ScriptError_t_SCRIPT_ERR_CHECKSIGVERIFY => Error::Ok(ScriptError::CheckSigVerify),
ScriptError_t_SCRIPT_ERR_OP_COUNT => Error::Ok(script_error::ScriptError::OpCount), cxx::ScriptError_t_SCRIPT_ERR_NUMEQUALVERIFY => Error::Ok(ScriptError::NumEqualVerify),
ScriptError_t_SCRIPT_ERR_STACK_SIZE => Error::Ok(script_error::ScriptError::StackSize),
ScriptError_t_SCRIPT_ERR_SIG_COUNT => Error::Ok(script_error::ScriptError::SigCount), cxx::ScriptError_t_SCRIPT_ERR_BAD_OPCODE => Error::Ok(ScriptError::BadOpcode),
ScriptError_t_SCRIPT_ERR_PUBKEY_COUNT => { cxx::ScriptError_t_SCRIPT_ERR_DISABLED_OPCODE => Error::Ok(ScriptError::DisabledOpcode),
Error::Ok(script_error::ScriptError::PubKeyCount) cxx::ScriptError_t_SCRIPT_ERR_INVALID_STACK_OPERATION => {
Error::Ok(ScriptError::InvalidStackOperation)
}
cxx::ScriptError_t_SCRIPT_ERR_INVALID_ALTSTACK_OPERATION => {
Error::Ok(ScriptError::InvalidAltstackOperation)
}
cxx::ScriptError_t_SCRIPT_ERR_UNBALANCED_CONDITIONAL => {
Error::Ok(ScriptError::UnbalancedConditional)
} }
ScriptError_t_SCRIPT_ERR_VERIFY => Error::Ok(script_error::ScriptError::Verify), cxx::ScriptError_t_SCRIPT_ERR_NEGATIVE_LOCKTIME => {
ScriptError_t_SCRIPT_ERR_EQUALVERIFY => { Error::Ok(ScriptError::NegativeLockTime)
Error::Ok(script_error::ScriptError::EqualVerify)
} }
ScriptError_t_SCRIPT_ERR_CHECKMULTISIGVERIFY => { cxx::ScriptError_t_SCRIPT_ERR_UNSATISFIED_LOCKTIME => {
Error::Ok(script_error::ScriptError::CheckMultisigVerify) Error::Ok(ScriptError::UnsatisfiedLockTime)
}
ScriptError_t_SCRIPT_ERR_CHECKSIGVERIFY => {
Error::Ok(script_error::ScriptError::CheckSigVerify)
}
ScriptError_t_SCRIPT_ERR_NUMEQUALVERIFY => {
Error::Ok(script_error::ScriptError::NumEqualVerify)
} }
ScriptError_t_SCRIPT_ERR_BAD_OPCODE => Error::Ok(script_error::ScriptError::BadOpcode), cxx::ScriptError_t_SCRIPT_ERR_SIG_HASHTYPE => Error::Ok(ScriptError::SigHashType),
ScriptError_t_SCRIPT_ERR_DISABLED_OPCODE => { cxx::ScriptError_t_SCRIPT_ERR_SIG_DER => Error::Ok(ScriptError::SigDER),
Error::Ok(script_error::ScriptError::DisabledOpcode) cxx::ScriptError_t_SCRIPT_ERR_MINIMALDATA => Error::Ok(ScriptError::MinimalData),
} cxx::ScriptError_t_SCRIPT_ERR_SIG_PUSHONLY => Error::Ok(ScriptError::SigPushOnly),
ScriptError_t_SCRIPT_ERR_INVALID_STACK_OPERATION => { cxx::ScriptError_t_SCRIPT_ERR_SIG_HIGH_S => Error::Ok(ScriptError::SigHighS),
Error::Ok(script_error::ScriptError::InvalidStackOperation) cxx::ScriptError_t_SCRIPT_ERR_SIG_NULLDUMMY => Error::Ok(ScriptError::SigNullDummy),
} cxx::ScriptError_t_SCRIPT_ERR_PUBKEYTYPE => Error::Ok(ScriptError::PubKeyType),
ScriptError_t_SCRIPT_ERR_INVALID_ALTSTACK_OPERATION => { cxx::ScriptError_t_SCRIPT_ERR_CLEANSTACK => Error::Ok(ScriptError::CleanStack),
Error::Ok(script_error::ScriptError::InvalidAltstackOperation)
} cxx::ScriptError_t_SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS => {
ScriptError_t_SCRIPT_ERR_UNBALANCED_CONDITIONAL => { Error::Ok(ScriptError::DiscourageUpgradableNOPs)
Error::Ok(script_error::ScriptError::UnbalancedConditional)
} }
ScriptError_t_SCRIPT_ERR_NEGATIVE_LOCKTIME => { cxx::ScriptError_t_SCRIPT_ERR_VERIFY_SCRIPT => Error::VerifyScript,
Error::Ok(script_error::ScriptError::NegativeLockTime)
}
ScriptError_t_SCRIPT_ERR_UNSATISFIED_LOCKTIME => {
Error::Ok(script_error::ScriptError::UnsatisfiedLockTime)
}
ScriptError_t_SCRIPT_ERR_SIG_HASHTYPE => {
Error::Ok(script_error::ScriptError::SigHashType)
}
ScriptError_t_SCRIPT_ERR_SIG_DER => Error::Ok(script_error::ScriptError::SigDER),
ScriptError_t_SCRIPT_ERR_MINIMALDATA => {
Error::Ok(script_error::ScriptError::MinimalData)
}
ScriptError_t_SCRIPT_ERR_SIG_PUSHONLY => {
Error::Ok(script_error::ScriptError::SigPushOnly)
}
ScriptError_t_SCRIPT_ERR_SIG_HIGH_S => Error::Ok(script_error::ScriptError::SigHighS),
ScriptError_t_SCRIPT_ERR_SIG_NULLDUMMY => {
Error::Ok(script_error::ScriptError::SigNullDummy)
}
ScriptError_t_SCRIPT_ERR_PUBKEYTYPE => Error::Ok(script_error::ScriptError::PubKeyType),
ScriptError_t_SCRIPT_ERR_CLEANSTACK => Error::Ok(script_error::ScriptError::CleanStack),
ScriptError_t_SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS => {
Error::Ok(script_error::ScriptError::DiscourageUpgradableNOPs)
}
ScriptError_t_SCRIPT_ERR_VERIFY_SCRIPT => Error::VerifyScript,
unknown => Error::Unknown(unknown.into()), unknown => Error::Unknown(unknown.into()),
} }
} }
@ -143,11 +127,9 @@ extern "C" fn sighash_callback(
} }
/// This steals a bit of the wrapper code from zebra_script, to provide the API that they want. /// This steals a bit of the wrapper code from zebra_script, to provide the API that they want.
impl ZcashScript for CxxInterpreter { impl<'a> ZcashScript for CxxInterpreter<'a> {
fn verify_callback( fn verify_callback(
sighash: SighashCalculator, &self,
lock_time: u32,
is_final: bool,
script_pub_key: &[u8], script_pub_key: &[u8],
signature_script: &[u8], signature_script: &[u8],
flags: VerificationFlags, flags: VerificationFlags,
@ -156,11 +138,11 @@ impl ZcashScript for CxxInterpreter {
// SAFETY: The `script` fields are created from a valid Rust `slice`. // SAFETY: The `script` fields are created from a valid Rust `slice`.
let ret = unsafe { let ret = unsafe {
zcash_script_verify_callback( cxx::zcash_script_verify_callback(
(&sighash as *const SighashCalculator) as *const c_void, (&self.sighash as *const SighashCalculator) as *const c_void,
Some(sighash_callback), Some(sighash_callback),
lock_time.into(), self.lock_time.into(),
if is_final { 1 } else { 0 }, if self.is_final { 1 } else { 0 },
script_pub_key.as_ptr(), script_pub_key.as_ptr(),
script_pub_key script_pub_key
.len() .len()
@ -185,13 +167,13 @@ impl ZcashScript for CxxInterpreter {
/// Returns the number of transparent signature operations in the /// Returns the number of transparent signature operations in the
/// transparent inputs and outputs of this transaction. /// transparent inputs and outputs of this transaction.
fn legacy_sigop_count_script(script: &[u8]) -> Result<u32, Error> { fn legacy_sigop_count_script(&self, script: &[u8]) -> Result<u32, Error> {
script script
.len() .len()
.try_into() .try_into()
.map_err(Error::InvalidScriptSize) .map_err(Error::InvalidScriptSize)
.map(|script_len| unsafe { .map(|script_len| unsafe {
zcash_script_legacy_sigop_count_script(script.as_ptr(), script_len) cxx::zcash_script_legacy_sigop_count_script(script.as_ptr(), script_len)
}) })
} }
} }
@ -200,11 +182,13 @@ impl ZcashScript for CxxInterpreter {
/// both results. This is more useful for testing than the impl that logs a warning if the results /// both results. This is more useful for testing than the impl that logs a warning if the results
/// differ and always returns the C++ result. /// differ and always returns the C++ result.
fn check_legacy_sigop_count_script<T: ZcashScript, U: ZcashScript>( fn check_legacy_sigop_count_script<T: ZcashScript, U: ZcashScript>(
first: &T,
second: &U,
script: &[u8], script: &[u8],
) -> (Result<u32, Error>, Result<u32, Error>) { ) -> (Result<u32, Error>, Result<u32, Error>) {
( (
T::legacy_sigop_count_script(script), first.legacy_sigop_count_script(script),
U::legacy_sigop_count_script(script), second.legacy_sigop_count_script(script),
) )
} }
@ -212,30 +196,15 @@ fn check_legacy_sigop_count_script<T: ZcashScript, U: ZcashScript>(
/// both results. This is more useful for testing than the impl that logs a warning if the results /// both results. This is more useful for testing than the impl that logs a warning if the results
/// differ and always returns the `T` result. /// differ and always returns the `T` result.
pub fn check_verify_callback<T: ZcashScript, U: ZcashScript>( pub fn check_verify_callback<T: ZcashScript, U: ZcashScript>(
sighash: SighashCalculator, first: &T,
lock_time: u32, second: &U,
is_final: bool,
script_pub_key: &[u8], script_pub_key: &[u8],
script_sig: &[u8], script_sig: &[u8],
flags: VerificationFlags, flags: VerificationFlags,
) -> (Result<(), Error>, Result<(), Error>) { ) -> (Result<(), Error>, Result<(), Error>) {
( (
T::verify_callback( first.verify_callback(script_pub_key, script_sig, flags),
sighash, second.verify_callback(script_pub_key, script_sig, flags),
lock_time,
is_final,
script_pub_key,
script_sig,
flags,
),
U::verify_callback(
sighash,
lock_time,
is_final,
script_pub_key,
script_sig,
flags,
),
) )
} }
@ -243,8 +212,8 @@ pub fn check_verify_callback<T: ZcashScript, U: ZcashScript>(
pub fn normalize_error(err: Error) -> Error { pub fn normalize_error(err: Error) -> Error {
match err { match err {
Error::Ok(serr) => Error::Ok(match serr { Error::Ok(serr) => Error::Ok(match serr {
script_error::ScriptError::ReadError { .. } => script_error::ScriptError::BadOpcode, ScriptError::ReadError { .. } => ScriptError::BadOpcode,
script_error::ScriptError::ScriptNumError(_) => script_error::ScriptError::UnknownError, ScriptError::ScriptNumError(_) => ScriptError::UnknownError,
_ => serr, _ => serr,
}), }),
_ => err, _ => err,
@ -253,14 +222,42 @@ pub fn normalize_error(err: Error) -> Error {
/// A tag to indicate that both the C++ and Rust implementations of zcash_script should be used, /// A tag to indicate that both the C++ and Rust implementations of zcash_script should be used,
/// with their results compared. /// with their results compared.
pub enum CxxRustComparisonInterpreter {} pub struct ComparisonInterpreter<T, U> {
first: T,
second: U,
}
pub fn cxx_rust_comparison_interpreter<'a>(
sighash: &'a SighashCalculator<'a>,
lock_time: u32,
is_final: bool,
flags: VerificationFlags,
) -> ComparisonInterpreter<
CxxInterpreter<'a>,
StepwiseInterpreter<DefaultStepEvaluator<CallbackTransactionSignatureChecker<'a>>>,
> {
ComparisonInterpreter {
first: CxxInterpreter {
sighash,
lock_time,
is_final,
},
second: rust_interpreter(
flags,
CallbackTransactionSignatureChecker {
sighash,
lock_time: lock_time.into(),
is_final,
},
),
}
}
/// This implementation is functionally equivalent to the `T` impl, but it also runs a second (`U`) /// This implementation is functionally equivalent to the `T` impl, but it also runs a second (`U`)
/// impl and logs a warning if they disagree. /// impl and logs a warning if they disagree.
impl ZcashScript for CxxRustComparisonInterpreter { impl<T: ZcashScript, U: ZcashScript> ZcashScript for ComparisonInterpreter<T, U> {
fn legacy_sigop_count_script(script: &[u8]) -> Result<u32, Error> { fn legacy_sigop_count_script(&self, script: &[u8]) -> Result<u32, Error> {
let (cxx, rust) = let (cxx, rust) = check_legacy_sigop_count_script(&self.first, &self.second, script);
check_legacy_sigop_count_script::<CxxInterpreter, RustInterpreter>(script);
if rust != cxx { if rust != cxx {
warn!( warn!(
"The Rust Zcash Script interpreter had a different sigop count ({:?}) from the C++ one ({:?}).", "The Rust Zcash Script interpreter had a different sigop count ({:?}) from the C++ one ({:?}).",
@ -271,21 +268,13 @@ impl ZcashScript for CxxRustComparisonInterpreter {
} }
fn verify_callback( fn verify_callback(
sighash: SighashCalculator, &self,
lock_time: u32,
is_final: bool,
script_pub_key: &[u8], script_pub_key: &[u8],
script_sig: &[u8], script_sig: &[u8],
flags: VerificationFlags, flags: VerificationFlags,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (cxx, rust) = check_verify_callback::<CxxInterpreter, RustInterpreter>( let (cxx, rust) =
sighash, check_verify_callback(&self.first, &self.second, script_pub_key, script_sig, flags);
lock_time,
is_final,
script_pub_key,
script_sig,
flags,
);
if rust.map_err(normalize_error) != cxx { if rust.map_err(normalize_error) != cxx {
// probably want to distinguish between // probably want to distinguish between
// - one succeeding when the other fails (bad), and // - one succeeding when the other fails (bad), and
@ -302,6 +291,10 @@ impl ZcashScript for CxxRustComparisonInterpreter {
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use super::*; use super::*;
use crate::{
interpreter::{State, StepFn},
script::{Operation, Script},
};
/// Ensures that flags represent a supported state. This avoids crashes in the C++ code, which /// Ensures that flags represent a supported state. This avoids crashes in the C++ code, which
/// break various tests. /// break various tests.
@ -317,6 +310,33 @@ pub mod testing {
/// A `usize` one larger than the longest allowed script, for testing bounds. /// A `usize` one larger than the longest allowed script, for testing bounds.
pub const OVERFLOW_SCRIPT_SIZE: usize = script::MAX_SCRIPT_SIZE + 1; pub const OVERFLOW_SCRIPT_SIZE: usize = script::MAX_SCRIPT_SIZE + 1;
/// This is the same as `DefaultStepEvaluator`, except that it skips `OP_EQUAL`, allowing us to
/// test comparison failures.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct BrokenStepEvaluator<T>(pub T);
impl<T: StepFn> StepFn for BrokenStepEvaluator<T> {
type Payload = T::Payload;
fn call<'a>(
&self,
pc: &'a [u8],
script: &Script,
state: &mut State,
payload: &mut T::Payload,
) -> Result<&'a [u8], ScriptError> {
self.0.call(
if pc[0] == Operation::OP_EQUAL.into() {
&pc[1..]
} else {
pc
},
script,
state,
payload,
)
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -352,16 +372,26 @@ mod tests {
#[test] #[test]
fn it_works() { fn it_works() {
let n_lock_time: u32 = 2410374; let lock_time: u32 = 2410374;
let is_final: bool = true; let is_final: bool = true;
let script_pub_key = &SCRIPT_PUBKEY; let script_pub_key = &SCRIPT_PUBKEY;
let script_sig = &SCRIPT_SIG; let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>( let ret = check_verify_callback(
&sighash, &CxxInterpreter {
n_lock_time, sighash: &sighash,
is_final, lock_time,
is_final,
},
&rust_interpreter(
flags,
CallbackTransactionSignatureChecker {
sighash: &sighash,
lock_time: lock_time.into(),
is_final,
},
),
script_pub_key, script_pub_key,
script_sig, script_sig,
flags, flags,
@ -373,44 +403,63 @@ mod tests {
#[test] #[test]
fn it_fails_on_invalid_sighash() { fn it_fails_on_invalid_sighash() {
let n_lock_time: u32 = 2410374; let lock_time: u32 = 2410374;
let is_final: bool = true; let is_final: bool = true;
let script_pub_key = &SCRIPT_PUBKEY; let script_pub_key = &SCRIPT_PUBKEY;
let script_sig = &SCRIPT_SIG; let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let ret = check_verify_callback(
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>( &CxxInterpreter {
&invalid_sighash, sighash: &invalid_sighash,
n_lock_time, lock_time,
is_final, is_final,
},
&rust_interpreter(
flags,
CallbackTransactionSignatureChecker {
sighash: &invalid_sighash,
lock_time: lock_time.into(),
is_final,
},
),
script_pub_key, script_pub_key,
script_sig, script_sig,
flags, flags,
); );
assert_eq!(ret.0, ret.1.map_err(normalize_error)); assert_eq!(ret.0, ret.1.map_err(normalize_error));
assert_eq!(ret.0, Err(Error::Ok(script_error::ScriptError::EvalFalse))); assert_eq!(ret.0, Err(Error::Ok(ScriptError::EvalFalse)));
} }
#[test] #[test]
fn it_fails_on_missing_sighash() { fn it_fails_on_missing_sighash() {
let n_lock_time: u32 = 2410374; let lock_time: u32 = 2410374;
let is_final: bool = true; let is_final: bool = true;
let script_pub_key = &SCRIPT_PUBKEY; let script_pub_key = &SCRIPT_PUBKEY;
let script_sig = &SCRIPT_SIG; let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>( let ret = check_verify_callback(
&missing_sighash, &CxxInterpreter {
n_lock_time, sighash: &missing_sighash,
is_final, lock_time,
is_final,
},
&rust_interpreter(
flags,
CallbackTransactionSignatureChecker {
sighash: &missing_sighash,
lock_time: lock_time.into(),
is_final,
},
),
script_pub_key, script_pub_key,
script_sig, script_sig,
flags, flags,
); );
assert_eq!(ret.0, ret.1.map_err(normalize_error)); assert_eq!(ret.0, ret.1.map_err(normalize_error));
assert_eq!(ret.0, Err(Error::Ok(script_error::ScriptError::EvalFalse))); assert_eq!(ret.0, Err(Error::Ok(ScriptError::EvalFalse)));
} }
proptest! { proptest! {
@ -424,15 +473,26 @@ mod tests {
is_final in prop::bool::ANY, is_final in prop::bool::ANY,
pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE), pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE),
sig in prop::collection::vec(0..=0xffu8, 1..=OVERFLOW_SCRIPT_SIZE), sig in prop::collection::vec(0..=0xffu8, 1..=OVERFLOW_SCRIPT_SIZE),
flags in prop::bits::u32::masked(VerificationFlags::all().bits()), flag_bits in prop::bits::u32::masked(VerificationFlags::all().bits()),
) { ) {
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>( let flags = repair_flags(VerificationFlags::from_bits_truncate(flag_bits));
&missing_sighash, let ret = check_verify_callback(
&CxxInterpreter {
sighash: &sighash,
lock_time, lock_time,
is_final, is_final,
},
&rust_interpreter(
flags,
CallbackTransactionSignatureChecker {
sighash: &sighash,
lock_time: lock_time.into(),
is_final,
},
),
&pub_key[..], &pub_key[..],
&sig[..], &sig[..],
repair_flags(VerificationFlags::from_bits_truncate(flags)), flags,
); );
prop_assert_eq!(ret.0, ret.1.map_err(normalize_error), prop_assert_eq!(ret.0, ret.1.map_err(normalize_error),
"original Rust result: {:?}", ret.1); "original Rust result: {:?}", ret.1);
@ -445,18 +505,29 @@ mod tests {
is_final in prop::bool::ANY, is_final in prop::bool::ANY,
pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE), pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE),
sig in prop::collection::vec(0..=0x60u8, 0..=OVERFLOW_SCRIPT_SIZE), sig in prop::collection::vec(0..=0x60u8, 0..=OVERFLOW_SCRIPT_SIZE),
flags in prop::bits::u32::masked( flag_bits in prop::bits::u32::masked(
// Dont waste test cases on whether or not `SigPushOnly` is set. // Dont waste test cases on whether or not `SigPushOnly` is set.
(VerificationFlags::all() - VerificationFlags::SigPushOnly).bits()), (VerificationFlags::all() - VerificationFlags::SigPushOnly).bits()),
) { ) {
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>( let flags = repair_flags(VerificationFlags::from_bits_truncate(flag_bits))
&missing_sighash, | VerificationFlags::SigPushOnly;
let ret = check_verify_callback(
&CxxInterpreter {
sighash: &sighash,
lock_time, lock_time,
is_final, is_final,
},
&rust_interpreter(
flags,
CallbackTransactionSignatureChecker {
sighash: &sighash,
lock_time: lock_time.into(),
is_final,
},
),
&pub_key[..], &pub_key[..],
&sig[..], &sig[..],
repair_flags(VerificationFlags::from_bits_truncate(flags)) flags,
| VerificationFlags::SigPushOnly,
); );
prop_assert_eq!(ret.0, ret.1.map_err(normalize_error), prop_assert_eq!(ret.0, ret.1.map_err(normalize_error),
"original Rust result: {:?}", ret.1); "original Rust result: {:?}", ret.1);

View File

@ -269,6 +269,9 @@ impl From<Operation> for u8 {
pub struct ScriptNum(i64); pub struct ScriptNum(i64);
impl ScriptNum { impl ScriptNum {
pub const ZERO: ScriptNum = ScriptNum(0);
pub const ONE: ScriptNum = ScriptNum(1);
const DEFAULT_MAX_NUM_SIZE: usize = 4; const DEFAULT_MAX_NUM_SIZE: usize = 4;
pub fn new( pub fn new(

View File

@ -41,9 +41,7 @@ pub trait ZcashScript {
/// ///
/// Note that script verification failure is indicated by `Err(Error::Ok)`. /// Note that script verification failure is indicated by `Err(Error::Ok)`.
fn verify_callback( fn verify_callback(
sighash_callback: SighashCalculator, &self,
lock_time: u32,
is_final: bool,
script_pub_key: &[u8], script_pub_key: &[u8],
script_sig: &[u8], script_sig: &[u8],
flags: VerificationFlags, flags: VerificationFlags,
@ -51,39 +49,403 @@ pub trait ZcashScript {
/// Returns the number of transparent signature operations in the input or /// Returns the number of transparent signature operations in the input or
/// output script pointed to by script. /// output script pointed to by script.
fn legacy_sigop_count_script(script: &[u8]) -> Result<u32, Error>; fn legacy_sigop_count_script(&self, script: &[u8]) -> Result<u32, Error>;
} }
/// A tag to indicate that the Rust implementation of zcash_script should be used. pub fn stepwise_verify<F>(
pub enum RustInterpreter {} script_pub_key: &[u8],
script_sig: &[u8],
flags: VerificationFlags,
payload: &mut F::Payload,
stepper: &F,
) -> Result<(), Error>
where
F: StepFn,
{
verify_script(
&Script(script_sig),
&Script(script_pub_key),
flags,
payload,
stepper,
)
.map_err(Error::Ok)
}
impl ZcashScript for RustInterpreter { /// A payload for comparing the results of two steppers.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StepResults<T, U> {
/// This contains the step-wise states of the steppers as long as they were identical. Its
/// `head` contains the initial state and its `tail` has a 1:1 correspondence to the opcodes
/// (not to the bytes).
pub identical_states: Vec<State>,
/// If the execution matched the entire way, then this contains `None`. If there was a
/// divergence, then this contains `Some` with a pair of `Result`s one representing each
/// steppers outcome at the point at which they diverged.
pub diverging_result: Option<(Result<State, ScriptError>, Result<State, ScriptError>)>,
/// The final payload of the first stepper.
pub payload_l: T,
/// The final payload of the second stepper.
pub payload_r: U,
}
impl<T, U> StepResults<T, U> {
pub fn initial(payload_l: T, payload_r: U) -> Self {
StepResults {
identical_states: vec![],
diverging_result: None,
payload_l,
payload_r,
}
}
}
/// This compares two `ZcashScript` implementations in a deep way checking the entire `State` step
/// by step. Note that this has some tradeoffs: one is performance. Another is that it doesnt run
/// the entire codepath of either implementation. The setup/wrapup code is specific to this
/// definition, but any differences there should be caught very easily by other testing mechanisms
/// (like `check_verify_callback`).
///
/// This returns a very debuggable result. See `StepResults` for details.
pub struct ComparisonStepEvaluator<'a, T, U> {
pub eval_step_l: &'a dyn StepFn<Payload = T>,
pub eval_step_r: &'a dyn StepFn<Payload = U>,
}
impl<'a, T: Clone, U: Clone> StepFn for ComparisonStepEvaluator<'a, T, U> {
type Payload = StepResults<T, U>;
fn call<'b>(
&self,
pc: &'b [u8],
script: &Script,
state: &mut State,
payload: &mut StepResults<T, U>,
) -> Result<&'b [u8], ScriptError> {
let mut right_state = (*state).clone();
let left = self
.eval_step_l
.call(pc, script, state, &mut payload.payload_l);
let right = self
.eval_step_r
.call(pc, script, &mut right_state, &mut payload.payload_r);
match (left, right) {
(Ok(_), Ok(_)) => {
if *state == right_state {
payload.identical_states.push(state.clone());
left
} else {
// In this case, the script hasnt failed, but we stop running
// anything
payload.diverging_result = Some((
left.map(|_| state.clone()),
right.map(|_| right_state.clone()),
));
Err(ScriptError::UnknownError)
}
}
// at least one is `Err`
(_, _) => {
if left != right {
payload.diverging_result = Some((
left.map(|_| state.clone()),
right.map(|_| right_state.clone()),
));
}
left.and(right)
}
}
}
}
pub struct StepwiseInterpreter<F>
where
F: StepFn,
{
initial_payload: F::Payload,
stepper: F,
}
impl<F: StepFn> StepwiseInterpreter<F> {
pub fn new(initial_payload: F::Payload, stepper: F) -> Self {
StepwiseInterpreter {
initial_payload,
stepper,
}
}
}
pub fn rust_interpreter<C: SignatureChecker + Copy>(
flags: VerificationFlags,
checker: C,
) -> StepwiseInterpreter<DefaultStepEvaluator<C>> {
StepwiseInterpreter {
initial_payload: (),
stepper: DefaultStepEvaluator { flags, checker },
}
}
impl<F: StepFn> ZcashScript for StepwiseInterpreter<F> {
/// Returns the number of transparent signature operations in the /// Returns the number of transparent signature operations in the
/// transparent inputs and outputs of this transaction. /// transparent inputs and outputs of this transaction.
fn legacy_sigop_count_script(script: &[u8]) -> Result<u32, Error> { fn legacy_sigop_count_script(&self, script: &[u8]) -> Result<u32, Error> {
let cscript = Script(script); let cscript = Script(script);
Ok(cscript.get_sig_op_count(false)) Ok(cscript.get_sig_op_count(false))
} }
fn verify_callback( fn verify_callback(
sighash: SighashCalculator, &self,
lock_time: u32,
is_final: bool,
script_pub_key: &[u8], script_pub_key: &[u8],
script_sig: &[u8], script_sig: &[u8],
flags: VerificationFlags, flags: VerificationFlags,
) -> Result<(), Error> { ) -> Result<(), Error> {
let lock_time_num = lock_time.into(); let mut payload = self.initial_payload.clone();
verify_script( stepwise_verify(
&Script(script_sig), script_pub_key,
&Script(script_pub_key), script_sig,
flags, flags,
&CallbackTransactionSignatureChecker { &mut payload,
sighash, &self.stepper,
lock_time: &lock_time_num,
is_final,
},
) )
.map_err(Error::Ok) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::*;
use hex::FromHex;
use proptest::prelude::*;
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>()
.copied()
}
fn invalid_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
hex::decode("08c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c")
.unwrap()
.as_slice()
.first_chunk::<32>()
.copied()
}
fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
None
}
#[test]
fn it_works() {
let n_lock_time: u32 = 2410374;
let is_final: bool = true;
let script_pub_key = &SCRIPT_PUBKEY;
let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let checker = CallbackTransactionSignatureChecker {
sighash: &sighash,
lock_time: n_lock_time.into(),
is_final,
};
let rust_stepper = DefaultStepEvaluator { flags, checker };
let stepper = ComparisonStepEvaluator {
eval_step_l: &rust_stepper,
eval_step_r: &rust_stepper,
};
let mut res = StepResults::initial((), ());
let ret = stepwise_verify(script_pub_key, script_sig, flags, &mut res, &stepper);
if res.diverging_result != None {
panic!("invalid result: {:?}", res);
}
assert_eq!(ret, Ok(()));
}
#[test]
fn broken_stepper_causes_divergence() {
let n_lock_time: u32 = 2410374;
let is_final: bool = true;
let script_pub_key = &SCRIPT_PUBKEY;
let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let checker = CallbackTransactionSignatureChecker {
sighash: &sighash,
lock_time: n_lock_time.into(),
is_final,
};
let rust_stepper = DefaultStepEvaluator { flags, checker };
let broken_stepper = BrokenStepEvaluator(rust_stepper);
let stepper = ComparisonStepEvaluator {
eval_step_l: &rust_stepper,
eval_step_r: &broken_stepper,
};
let mut res = StepResults::initial((), ());
let ret = stepwise_verify(script_pub_key, script_sig, flags, &mut res, &stepper);
// The final return value is from whichever stepper failed.
assert_eq!(
ret,
Err(Error::Ok(ScriptError::ReadError {
expected_bytes: 1,
available_bytes: 0,
}))
);
// `State`s are large, so we just check that there was some progress in lock step, and a
// divergence.
match res {
StepResults {
identical_states,
diverging_result:
Some((
Ok(state),
Err(ScriptError::ReadError {
expected_bytes: 1,
available_bytes: 0,
}),
)),
payload_l: (),
payload_r: (),
} => {
assert!(
identical_states.len() == 6
&& state.stack().size() == 4
&& state.altstack().empty()
&& state.op_count() == 2
&& state.vexec().empty()
);
}
_ => {
panic!("invalid result: {:?}", res);
}
}
}
#[test]
fn it_fails_on_invalid_sighash() {
let n_lock_time: u32 = 2410374;
let is_final: bool = true;
let script_pub_key = &SCRIPT_PUBKEY;
let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let checker = CallbackTransactionSignatureChecker {
sighash: &invalid_sighash,
lock_time: n_lock_time.into(),
is_final,
};
let rust_stepper = DefaultStepEvaluator { flags, checker };
let stepper = ComparisonStepEvaluator {
eval_step_l: &rust_stepper,
eval_step_r: &rust_stepper,
};
let mut res = StepResults::initial((), ());
let ret = stepwise_verify(script_pub_key, script_sig, flags, &mut res, &stepper);
if res.diverging_result != None {
panic!("mismatched result: {:?}", res);
}
assert_eq!(ret, Err(Error::Ok(ScriptError::EvalFalse)));
}
#[test]
fn it_fails_on_missing_sighash() {
let n_lock_time: u32 = 2410374;
let is_final: bool = true;
let script_pub_key = &SCRIPT_PUBKEY;
let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let checker = CallbackTransactionSignatureChecker {
sighash: &missing_sighash,
lock_time: n_lock_time.into(),
is_final,
};
let rust_stepper = DefaultStepEvaluator { flags, checker };
let stepper = ComparisonStepEvaluator {
eval_step_l: &rust_stepper,
eval_step_r: &rust_stepper,
};
let mut res = StepResults::initial((), ());
let ret = stepwise_verify(script_pub_key, script_sig, flags, &mut res, &stepper);
if res.diverging_result != None {
panic!("mismatched result: {:?}", res);
}
assert_eq!(ret, Err(Error::Ok(ScriptError::EvalFalse)));
}
proptest! {
// The stepwise comparison tests are significantly slower than the simple comparison tests,
// so run fewer iterations.
#![proptest_config(ProptestConfig {
cases: 2_000, .. ProptestConfig::default()
})]
#[test]
fn test_arbitrary_scripts(
lock_time in prop::num::u32::ANY,
is_final in prop::bool::ANY,
pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE),
sig in prop::collection::vec(0..=0xffu8, 1..=OVERFLOW_SCRIPT_SIZE),
flags in prop::bits::u32::masked(VerificationFlags::all().bits()),
) {
let checker = CallbackTransactionSignatureChecker {
sighash: &missing_sighash,
lock_time: lock_time.into(),
is_final,
};
let flags = repair_flags(VerificationFlags::from_bits_truncate(flags));
let rust_stepper = DefaultStepEvaluator { flags, checker };
let stepper = ComparisonStepEvaluator {
eval_step_l: &rust_stepper,
eval_step_r: &rust_stepper,
};
let mut res = StepResults::initial((), ());
let _ = stepwise_verify(&pub_key[..], &sig[..], flags, &mut res, &stepper);
if res.diverging_result != None {
panic!("mismatched result: {:?}", res);
}
}
/// Similar to `test_arbitrary_scripts`, but ensures the `sig` only contains pushes.
#[test]
fn test_restricted_sig_scripts(
lock_time in prop::num::u32::ANY,
is_final in prop::bool::ANY,
pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE),
sig in prop::collection::vec(0..=0x60u8, 0..=OVERFLOW_SCRIPT_SIZE),
flags in prop::bits::u32::masked(
// Dont waste test cases on whether or not `SigPushOnly` is set.
(VerificationFlags::all() - VerificationFlags::SigPushOnly).bits()),
) {
let checker = CallbackTransactionSignatureChecker {
sighash: &missing_sighash,
lock_time: lock_time.into(),
is_final,
};
let flags = repair_flags(VerificationFlags::from_bits_truncate(flags)) | VerificationFlags::SigPushOnly;
let rust_stepper = DefaultStepEvaluator { flags, checker };
let stepper = ComparisonStepEvaluator {
eval_step_l: &rust_stepper,
eval_step_r: &rust_stepper,
};
let mut res = StepResults::initial((), ());
let _ = stepwise_verify(&pub_key[..], &sig[..], flags, &mut res, &stepper);
if res.diverging_result != None {
panic!("mismatched result: {:?}", res);
}
}
} }
} }