Skip to content

Commit

Permalink
🧪 Add Test Scripts for testing Splitter Functionality (#3)
Browse files Browse the repository at this point in the history
# Objective

In order to further verify that the splitting logic works fine, we first
load some basic operations (such as big integer addition/multiplication)
and verify whether these operations are properly split.

## Added
- Struct `IOPair` and trait `SplitableScript` to represent logic that
must be handled by the splitting mechanism.
- Test Scripts
    - Big integer (`U254`) addition;
    - Big integer (`U254`) multiplication.
  • Loading branch information
ZamDimon authored Sep 18, 2024
1 parent 28ebffd commit 8c8ea23
Show file tree
Hide file tree
Showing 12 changed files with 841 additions and 104 deletions.
36 changes: 17 additions & 19 deletions splitter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
[package]
name = "splitter"
name = "bitvm2-splitter"
version = "0.1.0"
edition = "2021"

[dependencies]

# Bitcoin Libraries
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin", branch = "bitvm", features = ["rand-std"]}
bitcoin-script = { git = "https://github.com/BitVM/rust-bitcoin-script" }
bitcoin-scriptexec = { git = "https://github.com/BitVM/rust-bitcoin-scriptexec/"}
bitcoin-script-stack = { git = "https://github.com/FairgateLabs/rust-bitcoin-script-stack"}
# Some test script to test the splitter
bitcoin-window-mul = { git = "https://github.com/distributed-lab/bitcoin-window-mul.git" }

# General-purpose libraries
strum = "0.26"
strum_macros = "0.26"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.116"
tokio = { version = "1.37.0", features = ["full"] }

# Other Libraries
strum = "0.26"
strum_macros = "0.26"
hex = "0.4.3"
serde = { version = "1.0.197", features = ["derive"] }
num-bigint = "0.4.4"
# Crypto libraries
hex = "0.4.3"
num-bigint = { version = "0.4.4", features = ["rand"] }
num-traits = "0.2.18"
tokio = { version = "1.37.0", features = ["full"] }
serde_json = "1.0.116"
lazy_static = "1.4.0"
prettytable-rs = "0.10.0"
paste = "1.0"
seq-macro = "0.3.5"

[dev-dependencies]
# Random libraries
rand_chacha = "0.3.1"
rand = "0.8.5"
num-bigint = { version = "0.4.4", features = ["rand"] }
ark-std = "0.4.0"
konst = "0.3.9"
rand = "0.8.5"
ark-std = "0.4.0"
konst = "0.3.9"
126 changes: 41 additions & 85 deletions splitter/src/debug.rs
Original file line number Diff line number Diff line change
@@ -1,90 +1,45 @@
use bitcoin::{hashes::Hash, hex::DisplayHex, Opcode, TapLeafHash, Transaction};
use bitcoin::{hashes::Hash, ScriptBuf, TapLeafHash, Transaction};
use bitcoin_scriptexec::{Exec, ExecCtx, ExecError, ExecStats, Options, Stack, TxTemplate};
use core::fmt;

/// A wrapper for the stack types to print them better.
pub struct FmtStack(Stack);
impl fmt::Display for FmtStack {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut iter = self.0.iter_str().enumerate().peekable();
write!(f, "\n0:\t\t ")?;
while let Some((index, item)) = iter.next() {
write!(f, "0x{:8}", item.as_hex())?;
if iter.peek().is_some() {
if (index + 1) % f.width().unwrap() == 0 {
write!(f, "\n{}:\t\t", index + 1)?;
}
write!(f, " ")?;
}
}
Ok(())
}
}

impl FmtStack {
pub fn len(&self) -> usize {
self.0.len()
}

pub fn is_empty(&self) -> bool {
self.0.is_empty()
}

pub fn get(&self, index: usize) -> Vec<u8> {
self.0.get(index)
}
}

impl fmt::Debug for FmtStack {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self)?;
Ok(())
}
}

/// Information about the status of the script execution.
#[derive(Debug)]
pub struct ExecuteInfo {
pub success: bool,
pub error: Option<ExecError>,
pub final_stack: FmtStack,
pub remaining_script: String,
pub last_opcode: Option<Opcode>,
pub main_stack: Stack,
pub alt_stack: Stack,
pub stats: ExecStats,
}

impl fmt::Display for ExecuteInfo {
/// Formats the `ExecuteInfo` struct for display.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.success {
writeln!(f, "Script execution successful.")?;
} else {
writeln!(f, "Script execution failed!")?;
}

if let Some(ref error) = self.error {
writeln!(f, "Error: {:?}", error)?;
}
if !self.remaining_script.is_empty() {
writeln!(f, "Remaining Script: {}", self.remaining_script)?;
}
if !self.final_stack.is_empty() {
match f.width() {
None => writeln!(f, "Final Stack: {:4}", self.final_stack)?,
Some(width) => {
writeln!(f, "Final Stack: {:width$}", self.final_stack, width = width)?
}
}
}
if let Some(ref opcode) = self.last_opcode {
writeln!(f, "Last Opcode: {:?}", opcode)?;
}

writeln!(f, "Stats: {:?}", self.stats)?;
Ok(())
}
}

pub fn execute_script(script: bitcoin::ScriptBuf) -> ExecuteInfo {
/// Executes the given script and returns the result of the execution
/// (success, error, stack, etc.)
pub fn execute_script(script: ScriptBuf) -> ExecuteInfo {
let mut exec = Exec::new(
ExecCtx::Tapscript,
Options::default(),
Options {
// TODO: Figure our how to optimize stack_to_script function to avoid disabling require_minimal
require_minimal: false,
..Default::default()
},
TxTemplate {
tx: Transaction {
version: bitcoin::transaction::Version::TWO,
Expand All @@ -99,34 +54,31 @@ pub fn execute_script(script: bitcoin::ScriptBuf) -> ExecuteInfo {
script,
vec![],
)
.expect("error creating exec");
.expect("error when creating the execution body");

// Execute all the opcodes while possible
loop {
if exec.exec_next().is_err() {
break;
}
}
let res = exec.result().unwrap();

// Obtaining the result of the execution
let result = exec.result().unwrap();

ExecuteInfo {
success: res.success,
error: res.error.clone(),
last_opcode: res.opcode,
final_stack: FmtStack(exec.stack().clone()),
remaining_script: exec.remaining_script().to_asm_string(),
success: result.success,
error: result.error.clone(),
main_stack: exec.stack().clone(),
alt_stack: exec.altstack().clone(),
stats: exec.stats().clone(),
}
}

/// Prints the size of the script in bytes.
#[allow(dead_code)]
pub fn print_script_size(name: &str, script: bitcoin::ScriptBuf) {
println!("{} script is {} bytes in size", name, script.len());
}

// Execute a script on stack without `MAX_STACK_SIZE` limit.
// This function is only used for script test, not for production.
//
// NOTE: Only for test purposes.
/// Execute a script on stack without `MAX_STACK_SIZE` limit.
/// This function is only used for script test, not for production.
///
/// NOTE: Only for test purposes.
#[allow(dead_code)]
pub fn execute_script_no_stack_limit(script: bitcoin::ScriptBuf) -> ExecuteInfo {
// Get the default options for the script exec.
Expand All @@ -153,20 +105,23 @@ pub fn execute_script_no_stack_limit(script: bitcoin::ScriptBuf) -> ExecuteInfo
script,
vec![],
)
.expect("error creating exec");
.expect("error while creating the execution body");

// Execute all the opcodes while possible
loop {
if exec.exec_next().is_err() {
break;
}
}
let res = exec.result().unwrap();

// Get the result of the execution
let result = exec.result().unwrap();

ExecuteInfo {
success: res.success,
error: res.error.clone(),
last_opcode: res.opcode,
final_stack: FmtStack(exec.stack().clone()),
remaining_script: exec.remaining_script().to_asm_string(),
success: result.success,
error: result.error.clone(),
main_stack: exec.stack().clone(),
alt_stack: exec.altstack().clone(),
stats: exec.stats().clone(),
}
}
Expand Down Expand Up @@ -199,7 +154,8 @@ mod test {
}
OP_1
};

let exec_result = execute_script_no_stack_limit(script);
assert!(exec_result.success);
}
}
}
4 changes: 4 additions & 0 deletions splitter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ pub mod treepp {
pub use bitcoin::ScriptBuf as Script;
}

pub mod split;

pub(crate) mod debug;
pub(crate) mod test_scripts;
pub(crate) mod utils;

#[cfg(test)]
mod tests {
Expand Down
67 changes: 67 additions & 0 deletions splitter/src/split/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Module containing the logic of splitting the script into smaller parts

use bitcoin::script::Instruction;

use super::script::SplitResult;
use crate::{split::intermediate_state::IntermediateState, treepp::*};

/// Maximum size of the script in bytes
pub(super) const MAX_SCRIPT_SIZE: usize = 30000;

// TODO: Currently, the chunk size splits the script into the parts of the same size IN TERMS OF INSTRUCTIONS, not bytes.
/// Splits the given script into smaller parts
pub(super) fn split_into_shards(script: &Script, chunk_size: usize) -> Vec<Script> {
let instructions: Vec<Instruction> = script
.instructions()
.map(|instruction| instruction.expect("script is most likely corrupted"))
.collect();

instructions
.chunks(chunk_size)
.map(|chunk| {
let mut shard = Script::new();
for instruction in chunk {
shard.push_instruction(*instruction);
}

shard
})
.collect()
}

pub(super) fn naive_split(input: Script, script: Script) -> SplitResult {
// First, we split the script into smaller parts
let shards = split_into_shards(&script, MAX_SCRIPT_SIZE);
let mut intermediate_states: Vec<IntermediateState> = vec![];

// Then, we do the following steps:
// 1. We execute the first script with the input
// 2. We take the stack and write it to the intermediate results
// 3. We execute the second script with the saved intermediate results
// 4. Take the stask, save to the intermediate results
// 5. Repeat until the last script

intermediate_states.push(IntermediateState::from_input_script(&input, &shards[0]));

for shard in shards.clone().into_iter().skip(1) {
// Executing a piece of the script with the current input.
// NOTE #1: unwrap is safe to use here since intermediate_states is of length 1.
// NOTE #2: we need to feed in both the stack and the altstack

intermediate_states.push(IntermediateState::from_intermediate_result(
intermediate_states.last().unwrap(),
&shard,
));
}

assert_eq!(
intermediate_states.len(),
shards.len(),
"Intermediate results should be the same as the number of scripts"
);

SplitResult {
shards,
intermediate_states,
}
}
54 changes: 54 additions & 0 deletions splitter/src/split/intermediate_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! This module contains the [`IntermediateState`] struct, which is used to store the intermediate
//! state of the stack and altstack during the execution of a script split into the shards (subprograms).

use crate::{treepp::*, utils::stack_to_script};
use bitcoin_scriptexec::Stack;

pub struct IntermediateState {
pub stack: Stack,
pub altstack: Stack,
}

impl IntermediateState {
/// Executes the script with the given input and returns the intermediate result,
/// that is, the stack and altstack after the execution
pub fn from_input_script(input: &Script, script: &Script) -> Self {
let script = script! {
{ input.clone() }
{ script.clone() }
};

let result = execute_script(script);

Self {
stack: result.main_stack.clone(),
altstack: result.alt_stack.clone(),
}
}

/// Based on the previous intermediate result, executes the script with the stacks
/// and altstacks of the previous result and returns the new intermediate result
pub fn from_intermediate_result(result: &Self, script: &Script) -> Self {
let Self { stack, altstack } = result;

let insert_result_script = script! {
// Checks for length of the stack and altstack
// are used to avoid panic when referencing the first element
// of the stack or altstack (by using get(0) method)
if !stack.is_empty() {
{ stack_to_script(stack) }
}

if !altstack.is_empty() {
{ stack_to_script(altstack) }

for i in (0..altstack.len()).rev() {
{ i } OP_ROLL
OP_TOALTSTACK
}
}
};

Self::from_input_script(&insert_result_script, script)
}
}
9 changes: 9 additions & 0 deletions splitter/src/split/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Module that contains the implementation of the splitter
//! together with all auxiliary functions and data structures.

pub mod core;
pub mod intermediate_state;
pub mod script;

#[cfg(test)]
pub mod tests;
Loading

0 comments on commit 8c8ea23

Please sign in to comment.