diff --git a/Cargo.toml b/Cargo.toml index fa3038a..1d4eb18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bitcoin-splitter", "integration-tests"] +members = ["bitcoin-splitter", "integration-tests", "bitcoin-scriptexec"] resolver = "2" [workspace.dependencies] diff --git a/bitcoin-scriptexec/.editorconfig b/bitcoin-scriptexec/.editorconfig new file mode 100644 index 0000000..4b555f4 --- /dev/null +++ b/bitcoin-scriptexec/.editorconfig @@ -0,0 +1,4 @@ +# see https://editorconfig.org for more options, and setup instructions for yours editor + +[*.rs] +indent_style = tab diff --git a/bitcoin-scriptexec/.gitignore b/bitcoin-scriptexec/.gitignore new file mode 100644 index 0000000..f2f9e58 --- /dev/null +++ b/bitcoin-scriptexec/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock \ No newline at end of file diff --git a/bitcoin-scriptexec/Cargo.toml b/bitcoin-scriptexec/Cargo.toml new file mode 100644 index 0000000..145d5a2 --- /dev/null +++ b/bitcoin-scriptexec/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "bitcoin-scriptexec" +version = "0.0.0" +edition = "2021" +description = "Bitcoin Script interpreter/executor" +authors = ["Steven Roose "] +license = "CC0-1.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "btcexec" +path = "src/main.rs" +required-features = ["cli"] + +[features] +default = ["cli", "wasm"] +json = ["serde", "serde_json", "bitcoin/serde"] +cli = ["json", "clap"] +wasm = ["json", "wasm-bindgen", "serde-wasm-bindgen", "console_error_panic_hook", "getrandom/js"] + +[dependencies] +# bitcoin = "0.31.0" +bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin", branch = "bitvm" } +lazy_static = "1.4.0" + +# cli +clap = { version = "4", features = [ "derive" ], optional = true } + +# wasm +serde = { version = "1.0", features = [ "derive" ], optional = true } +serde_json = { version = "1.0", optional = true } +wasm-bindgen = { version = "0.2.87", optional = true } +serde-wasm-bindgen = { version = "0.6.1", optional = true } +console_error_panic_hook = { version = "0.1.7", optional = true } +# I think we need to mention this for secp256k1-sys to work +getrandom = { version = "0.2", optional = true } + +[patch.crates-io.base58check] +git = "https://github.com/rust-bitcoin/rust-bitcoin" +branch = "bitvm" + +[patch.crates-io.bitcoin] +git = "https://github.com/rust-bitcoin/rust-bitcoin" +branch = "bitvm" + +[patch.crates-io.bitcoin_hashes] +git = "https://github.com/rust-bitcoin/rust-bitcoin" +branch = "bitvm" + +[patch.crates-io.bitcoin-internals] +git = "https://github.com/rust-bitcoin/rust-bitcoin" +branch = "bitvm" + +[patch.crates-io.bitcoin-io] +git = "https://github.com/rust-bitcoin/rust-bitcoin" +branch = "bitvm" + +[patch.crates-io.bitcoin-units] +git = "https://github.com/rust-bitcoin/rust-bitcoin" +branch = "bitvm" diff --git a/bitcoin-scriptexec/LICENSE b/bitcoin-scriptexec/LICENSE new file mode 100644 index 0000000..6ca207e --- /dev/null +++ b/bitcoin-scriptexec/LICENSE @@ -0,0 +1,122 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + diff --git a/bitcoin-scriptexec/README.md b/bitcoin-scriptexec/README.md new file mode 100644 index 0000000..56dbe7d --- /dev/null +++ b/bitcoin-scriptexec/README.md @@ -0,0 +1,52 @@ + +bitcoin-scriptexec +================== + +A work-in-progress Bitcoin Script execution utility. + +**DISCLAIMER: DO NOT EVER, EVER, TRY TO USE THIS CRATE FOR CONSENSUS PURPOSES !!** + + +# Status + +This project is a work-in-progress mostly attempting to facilitate BitVM development. +It does not yet fully implement all opcodes, but as a library already gives you pretty +good insight into the internals of the execution in a step-wise manner. + + +# Usage + +## CLI + +You can simply use `cargo run` or build/intall the binary as follows: + +``` +# to build in debug mode +$ cargo build --locked +# to build in release (optimized) mode +$ cargo build --locked --release +# to install in ~/.cargo/bin +$ cargo install --locked --path . +``` + +### Usage + +The CLI currently takes only a single argument: the path to the ASM script file: + +``` +# using the binary +$ btcexec +# using cargo run +$ cargo run -- +``` + +## WASM + +There are wasm bindings provided. For API documentation, see the `src/wasm.rs`a file. + +To build the WASM bindings, [install wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) +and then run the following script: + +``` +./build-wasm.sh +``` diff --git a/bitcoin-scriptexec/build-wasm.sh b/bitcoin-scriptexec/build-wasm.sh new file mode 100755 index 0000000..f7bc520 --- /dev/null +++ b/bitcoin-scriptexec/build-wasm.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +wasm-pack build --target web -- --features wasm diff --git a/bitcoin-scriptexec/src/data_structures.rs b/bitcoin-scriptexec/src/data_structures.rs new file mode 100644 index 0000000..5a95807 --- /dev/null +++ b/bitcoin-scriptexec/src/data_structures.rs @@ -0,0 +1,185 @@ +use crate::{read_scriptint, ExecError}; +use alloc::rc::Rc; +use bitcoin::script; +use core::cell::RefCell; +use core::cmp::PartialEq; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum StackEntry { + Num(i64), + StrRef(Rc>>), +} + +impl StackEntry { + // This assumes the StackEntry fit in a u32 and will pad it with leading zeros to 4 bytes. + pub fn serialize_to_bytes(self) -> Vec { + match self { + StackEntry::Num(num) => { + assert!( + num <= u32::MAX.into(), + "There should not be entries with more than 32 bits on the stack at this point" + ); + num.to_le_bytes().to_vec() + } + StackEntry::StrRef(v) => { + let mut v = v.borrow().to_vec(); + assert!( + v.len() <= 4, + "There should not be entries with more than 32 bits on the stack at this point" + ); + while v.len() < 4 { + v.push(0) + } + v + } + } + } +} + +#[derive(Clone, Eq, Debug, PartialEq)] +pub struct Stack(Vec); + +impl Stack { + pub fn new() -> Self { + Self(Vec::with_capacity(1000)) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn last(&self) -> Result, ExecError> { + self.topstr(-1) + } + + pub fn from_u8_vec(v: Vec>) -> Self { + let mut res = Self::new(); + for entry in v { + res.0.push(StackEntry::StrRef(Rc::new(RefCell::new(entry)))); + } + res + } + + pub fn top(&self, offset: isize) -> Result<&StackEntry, ExecError> { + debug_assert!(offset < 0, "offsets should be < 0"); + self.0 + .len() + .checked_sub(offset.unsigned_abs()) + .map(|i| &self.0[i]) + .ok_or(ExecError::InvalidStackOperation) + } + + pub fn topstr(&self, offset: isize) -> Result, ExecError> { + let entry = self.top(offset)?; + match entry { + StackEntry::Num(v) => Ok(script::scriptint_vec(*v)), + StackEntry::StrRef(v) => Ok(v.borrow().to_vec()), + } + } + + pub fn topnum(&self, offset: isize, require_minimal: bool) -> Result { + let entry = self.top(offset)?; + match entry { + StackEntry::Num(v) => { + if *v <= i32::MAX as i64 { + Ok(*v) + } else { + Err(ExecError::ScriptIntNumericOverflow) + } + } + StackEntry::StrRef(v) => Ok(read_scriptint(v.borrow().as_slice(), 4, require_minimal)?), + } + } + + pub fn pushnum(&mut self, num: i64) { + self.0.push(StackEntry::Num(num)); + } + + pub fn pushstr(&mut self, v: &[u8]) { + self.0 + .push(StackEntry::StrRef(Rc::new(RefCell::new(v.to_vec())))); + } + + pub fn push(&mut self, v: StackEntry) { + self.0.push(v); + } + + pub fn needn(&self, min_nb_items: usize) -> Result<(), ExecError> { + if self.len() < min_nb_items { + Err(ExecError::InvalidStackOperation) + } else { + Ok(()) + } + } + + pub fn popn(&mut self, n: usize) -> Result<(), ExecError> { + for _ in 0..n { + self.0.pop().ok_or(ExecError::InvalidStackOperation)?; + } + Ok(()) + } + + pub fn pop(&mut self) -> Option { + self.0.pop() + } + + pub fn popstr(&mut self) -> Result, ExecError> { + let entry = self.0.pop().ok_or(ExecError::InvalidStackOperation)?; + match entry { + StackEntry::Num(v) => Ok(script::scriptint_vec(v)), + StackEntry::StrRef(v) => Ok(v.borrow().to_vec()), + } + } + + pub fn popnum(&mut self, require_minimal: bool) -> Result { + let entry = self.0.pop().ok_or(ExecError::InvalidStackOperation)?; + match entry { + StackEntry::Num(v) => { + if v <= i32::MAX as i64 { + Ok(v) + } else { + Err(ExecError::ScriptIntNumericOverflow) + } + } + StackEntry::StrRef(v) => Ok(read_scriptint(v.borrow().as_slice(), 4, require_minimal)?), + } + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn remove(&mut self, v: usize) { + self.0.remove(v); + } + + pub fn iter_str(&self) -> impl DoubleEndedIterator> + '_ { + self.0.iter().map(|v| match v { + StackEntry::Num(v) => script::scriptint_vec(*v), + StackEntry::StrRef(v) => v.borrow().to_vec(), + }) + } + + pub fn get(&self, index: usize) -> Vec { + match &self.0[index] { + StackEntry::Num(v) => script::scriptint_vec(*v), + StackEntry::StrRef(v) => v.borrow().to_vec(), + } + } + + // Will serialize the stack into a series of bytes such that every 4 bytes correspond to a u32 + // (or smaller) stack entry (smaller entries are padded with 0). + pub fn serialize_to_bytes(self) -> Vec { + let mut bytes = vec![]; + for entry in self.0 { + bytes.extend(entry.serialize_to_bytes()); + } + bytes + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} diff --git a/bitcoin-scriptexec/src/error.rs b/bitcoin-scriptexec/src/error.rs new file mode 100644 index 0000000..bc40453 --- /dev/null +++ b/bitcoin-scriptexec/src/error.rs @@ -0,0 +1,44 @@ +use bitcoin::blockdata::script; + +/// Error of a script execution. +/// +/// Equivalent to Bitcoin Core's `ScriptError_t`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExecError { + DisabledOpcode, + OpCodeseparator, + BadOpcode, + OpCount, + PushSize, + MinimalData, + InvalidStackOperation, + NegativeLocktime, + UnsatisfiedLocktime, + UnbalancedConditional, + TapscriptMinimalIf, + Verify, + OpReturn, + EqualVerify, + NumEqualVerify, + CheckSigVerify, + TapscriptValidationWeight, + PubkeyType, + SchnorrSigSize, + SchnorrSigHashtype, + SchnorrSig, + TapscriptCheckMultiSig, + PubkeyCount, + StackSize, + WitnessPubkeyType, + + // new ones for us + ScriptIntNumericOverflow, + Debug, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + Exec(ExecError), + InvalidScript(script::Error), + Other(&'static str), +} diff --git a/bitcoin-scriptexec/src/json.rs b/bitcoin-scriptexec/src/json.rs new file mode 100644 index 0000000..2ffb33d --- /dev/null +++ b/bitcoin-scriptexec/src/json.rs @@ -0,0 +1,88 @@ +use std::fmt; + +use bitcoin::hex::DisplayHex; +use bitcoin::{Opcode, Script}; +use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer}; + +use crate::ExecStats; + +/// Simple utility wrapper to serde-serialize using [fmt::Display]. +struct FmtSer<'a, T: fmt::Display>(&'a T); +impl<'a, T: fmt::Display> Serialize for FmtSer<'a, T> { + fn serialize(&self, s: S) -> Result { + s.collect_str(&self.0) + } +} + +/// Wrapper to fmt::Display a Script as ASM. +struct ScriptAsm<'a>(&'a Script); +impl<'a> fmt::Display for ScriptAsm<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt_asm(f) + } +} + +/// Wrapper to serialize a stack as hex elements. +struct StackSer<'a>(&'a [Vec]); +impl<'a> Serialize for StackSer<'a> { + fn serialize(&self, s: S) -> Result { + let mut seq = s.serialize_seq(Some(self.0.len()))?; + for i in self.0.iter() { + seq.serialize_element(&FmtSer(&i.as_hex()))?; + } + seq.end() + } +} + +pub struct RunStep<'a> { + pub remaining_script: &'a Script, + pub stack: &'a [Vec], + pub altstack: &'a [Vec], + pub stats: Option<&'a ExecStats>, +} + +impl<'a> Serialize for RunStep<'a> { + fn serialize(&self, s: S) -> Result { + let mut m = s.serialize_map(None)?; + m.serialize_entry( + "remaining_script_hex", + &FmtSer(&self.remaining_script.as_bytes().as_hex()), + )?; + m.serialize_entry( + "remaining_script_asm", + &FmtSer(&ScriptAsm(self.remaining_script)), + )?; + m.serialize_entry("stack", &StackSer(self.stack))?; + m.serialize_entry("altstack", &StackSer(self.altstack))?; + if let Some(ref stats) = self.stats { + m.serialize_entry("stats", stats)?; + } + m.end() + } +} + +pub struct RunResult<'a> { + pub success: bool, + pub error: Option, + pub opcode: Option, + pub final_stack: &'a [Vec], + pub stats: Option<&'a ExecStats>, +} + +impl<'a> Serialize for RunResult<'a> { + fn serialize(&self, s: S) -> Result { + let mut m = s.serialize_map(None)?; + m.serialize_entry("success", &self.success)?; + if let Some(ref err) = self.error { + m.serialize_entry("error", err)?; + } + if let Some(opcode) = self.opcode { + m.serialize_entry("opcode", &FmtSer(&opcode))?; + } + m.serialize_entry("final_stack", &StackSer(self.final_stack))?; + if let Some(ref stats) = self.stats { + m.serialize_entry("stats", stats)?; + } + m.end() + } +} diff --git a/bitcoin-scriptexec/src/lib.rs b/bitcoin-scriptexec/src/lib.rs new file mode 100644 index 0000000..e778361 --- /dev/null +++ b/bitcoin-scriptexec/src/lib.rs @@ -0,0 +1,1024 @@ +extern crate alloc; +extern crate core; + +use alloc::borrow::Cow; +use core::cmp; + +use bitcoin::consensus::Encodable; +use bitcoin::hashes::{hash160, ripemd160, sha1, sha256, sha256d, Hash}; +use bitcoin::opcodes::{all::*, Opcode}; +use bitcoin::script::{self, Instruction, Instructions, Script, ScriptBuf}; +use bitcoin::sighash::SighashCache; +use bitcoin::taproot::{self, TapLeafHash}; +use bitcoin::transaction::{self, Transaction, TxOut}; + +#[macro_use] +mod macros; + +mod utils; +use utils::ConditionStack; + +mod signatures; + +mod error; +pub use error::{Error, ExecError}; + +#[cfg(feature = "json")] +pub mod json; +#[cfg(feature = "wasm")] +mod wasm; + +mod data_structures; +pub use data_structures::Stack; + +/// Maximum number of non-push operations per script +const MAX_OPS_PER_SCRIPT: usize = 201; + +/// Maximum number of bytes pushable to the stack +const MAX_SCRIPT_ELEMENT_SIZE: usize = 520; + +/// Maximum number of values on script interpreter stack +const MAX_STACK_SIZE: usize = 1000; + +/// If this flag is set, CTxIn::nSequence is NOT interpreted as a +/// relative lock-time. +/// It skips SequenceLocks() for any input that has it set (BIP 68). +/// It fails OP_CHECKSEQUENCEVERIFY/CheckSequence() for any input that has +/// it set (BIP 112). +const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31; + +/// How much weight budget is added to the witness size (Tapscript only, see BIP 342). +const VALIDATION_WEIGHT_OFFSET: i64 = 50; + +/// Validation weight per passing signature (Tapscript only, see BIP 342). +const VALIDATION_WEIGHT_PER_SIGOP_PASSED: i64 = 50; + +// Maximum number of public keys per multisig +const _MAX_PUBKEYS_PER_MULTISIG: i64 = 20; + +/// Used to enable experimental script features. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Experimental { + /// Enable an experimental implementation of OP_CAT. + pub op_cat: bool, +} + +/// Used to fine-tune different variables during execution. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Options { + /// Require data pushes be minimally encoded. + pub require_minimal: bool, //TODO(stevenroose) double check all fRequireMinimal usage in Core + /// Verify OP_CHECKLOCKTIMEVERIFY. + pub verify_cltv: bool, + /// Verify OP_CHECKSEQUENCEVERIFY. + pub verify_csv: bool, + /// Verify conditionals are minimally encoded. + pub verify_minimal_if: bool, + /// Enfore a strict limit of 1000 total stack items. + pub enforce_stack_limit: bool, + + pub experimental: Experimental, +} + +impl Default for Options { + fn default() -> Self { + Options { + require_minimal: true, + verify_cltv: true, + verify_csv: true, + verify_minimal_if: true, + enforce_stack_limit: true, + experimental: Experimental { op_cat: true }, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecCtx { + Legacy, + SegwitV0, + Tapscript, +} + +pub struct TxTemplate { + pub tx: Transaction, + pub prevouts: Vec, + pub input_idx: usize, + pub taproot_annex_scriptleaf: Option<(TapLeafHash, Option>)>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecutionResult { + pub success: bool, + pub error: Option, + pub opcode: Option, + pub final_stack: Stack, +} + +impl ExecutionResult { + fn from_final_stack(ctx: ExecCtx, final_stack: Stack) -> ExecutionResult { + ExecutionResult { + success: match ctx { + ExecCtx::Legacy => { + if final_stack.is_empty() { + false + } else { + script::read_scriptbool(&final_stack.last().unwrap()) + } + } + ExecCtx::SegwitV0 | ExecCtx::Tapscript => { + if final_stack.len() != 1 { + false + } else { + script::read_scriptbool(&final_stack.last().unwrap()) + } + } + }, + final_stack, + error: None, + opcode: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ExecStats { + /// The highest number of stack items occurred during execution. + /// This counts both the stack and the altstack. + pub max_nb_stack_items: usize, + + /// The number of opcodes executed, plus an additional one + /// per signature in CHECKMULTISIG. + pub opcode_count: usize, + + /// The validation weight execution started with. + pub start_validation_weight: i64, + /// The current remaining validation weight. + pub validation_weight: i64, +} + +/// Partial execution of a script. +pub struct Exec { + ctx: ExecCtx, + opt: Options, + tx: TxTemplate, + result: Option, + + sighashcache: SighashCache, + script: &'static Script, + instructions: Instructions<'static>, + current_position: usize, + cond_stack: ConditionStack, + stack: Stack, + altstack: Stack, + last_codeseparator_pos: Option, + // Initially set to the whole script, but updated when + // OP_CODESEPARATOR is encountered. + script_code: &'static Script, + + opcode_count: usize, + validation_weight: i64, + + // runtime statistics + stats: ExecStats, +} + +impl std::ops::Drop for Exec { + fn drop(&mut self) { + // we need to safely drop the script we allocated + unsafe { + let script = core::mem::replace(&mut self.script, Script::from_bytes(&[])); + let _ = Box::from_raw(script as *const Script as *mut Script); + } + } +} + +impl Exec { + pub fn new( + ctx: ExecCtx, + opt: Options, + tx: TxTemplate, + script: ScriptBuf, + script_witness: Vec>, + ) -> Result { + if ctx == ExecCtx::Tapscript { + if tx.taproot_annex_scriptleaf.is_none() { + return Err(Error::Other("missing taproot tx info in tapscript context")); + } + + if let Some((_, Some(ref annex))) = tx.taproot_annex_scriptleaf { + if annex.first() != Some(&taproot::TAPROOT_ANNEX_PREFIX) { + return Err(Error::Other("invalid annex: missing prefix")); + } + } + } + + // We want to make sure the script is valid so we don't have to throw parsing errors + // while executing. + let instructions = if opt.require_minimal { + script.instructions_minimal() + } else { + script.instructions() + }; + if let Some(err) = instructions.clone().find_map(|res| res.err()) { + return Err(Error::InvalidScript(err)); + } + + // ***** + // Make sure there is no more possible exit path after this point! + // Otherwise we are leaking memory. + // ***** + + // We box alocate the script to get a static Instructions iterator. + // We will manually drop this allocation in the ops::Drop impl. + let script = Box::leak(script.into_boxed_script()) as &'static Script; + let instructions = if opt.require_minimal { + script.instructions_minimal() + } else { + script.instructions() + }; + + //TODO(stevenroose) make this more efficient + let witness_size = + Encodable::consensus_encode(&script_witness, &mut bitcoin::io::sink()).unwrap(); + let start_validation_weight = VALIDATION_WEIGHT_OFFSET + witness_size as i64; + + let mut ret = Exec { + ctx, + result: None, + + sighashcache: SighashCache::new(tx.tx.clone()), + script, + instructions, + current_position: 0, + cond_stack: ConditionStack::new(), + //TODO(stevenroose) does this need to be reversed? + stack: Stack::from_u8_vec(script_witness), + altstack: Stack::new(), + opcode_count: 0, + validation_weight: start_validation_weight, + last_codeseparator_pos: None, + script_code: script, + + opt, + tx, + + stats: ExecStats { + start_validation_weight, + validation_weight: start_validation_weight, + ..Default::default() + }, + }; + ret.update_stats(); + Ok(ret) + } + + pub fn with_stack( + ctx: ExecCtx, + opt: Options, + tx: TxTemplate, + script: ScriptBuf, + script_witness: Vec>, + stack: Stack, + altstack: Stack, + ) -> Result { + let mut ret = Self::new(ctx, opt, tx, script, script_witness); + if let Ok(exec) = &mut ret { + exec.stack = stack; + exec.altstack = altstack; + } + ret + } + ////////////////// + // SOME GETTERS // + ////////////////// + + pub fn result(&self) -> Option<&ExecutionResult> { + self.result.as_ref() + } + + pub fn script_position(&self) -> usize { + self.script.len() - self.instructions.as_script().len() + } + + pub fn remaining_script(&self) -> &Script { + let pos = self.script_position(); + &self.script[pos..] + } + + pub fn stack(&self) -> &Stack { + &self.stack + } + + pub fn altstack(&self) -> &Stack { + &self.altstack + } + + pub fn stats(&self) -> &ExecStats { + &self.stats + } + + /////////////// + // UTILITIES // + /////////////// + + fn fail(&mut self, err: ExecError) -> Result<(), &ExecutionResult> { + let res = ExecutionResult { + success: false, + error: Some(err), + opcode: None, + final_stack: self.stack.clone(), + }; + self.result = Some(res); + Err(self.result.as_ref().unwrap()) + } + + fn failop(&mut self, err: ExecError, op: Opcode) -> Result<(), &ExecutionResult> { + let res = ExecutionResult { + success: false, + error: Some(err), + opcode: Some(op), + final_stack: self.stack.clone(), + }; + self.result = Some(res); + Err(self.result.as_ref().unwrap()) + } + + fn check_lock_time(&mut self, lock_time: i64) -> bool { + use bitcoin::locktime::absolute::LockTime; + let lock_time = match lock_time.try_into() { + Ok(l) => LockTime::from_consensus(l), + Err(_) => return false, + }; + + match (lock_time, self.tx.tx.lock_time) { + (LockTime::Blocks(h1), LockTime::Blocks(h2)) if h1 > h2 => return false, + (LockTime::Seconds(t1), LockTime::Seconds(t2)) if t1 > t2 => return false, + (LockTime::Blocks(_), LockTime::Seconds(_)) => return false, + (LockTime::Seconds(_), LockTime::Blocks(_)) => return false, + _ => {} + } + + if self.tx.tx.input[self.tx.input_idx].sequence.is_final() { + return false; + } + + true + } + + fn check_sequence(&mut self, sequence: i64) -> bool { + use bitcoin::locktime::relative::LockTime; + + // Fail if the transaction's version number is not set high + // enough to trigger BIP 68 rules. + if self.tx.tx.version < transaction::Version::TWO { + return false; + } + + let input_sequence = self.tx.tx.input[self.tx.input_idx].sequence; + let input_lock_time = match input_sequence.to_relative_lock_time() { + Some(lt) => lt, + None => return false, + }; + + let lock_time = match LockTime::from_num(sequence) { + Some(lt) => lt, + None => return false, + }; + + match (lock_time, input_lock_time) { + (LockTime::Blocks(h1), LockTime::Blocks(h2)) if h1 > h2 => return false, + (LockTime::Time(t1), LockTime::Time(t2)) if t1 > t2 => return false, + (LockTime::Blocks(_), LockTime::Time(_)) => return false, + (LockTime::Time(_), LockTime::Blocks(_)) => return false, + _ => {} + } + + true + } + + fn check_sig_pre_tap(&mut self, sig: &[u8], pk: &[u8]) -> Result { + //TODO(stevenroose) somehow sigops limit should be checked somewhere + + // Drop the signature in pre-segwit scripts but not segwit scripts + let mut scriptcode = Cow::Borrowed(self.script_code.as_bytes()); + if self.ctx == ExecCtx::Legacy { + let mut i = 0; + while i < scriptcode.len() - sig.len() { + if &scriptcode[i..i + sig.len()] == sig { + scriptcode.to_mut().drain(i..i + sig.len()); + } else { + i += 1; + } + } + } + + //TODO(stevenroose) the signature and pk encoding checks we use here + // might not be exactly identical to Core's + + if self.ctx == ExecCtx::SegwitV0 && pk.len() == 65 { + return Err(ExecError::WitnessPubkeyType); + } + + Ok(self.check_sig_ecdsa(sig, pk, &scriptcode)) + } + + fn check_sig_tap(&mut self, sig: &[u8], pk: &[u8]) -> Result { + if !sig.is_empty() { + self.validation_weight -= VALIDATION_WEIGHT_PER_SIGOP_PASSED; + if self.validation_weight < 0 { + return Err(ExecError::TapscriptValidationWeight); + } + } + + if pk.is_empty() { + Err(ExecError::PubkeyType) + } else if pk.len() == 32 { + if !sig.is_empty() { + self.check_sig_schnorr(sig, pk)?; + Ok(true) + } else { + Ok(false) + } + } else { + Ok(true) + } + } + + fn check_sig(&mut self, sig: &[u8], pk: &[u8]) -> Result { + match self.ctx { + ExecCtx::Legacy | ExecCtx::SegwitV0 => self.check_sig_pre_tap(sig, pk), + ExecCtx::Tapscript => self.check_sig_tap(sig, pk), + } + } + + /////////////// + // EXECUTION // + /////////////// + + /// Returns true when execution is done. + pub fn exec_next(&mut self) -> Result<(), &ExecutionResult> { + if let Some(ref res) = self.result { + return Err(res); + } + + self.current_position = self.script.len() - self.instructions.as_script().len(); + let instruction = match self.instructions.next() { + Some(Ok(i)) => i, + None => { + let res = ExecutionResult::from_final_stack(self.ctx, self.stack.clone()); + self.result = Some(res); + return Err(self.result.as_ref().unwrap()); + } + Some(Err(_)) => unreachable!("we checked the script beforehand"), + }; + + let exec = self.cond_stack.all_true(); + match instruction { + Instruction::PushBytes(p) => { + if p.len() > MAX_SCRIPT_ELEMENT_SIZE { + return self.fail(ExecError::PushSize); + } + if exec { + self.stack.pushstr(p.as_bytes()); + } + } + Instruction::Op(op) => { + // Some things we do even when we're not executing. + + // Note how OP_RESERVED does not count towards the opcode limit. + if (self.ctx == ExecCtx::Legacy || self.ctx == ExecCtx::SegwitV0) && op.to_u8() > OP_PUSHNUM_16.to_u8() { + self.opcode_count += 1; + if self.opcode_count > MAX_OPS_PER_SCRIPT { + return self.fail(ExecError::OpCount); + } + } + + match op { + OP_CAT if !self.opt.experimental.op_cat || self.ctx != ExecCtx::Tapscript => { + return self.failop(ExecError::DisabledOpcode, op); + } + OP_SUBSTR | OP_LEFT | OP_RIGHT | OP_INVERT | OP_AND | OP_OR | OP_XOR + | OP_2MUL | OP_2DIV | OP_MUL | OP_DIV | OP_MOD | OP_LSHIFT | OP_RSHIFT => { + return self.failop(ExecError::DisabledOpcode, op); + } + OP_RESERVED => { + return self.failop(ExecError::Debug, op); + } + + _ => {} + } + + if exec || (op.to_u8() >= OP_IF.to_u8() && op.to_u8() <= OP_ENDIF.to_u8()) { + if let Err(err) = self.exec_opcode(op) { + return self.failop(err, op); + } + } + } + } + + self.update_stats(); + Ok(()) + } + + fn exec_opcode(&mut self, op: Opcode) -> Result<(), ExecError> { + let exec = self.cond_stack.all_true(); + + // Remember to leave stack intact until all errors have occurred. + match op { + // + // Push value + OP_PUSHNUM_NEG1 | OP_PUSHNUM_1 | OP_PUSHNUM_2 | OP_PUSHNUM_3 | OP_PUSHNUM_4 + | OP_PUSHNUM_5 | OP_PUSHNUM_6 | OP_PUSHNUM_7 | OP_PUSHNUM_8 | OP_PUSHNUM_9 + | OP_PUSHNUM_10 | OP_PUSHNUM_11 | OP_PUSHNUM_12 | OP_PUSHNUM_13 | OP_PUSHNUM_14 + | OP_PUSHNUM_15 | OP_PUSHNUM_16 => { + let n = op.to_u8() - (OP_PUSHNUM_1.to_u8() - 2); + self.stack.pushnum((n as i64) - 1); + } + + // + // Control + OP_NOP => {} + + OP_CLTV if self.opt.verify_cltv => { + let top = self.stack.topstr(-1)?; + + // Note that elsewhere numeric opcodes are limited to + // operands in the range -2**31+1 to 2**31-1, however it is + // legal for opcodes to produce results exceeding that + // range. This limitation is implemented by CScriptNum's + // default 4-byte limit. + // + // If we kept to that limit we'd have a year 2038 problem, + // even though the nLockTime field in transactions + // themselves is uint32 which only becomes meaningless + // after the year 2106. + // + // Thus as a special case we tell CScriptNum to accept up + // to 5-byte bignums, which are good until 2**39-1, well + // beyond the 2**32-1 limit of the nLockTime field itself. + let n = read_scriptint(&top, 5, self.opt.require_minimal)?; + + if n < 0 { + return Err(ExecError::NegativeLocktime); + } + + if !self.check_lock_time(n) { + return Err(ExecError::UnsatisfiedLocktime); + } + } + OP_CLTV => {} // otherwise nop + + OP_CSV if self.opt.verify_csv => { + let top = self.stack.topstr(-1)?; + + // nSequence, like nLockTime, is a 32-bit unsigned integer + // field. See the comment in CHECKLOCKTIMEVERIFY regarding + // 5-byte numeric operands. + let n = read_scriptint(&top, 5, self.opt.require_minimal)?; + + if n < 0 { + return Err(ExecError::NegativeLocktime); + } + + //TODO(stevenroose) check this logic + //TODO(stevenroose) check if this cast is ok + if n & SEQUENCE_LOCKTIME_DISABLE_FLAG as i64 == 0 && !self.check_sequence(n) { + return Err(ExecError::UnsatisfiedLocktime); + } + } + OP_CSV => {} // otherwise nop + + OP_NOP1 | OP_NOP4 | OP_NOP5 | OP_NOP6 | OP_NOP7 | OP_NOP8 | OP_NOP9 | OP_NOP10 => { + // nops + } + + OP_IF | OP_NOTIF => { + if exec { + let top = self.stack.topstr(-1)?; + + // Tapscript requires minimal IF/NOTIF inputs as a consensus rule. + if self.ctx == ExecCtx::Tapscript { + // The input argument to the OP_IF and OP_NOTIF opcodes must be either + // exactly 0 (the empty vector) or exactly 1 (the one-byte vector with value 1). + if top.len() > 1 || (top.len() == 1 && top[0] != 1) { + return Err(ExecError::TapscriptMinimalIf); + } + } + // Under segwit v0 only enabled as policy. + if self.opt.verify_minimal_if && self.ctx == ExecCtx::SegwitV0 && (top.len() > 1 || (top.len() == 1 && top[0] != 1)) { + return Err(ExecError::TapscriptMinimalIf); + } + let b = if op == OP_NOTIF { + !script::read_scriptbool(&top) + } else { + script::read_scriptbool(&top) + }; + self.stack.pop().unwrap(); + self.cond_stack.push(b); + } else { + self.cond_stack.push(false); + } + } + + OP_ELSE => { + if !self.cond_stack.toggle_top() { + return Err(ExecError::UnbalancedConditional); + } + } + + OP_ENDIF => { + if !self.cond_stack.pop() { + return Err(ExecError::UnbalancedConditional); + } + } + + OP_VERIFY => { + let top = self.stack.topstr(-1)?; + + if !script::read_scriptbool(&top) { + return Err(ExecError::Verify); + } else { + self.stack.pop().unwrap(); + } + } + + OP_RETURN => return Err(ExecError::OpReturn), + + // + // Stack operations + OP_TOALTSTACK => { + let top = self.stack.pop().ok_or(ExecError::InvalidStackOperation)?; + self.altstack.push(top); + } + + OP_FROMALTSTACK => { + let top = self + .altstack + .pop() + .ok_or(ExecError::InvalidStackOperation)?; + self.stack.push(top); + } + + OP_2DROP => { + // (x1 x2 -- ) + self.stack.needn(2)?; + self.stack.popn(2).unwrap(); + } + + OP_2DUP => { + // (x1 x2 -- x1 x2 x1 x2) + let x1 = self.stack.top(-2)?.clone(); + let x2 = self.stack.top(-1)?.clone(); + self.stack.push(x1); + self.stack.push(x2); + } + + OP_3DUP => { + // (x1 x2 x3 -- x1 x2 x3 x1 x2 x3) + let x1 = self.stack.top(-3)?.clone(); + let x2 = self.stack.top(-2)?.clone(); + let x3 = self.stack.top(-1)?.clone(); + self.stack.push(x1); + self.stack.push(x2); + self.stack.push(x3); + } + + OP_2OVER => { + // (x1 x2 x3 x4 -- x1 x2 x3 x4 x1 x2) + self.stack.needn(4)?; + let x1 = self.stack.top(-4)?.clone(); + let x2 = self.stack.top(-3)?.clone(); + self.stack.push(x1); + self.stack.push(x2); + } + + OP_2ROT => { + // (x1 x2 x3 x4 x5 x6 -- x3 x4 x5 x6 x1 x2) + self.stack.needn(6)?; + let x6 = self.stack.pop().unwrap(); + let x5 = self.stack.pop().unwrap(); + let x4 = self.stack.pop().unwrap(); + let x3 = self.stack.pop().unwrap(); + let x2 = self.stack.pop().unwrap(); + let x1 = self.stack.pop().unwrap(); + self.stack.push(x3); + self.stack.push(x4); + self.stack.push(x5); + self.stack.push(x6); + self.stack.push(x1); + self.stack.push(x2); + } + + OP_2SWAP => { + // (x1 x2 x3 x4 -- x3 x4 x1 x2) + self.stack.needn(4)?; + let x4 = self.stack.pop().unwrap(); + let x3 = self.stack.pop().unwrap(); + let x2 = self.stack.pop().unwrap(); + let x1 = self.stack.pop().unwrap(); + self.stack.push(x3); + self.stack.push(x4); + self.stack.push(x1); + self.stack.push(x2); + } + + OP_IFDUP => { + // (x - 0 | x x) + let top = self.stack.topstr(-1)?; + if script::read_scriptbool(&top) { + self.stack.push(self.stack.top(-1)?.clone()); + } + } + + OP_DEPTH => { + // -- stacksize + self.stack.pushnum(self.stack.len() as i64); + } + + OP_DROP => { + // (x -- ) + if self.stack.pop().is_none() { + return Err(ExecError::InvalidStackOperation); + } + } + + OP_DUP => { + // (x -- x x) + let top = self.stack.top(-1)?.clone(); + self.stack.push(top); + } + + OP_NIP => { + // (x1 x2 -- x2) + self.stack.needn(2)?; + let x2 = self.stack.pop().unwrap(); + self.stack.pop().unwrap(); + self.stack.push(x2); + } + + OP_OVER => { + // (x1 x2 -- x1 x2 x1) + let under_top = self.stack.top(-2)?.clone(); + self.stack.push(under_top); + } + + OP_PICK | OP_ROLL => { + // (xn ... x2 x1 x0 n - xn ... x2 x1 x0 xn) + // (xn ... x2 x1 x0 n - ... x2 x1 x0 xn) + let x = self.stack.topnum(-1, self.opt.require_minimal)?; + if x < 0 || x >= self.stack.len() as i64 { + return Err(ExecError::InvalidStackOperation); + } + self.stack.pop().unwrap(); + let elem = self.stack.top(-x as isize - 1).unwrap().clone(); + if op == OP_ROLL { + self.stack.remove(self.stack.len() - x as usize - 1); + } + self.stack.push(elem); + } + + OP_ROT => { + // (x1 x2 x3 -- x2 x3 x1) + self.stack.needn(3)?; + let x3 = self.stack.pop().unwrap(); + let x2 = self.stack.pop().unwrap(); + let x1 = self.stack.pop().unwrap(); + self.stack.push(x2); + self.stack.push(x3); + self.stack.push(x1); + } + + OP_SWAP => { + // (x1 x2 -- x2 x1) + self.stack.needn(2)?; + let x2 = self.stack.pop().unwrap(); + let x1 = self.stack.pop().unwrap(); + self.stack.push(x2); + self.stack.push(x1); + } + + OP_TUCK => { + // (x1 x2 -- x2 x1 x2) + self.stack.needn(2)?; + let x2 = self.stack.pop().unwrap(); + let x1 = self.stack.pop().unwrap(); + self.stack.push(x2.clone()); + self.stack.push(x1); + self.stack.push(x2); + } + + OP_CAT if self.opt.experimental.op_cat && self.ctx == ExecCtx::Tapscript => { + // (x1 x2 -- x1|x2) + self.stack.needn(2)?; + let x2 = self.stack.popstr().unwrap(); + let x1 = self.stack.popstr().unwrap(); + let ret: Vec = x1.into_iter().chain(x2).collect(); + if ret.len() > MAX_SCRIPT_ELEMENT_SIZE { + return Err(ExecError::PushSize); + } + self.stack.pushstr(&ret); + } + + OP_SIZE => { + // (in -- in size) + let top = self.stack.topstr(-1)?; + self.stack.pushnum(top.len() as i64); + } + + // + // Bitwise logic + OP_EQUAL | OP_EQUALVERIFY => { + // (x1 x2 - bool) + self.stack.needn(2)?; + let x2 = self.stack.popstr().unwrap(); + let x1 = self.stack.popstr().unwrap(); + let equal = x1 == x2; + if op == OP_EQUALVERIFY && !equal { + return Err(ExecError::EqualVerify); + } + if op == OP_EQUAL { + let item = if equal { 1 } else { 0 }; + self.stack.pushnum(item); + } + } + + // + // Numeric + OP_1ADD | OP_1SUB | OP_NEGATE | OP_ABS | OP_NOT | OP_0NOTEQUAL => { + // (in -- out) + let x = self.stack.topnum(-1, self.opt.require_minimal)?; + let res = match op { + OP_1ADD => x + .checked_add(1) + .ok_or(ExecError::ScriptIntNumericOverflow)?, + OP_1SUB => x + .checked_sub(1) + .ok_or(ExecError::ScriptIntNumericOverflow)?, + OP_NEGATE => x.checked_neg().ok_or(ExecError::ScriptIntNumericOverflow)?, + OP_ABS => x.abs(), + OP_NOT => (x == 0) as i64, + OP_0NOTEQUAL => (x != 0) as i64, + _ => unreachable!(), + }; + self.stack.pop().unwrap(); + self.stack.pushnum(res); + } + + OP_ADD + | OP_SUB + | OP_BOOLAND + | OP_BOOLOR + | OP_NUMEQUAL + | OP_NUMEQUALVERIFY + | OP_NUMNOTEQUAL + | OP_LESSTHAN + | OP_GREATERTHAN + | OP_LESSTHANOREQUAL + | OP_GREATERTHANOREQUAL + | OP_MIN + | OP_MAX => { + // (x1 x2 -- out) + let x1 = self.stack.topnum(-2, self.opt.require_minimal)?; + let x2 = self.stack.topnum(-1, self.opt.require_minimal)?; + let res = match op { + OP_ADD => x1 + .checked_add(x2) + .ok_or(ExecError::ScriptIntNumericOverflow)?, + OP_SUB => x1 + .checked_sub(x2) + .ok_or(ExecError::ScriptIntNumericOverflow)?, + OP_BOOLAND => (x1 != 0 && x2 != 0) as i64, + OP_BOOLOR => (x1 != 0 || x2 != 0) as i64, + OP_NUMEQUAL => (x1 == x2) as i64, + OP_NUMEQUALVERIFY => (x1 == x2) as i64, + OP_NUMNOTEQUAL => (x1 != x2) as i64, + OP_LESSTHAN => (x1 < x2) as i64, + OP_GREATERTHAN => (x1 > x2) as i64, + OP_LESSTHANOREQUAL => (x1 <= x2) as i64, + OP_GREATERTHANOREQUAL => (x1 >= x2) as i64, + OP_MIN => cmp::min(x1, x2), + OP_MAX => cmp::max(x1, x2), + _ => unreachable!(), + }; + if op == OP_NUMEQUALVERIFY && res == 0 { + return Err(ExecError::NumEqualVerify); + } + self.stack.popn(2).unwrap(); + if op != OP_NUMEQUALVERIFY { + self.stack.pushnum(res); + } + } + + OP_WITHIN => { + // (x min max -- out) + let x1 = self.stack.topnum(-3, self.opt.require_minimal)?; + let x2 = self.stack.topnum(-2, self.opt.require_minimal)?; + let x3 = self.stack.topnum(-1, self.opt.require_minimal)?; + self.stack.popn(3).unwrap(); + let res = x2 <= x1 && x1 < x3; + let item = if res { 1 } else { 0 }; + self.stack.pushnum(item); + } + + // + // Crypto + + // (in -- hash) + OP_RIPEMD160 => { + let top = self.stack.popstr()?; + self.stack + .pushstr(&ripemd160::Hash::hash(&top[..]).to_byte_array()); + } + OP_SHA1 => { + let top = self.stack.popstr()?; + self.stack + .pushstr(&sha1::Hash::hash(&top[..]).to_byte_array()); + } + OP_SHA256 => { + let top = self.stack.popstr()?; + self.stack + .pushstr(&sha256::Hash::hash(&top[..]).to_byte_array()); + } + OP_HASH160 => { + let top = self.stack.popstr()?; + self.stack + .pushstr(&hash160::Hash::hash(&top[..]).to_byte_array()); + } + OP_HASH256 => { + let top = self.stack.popstr()?; + self.stack + .pushstr(&sha256d::Hash::hash(&top[..]).to_byte_array()); + } + + OP_CODESEPARATOR => { + // Store this CODESEPARATOR position and update the scriptcode. + self.last_codeseparator_pos = Some(self.current_position as u32); + self.script_code = &self.script[self.current_position..]; + } + + OP_CHECKSIG | OP_CHECKSIGVERIFY => { + let sig = self.stack.topstr(-2)?.clone(); + let pk = self.stack.topstr(-1)?.clone(); + let res = self.check_sig(&sig, &pk)?; + self.stack.popn(2).unwrap(); + if op == OP_CHECKSIGVERIFY && !res { + return Err(ExecError::CheckSigVerify); + } + if op == OP_CHECKSIG { + let ret = if res { 1 } else { 0 }; + self.stack.pushnum(ret); + } + } + + OP_CHECKSIGADD => { + if self.ctx == ExecCtx::Legacy || self.ctx == ExecCtx::SegwitV0 { + return Err(ExecError::BadOpcode); + } + let sig = self.stack.topstr(-3)?.clone(); + let mut n = self.stack.topnum(-2, self.opt.require_minimal)?; + let pk = self.stack.topstr(-1)?.clone(); + let res = self.check_sig(&sig, &pk)?; + self.stack.popn(3).unwrap(); + if res { + n += 1; + } + self.stack.pushnum(n); + } + + OP_CHECKMULTISIG | OP_CHECKMULTISIGVERIFY => { + unimplemented!(); + } + + // remainder + _ => return Err(ExecError::BadOpcode), + } + + if self.opt.enforce_stack_limit && self.stack.len() + self.altstack.len() > MAX_STACK_SIZE { + return Err(ExecError::StackSize); + } + + Ok(()) + } + + //////////////// + // STATISTICS // + //////////////// + + fn update_stats(&mut self) { + let stack_items = self.stack.len() + self.altstack.len(); + self.stats.max_nb_stack_items = cmp::max(self.stats.max_nb_stack_items, stack_items); + + self.stats.opcode_count = self.opcode_count; + self.stats.validation_weight = self.validation_weight; + } +} + +fn read_scriptint(item: &[u8], size: usize, minimal: bool) -> Result { + script::read_scriptint_size(item, size, minimal).map_err(|e| match e { + script::ScriptIntError::NonMinimalPush => ExecError::MinimalData, + // only possible if size is 4 or lower + script::ScriptIntError::NumericOverflow => ExecError::ScriptIntNumericOverflow, + }) +} diff --git a/bitcoin-scriptexec/src/macros.rs b/bitcoin-scriptexec/src/macros.rs new file mode 100644 index 0000000..52c6fd1 --- /dev/null +++ b/bitcoin-scriptexec/src/macros.rs @@ -0,0 +1,10 @@ +#[macro_export] +macro_rules! or_else { + ($e:expr, $($else:tt)+) => { + if let Some(v) = $e { + v + } else { + return $($else)+; + } + }; +} diff --git a/bitcoin-scriptexec/src/main.rs b/bitcoin-scriptexec/src/main.rs new file mode 100644 index 0000000..6a6d54b --- /dev/null +++ b/bitcoin-scriptexec/src/main.rs @@ -0,0 +1,130 @@ +use std::fmt; +use std::io::{self, Write}; +use std::path::PathBuf; + +use bitcoin::hashes::Hash; +use bitcoin::hex::DisplayHex; +use bitcoin::taproot::TapLeafHash; +use bitcoin::{ScriptBuf, Transaction}; +use clap::Parser; + +use bitcoin_scriptexec::*; + +#[derive(Parser)] +#[command(author = "Steven Roose ", version, about)] +struct Args { + /// filepath to script ASM file + #[arg(required = true)] + script_path: PathBuf, + /// Whether to print debug info + #[arg(long)] + debug: bool, + /// Whether to output result in JSON. + #[arg(long)] + json: bool, +} + +/// A wrapper for the stack types to print them better. +struct FmtStack<'a>(&'a Stack); +impl<'a> fmt::Display for FmtStack<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut iter = self.0.iter_str().rev().peekable(); + while let Some(item) = iter.next() { + write!(f, "<{}>", item.as_hex())?; + if iter.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } +} + +fn inner_main() -> Result<(), String> { + let args = Args::parse(); + + let script_asm = std::fs::read_to_string(args.script_path).expect("error reading script file"); + let script = ScriptBuf::parse_asm(&script_asm).expect("error parsing script"); + println!("Script in hex: {}", script.as_bytes().to_lower_hex_string()); + println!("Script size: {} bytes", script.as_bytes().len()); + + let start = std::time::Instant::now(); + let mut exec = Exec::new( + ExecCtx::Tapscript, + Options::default(), + TxTemplate { + tx: Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::locktime::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }, + prevouts: vec![], + input_idx: 0, + taproot_annex_scriptleaf: Some((TapLeafHash::all_zeros(), None)), + }, + script, + vec![], + ) + .expect("error creating exec"); + + const SEP: &str = "--------------------------------------------------"; + + let mut out = io::stdout(); + println!("{}", SEP); + loop { + if args.debug { + if args.json { + let step = json::RunStep { + remaining_script: exec.remaining_script(), + stack: &exec.stack().iter_str().collect::>>(), + altstack: &exec.altstack().iter_str().collect::>>(), + stats: Some(exec.stats()), + }; + serde_json::to_writer(&out, &step).expect("I/O error"); + out.write_all('\n'.to_string().as_bytes()).expect("I/O error"); + } else { + println!( + "Remaining script: {}", + exec.remaining_script().to_asm_string() + ); + println!("Stack: {}", FmtStack(exec.stack())); + println!("AltStack: {}", FmtStack(exec.altstack())); + println!("{}", SEP); + } + } + + if exec.exec_next().is_err() { + break; + } + } + + let res = exec.result().unwrap().clone(); + if args.json { + let ret = json::RunResult { + success: res.success, + error: res.error.map(|e| format!("{:?}", e)), //TODO(stevenroose) fmt::Display + opcode: res.opcode, + final_stack: &res.final_stack.iter_str().collect::>>(), + stats: Some(exec.stats()), + }; + serde_json::to_writer(&out, &ret).expect("I/O error"); + } else { + println!("Execution ended. Success: {}", res.success); + print!("Final stack: {}", FmtStack(&res.final_stack)); + println!(); + if !res.success { + println!("Failed on opcode: {:?}", res.opcode); + println!("Error: {:?}", res.error); + } + println!("Stats:\n{:#?}", exec.stats()); + println!("Time elapsed: {}ms", start.elapsed().as_millis()); + } + + Ok(()) +} + +fn main() { + if let Err(e) = inner_main() { + eprintln!("ERROR: {}", e); + } +} diff --git a/bitcoin-scriptexec/src/signatures.rs b/bitcoin-scriptexec/src/signatures.rs new file mode 100644 index 0000000..98c274f --- /dev/null +++ b/bitcoin-scriptexec/src/signatures.rs @@ -0,0 +1,101 @@ +use bitcoin::secp256k1::{self, PublicKey, XOnlyPublicKey}; +use bitcoin::sighash::{Annex, EcdsaSighashType, Prevouts, TapSighashType}; + +use crate::*; + +lazy_static::lazy_static! { + static ref SECP: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); +} + +impl Exec { + pub fn check_sig_ecdsa(&mut self, sig: &[u8], pk: &[u8], script_code: &[u8]) -> bool { + let pk = match PublicKey::from_slice(pk) { + Ok(pk) => pk, + Err(_) => return false, + }; + + if sig.is_empty() { + return false; + } + + let hashtype = *sig.last().unwrap(); + let sig = match secp256k1::ecdsa::Signature::from_der(&sig[0..sig.len() - 1]) { + Ok(s) => s, + Err(_) => return false, + }; + + let sighash = if self.ctx == ExecCtx::SegwitV0 { + self.sighashcache + .p2wsh_signature_hash( + self.tx.input_idx, + Script::from_bytes(script_code), + self.tx.prevouts[self.tx.input_idx].value, + //TODO(stevenroose) this might not actually emulate consensus behavior + EcdsaSighashType::from_consensus(hashtype as u32), + ) + .expect("only happens on prevout index out of bounds") + .into() + } else if self.ctx == ExecCtx::Legacy { + self.sighashcache + .legacy_signature_hash( + self.tx.input_idx, + Script::from_bytes(script_code), + hashtype as u32, + ) + .expect("TODO(stevenroose) seems to only happen if prevout index out of bound") + .into() + } else { + unreachable!(); + }; + + SECP.verify_ecdsa(&sighash, &sig, &pk).is_ok() + } + + /// pk should be passed as 32-bytes. + pub fn check_sig_schnorr(&mut self, sig: &[u8], pk: &[u8]) -> Result<(), ExecError> { + assert_eq!(pk.len(), 32); + + if sig.len() != 64 && sig.len() != 65 { + return Err(ExecError::SchnorrSigSize); + } + + let pk = XOnlyPublicKey::from_slice(pk).expect("TODO(stevenroose) what to do here?"); + let (sig, hashtype) = if sig.len() == 65 { + let b = *sig.last().unwrap(); + let sig = secp256k1::schnorr::Signature::from_slice(&sig[0..sig.len() - 1]) + .map_err(|_| ExecError::SchnorrSig)?; + + if b == TapSighashType::Default as u8 { + return Err(ExecError::SchnorrSigHashtype); + } + //TODO(stevenroose) core does not error here + let sht = + TapSighashType::from_consensus_u8(b).map_err(|_| ExecError::SchnorrSigHashtype)?; + (sig, sht) + } else { + let sig = secp256k1::schnorr::Signature::from_slice(sig) + .map_err(|_| ExecError::SchnorrSig)?; + (sig, TapSighashType::Default) + }; + + let (leaf_hash, annex) = self.tx.taproot_annex_scriptleaf.as_ref().unwrap(); + let sighash = self + .sighashcache + .taproot_signature_hash( + self.tx.input_idx, + &Prevouts::All(&self.tx.prevouts), + annex + .as_ref() + .map(|a| Annex::new(a).expect("we checked annex prefix before")), + Some((*leaf_hash, self.last_codeseparator_pos.unwrap_or(u32::MAX))), + hashtype, + ) + .expect("TODO(stevenroose) seems to only happen if prevout index out of bound"); + + if SECP.verify_schnorr(&sig, &sighash.into(), &pk) != Ok(()) { + return Err(ExecError::SchnorrSig); + } + + Ok(()) + } +} diff --git a/bitcoin-scriptexec/src/utils.rs b/bitcoin-scriptexec/src/utils.rs new file mode 100644 index 0000000..c4cace4 --- /dev/null +++ b/bitcoin-scriptexec/src/utils.rs @@ -0,0 +1,80 @@ +/// A data type to abstract out the condition stack during script execution. +/// +/// Conceptually it acts like a vector of booleans, one for each level of nested +/// IF/THEN/ELSE, indicating whether we're in the active or inactive branch of +/// each. +/// +/// The elements on the stack cannot be observed individually; we only need to +/// expose whether the stack is empty and whether or not any false values are +/// present at all. To implement OP_ELSE, a toggle_top modifier is added, which +/// flips the last value without returning it. +/// +/// This uses an optimized implementation that does not materialize the +/// actual stack. Instead, it just stores the size of the would-be stack, +/// and the position of the first false value in it. +pub struct ConditionStack { + /// The size of the implied stack. + size: usize, + /// The position of the first false value on the implied stack, + /// or NO_FALSE if all true. + first_false_pos: usize, +} + +impl ConditionStack { + /// A constant for first_false_pos to indicate there are no falses. + const NO_FALSE: usize = usize::MAX; + + pub fn new() -> Self { + Self { + size: 0, + first_false_pos: Self::NO_FALSE, + } + } + + pub fn all_true(&self) -> bool { + self.first_false_pos == Self::NO_FALSE + } + + pub fn push(&mut self, v: bool) { + if self.first_false_pos == Self::NO_FALSE && !v { + // The stack consists of all true values, and a false is added. + // The first false value will appear at the current size. + self.first_false_pos = self.size; + } + self.size += 1; + } + + /// Returns [false] if it was empty, [true] otherwise. + /// + /// Note that the popped value is not returned. + pub fn pop(&mut self) -> bool { + if self.size == 0 { + false + } else { + self.size -= 1; + if self.first_false_pos == self.size { + // When popping off the first false value, everything becomes true. + self.first_false_pos = Self::NO_FALSE; + } + true + } + } + + pub fn toggle_top(&mut self) -> bool { + if self.size == 0 { + false + } else { + if self.first_false_pos == Self::NO_FALSE { + // The current stack is all true values; the first false will be the top. + self.first_false_pos = self.size - 1; + } else if self.first_false_pos == self.size - 1 { + // The top is the first false value; toggling it will make everything true. + self.first_false_pos = Self::NO_FALSE; + } else { + // There is a false value, but not on top. No action is needed as toggling + // anything but the first false value is unobservable. + } + true + } + } +} diff --git a/bitcoin-scriptexec/src/wasm.rs b/bitcoin-scriptexec/src/wasm.rs new file mode 100644 index 0000000..69136ad --- /dev/null +++ b/bitcoin-scriptexec/src/wasm.rs @@ -0,0 +1,104 @@ +use bitcoin::hashes::Hash; +use bitcoin::hex::{DisplayHex, FromHex}; +use bitcoin::taproot::TapLeafHash; +use bitcoin::{ScriptBuf, Transaction}; +use serde_json::json; +use wasm_bindgen::prelude::*; + +use crate::*; + +/// Compile ASM into script hex. +#[wasm_bindgen] +pub fn script_asm_to_hex(script_asm: &str) -> Result { + let script = + ScriptBuf::parse_asm(script_asm).map_err(|e| format!("error parsing script: {:?}", e))?; + Ok(script.as_bytes().as_hex().to_string()) +} + +/// Decode compiled script hex into ASM. +#[wasm_bindgen] +pub fn script_hex_to_asm(script_hex: &str) -> Result { + let script = ScriptBuf::from_hex(script_hex).map_err(|e| format!("invalid hex: {}", e))?; + Ok(script.to_asm_string()) +} + +/// Run the given script. +/// +/// Fields on the return value are: +/// - success: bool +/// - final_stack: list of hex stack items after execution +/// - error: (optional) error that caused execution halt +/// - last_opcode: (optional) last opcode run before error produced +/// - stats: execution runtime statistics with following fields: +/// - max_nb_stack_items +/// - max_stack_size +/// - max_stack_item_size +/// - start_validation_weight +/// - validation_weight +#[wasm_bindgen] +#[allow(clippy::boxed_local)] // NOTE(Velnbur): just a wasm_bindgen thing +pub fn run_script(script_hex: &str, script_witness: Box<[JsValue]>) -> Result { + console_error_panic_hook::set_once(); + + let script = + ScriptBuf::from_hex(script_hex).map_err(|e| format!("invalid hex script: {:?}", e))?; + let witness = { + let mut ret = Vec::with_capacity(script_witness.len()); + for item in script_witness.iter() { + let hex = item + .as_string() + .ok_or("script witness must be list of hex strings")?; + let bytes = Vec::from_hex(&hex).map_err(|_| "invalid hex in script witness")?; + ret.push(bytes); + } + ret + }; + + let mut exec = Exec::new( + ExecCtx::Tapscript, + Options::default(), + TxTemplate { + tx: Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::locktime::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }, + prevouts: vec![], + input_idx: 0, + taproot_annex_scriptleaf: Some((TapLeafHash::all_zeros(), None)), + }, + script, + witness, + ) + .map_err(|e| format!("error creating exec: {:?}", e))?; + + loop { + if let Err(res) = exec.exec_next() { + let res = res.clone(); + let mut ret = json!({ + "success": res.success, + "final_stack": res.final_stack.iter_str() + .map(|i| i.as_hex().to_string()) + .collect::>(), + "stats": serde_json::to_value(exec.stats()).unwrap(), + }); + if !res.success { + let obj = ret.as_object_mut().unwrap(); + obj.insert( + "last_opcode".into(), + res.opcode.map(|o| o.to_string()).unwrap_or_default().into(), + ); + obj.insert( + "error".into(), + res.error + .map(|o| format!("{:?}", o)) + .unwrap_or_default() + .into(), + ); + } + + return Ok(serde_wasm_bindgen::to_value(&ret).unwrap()); + } + } +}