From fdcfdcb9dee0ab45146a34e0fc3592e5e797a605 Mon Sep 17 00:00:00 2001 From: Tomas Szejnfeld Sirkis Date: Mon, 2 Mar 2026 12:27:25 -0300 Subject: [PATCH] Merge new updates from BitVM into bitvmx branch --- .gitignore | 4 +- Cargo.toml | 17 +- LICENSE | 4 +- README.md | 38 +- macro/Cargo.toml | 16 + {src => macro/src}/generate.rs | 7 +- macro/src/lib.rs | 15 + macro/src/parse.rs | 784 +++++++++++++++++++++++++++++++++ src/builder.rs | 436 ++++++++++++++++++ src/lib.rs | 277 +----------- src/parse.rs | 407 ----------------- tests/test.rs | 169 +++++-- 12 files changed, 1440 insertions(+), 734 deletions(-) create mode 100644 macro/Cargo.toml rename {src => macro/src}/generate.rs (88%) create mode 100644 macro/src/lib.rs create mode 100644 macro/src/parse.rs create mode 100644 src/builder.rs delete mode 100644 src/parse.rs diff --git a/.gitignore b/.gitignore index 96ef6c0..972b0c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/target -Cargo.lock +**/target +**/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 319b64b..d43972e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,29 @@ +[workspace] +members = ["macro"] + [package] name = "bitcoin-script" -version = "0.2.0" -authors = ["Matt Bell ", "Lukas George lukas@zerosync.org"] +version = "0.4.0" +authors = ["Lukas George "] edition = "2021" description = "Inline Bitcoin scripts" license = "MIT" repository = "https://github.com/FairgateLabs/rust-bitcoin-script" -[lib] -proc-macro = true +[features] +serde = ["dep:serde", "bitcoin/serde"] [dependencies] +stdext = "0.3.3" +serde = { version = "1", features = ["derive"], optional = true } bitcoin = "0.32.6" quote = "1.0.30" proc-macro-error = "1.0.4" lazy_static = "1.4.0" hex = "0.4.3" proc-macro2 = "1.0.51" +script-macro = { path = "./macro" } bitcoin-opcode-utils = { git = "https://github.com/FairgateLabs/rust-bitcoin-opcode-utils" } + +[dev-dependencies] +bincode = "1.3.3" diff --git a/LICENSE b/LICENSE index b799c50..48cfdb1 100644 --- a/LICENSE +++ b/LICENSE @@ -27,5 +27,5 @@ SOFTWARE. This project includes code with no license: - Bitcoin scripts inline in Rust - - Original source: [bitcoin-script](https://github.com/BitVM/rust-bitcoin-script_old) - - License: None \ No newline at end of file + - Original source: [bitcoin-script](https://github.com/BitVM/rust-bitcoin-script) + - License: MIT diff --git a/README.md b/README.md index 91d4a05..7c3e195 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,14 @@ -# bitcoin-script +# Bitvm Bitcoin Script -[![Rust](https://github.com/mappum/rust-bitcoin-script/workflows/Rust/badge.svg)](https://github.com/mappum/rust-bitcoin-script/actions?query=workflow%3ARust) -[![crates.io](https://img.shields.io/crates/v/bitcoin-script.svg)](https://crates.io/crates/bitcoin-script) -[![docs.rs](https://docs.rs/bitcoin-script/badge.svg)](https://docs.rs/bitcoin-script) - -**Bitcoin scripts inline in Rust.** - ---- +Utilities used in the official [BitVM](https://github.com/BitVM/BitVM) implementation to generate Bitcoin Script. Heavily inspired by [rust-bitcoin-script's inline macro](https://github.com/mappum/rust-bitcoin-script). ## Usage -This crate exports a `script!` macro which can be used to build Bitcoin scripts. The macro returns the [`Script`](https://docs.rs/bitcoin/latest/bitcoin/struct.ScriptBuf.html) type from the [`bitcoin`](https://github.com/rust-bitcoin/rust-bitcoin) crate. +This crate exports a `script!` macro which can be used to build structured Bitcoin scripts and compiled to the [`Script`](https://docs.rs/bitcoin/latest/bitcoin/struct.ScriptBuf.html) type from the [`bitcoin`](https://github.com/rust-bitcoin/rust-bitcoin) crate. **Example:** ```rust -#![feature(proc_macro_hygiene)] - use bitcoin_script::bitcoin_script; let htlc_script = script! { @@ -28,6 +20,8 @@ let htlc_script = script! { OP_EQUALVERIFY OP_CHECKSIG }; + +let script_buf = htlc_script.compile(); ``` ### Syntax @@ -77,6 +71,7 @@ Rust expressions of the following types are supported: - [`bitcoin::PublicKey`](https://docs.rs/bitcoin/latest/bitcoin/struct.PublicKey.html) - [`bitcoin::XOnlyPublicKey`](https://docs.rs/bitcoin/latest/bitcoin/struct.XOnlyPublicKey.html) - [`bitcoin::ScriptBuf`](https://docs.rs/bitcoin/latest/bitcoin/struct.ScriptBuf.html) +- `StructuredScript` ```rust let bytes = vec![1, 2, 3]; @@ -90,5 +85,22 @@ let script = script! { }; ``` -### Optimality -Obsolete Opcodes are automatically removed when using the `optimal_opcodes` branch. E.g. a sequence of `OP_0 OP_ROLL` will be optimized away. +#### Conditional Scipt Generation + +For-loops and if-else-statements are supported inside the script and will be unrolled when the scripts are generated. + +```rust +let loop_count = 10; + +let script = script! { + for i in 0..loop_count { + if i % 2 == 0 { + OP_ADD + } else { + OP_DUP + OP_ADD + } + } +}; + +``` diff --git a/macro/Cargo.toml b/macro/Cargo.toml new file mode 100644 index 0000000..914a3e8 --- /dev/null +++ b/macro/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "script-macro" +version = "0.4.0" +authors = ["Lukas George lukas@zerosync.org"] +edition = "2021" +description = "Inline Bitcoin scripts proc_macro" +license = "MIT" + +[lib] +proc-macro = true + +[dependencies] +bitcoin = "0.32.5" +quote = "1.0.23" +proc-macro-error = "1.0.4" +proc-macro2 = "1.0.51" diff --git a/src/generate.rs b/macro/src/generate.rs similarity index 88% rename from src/generate.rs rename to macro/src/generate.rs index 3320326..fd2b1dc 100644 --- a/src/generate.rs +++ b/macro/src/generate.rs @@ -4,7 +4,9 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned}; pub fn generate(syntax: Vec<(Syntax, Span)>) -> TokenStream { - let mut tokens = quote!(pushable::Builder::new()); + let mut tokens = quote!(::bitcoin_script::Script::new( + ::bitcoin_script::function_name!() + )); for (item, span) in syntax { let push = match item { @@ -15,8 +17,7 @@ pub fn generate(syntax: Vec<(Syntax, Span)>) -> TokenStream { }; tokens.extend(push); } - - tokens.extend(quote!(.0.into_script())); + // tokens.extend(quote! {.analyze_stack()}); // for debug tokens } diff --git a/macro/src/lib.rs b/macro/src/lib.rs new file mode 100644 index 0000000..11ecc44 --- /dev/null +++ b/macro/src/lib.rs @@ -0,0 +1,15 @@ +mod generate; +mod parse; + +use generate::generate; +use parse::parse; +use proc_macro::TokenStream; +use proc_macro_error::{proc_macro_error, set_dummy}; +use quote::quote; + +#[proc_macro] +#[proc_macro_error] +pub fn script(tokens: TokenStream) -> TokenStream { + set_dummy(quote!((::bitcoin::Script::new()))); + generate(parse(tokens.into())).into() +} diff --git a/macro/src/parse.rs b/macro/src/parse.rs new file mode 100644 index 0000000..460f3fa --- /dev/null +++ b/macro/src/parse.rs @@ -0,0 +1,784 @@ +use bitcoin::hex::FromHex; +use bitcoin::{ + blockdata::opcodes::Opcode, + opcodes::{all::*, OP_0, OP_FALSE, OP_NOP2, OP_NOP3, OP_TRUE}, +}; +use proc_macro2::{ + Delimiter, Span, TokenStream, + TokenTree::{self, *}, +}; +use quote::quote; +use std::iter::Peekable; +use std::str::FromStr; + +#[derive(Debug)] +pub enum Syntax { + Opcode(Opcode), + Escape(TokenStream), + Bytes(Vec), + Int(i64), +} + +macro_rules! emit_error { + ($span:expr, $($message:expr),*) => {{ + #[cfg(not(test))] + proc_macro_error::emit_error!($span, $($message),*); + + #[cfg(test)] + panic!($($message),*); + + #[allow(unreachable_code)] + { + panic!(); + } + }} +} + +macro_rules! abort { + ($span:expr, $($message:expr),*) => {{ + #[cfg(not(test))] + proc_macro_error::abort!($span, $($message),*); + + #[cfg(test)] + panic!($($message),*); + }} +} + +/// Generates a function that parses a string into an [`Opcode`]. +macro_rules! generate_opcode_parser { + ($($op:ident => $val:expr, $doc:expr);*) => { + fn parse_opcode(s: &str) -> Result { + match s { + // Special cases with aliases + "OP_0" => Ok(OP_0), + "OP_TRUE" | "TRUE" => Ok(OP_TRUE), + "OP_FALSE" | "FALSE" => Ok(OP_FALSE), + "OP_NOP2" | "NOP2" => Ok(OP_NOP2), + "OP_NOP3" | "NOP3" => Ok(OP_NOP3), + "OP_1" => Ok(OP_PUSHNUM_1), + "OP_2" => Ok(OP_PUSHNUM_2), + "OP_3" => Ok(OP_PUSHNUM_3), + "OP_4" => Ok(OP_PUSHNUM_4), + "OP_5" => Ok(OP_PUSHNUM_5), + "OP_6" => Ok(OP_PUSHNUM_6), + "OP_7" => Ok(OP_PUSHNUM_7), + "OP_8" => Ok(OP_PUSHNUM_8), + "OP_9" => Ok(OP_PUSHNUM_9), + "OP_10" => Ok(OP_PUSHNUM_10), + "OP_11" => Ok(OP_PUSHNUM_11), + "OP_12" => Ok(OP_PUSHNUM_12), + "OP_13" => Ok(OP_PUSHNUM_13), + "OP_14" => Ok(OP_PUSHNUM_14), + "OP_15" => Ok(OP_PUSHNUM_15), + "OP_16" => Ok(OP_PUSHNUM_16), + $( + // For all other opcodes, match both with and without OP_ prefix + s if s == stringify!($op) || s == &stringify!($op)[3..] => Ok($op), + )* + _ => Err(()), + } + } + } +} + +generate_opcode_parser! { + OP_PUSHBYTES_0 => 0x00, "Push an empty array onto the stack."; + OP_PUSHBYTES_1 => 0x01, "Push the next byte as an array onto the stack."; + OP_PUSHBYTES_2 => 0x02, "Push the next 2 bytes as an array onto the stack."; + OP_PUSHBYTES_3 => 0x03, "Push the next 3 bytes as an array onto the stack."; + OP_PUSHBYTES_4 => 0x04, "Push the next 4 bytes as an array onto the stack."; + OP_PUSHBYTES_5 => 0x05, "Push the next 5 bytes as an array onto the stack."; + OP_PUSHBYTES_6 => 0x06, "Push the next 6 bytes as an array onto the stack."; + OP_PUSHBYTES_7 => 0x07, "Push the next 7 bytes as an array onto the stack."; + OP_PUSHBYTES_8 => 0x08, "Push the next 8 bytes as an array onto the stack."; + OP_PUSHBYTES_9 => 0x09, "Push the next 9 bytes as an array onto the stack."; + OP_PUSHBYTES_10 => 0x0a, "Push the next 10 bytes as an array onto the stack."; + OP_PUSHBYTES_11 => 0x0b, "Push the next 11 bytes as an array onto the stack."; + OP_PUSHBYTES_12 => 0x0c, "Push the next 12 bytes as an array onto the stack."; + OP_PUSHBYTES_13 => 0x0d, "Push the next 13 bytes as an array onto the stack."; + OP_PUSHBYTES_14 => 0x0e, "Push the next 14 bytes as an array onto the stack."; + OP_PUSHBYTES_15 => 0x0f, "Push the next 15 bytes as an array onto the stack."; + OP_PUSHBYTES_16 => 0x10, "Push the next 16 bytes as an array onto the stack."; + OP_PUSHBYTES_17 => 0x11, "Push the next 17 bytes as an array onto the stack."; + OP_PUSHBYTES_18 => 0x12, "Push the next 18 bytes as an array onto the stack."; + OP_PUSHBYTES_19 => 0x13, "Push the next 19 bytes as an array onto the stack."; + OP_PUSHBYTES_20 => 0x14, "Push the next 20 bytes as an array onto the stack."; + OP_PUSHBYTES_21 => 0x15, "Push the next 21 bytes as an array onto the stack."; + OP_PUSHBYTES_22 => 0x16, "Push the next 22 bytes as an array onto the stack."; + OP_PUSHBYTES_23 => 0x17, "Push the next 23 bytes as an array onto the stack."; + OP_PUSHBYTES_24 => 0x18, "Push the next 24 bytes as an array onto the stack."; + OP_PUSHBYTES_25 => 0x19, "Push the next 25 bytes as an array onto the stack."; + OP_PUSHBYTES_26 => 0x1a, "Push the next 26 bytes as an array onto the stack."; + OP_PUSHBYTES_27 => 0x1b, "Push the next 27 bytes as an array onto the stack."; + OP_PUSHBYTES_28 => 0x1c, "Push the next 28 bytes as an array onto the stack."; + OP_PUSHBYTES_29 => 0x1d, "Push the next 29 bytes as an array onto the stack."; + OP_PUSHBYTES_30 => 0x1e, "Push the next 30 bytes as an array onto the stack."; + OP_PUSHBYTES_31 => 0x1f, "Push the next 31 bytes as an array onto the stack."; + OP_PUSHBYTES_32 => 0x20, "Push the next 32 bytes as an array onto the stack."; + OP_PUSHBYTES_33 => 0x21, "Push the next 33 bytes as an array onto the stack."; + OP_PUSHBYTES_34 => 0x22, "Push the next 34 bytes as an array onto the stack."; + OP_PUSHBYTES_35 => 0x23, "Push the next 35 bytes as an array onto the stack."; + OP_PUSHBYTES_36 => 0x24, "Push the next 36 bytes as an array onto the stack."; + OP_PUSHBYTES_37 => 0x25, "Push the next 37 bytes as an array onto the stack."; + OP_PUSHBYTES_38 => 0x26, "Push the next 38 bytes as an array onto the stack."; + OP_PUSHBYTES_39 => 0x27, "Push the next 39 bytes as an array onto the stack."; + OP_PUSHBYTES_40 => 0x28, "Push the next 40 bytes as an array onto the stack."; + OP_PUSHBYTES_41 => 0x29, "Push the next 41 bytes as an array onto the stack."; + OP_PUSHBYTES_42 => 0x2a, "Push the next 42 bytes as an array onto the stack."; + OP_PUSHBYTES_43 => 0x2b, "Push the next 43 bytes as an array onto the stack."; + OP_PUSHBYTES_44 => 0x2c, "Push the next 44 bytes as an array onto the stack."; + OP_PUSHBYTES_45 => 0x2d, "Push the next 45 bytes as an array onto the stack."; + OP_PUSHBYTES_46 => 0x2e, "Push the next 46 bytes as an array onto the stack."; + OP_PUSHBYTES_47 => 0x2f, "Push the next 47 bytes as an array onto the stack."; + OP_PUSHBYTES_48 => 0x30, "Push the next 48 bytes as an array onto the stack."; + OP_PUSHBYTES_49 => 0x31, "Push the next 49 bytes as an array onto the stack."; + OP_PUSHBYTES_50 => 0x32, "Push the next 50 bytes as an array onto the stack."; + OP_PUSHBYTES_51 => 0x33, "Push the next 51 bytes as an array onto the stack."; + OP_PUSHBYTES_52 => 0x34, "Push the next 52 bytes as an array onto the stack."; + OP_PUSHBYTES_53 => 0x35, "Push the next 53 bytes as an array onto the stack."; + OP_PUSHBYTES_54 => 0x36, "Push the next 54 bytes as an array onto the stack."; + OP_PUSHBYTES_55 => 0x37, "Push the next 55 bytes as an array onto the stack."; + OP_PUSHBYTES_56 => 0x38, "Push the next 56 bytes as an array onto the stack."; + OP_PUSHBYTES_57 => 0x39, "Push the next 57 bytes as an array onto the stack."; + OP_PUSHBYTES_58 => 0x3a, "Push the next 58 bytes as an array onto the stack."; + OP_PUSHBYTES_59 => 0x3b, "Push the next 59 bytes as an array onto the stack."; + OP_PUSHBYTES_60 => 0x3c, "Push the next 60 bytes as an array onto the stack."; + OP_PUSHBYTES_61 => 0x3d, "Push the next 61 bytes as an array onto the stack."; + OP_PUSHBYTES_62 => 0x3e, "Push the next 62 bytes as an array onto the stack."; + OP_PUSHBYTES_63 => 0x3f, "Push the next 63 bytes as an array onto the stack."; + OP_PUSHBYTES_64 => 0x40, "Push the next 64 bytes as an array onto the stack."; + OP_PUSHBYTES_65 => 0x41, "Push the next 65 bytes as an array onto the stack."; + OP_PUSHBYTES_66 => 0x42, "Push the next 66 bytes as an array onto the stack."; + OP_PUSHBYTES_67 => 0x43, "Push the next 67 bytes as an array onto the stack."; + OP_PUSHBYTES_68 => 0x44, "Push the next 68 bytes as an array onto the stack."; + OP_PUSHBYTES_69 => 0x45, "Push the next 69 bytes as an array onto the stack."; + OP_PUSHBYTES_70 => 0x46, "Push the next 70 bytes as an array onto the stack."; + OP_PUSHBYTES_71 => 0x47, "Push the next 71 bytes as an array onto the stack."; + OP_PUSHBYTES_72 => 0x48, "Push the next 72 bytes as an array onto the stack."; + OP_PUSHBYTES_73 => 0x49, "Push the next 73 bytes as an array onto the stack."; + OP_PUSHBYTES_74 => 0x4a, "Push the next 74 bytes as an array onto the stack."; + OP_PUSHBYTES_75 => 0x4b, "Push the next 75 bytes as an array onto the stack."; + OP_PUSHDATA1 => 0x4c, "Read the next byte as N; push the next N bytes as an array onto the stack."; + OP_PUSHDATA2 => 0x4d, "Read the next 2 bytes as N; push the next N bytes as an array onto the stack."; + OP_PUSHDATA4 => 0x4e, "Read the next 4 bytes as N; push the next N bytes as an array onto the stack."; + OP_PUSHNUM_NEG1 => 0x4f, "Push the array `0x81` onto the stack."; + OP_RESERVED => 0x50, "Synonym for OP_RETURN."; + OP_PUSHNUM_1 => 0x51, "Push the array `0x01` onto the stack."; + OP_PUSHNUM_2 => 0x52, "Push the array `0x02` onto the stack."; + OP_PUSHNUM_3 => 0x53, "Push the array `0x03` onto the stack."; + OP_PUSHNUM_4 => 0x54, "Push the array `0x04` onto the stack."; + OP_PUSHNUM_5 => 0x55, "Push the array `0x05` onto the stack."; + OP_PUSHNUM_6 => 0x56, "Push the array `0x06` onto the stack."; + OP_PUSHNUM_7 => 0x57, "Push the array `0x07` onto the stack."; + OP_PUSHNUM_8 => 0x58, "Push the array `0x08` onto the stack."; + OP_PUSHNUM_9 => 0x59, "Push the array `0x09` onto the stack."; + OP_PUSHNUM_10 => 0x5a, "Push the array `0x0a` onto the stack."; + OP_PUSHNUM_11 => 0x5b, "Push the array `0x0b` onto the stack."; + OP_PUSHNUM_12 => 0x5c, "Push the array `0x0c` onto the stack."; + OP_PUSHNUM_13 => 0x5d, "Push the array `0x0d` onto the stack."; + OP_PUSHNUM_14 => 0x5e, "Push the array `0x0e` onto the stack."; + OP_PUSHNUM_15 => 0x5f, "Push the array `0x0f` onto the stack."; + OP_PUSHNUM_16 => 0x60, "Push the array `0x10` onto the stack."; + OP_NOP => 0x61, "Does nothing."; + OP_VER => 0x62, "Synonym for OP_RETURN."; + OP_IF => 0x63, "Pop and execute the next statements if a nonzero element was popped."; + OP_NOTIF => 0x64, "Pop and execute the next statements if a zero element was popped."; + OP_VERIF => 0x65, "Fail the script unconditionally, does not even need to be executed."; + OP_VERNOTIF => 0x66, "Fail the script unconditionally, does not even need to be executed."; + OP_ELSE => 0x67, "Execute statements if those after the previous OP_IF were not, and vice-versa. \ + If there is no previous OP_IF, this acts as a RETURN."; + OP_ENDIF => 0x68, "Pop and execute the next statements if a zero element was popped."; + OP_VERIFY => 0x69, "If the top value is zero or the stack is empty, fail; otherwise, pop the stack."; + OP_RETURN => 0x6a, "Fail the script immediately. (Must be executed.)."; + OP_TOALTSTACK => 0x6b, "Pop one element from the main stack onto the alt stack."; + OP_FROMALTSTACK => 0x6c, "Pop one element from the alt stack onto the main stack."; + OP_2DROP => 0x6d, "Drops the top two stack items."; + OP_2DUP => 0x6e, "Duplicates the top two stack items as AB -> ABAB."; + OP_3DUP => 0x6f, "Duplicates the two three stack items as ABC -> ABCABC."; + OP_2OVER => 0x70, "Copies the two stack items of items two spaces back to the front, as xxAB -> ABxxAB."; + OP_2ROT => 0x71, "Moves the two stack items four spaces back to the front, as xxxxAB -> ABxxxx."; + OP_2SWAP => 0x72, "Swaps the top two pairs, as ABCD -> CDAB."; + OP_IFDUP => 0x73, "Duplicate the top stack element unless it is zero."; + OP_DEPTH => 0x74, "Push the current number of stack items onto the stack."; + OP_DROP => 0x75, "Drops the top stack item."; + OP_DUP => 0x76, "Duplicates the top stack item."; + OP_NIP => 0x77, "Drops the second-to-top stack item."; + OP_OVER => 0x78, "Copies the second-to-top stack item, as xA -> AxA."; + OP_PICK => 0x79, "Pop the top stack element as N. Copy the Nth stack element to the top."; + OP_ROLL => 0x7a, "Pop the top stack element as N. Move the Nth stack element to the top."; + OP_ROT => 0x7b, "Rotate the top three stack items, as [top next1 next2] -> [next2 top next1]."; + OP_SWAP => 0x7c, "Swap the top two stack items."; + OP_TUCK => 0x7d, "Copy the top stack item to before the second item, as [top next] -> [top next top]."; + OP_CAT => 0x7e, "Fail the script unconditionally, does not even need to be executed."; + OP_SUBSTR => 0x7f, "Fail the script unconditionally, does not even need to be executed."; + OP_LEFT => 0x80, "Fail the script unconditionally, does not even need to be executed."; + OP_RIGHT => 0x81, "Fail the script unconditionally, does not even need to be executed."; + OP_SIZE => 0x82, "Pushes the length of the top stack item onto the stack."; + OP_INVERT => 0x83, "Fail the script unconditionally, does not even need to be executed."; + OP_AND => 0x84, "Fail the script unconditionally, does not even need to be executed."; + OP_OR => 0x85, "Fail the script unconditionally, does not even need to be executed."; + OP_XOR => 0x86, "Fail the script unconditionally, does not even need to be executed."; + OP_EQUAL => 0x87, "Pushes 1 if the inputs are exactly equal, 0 otherwise."; + OP_EQUALVERIFY => 0x88, "Returns success if the inputs are exactly equal, failure otherwise."; + OP_RESERVED1 => 0x89, "Synonym for OP_RETURN."; + OP_RESERVED2 => 0x8a, "Synonym for OP_RETURN."; + OP_1ADD => 0x8b, "Increment the top stack element in place."; + OP_1SUB => 0x8c, "Decrement the top stack element in place."; + OP_2MUL => 0x8d, "Fail the script unconditionally, does not even need to be executed."; + OP_2DIV => 0x8e, "Fail the script unconditionally, does not even need to be executed."; + OP_NEGATE => 0x8f, "Multiply the top stack item by -1 in place."; + OP_ABS => 0x90, "Absolute value the top stack item in place."; + OP_NOT => 0x91, "Map 0 to 1 and everything else to 0, in place."; + OP_0NOTEQUAL => 0x92, "Map 0 to 0 and everything else to 1, in place."; + OP_ADD => 0x93, "Pop two stack items and push their sum."; + OP_SUB => 0x94, "Pop two stack items and push the second minus the top."; + OP_MUL => 0x95, "Fail the script unconditionally, does not even need to be executed."; + OP_DIV => 0x96, "Fail the script unconditionally, does not even need to be executed."; + OP_MOD => 0x97, "Fail the script unconditionally, does not even need to be executed."; + OP_LSHIFT => 0x98, "Fail the script unconditionally, does not even need to be executed."; + OP_RSHIFT => 0x99, "Fail the script unconditionally, does not even need to be executed."; + OP_BOOLAND => 0x9a, "Pop the top two stack items and push 1 if both are nonzero, else push 0."; + OP_BOOLOR => 0x9b, "Pop the top two stack items and push 1 if either is nonzero, else push 0."; + OP_NUMEQUAL => 0x9c, "Pop the top two stack items and push 1 if both are numerically equal, else push 0."; + OP_NUMEQUALVERIFY => 0x9d, "Pop the top two stack items and return success if both are numerically equal, else return failure."; + OP_NUMNOTEQUAL => 0x9e, "Pop the top two stack items and push 0 if both are numerically equal, else push 1."; + OP_LESSTHAN => 0x9f, "Pop the top two items; push 1 if the second is less than the top, 0 otherwise."; + OP_GREATERTHAN => 0xa0, "Pop the top two items; push 1 if the second is greater than the top, 0 otherwise."; + OP_LESSTHANOREQUAL => 0xa1, "Pop the top two items; push 1 if the second is <= the top, 0 otherwise."; + OP_GREATERTHANOREQUAL => 0xa2, "Pop the top two items; push 1 if the second is >= the top, 0 otherwise."; + OP_MIN => 0xa3, "Pop the top two items; push the smaller."; + OP_MAX => 0xa4, "Pop the top two items; push the larger."; + OP_WITHIN => 0xa5, "Pop the top three items; if the top is >= the second and < the third, push 1, otherwise push 0."; + OP_RIPEMD160 => 0xa6, "Pop the top stack item and push its RIPEMD160 hash."; + OP_SHA1 => 0xa7, "Pop the top stack item and push its SHA1 hash."; + OP_SHA256 => 0xa8, "Pop the top stack item and push its SHA256 hash."; + OP_HASH160 => 0xa9, "Pop the top stack item and push its RIPEMD(SHA256) hash."; + OP_HASH256 => 0xaa, "Pop the top stack item and push its SHA256(SHA256) hash."; + OP_CODESEPARATOR => 0xab, "Ignore this and everything preceding when deciding what to sign when signature-checking."; + OP_CHECKSIG => 0xac, " pushing 1/0 for success/failure."; + OP_CHECKSIGVERIFY => 0xad, " returning success/failure."; + OP_CHECKMULTISIG => 0xae, "Pop N, N pubkeys, M, M signatures, a dummy (due to bug in reference code), \ + and verify that all M signatures are valid. Push 1 for 'all valid', 0 otherwise."; + OP_CHECKMULTISIGVERIFY => 0xaf, "Like the above but return success/failure."; + OP_NOP1 => 0xb0, "Does nothing."; + OP_CLTV => 0xb1, ""; + OP_CSV => 0xb2, ""; + OP_NOP4 => 0xb3, "Does nothing."; + OP_NOP5 => 0xb4, "Does nothing."; + OP_NOP6 => 0xb5, "Does nothing."; + OP_NOP7 => 0xb6, "Does nothing."; + OP_NOP8 => 0xb7, "Does nothing."; + OP_NOP9 => 0xb8, "Does nothing."; + OP_NOP10 => 0xb9, "Does nothing."; + // Every other opcode acts as OP_RETURN + OP_CHECKSIGADD => 0xba, "OP_CHECKSIGADD post tapscript."; + OP_RETURN_187 => 0xbb, "Synonym for OP_RETURN."; + OP_RETURN_188 => 0xbc, "Synonym for OP_RETURN."; + OP_RETURN_189 => 0xbd, "Synonym for OP_RETURN."; + OP_RETURN_190 => 0xbe, "Synonym for OP_RETURN."; + OP_RETURN_191 => 0xbf, "Synonym for OP_RETURN."; + OP_RETURN_192 => 0xc0, "Synonym for OP_RETURN."; + OP_RETURN_193 => 0xc1, "Synonym for OP_RETURN."; + OP_RETURN_194 => 0xc2, "Synonym for OP_RETURN."; + OP_RETURN_195 => 0xc3, "Synonym for OP_RETURN."; + OP_RETURN_196 => 0xc4, "Synonym for OP_RETURN."; + OP_RETURN_197 => 0xc5, "Synonym for OP_RETURN."; + OP_RETURN_198 => 0xc6, "Synonym for OP_RETURN."; + OP_RETURN_199 => 0xc7, "Synonym for OP_RETURN."; + OP_RETURN_200 => 0xc8, "Synonym for OP_RETURN."; + OP_RETURN_201 => 0xc9, "Synonym for OP_RETURN."; + OP_RETURN_202 => 0xca, "Synonym for OP_RETURN."; + OP_RETURN_203 => 0xcb, "Synonym for OP_RETURN."; + OP_RETURN_204 => 0xcc, "Synonym for OP_RETURN."; + OP_RETURN_205 => 0xcd, "Synonym for OP_RETURN."; + OP_RETURN_206 => 0xce, "Synonym for OP_RETURN."; + OP_RETURN_207 => 0xcf, "Synonym for OP_RETURN."; + OP_RETURN_208 => 0xd0, "Synonym for OP_RETURN."; + OP_RETURN_209 => 0xd1, "Synonym for OP_RETURN."; + OP_RETURN_210 => 0xd2, "Synonym for OP_RETURN."; + OP_RETURN_211 => 0xd3, "Synonym for OP_RETURN."; + OP_RETURN_212 => 0xd4, "Synonym for OP_RETURN."; + OP_RETURN_213 => 0xd5, "Synonym for OP_RETURN."; + OP_RETURN_214 => 0xd6, "Synonym for OP_RETURN."; + OP_RETURN_215 => 0xd7, "Synonym for OP_RETURN."; + OP_RETURN_216 => 0xd8, "Synonym for OP_RETURN."; + OP_RETURN_217 => 0xd9, "Synonym for OP_RETURN."; + OP_RETURN_218 => 0xda, "Synonym for OP_RETURN."; + OP_RETURN_219 => 0xdb, "Synonym for OP_RETURN."; + OP_RETURN_220 => 0xdc, "Synonym for OP_RETURN."; + OP_RETURN_221 => 0xdd, "Synonym for OP_RETURN."; + OP_RETURN_222 => 0xde, "Synonym for OP_RETURN."; + OP_RETURN_223 => 0xdf, "Synonym for OP_RETURN."; + OP_RETURN_224 => 0xe0, "Synonym for OP_RETURN."; + OP_RETURN_225 => 0xe1, "Synonym for OP_RETURN."; + OP_RETURN_226 => 0xe2, "Synonym for OP_RETURN."; + OP_RETURN_227 => 0xe3, "Synonym for OP_RETURN."; + OP_RETURN_228 => 0xe4, "Synonym for OP_RETURN."; + OP_RETURN_229 => 0xe5, "Synonym for OP_RETURN."; + OP_RETURN_230 => 0xe6, "Synonym for OP_RETURN."; + OP_RETURN_231 => 0xe7, "Synonym for OP_RETURN."; + OP_RETURN_232 => 0xe8, "Synonym for OP_RETURN."; + OP_RETURN_233 => 0xe9, "Synonym for OP_RETURN."; + OP_RETURN_234 => 0xea, "Synonym for OP_RETURN."; + OP_RETURN_235 => 0xeb, "Synonym for OP_RETURN."; + OP_RETURN_236 => 0xec, "Synonym for OP_RETURN."; + OP_RETURN_237 => 0xed, "Synonym for OP_RETURN."; + OP_RETURN_238 => 0xee, "Synonym for OP_RETURN."; + OP_RETURN_239 => 0xef, "Synonym for OP_RETURN."; + OP_RETURN_240 => 0xf0, "Synonym for OP_RETURN."; + OP_RETURN_241 => 0xf1, "Synonym for OP_RETURN."; + OP_RETURN_242 => 0xf2, "Synonym for OP_RETURN."; + OP_RETURN_243 => 0xf3, "Synonym for OP_RETURN."; + OP_RETURN_244 => 0xf4, "Synonym for OP_RETURN."; + OP_RETURN_245 => 0xf5, "Synonym for OP_RETURN."; + OP_RETURN_246 => 0xf6, "Synonym for OP_RETURN."; + OP_RETURN_247 => 0xf7, "Synonym for OP_RETURN."; + OP_RETURN_248 => 0xf8, "Synonym for OP_RETURN."; + OP_RETURN_249 => 0xf9, "Synonym for OP_RETURN."; + OP_RETURN_250 => 0xfa, "Synonym for OP_RETURN."; + OP_RETURN_251 => 0xfb, "Synonym for OP_RETURN."; + OP_RETURN_252 => 0xfc, "Synonym for OP_RETURN."; + OP_RETURN_253 => 0xfd, "Synonym for OP_RETURN."; + OP_RETURN_254 => 0xfe, "Synonym for OP_RETURN."; + OP_INVALIDOPCODE => 0xff, "Synonym for OP_RETURN." +} + +pub fn parse(tokens: TokenStream) -> Vec<(Syntax, Span)> { + let mut tokens = tokens.into_iter().peekable(); + let mut syntax = Vec::with_capacity(2048); + + while let Some(token) = tokens.next() { + let token_str = token.to_string(); + syntax.push(match (&token, token_str.as_ref()) { + // Wrap for loops such that they return a Vec + (Ident(_), "for") => parse_for_loop(token, &mut tokens), + // Wrap if-else statements such that they return a Vec + (Ident(_), "if") => parse_if(token, &mut tokens), + // Replace DEBUG with OP_RESERVED + (Ident(_), "DEBUG") => (Syntax::Opcode(OP_RESERVED), token.span()), + + // identifier, look up opcode + (Ident(_), _) => match parse_opcode(&token_str) { + Ok(opcode) => (Syntax::Opcode(opcode), token.span()), + Err(_) => { + let span = token.span(); + let mut pseudo_stream = TokenStream::from(token); + pseudo_stream.extend(TokenStream::from_str("()")); + (Syntax::Escape(pseudo_stream), span) + } + }, + + (Group(inner), _) => { + let escape = inner.stream().clone(); + (Syntax::Escape(escape), token.span()) + } + + // '<', start of escape (parse until first '>') + (Punct(_), "<") => parse_escape(token, &mut tokens), + + // '~' start of escape (parse until the next '~') ignores '<' and '>' + (Punct(_), "~") => parse_escape_extra(token, &mut tokens), + + // literal, push data (int or bytes) + (Literal(_), _) => parse_data(token), + + // negative sign, parse negative int + (Punct(_), "-") => parse_negative_int(token, &mut tokens), + + // anything else is invalid + _ => abort!(token.span(), "unexpected token"), + }); + } + syntax +} + +fn parse_if(token: TokenTree, tokens: &mut Peekable) -> (Syntax, Span) +where + T: Iterator, +{ + // Use a Vec here to get rid of warnings when the variable is overwritten + let mut escape = quote! { + let mut script_var = bitcoin_script::Script::new("if"); + }; + escape.extend(std::iter::once(token.clone())); + + while let Some(if_token) = tokens.next() { + match if_token { + Group(block) if block.delimiter() == Delimiter::Brace => { + let inner_block = block.stream(); + escape.extend(quote! { + { + script_var = script_var.push_env_script(script! { + #inner_block + }); + } + }); + + match tokens.peek() { + Some(else_token) if else_token.to_string().as_str() == "else" => continue, + _ => break, + } + } + _ => { + escape.extend(std::iter::once(if_token)); + continue; + } + }; + } + escape = quote! { + { + #escape; + script_var + } + }; + (Syntax::Escape(escape), token.span()) +} + +fn parse_for_loop(token: TokenTree, tokens: &mut T) -> (Syntax, Span) +where + T: Iterator, +{ + let mut escape = quote! { + let mut script_var = bitcoin_script::Script::new("for"); + }; + escape.extend(std::iter::once(token.clone())); + + for for_token in tokens.by_ref() { + match for_token { + Group(block) if block.delimiter() == Delimiter::Brace => { + let inner_block = block.stream(); + escape.extend(quote! { + { + script_var = script_var.push_env_script(script !{ + #inner_block + }); + } + script_var + }); + break; + } + _ => { + escape.extend(std::iter::once(for_token)); + continue; + } + }; + } + + (Syntax::Escape(quote! { { #escape } }), token.span()) +} + +fn parse_escape(token: TokenTree, tokens: &mut T) -> (Syntax, Span) +where + T: Iterator, +{ + let mut escape = TokenStream::new(); + let mut span = token.span(); + + loop { + let token = tokens + .next() + .unwrap_or_else(|| abort!(token.span(), "unterminated escape")); + let token_str = token.to_string(); + + span = span.join(token.span()).unwrap_or(token.span()); + + // end of escape + if let (Punct(_), ">") = (&token, token_str.as_ref()) { + break; + } + + escape.extend(TokenStream::from(token)); + } + + (Syntax::Escape(escape), span) +} + +fn parse_escape_extra(token: TokenTree, tokens: &mut T) -> (Syntax, Span) +where + T: Iterator, +{ + let mut escape = TokenStream::new(); + let mut span = token.span(); + + loop { + let token = tokens + .next() + .unwrap_or_else(|| abort!(token.span(), "unterminated escape")); + let token_str = token.to_string(); + + span = span.join(token.span()).unwrap_or(token.span()); + + // end of escape + if let (Punct(_), "~") = (&token, token_str.as_ref()) { + break; + } + + escape.extend(TokenStream::from(token)); + } + + (Syntax::Escape(escape), span) +} + +fn parse_data(token: TokenTree) -> (Syntax, Span) { + if token.to_string().starts_with("0x") { + if token + .to_string() + .strip_prefix("0x") + .unwrap_or_else(|| unreachable!()) + .trim_start_matches('0') + .len() + <= 8 + { + parse_hex_int(token) + } else { + parse_bytes(token) + } + } else { + parse_int(token, false) + } +} + +fn parse_bytes(token: TokenTree) -> (Syntax, Span) { + let hex_bytes = &token.to_string()[2..]; + let bytes = Vec::::from_hex(hex_bytes).unwrap_or_else(|err| { + emit_error!(token.span(), "invalid hex literal ({})", err); + }); + (Syntax::Bytes(bytes), token.span()) +} + +fn parse_hex_int(token: TokenTree) -> (Syntax, Span) { + let token_str = &token.to_string()[2..]; + let n: u32 = u32::from_str_radix(token_str, 16).unwrap_or_else(|err| { + emit_error!(token.span(), "invalid hex string ({})", err); + }); + (Syntax::Int(n as i64), token.span()) +} + +fn parse_int(token: TokenTree, negative: bool) -> (Syntax, Span) { + let token_str = token.to_string(); + let n: i64 = token_str.parse().unwrap_or_else(|err| { + emit_error!(token.span(), "invalid number literal ({})", err); + }); + let n = if negative { -n } else { n }; + (Syntax::Int(n), token.span()) +} + +fn parse_negative_int(token: TokenTree, tokens: &mut T) -> (Syntax, Span) +where + T: Iterator, +{ + let fail = || { + #[allow(unused_variables)] + let span = token.span(); + emit_error!( + span, + "expected negative sign to be followed by number literal" + ); + }; + + let maybe_token = tokens.next(); + + if let Some(token) = maybe_token { + if let Literal(_) = token { + parse_int(token, true) + } else { + fail() + } + } else { + fail() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::blockdata::opcodes::all as opcodes; + use quote::quote; + + macro_rules! test_opcode { + ($name:ident, $input:expr, $expected:expr) => { + #[test] + fn $name() { + let syntax = parse(quote!($input)); + if let Syntax::Opcode(opcode) = &syntax[0].0 { + assert_eq!(*opcode, $expected); + } else { + panic!("Expected Syntax::Opcode, got {:?}", syntax[0].0); + } + } + }; + } + + macro_rules! test_invalid_opcode { + ($name:ident, $input:expr) => { + #[test] + fn $name() { + let syntax = parse(quote!($input)); + assert!(matches!(syntax[0].0, Syntax::Escape(_))); + } + }; + } + + #[test] + fn parse_empty() { + assert!(parse(quote!()).is_empty()); + } + + #[test] + #[should_panic(expected = "unexpected token")] + fn parse_unexpected_token() { + parse(quote!(OP_CHECKSIG &)); + } + + //#[test] + //#[should_panic(expected = "unknown opcode \"A\"")] + //fn parse_invalid_opcode() { + // parse(quote!(OP_CHECKSIG A B)); + //} + + // Basic opcode tests + test_opcode!(parse_op_0, OP_0, OP_0); + test_opcode!(parse_op_false, FALSE, OP_FALSE); + test_opcode!(parse_op_true, TRUE, OP_TRUE); + test_opcode!(parse_op_checksig, OP_CHECKSIG, OP_CHECKSIG); + test_opcode!(parse_op_hash160, OP_HASH160, OP_HASH160); + + // Test numeric opcodes + test_opcode!(parse_op_1, OP_1, OP_PUSHNUM_1); + test_opcode!(parse_op_2, OP_2, OP_PUSHNUM_2); + test_opcode!(parse_op_3, OP_3, OP_PUSHNUM_3); + test_opcode!(parse_op_16, OP_16, OP_PUSHNUM_16); + + // Test aliases + test_opcode!(parse_checksig_no_prefix, CHECKSIG, OP_CHECKSIG); + test_opcode!(parse_hash160_no_prefix, HASH160, OP_HASH160); + + // Test special cases + test_opcode!(parse_nop2, OP_NOP2, OP_CLTV); + test_opcode!(parse_nop3, OP_NOP3, OP_CSV); + test_opcode!(parse_debug, DEBUG, OP_RESERVED); + + // Test invalid opcodes + test_invalid_opcode!(parse_invalid_opcode, INVALID_OPCODE); + test_invalid_opcode!(parse_unknown_identifier, UNKNOWN); + + // Test complex scripts + #[test] + fn parse_complex_script() { + let syntax = parse(quote! { + OP_DUP OP_HASH160 0x14 0x89abcdef89abcdef89abcdef89abcdef89abcdef OP_EQUALVERIFY OP_CHECKSIG + }); + + assert_eq!(syntax.len(), 6); + assert!(matches!(syntax[0].0, Syntax::Opcode(OP_DUP))); + assert!(matches!(syntax[1].0, Syntax::Opcode(OP_HASH160))); + assert!(matches!(syntax[2].0, Syntax::Int(20))); // 0x14 = 20 + assert!(matches!(syntax[3].0, Syntax::Bytes(_))); + assert!(matches!(syntax[4].0, Syntax::Opcode(OP_EQUALVERIFY))); + assert!(matches!(syntax[5].0, Syntax::Opcode(OP_CHECKSIG))); + } + + #[test] + fn parse_p2pkh_script() { + let syntax = parse(quote! { + OP_DUP + OP_HASH160 + + OP_EQUALVERIFY + OP_CHECKSIG + }); + + assert_eq!(syntax.len(), 5); + assert!(matches!(syntax[0].0, Syntax::Opcode(OP_DUP))); + assert!(matches!(syntax[1].0, Syntax::Opcode(OP_HASH160))); + assert!(matches!(syntax[2].0, Syntax::Escape(_))); + assert!(matches!(syntax[3].0, Syntax::Opcode(OP_EQUALVERIFY))); + assert!(matches!(syntax[4].0, Syntax::Opcode(OP_CHECKSIG))); + } + + #[test] + fn parse_opcodes() { + let syntax = parse(quote!(OP_CHECKSIG OP_HASH160)); + + if let Syntax::Opcode(opcode) = syntax[0].0 { + assert_eq!(opcode, opcodes::OP_CHECKSIG); + } else { + panic!(); + } + + if let Syntax::Opcode(opcode) = syntax[1].0 { + assert_eq!(opcode, opcodes::OP_HASH160); + } else { + panic!(); + } + } + + #[test] + #[should_panic(expected = "unterminated escape")] + fn parse_unterminated_escape() { + parse(quote!(OP_CHECKSIG < abc)); + } + + #[test] + fn parse_escape() { + let syntax = parse(quote!(OP_CHECKSIG)); + + if let Syntax::Escape(tokens) = &syntax[1].0 { + let tokens = tokens.clone().into_iter().collect::>(); + + assert_eq!(tokens.len(), 1); + if let TokenTree::Ident(_) = tokens[0] { + assert_eq!(tokens[0].to_string(), "abc"); + } else { + panic!() + } + } else { + panic!() + } + } + + #[test] + #[should_panic(expected = "invalid number literal (invalid digit found in string)")] + fn parse_invalid_int() { + parse(quote!(OP_CHECKSIG 12g34)); + } + + #[test] + fn parse_int() { + let syntax = parse(quote!(OP_CHECKSIG 1234)); + + if let Syntax::Int(n) = syntax[1].0 { + assert_eq!(n, 1234i64); + } else { + panic!() + } + } + + #[test] + #[should_panic(expected = "expected negative sign to be followed by number literal")] + fn parse_invalid_negative_sign() { + parse(quote!(OP_CHECKSIG - OP_HASH160)); + } + + #[test] + fn parse_negative_int() { + let syntax = parse(quote!(OP_CHECKSIG - 1234)); + + if let Syntax::Int(n) = syntax[1].0 { + assert_eq!(n, -1234i64); + } else { + panic!() + } + } + + #[test] + fn parse_hex() { + let syntax = parse(quote!(OP_CHECKSIG 0x123456789abcde)); + + if let Syntax::Bytes(bytes) = &syntax[1].0 { + assert_eq!(bytes, &vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde]); + } else { + panic!("Unable to cast Syntax as Syntax::Bytes") + } + } +} diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..1bc2fff --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,436 @@ +use bitcoin::blockdata::opcodes::Opcode; +use bitcoin::blockdata::script::{Instruction, PushBytes, PushBytesBuf, ScriptBuf}; +use bitcoin::opcodes::{OP_0, OP_TRUE}; +use bitcoin::script::write_scriptint; +use bitcoin::Witness; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::hash::{DefaultHasher, Hash, Hasher}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Hash, PartialEq)] +pub enum Block { + Call(u64), + Script(ScriptBuf), +} + +impl Block { + fn new_script() -> Self { + let buf = ScriptBuf::new(); + Block::Script(buf) + } +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, PartialEq)] +pub struct StructuredScript { + size: usize, + pub debug_identifier: String, + pub blocks: Vec, //List? + script_map: HashMap, +} + +impl Hash for StructuredScript { + fn hash(&self, state: &mut H) { + self.blocks.hash(state); + } +} + +fn calculate_hash(t: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + t.hash(&mut hasher); + hasher.finish() +} + +impl StructuredScript { + pub fn new(debug_info: &str) -> Self { + StructuredScript { + size: 0, + debug_identifier: debug_info.to_string(), + blocks: Vec::new(), + script_map: HashMap::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.size == 0 + } + + pub fn len(&self) -> usize { + self.size + } + + pub fn add_structured_script(&mut self, id: u64, script: StructuredScript) { + self.script_map.entry(id).or_insert(script); + } + + pub fn get_structured_script(&self, id: &u64) -> &StructuredScript { + self.script_map + .get(id) + .unwrap_or_else(|| panic!("script id: {} not found in script_map.", id)) + } + + // Return the debug information of the Opcode at position + pub fn debug_info(&self, position: usize) -> String { + let mut current_pos = 0; + for block in &self.blocks { + assert!(current_pos <= position, "Target position not found"); + match block { + Block::Call(id) => { + //let called_script = self.get_structured_script(id); + let called_script = self + .script_map + .get(id) + .expect("Missing entry for a called script"); + if position >= current_pos && position < current_pos + called_script.len() { + return called_script.debug_info(position - current_pos); + } + current_pos += called_script.len(); + } + Block::Script(script_buf) => { + if position >= current_pos && position < current_pos + script_buf.len() { + return self.debug_identifier.clone(); + } + current_pos += script_buf.len(); + } + } + } + panic!("No blocks in the structured script"); + } + + fn get_script_block(&mut self) -> &mut ScriptBuf { + // Check if the last block is a Script block + let is_script_block = matches!(self.blocks.last_mut(), Some(Block::Script(_))); + + // Create a new Script block if necessary + if !is_script_block { + self.blocks.push(Block::new_script()); + } + + if let Some(Block::Script(ref mut script)) = self.blocks.last_mut() { + script + } else { + unreachable!() + } + } + + pub fn push_opcode(mut self, data: Opcode) -> StructuredScript { + self.size += 1; + let script = self.get_script_block(); + script.push_opcode(data); + self + } + + pub fn push_script(mut self, data: ScriptBuf) -> StructuredScript { + let mut pos = 0; + for instruction in data.instructions() { + match instruction { + Ok(Instruction::Op(_)) => pos += 1, + Ok(Instruction::PushBytes(pushbytes)) => pos += pushbytes.len() + 1, + _ => (), + }; + } + assert_eq!(data.len(), pos, "Pos counting seems to be off"); + self.size += data.len(); + self.blocks.push(Block::Script(data)); + self + } + + pub fn push_env_script(mut self, mut data: StructuredScript) -> StructuredScript { + if data.is_empty() { + return self; + } + if self.is_empty() { + return data; + } + + data.debug_identifier = format!("{} {}", self.debug_identifier, data.debug_identifier); + self.size += data.len(); + let id = calculate_hash(&data); + self.blocks.push(Block::Call(id)); + // Register script in the script map + self.add_structured_script(id, data); + self + } + + /// Compiles the script to bytes. + fn compile_to_bytes(&self) -> Vec { + #[derive(Debug)] + enum Task<'a> { + CompileCall { + id: u64, + called_script: &'a StructuredScript, + }, + PushRaw(&'a ScriptBuf), + UpdateCache { + id: u64, + called_script_start: usize, + }, + } + + fn push_script<'a>(script: &'a StructuredScript, tasks: &mut Vec>) { + for block in script.blocks.iter().rev() { + match block { + Block::Call(id) => { + let called_script = script + .script_map + .get(id) + .expect("missing entry for called script"); + tasks.push(Task::CompileCall { + id: *id, + called_script, + }); + } + Block::Script(buffer) => tasks.push(Task::PushRaw(buffer)), + } + } + } + + let mut tasks = Vec::new(); + let mut cache = HashMap::new(); + let mut script: Vec = Vec::with_capacity(self.size); + push_script(self, &mut tasks); + + while let Some(task) = tasks.pop() { + match task { + Task::CompileCall { id, called_script } => { + match cache.get(&id) { + Some(called_start) => { + // Copy the already compiled called_script from the position it was + // inserted in the compiled script. + let start = script.len(); + let end = start + called_script.len(); + // TODO: Check if assertion is always true due to code invariants + assert!( + end <= script.capacity(), + "Not enough capacity allocated for compiled script" + ); + unsafe { + script.set_len(end); + + let src_ptr = script.as_ptr().add(*called_start); + let dst_ptr = script.as_mut_ptr().add(start); + + std::ptr::copy_nonoverlapping( + src_ptr, + dst_ptr, + called_script.len(), + ); + } + } + None => { + tasks.push(Task::UpdateCache { + id, + called_script_start: script.len(), + }); + push_script(called_script, &mut tasks); + } + } + } + Task::PushRaw(buffer) => { + let source_script = buffer.as_bytes(); + let start = script.len(); + let end = start + source_script.len(); + // TODO: Check if assertion is always true due to code invariants + assert!( + end <= script.capacity(), + "Not enough capacity allocated for compiled script" + ); + unsafe { + script.set_len(end); + + let src_ptr = source_script.as_ptr(); + let dst_ptr = script.as_mut_ptr().add(start); + + std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, source_script.len()); + } + } + Task::UpdateCache { + id, + called_script_start, + } => { + cache.insert(id, called_script_start); + } + } + } + + script + } + + pub fn compile(self) -> ScriptBuf { + let script = self.compile_to_bytes(); + // Ensure that the builder has minimal opcodes: + let script_buf = ScriptBuf::from_bytes(script); + let mut instructions_iter = script_buf.instructions(); + for result in script_buf.instructions_minimal() { + let instruction = instructions_iter.next(); + match result { + Ok(_) => (), + Err(err) => { + panic!( + "Error while parsing script instruction: {:?}, {:?}", + err, instruction + ); + } + } + } + script_buf + } + + pub fn push_int(self, data: i64) -> StructuredScript { + // We can special-case -1, 1-16 + if data == -1 || (1..=16).contains(&data) { + let opcode = Opcode::from((data - 1 + OP_TRUE.to_u8() as i64) as u8); + self.push_opcode(opcode) + } + // We can also special-case zero + else if data == 0 { + self.push_opcode(OP_0) + } + // Otherwise encode it as data + else { + self.push_int_non_minimal(data) + } + } + fn push_int_non_minimal(self, data: i64) -> StructuredScript { + let mut buf = [0u8; 8]; + let len = write_scriptint(&mut buf, data); + self.push_slice(&<&PushBytes>::from(&buf)[..len]) + } + + pub fn push_slice>(mut self, data: T) -> StructuredScript { + let script = self.get_script_block(); + let old_size = script.len(); + script.push_slice(data); + self.size += script.len() - old_size; + self + } + + pub fn push_key(self, key: &::bitcoin::PublicKey) -> StructuredScript { + if key.compressed { + self.push_slice(key.inner.serialize()) + } else { + self.push_slice(key.inner.serialize_uncompressed()) + } + } + + pub fn push_x_only_key(self, x_only_key: &::bitcoin::XOnlyPublicKey) -> StructuredScript { + self.push_slice(x_only_key.serialize()) + } + + pub fn push_expression(self, expression: T) -> StructuredScript { + expression.bitcoin_script_push(self) + } +} + +// We split up the bitcoin_script_push function to allow pushing a single u8 value as +// an integer (i64), Vec as raw data and Vec for any T: Pushable object that is +// not a u8. Otherwise the Vec and Vec definitions conflict. +trait NotU8Pushable { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript; +} +impl NotU8Pushable for i64 { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_int(self) + } +} +impl NotU8Pushable for i32 { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_int(self as i64) + } +} +impl NotU8Pushable for u32 { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_int(self as i64) + } +} +impl NotU8Pushable for usize { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_int(i64::try_from(self).expect("Usize does not fit in i64")) + } +} +impl NotU8Pushable for Vec { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + match self[..] { + [x] if (1..=16).contains(&x) => { + // use decicated opcode for pushing a single byte with value 1 <= x <= 16. + // Note that we don't use a special opcode used for pushing the value 0 - pushing this + // as an integer would push an empty array rather than a 0x00 value + builder.push_int(x.into()) + } + [129] => { + // 129 is equivalent to -1 when interpreting as signed - use dedicated opcode for pushing this + builder.push_int(-1) + } + _ => builder.push_slice(PushBytesBuf::try_from(self).unwrap()), + } + } +} +impl NotU8Pushable for ::bitcoin::PublicKey { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_key(&self) + } +} +impl NotU8Pushable for ::bitcoin::XOnlyPublicKey { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_x_only_key(&self) + } +} +impl NotU8Pushable for Witness { + fn bitcoin_script_push(self, mut builder: StructuredScript) -> StructuredScript { + for element in self.into_iter() { + match element[..] { + [x] if (1..=16).contains(&x) => { + // use decicated opcode for pushing a single byte with value 1 <= x <= 16. + // Note that we don't use a special opcode used for pushing the value 0 - pushing this + // as an integer would push an empty array rather than a 0x00 value + builder = builder.push_int(x.into()); + } + [129] => { + // 129 is equivalent to -1 when interpreting as signed - use dedicated opcode for pushing this + builder = builder.push_int(-1); + } + _ => { + builder = builder.push_slice(PushBytesBuf::try_from(element.to_vec()).unwrap()); + } + } + } + builder + } +} +impl NotU8Pushable for StructuredScript { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_env_script(self) + } +} +impl NotU8Pushable for Vec { + fn bitcoin_script_push(self, mut builder: StructuredScript) -> StructuredScript { + for pushable in self { + builder = pushable.bitcoin_script_push(builder); + } + builder + } +} +impl NotU8Pushable for ScriptBuf { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_script(self) + } +} + +pub trait Pushable { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript; +} +impl Pushable for T { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + NotU8Pushable::bitcoin_script_push(self, builder) + } +} + +impl Pushable for u8 { + fn bitcoin_script_push(self, builder: StructuredScript) -> StructuredScript { + builder.push_int(self as i64) + } +} diff --git a/src/lib.rs b/src/lib.rs index 02bc992..39c261e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,274 +1,5 @@ -//! [![Rust](https://github.com/mappum/rust-bitcoin-script/workflows/Rust/badge.svg)](https://github.com/mappum/rust-bitcoin-script/actions?query=workflow%3ARust) -//! [![crates.io](https://img.shields.io/crates/v/bitcoin-script.svg)](https://crates.io/crates/bitcoin-script) -//! [![docs.rs](https://docs.rs/bitcoin-script/badge.svg)](https://docs.rs/bitcoin-script) -//! -//! **Bitcoin scripts inline in Rust.** -//! -//! --- -//! -//! ## Usage -//! -//! This crate exports a `script!` macro which can be used to build -//! Bitcoin scripts. The macro returns the -//! [`Script`](https://docs.rs/bitcoin/0.23.0/bitcoin/blockdata/script/struct.Script.html) -//! type from the [`bitcoin`](https://github.com/rust-bitcoin/rust-bitcoin) -//! crate. -//! -//! **Example:** -//! -//! ```rust -//! # use bitcoin_script::{script, define_pushable}; -//! -//! # define_pushable!(); -//! # let digest = 0; -//! # let seller_pubkey_hash = 0; -//! # let buyer_pubkey_hash = 0; -//! -//! let htlc_script = script! { -//! OP_IF -//! OP_SHA256 OP_EQUALVERIFY OP_DUP OP_SHA256 -//! OP_ELSE -//! 100 OP_CSV OP_DROP OP_DUP OP_HASH160 -//! OP_ENDIF -//! OP_EQUALVERIFY -//! OP_CHECKSIG -//! }; -//! ``` -//! -//! **NOTE:** As of rustc 1.41, the Rust compiler prevents using procedural -//! macros as expressions. To use this macro you'll need to be on nightly and -//! add `#![feature(proc_macro_hygiene)]` to the root of your crate. This will -//! be stablized in the near future, the PR can be found here: -//! https://github.com/rust-lang/rust/pull/68717 -//! -//! ### Syntax -//! -//! Scripts are based on the standard syntax made up of opcodes, base-10 -//! integers, or hex string literals. Additionally, Rust expressions can be -//! interpolated in order to support dynamically capturing Rust variables or -//! computing values (delimited by ``). -//! -//! Whitespace is ignored - scripts can be formatted in the author's preferred -//! style. -//! -//! #### Opcodes -//! -//! All normal opcodes are available, in the form `OP_X`. -//! -//! ```rust -//! # use bitcoin_script::{script, define_pushable}; -//! # define_pushable!(); -//! let script = script!(OP_CHECKSIG OP_VERIFY); -//! ``` -//! -//! #### Integer Literals -//! -//! Positive and negative 64-bit integer literals can be used, and will resolve to their most efficient encoding. -//! -//! For example: -//! -`2` will resolve to `OP_PUSHNUM_2` (`0x52`) -//! -`255` will resolve to a length-delimited varint: `0x02ff00` (note the extra zero byte, due to the way Bitcoin scripts use the most-significant bit to represent the sign)` -//! -//! ```rust -//! # use bitcoin_script::{script, define_pushable}; -//! # define_pushable!(); -//! let script = script!(123 -456 999999); -//! ``` -//! -//! #### Hex Literals -//! -//! Hex strings can be specified, prefixed with `0x`. -//! -//! ```rust -//! # use bitcoin_script::{script, define_pushable}; -//! # define_pushable!(); -//! let script = script!( -//! 0x0102030405060708090a0b0c0d0e0f OP_HASH160 -//! ); -//! ``` -//! -//! #### Escape Sequences -//! -//! Dynamic Rust expressions are supported inside the script, surrounded by rust delimiters (e.g. "{ }" or "( )"), angle brackets ("< >") or tilde ("~ ~"). In many cases, this will just be a variable identifier, but this can also be a function call, closure or arithmetic. -//! -//! Rust expressions of the following types are supported: -//! -//! - `i64`, `i32`, `u32`, -//! - `Vec` -//! - [`bitcoin::PublicKey`](https://docs.rs/bitcoin/latest/bitcoin/struct.PublicKey.html) -//! - [`bitcoin::ScriptBuf`](https://docs.rs/bitcoin/latest/bitcoin/blockdata/script/struct.ScriptBuf.html) -//! - And Vec<> variants of all the above types -//! -//! -//! ```rust -//! # use bitcoin_script::{script, define_pushable}; -//! # define_pushable!(); -//! let bytes = vec![1, 2, 3]; -//! -//! let script = script! { -//! OP_CHECKSIGVERIFY -//! -//! <2016 * 5> OP_CSV -//! }; -//! ``` +pub mod builder; -mod generate; -mod parse; - -use generate::generate; -use parse::parse; -use proc_macro::TokenStream; -use proc_macro_error::{proc_macro_error, set_dummy}; -use quote::quote; - -#[proc_macro] -#[proc_macro_error] -pub fn script(tokens: TokenStream) -> TokenStream { - set_dummy(quote!((::bitcoin::Script::new()))); - generate(parse(tokens.into())).into() -} - -#[proc_macro] -pub fn define_pushable(_: TokenStream) -> TokenStream { - quote!( - pub mod pushable { - - use bitcoin::blockdata::opcodes::{all::*, Opcode}; - use bitcoin::blockdata::script::Builder as BitcoinBuilder; - use bitcoin::blockdata::script::{Instruction, PushBytes, PushBytesBuf, Script}; - use std::convert::TryFrom; - - pub struct Builder(pub BitcoinBuilder); - - impl Builder { - pub fn new() -> Self { - let builder = BitcoinBuilder::new(); - Builder(builder) - } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } - - pub fn as_script(&self) -> &Script { - self.0.as_script() - } - - pub fn push_opcode(mut self, opcode: Opcode) -> Builder { - self.0 = self.0.push_opcode(opcode); - self - } - - pub fn push_int(mut self, int: i64) -> Builder { - self.0 = self.0.push_int(int); - self - } - - pub fn push_slice>(mut self, data: T) -> Builder { - self.0 = self.0.push_slice(data); - self - } - - pub fn push_key(mut self, pub_key: &::bitcoin::PublicKey) -> Builder { - self.0 = self.0.push_key(pub_key); - self - } - - pub fn push_x_only_key( - mut self, - x_only_key: &::bitcoin::XOnlyPublicKey, - ) -> Builder { - self.0 = self.0.push_x_only_key(x_only_key); - self - } - - pub fn push_expression(self, expression: T) -> Builder { - let builder = expression.bitcoin_script_push(self); - builder - } - } - - impl From> for Builder { - fn from(v: Vec) -> Builder { - let builder = BitcoinBuilder::from(v); - Builder(builder) - } - } - // We split up the bitcoin_script_push function to allow pushing a single u8 value as - // an integer (i64), Vec as raw data and Vec for any T: Pushable object that is - // not a u8. Otherwise the Vec and Vec definitions conflict. - trait NotU8Pushable { - fn bitcoin_script_push(self, builder: Builder) -> Builder; - } - impl NotU8Pushable for i64 { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_int(self) - } - } - impl NotU8Pushable for i32 { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_int(self as i64) - } - } - impl NotU8Pushable for u32 { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_int(self as i64) - } - } - impl NotU8Pushable for usize { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_int( - i64::try_from(self).unwrap_or_else(|_| panic!("Usize does not fit in i64")), - ) - } - } - impl NotU8Pushable for Vec { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_slice(PushBytesBuf::try_from(self).unwrap()) - } - } - impl NotU8Pushable for ::bitcoin::PublicKey { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_key(&self) - } - } - impl NotU8Pushable for ::bitcoin::XOnlyPublicKey { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_x_only_key(&self) - } - } - impl NotU8Pushable for ::bitcoin::ScriptBuf { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - let mut script_vec = - Vec::with_capacity(builder.0.as_bytes().len() + self.as_bytes().len()); - script_vec.extend_from_slice(builder.as_bytes()); - script_vec.extend_from_slice(self.as_bytes()); - Builder::from(script_vec) - } - } - impl NotU8Pushable for Vec { - fn bitcoin_script_push(self, mut builder: Builder) -> Builder { - for pushable in self { - builder = pushable.bitcoin_script_push(builder); - } - builder - } - } - pub trait Pushable { - fn bitcoin_script_push(self, builder: Builder) -> Builder; - } - impl Pushable for T { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - NotU8Pushable::bitcoin_script_push(self, builder) - } - } - - impl Pushable for u8 { - fn bitcoin_script_push(self, builder: Builder) -> Builder { - builder.push_int(self as i64) - } - } - } - ) - .into() -} +pub use crate::builder::StructuredScript as Script; +pub use script_macro::script; +pub use stdext::function_name; diff --git a/src/parse.rs b/src/parse.rs deleted file mode 100644 index 6413a5b..0000000 --- a/src/parse.rs +++ /dev/null @@ -1,407 +0,0 @@ -use bitcoin::{opcodes::all::OP_RESERVED, Opcode}; -use proc_macro2::{ - Delimiter, Span, TokenStream, - TokenTree::{self, *}, -}; -use quote::quote; -use std::iter::Peekable; -use std::str::FromStr; - -use bitcoin_opcode_utils::from_str; - -#[derive(Debug)] -pub enum Syntax { - Opcode(Opcode), - Escape(TokenStream), - Bytes(Vec), - Int(i64), -} - -macro_rules! emit_error { - ($span:expr, $($message:expr),*) => {{ - #[cfg(not(test))] - proc_macro_error::emit_error!($span, $($message),*); - - #[cfg(test)] - panic!($($message),*); - - #[allow(unreachable_code)] - { - panic!(); - } - }} -} - -macro_rules! abort { - ($span:expr, $($message:expr),*) => {{ - #[cfg(not(test))] - proc_macro_error::abort!($span, $($message),*); - - #[cfg(test)] - panic!($($message),*); - }} -} - -pub fn parse(tokens: TokenStream) -> Vec<(Syntax, Span)> { - let mut tokens = tokens.into_iter().peekable(); - let mut syntax = Vec::with_capacity(2048); - - while let Some(token) = tokens.next() { - let token_str = token.to_string(); - syntax.push(match (&token, token_str.as_ref()) { - // Wrap for loops such that they return a Vec - (Ident(_), ident_str) if ident_str == "for" => parse_for_loop(token, &mut tokens), - // Wrap if-else statements such that they return a Vec - (Ident(_), ident_str) if ident_str == "if" => parse_if(token, &mut tokens), - // Replace DEBUG with OP_RESERVED - (Ident(_), ident_str) if ident_str == "DEBUG" => { - (Syntax::Opcode(OP_RESERVED), token.span()) - } - - // identifier, look up opcode - (Ident(_), _) => { - match from_str(&token_str) { - Ok(opcode) => (Syntax::Opcode(opcode), token.span()), - // Not a native Bitcoin opcode - // Allow functions without arguments to be identified by just their name - _ => { - let span = token.span(); - let mut pseudo_stream = TokenStream::from(token); - pseudo_stream.extend(TokenStream::from_str("()")); - (Syntax::Escape(pseudo_stream), span) - } - } - } - - (Group(inner), _) => { - let escape = TokenStream::from(inner.stream().clone()); - (Syntax::Escape(escape), token.span()) - } - - // '<', start of escape (parse until first '>') - (Punct(_), "<") => parse_escape(token, &mut tokens), - - // '~' start of escape (parse until the next '~') ignores '<' and '>' - (Punct(_), "~") => parse_escape_extra(token, &mut tokens), - - // literal, push data (int or bytes) - (Literal(_), _) => parse_data(token), - - // negative sign, parse negative int - (Punct(_), "-") => parse_negative_int(token, &mut tokens), - - // anything else is invalid - _ => abort!(token.span(), "unexpected token"), - }); - } - syntax -} - -fn parse_if(token: TokenTree, tokens: &mut Peekable) -> (Syntax, Span) -where - T: Iterator, -{ - // Use a Vec here to get rid of warnings when the variable is overwritten - let mut escape = quote! { - let mut script_var = Vec::with_capacity(256); - }; - escape.extend(std::iter::once(token.clone())); - - while let Some(if_token) = tokens.next() { - match if_token { - Group(block) if block.delimiter() == Delimiter::Brace => { - let inner_block = block.stream(); - escape.extend(quote! { - { - script_var.extend_from_slice(script! { - #inner_block - }.as_bytes()); - } - }); - - match tokens.peek() { - Some(else_token) if else_token.to_string().as_str() == "else" => continue, - _ => break, - } - } - _ => { - escape.extend(std::iter::once(if_token)); - continue; - } - }; - } - escape = quote! { - { - #escape; - bitcoin::script::ScriptBuf::from(script_var) - } - } - .into(); - (Syntax::Escape(escape), token.span()) -} - -fn parse_for_loop(token: TokenTree, tokens: &mut T) -> (Syntax, Span) -where - T: Iterator, -{ - let mut escape = quote! { - let mut script_var = vec![]; - }; - escape.extend(std::iter::once(token.clone())); - - while let Some(for_token) = tokens.next() { - match for_token { - Group(block) if block.delimiter() == Delimiter::Brace => { - let inner_block = block.stream(); - escape.extend(quote! { - { - let next_script = script !{ - #inner_block - }; - script_var.extend_from_slice(next_script.as_bytes()); - } - bitcoin::script::ScriptBuf::from(script_var) - }); - break; - } - _ => { - escape.extend(std::iter::once(for_token)); - continue; - } - }; - } - - (Syntax::Escape(quote! { { #escape } }.into()), token.span()) -} - -fn parse_escape(token: TokenTree, tokens: &mut T) -> (Syntax, Span) -where - T: Iterator, -{ - let mut escape = TokenStream::new(); - let mut span = token.span(); - - loop { - let token = tokens - .next() - .unwrap_or_else(|| abort!(token.span(), "unterminated escape")); - let token_str = token.to_string(); - - span = span.join(token.span()).unwrap_or(token.span()); - - // end of escape - if let (Punct(_), ">") = (&token, token_str.as_ref()) { - break; - } - - escape.extend(TokenStream::from(token)); - } - - (Syntax::Escape(escape), span) -} - -fn parse_escape_extra(token: TokenTree, tokens: &mut T) -> (Syntax, Span) -where - T: Iterator, -{ - let mut escape = TokenStream::new(); - let mut span = token.span(); - - loop { - let token = tokens - .next() - .unwrap_or_else(|| abort!(token.span(), "unterminated escape")); - let token_str = token.to_string(); - - span = span.join(token.span()).unwrap_or(token.span()); - - // end of escape - if let (Punct(_), "~") = (&token, token_str.as_ref()) { - break; - } - - escape.extend(TokenStream::from(token)); - } - - (Syntax::Escape(escape), span) -} - -fn parse_data(token: TokenTree) -> (Syntax, Span) { - if token.to_string().starts_with("0x") { - if token - .to_string() - .strip_prefix("0x") - .unwrap_or_else(|| unreachable!()) - .trim_start_matches('0') - .len() - <= 8 - { - parse_hex_int(token) - } else { - parse_bytes(token) - } - } else { - parse_int(token, false) - } -} - -fn parse_bytes(token: TokenTree) -> (Syntax, Span) { - let hex_bytes = &token.to_string()[2..]; - let bytes = hex::decode(hex_bytes).unwrap_or_else(|err| { - emit_error!(token.span(), "invalid hex literal ({})", err); - }); - (Syntax::Bytes(bytes), token.span()) -} - -fn parse_hex_int(token: TokenTree) -> (Syntax, Span) { - let token_str = &token.to_string()[2..]; - let n: u32 = u32::from_str_radix(token_str, 16).unwrap_or_else(|err| { - emit_error!(token.span(), "invalid hex string ({})", err); - }); - (Syntax::Int(n as i64), token.span()) -} - -fn parse_int(token: TokenTree, negative: bool) -> (Syntax, Span) { - let token_str = token.to_string(); - let n: i64 = token_str.parse().unwrap_or_else(|err| { - emit_error!(token.span(), "invalid number literal ({})", err); - }); - let n = if negative { n * -1 } else { n }; - (Syntax::Int(n), token.span()) -} - -fn parse_negative_int(token: TokenTree, tokens: &mut T) -> (Syntax, Span) -where - T: Iterator, -{ - let fail = || { - #[allow(unused_variables)] - let span = token.span(); - emit_error!( - span, - "expected negative sign to be followed by number literal" - ); - }; - - let maybe_token = tokens.next(); - - if let Some(token) = maybe_token { - if let Literal(_) = token { - parse_int(token, true) - } else { - fail() - } - } else { - fail() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bitcoin::blockdata::opcodes::all as opcodes; - use quote::quote; - - #[test] - fn parse_empty() { - assert!(parse(quote!()).is_empty()); - } - - #[test] - #[should_panic(expected = "unexpected token")] - fn parse_unexpected_token() { - parse(quote!(OP_CHECKSIG &)); - } - - //#[test] - //#[should_panic(expected = "unknown opcode \"A\"")] - //fn parse_invalid_opcode() { - // parse(quote!(OP_CHECKSIG A B)); - //} - - #[test] - fn parse_opcodes() { - let syntax = parse(quote!(OP_CHECKSIG OP_HASH160)); - - if let Syntax::Opcode(opcode) = syntax[0].0 { - assert_eq!(opcode, opcodes::OP_CHECKSIG); - } else { - panic!(); - } - - if let Syntax::Opcode(opcode) = syntax[1].0 { - assert_eq!(opcode, opcodes::OP_HASH160); - } else { - panic!(); - } - } - - #[test] - #[should_panic(expected = "unterminated escape")] - fn parse_unterminated_escape() { - parse(quote!(OP_CHECKSIG < abc)); - } - - #[test] - fn parse_escape() { - let syntax = parse(quote!(OP_CHECKSIG)); - - if let Syntax::Escape(tokens) = &syntax[1].0 { - let tokens = tokens.clone().into_iter().collect::>(); - - assert_eq!(tokens.len(), 1); - if let TokenTree::Ident(_) = tokens[0] { - assert_eq!(tokens[0].to_string(), "abc"); - } else { - panic!() - } - } else { - panic!() - } - } - - #[test] - #[should_panic(expected = "invalid number literal (invalid digit found in string)")] - fn parse_invalid_int() { - parse(quote!(OP_CHECKSIG 12g34)); - } - - #[test] - fn parse_int() { - let syntax = parse(quote!(OP_CHECKSIG 1234)); - - if let Syntax::Int(n) = syntax[1].0 { - assert_eq!(n, 1234i64); - } else { - panic!() - } - } - - #[test] - #[should_panic(expected = "expected negative sign to be followed by number literal")] - fn parse_invalid_negative_sign() { - parse(quote!(OP_CHECKSIG - OP_HASH160)); - } - - #[test] - fn parse_negative_int() { - let syntax = parse(quote!(OP_CHECKSIG - 1234)); - - if let Syntax::Int(n) = syntax[1].0 { - assert_eq!(n, -1234i64); - } else { - panic!() - } - } - - #[test] - fn parse_hex() { - let syntax = parse(quote!(OP_CHECKSIG 0x123456789abcde)); - - if let Syntax::Bytes(bytes) = &syntax[1].0 { - assert_eq!(bytes, &vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde]); - } else { - panic!("Unable to cast Syntax as Syntax::Bytes") - } - } -} diff --git a/tests/test.rs b/tests/test.rs index e3127e3..db7440d 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,7 +1,9 @@ -use bitcoin::{opcodes::all::OP_ADD, ScriptBuf}; -use bitcoin_script::{define_pushable, script}; - -define_pushable!(); +use bitcoin::{ + consensus::{encode, Encodable}, + opcodes::all::OP_ADD, + Witness, +}; +use bitcoin_script::{script, Script}; #[test] fn test_generic() { @@ -18,7 +20,7 @@ fn test_generic() { ); assert_eq!( - script.to_bytes(), + script.compile().as_bytes(), vec![169, 2, 210, 4, 2, 255, 0, 79, 2, 255, 128, 3, 205, 171, 0, 82, 81, 82, 83, 84] ); } @@ -42,7 +44,7 @@ fn test_pushable_vectors() { ); assert_eq!( - script.to_bytes(), + script.compile().to_bytes(), vec![81, 82, 83, 84, 85, 86, 87, 88, 147, 81, 0] ); } @@ -76,20 +78,32 @@ fn test_minimal_byte_opcode() { ); assert_eq!( - script.to_bytes(), + script.compile().to_bytes(), vec![0, 0, 81, 82, 83, 84, 85, 86, 87, 88, 89, 96, 1, 17, 2, 210, 0, 2, 210, 0] ); } -fn script_from_func() -> ScriptBuf { - return script! { OP_ADD }; +fn script_from_func() -> Script { + script! { OP_ADD } +} + +#[test] +fn test_simple_loop() { + let script = script! { + for _ in 0..3 { + OP_ADD + } + }; + + assert_eq!(script.compile().to_bytes(), vec![147, 147, 147]) } #[test] -fn test_for_loop() { +#[should_panic] // Optimization is not yet implemented. +fn test_for_loop_optimized() { let script = script! { for i in 0..3 { - for k in 0..(3 as u32) { + for k in 0..3_u32 { OP_ADD script_from_func OP_SWAP @@ -101,7 +115,7 @@ fn test_for_loop() { }; assert_eq!( - script.to_bytes(), + script.compile().to_bytes(), vec![ 147, 147, 124, 0, 0, 147, 147, 124, 0, 139, 147, 124, 0, 82, 147, 147, 124, 81, 0, 147, 147, 124, 81, 139, 147, 124, 81, 82, 147, 147, 124, 82, 0, 147, 147, 124, 82, 139, 147, @@ -133,24 +147,34 @@ fn test_if() { } }; - assert_eq!(script.to_bytes(), vec![83, 85]); + assert_eq!(script.compile().to_bytes(), vec![83, 85]); } #[test] fn test_performance_loop() { - let loop_script = script! { - OP_ADD - OP_ADD + let mut nested_script = script! { OP_ADD }; + for _ in 0..20 { + nested_script = script! { + { nested_script.clone() } + { nested_script.clone() } + } + } + println!("Subscript size: {}", nested_script.len()); + let script = script! { - for _ in 0..5_000_000 { - {loop_script.clone()} + for _ in 0..10 { + {nested_script.clone()} } }; - assert_eq!(script.as_bytes()[5_000_000 - 1], 147) + println!("Expected size: {}", script.len()); + let compiled_script = script.compile(); + println!("Compiled size {}", compiled_script.len()); + + assert_eq!(compiled_script.as_bytes()[5_000_000 - 1], 147) } #[test] @@ -177,7 +201,7 @@ fn test_performance_if() { } }; - assert_eq!(script.as_bytes()[5_000_000 - 1], 147) + assert_eq!(script.compile().as_bytes()[5_000_000 - 1], 147) } #[test] @@ -192,7 +216,7 @@ fn test_simple() { }; assert_eq!( - script.as_bytes(), + script.compile().as_bytes(), vec![ 86, 122, 91, 122, 86, 122, 92, 122, 86, 122, 93, 122, 86, 122, 94, 122, 86, 122, 95, 122, 86, 122, 96, 122 @@ -201,6 +225,7 @@ fn test_simple() { } #[test] +#[should_panic] // Optimization is not yet implemented. fn test_non_optimal_opcodes() { let script = script! { OP_0 @@ -213,21 +238,105 @@ fn test_non_optimal_opcodes() { OP_DROP OP_DROP - for i in 0..4 { - OP_ROLL - { i } - } + //for i in 0..4 { + // OP_ROLL + // { i } + //} - for i in 0..4 { - { i } - OP_ROLL - } + //for i in 0..4 { + // { i } + // OP_ROLL + //} }; println!("{:?}", script); assert_eq!( - script.as_bytes(), + script.compile().as_bytes(), vec![124, 109, 122, 124, 123, 83, 124, 123, 83, 122] ); } + +#[test] +fn test_push_witness() { + for i in 0..512 { + for x in vec![0, 37, 42, 127, 128, 129, 211, 255] { + let mut witness = Witness::new(); + let vec = vec![x; i]; + witness.push(vec.clone()); + let script = script! { + { witness } + }; + let reference_script = script! { + { vec } + }; + assert_eq!( + script.compile().as_bytes(), + reference_script.compile().as_bytes(), + "here" + ); + } + } + + let mut witness = Witness::new(); + witness.push([]); //presentation of 0 with `consensus_encode` is [0] instead of [], but `push_int` in this repository encodes 0 as [] + for i in 1..16 { + let mut varint = Vec::new(); + encode::VarInt(i).consensus_encode(&mut varint).unwrap(); + witness.push(varint); + } + + let mut forty_two_varint = Vec::new(); + encode::VarInt(42u64) + .consensus_encode(&mut forty_two_varint) + .unwrap(); + witness.push(forty_two_varint); + let script = script! { + { witness } + }; + + let reference_script = script! { + for i in 0..16 { + { i } + } + { 42 } + }; + assert_eq!( + script.compile().as_bytes(), + reference_script.compile().as_bytes() + ); +} + +#[test] +fn test_push_scriptbuf() { + let script_buf = script! { + { 1 } + } + .compile(); + let script = script! { + { script_buf.clone() } + }; + assert_eq!(script_buf, script.compile()); +} + +#[cfg(feature = "serde")] +#[test] +fn test_serialization() { + let script = script! { + // Example script + for i in 0..10 { + {i} + {i*2} + {i*4} + OP_ADD + OP_ADD + } + }; + + let binary_data = bincode::serialize(&script).unwrap(); + println!("Script size: {} bytes", script.len()); + println!("Binary size: {} bytes", binary_data.len()); + + let deserialized: Script = bincode::deserialize(&binary_data).unwrap(); + assert_eq!(deserialized, script); +}