diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..53811172 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @b-j-roberts diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..902a2cce --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ + + +- [ ] issue # +- [ ] follows contribution [guide](https://github.com/keep-starknet-strange/shinigami/blob/main/CONTRIBUTING.md) +- [ ] code change includes tests + + diff --git a/.github/linter/base_style.rb b/.github/linter/base_style.rb new file mode 100644 index 00000000..8d981050 --- /dev/null +++ b/.github/linter/base_style.rb @@ -0,0 +1,4 @@ +all +# lame rules +exclude_rule 'MD002' +exclude_rule 'MD041' diff --git a/.github/linter/readme_style.rb b/.github/linter/readme_style.rb new file mode 100644 index 00000000..02e6b483 --- /dev/null +++ b/.github/linter/readme_style.rb @@ -0,0 +1,12 @@ +all +# allow inline HTML for README fmt +exclude_rule 'MD033' +# badges trigger rule +exclude_rule 'MD034' +# README img serves as 'First Header' +exclude_rule 'MD002' +exclude_rule 'MD041' +# TODO: disable/enable not working for all-contribs +exclude_rule 'MD013' +# Allow no endline at the end +exclude_rule 'MD047' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..a46b0608 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,18 @@ +name: build + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: +permissions: read-all + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: asdf-vm/actions/install@v3 + - run: scarb fmt --check + - run: scarb build diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..1ceaa74f --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,27 @@ +name: check + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: +permissions: read-all + +jobs: + markdown: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: | + sudo gem install mdl + mdl -s .github/linter/readme_style.rb README.md + mdl -s .github/linter/base_style.rb .github + mdl -s .github/linter/book_style.rb book + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: asdf-vm/actions/install@v3 + - run: scarb test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..aa071c5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Sys files +**/.DS_Store + +# Scarb and Starknet Foundry +**/target +**/.snfoundry_cache/ + +# Starkli +**/account.json +**/keystore.json + +# All Contributors CLI +/package.json +/yarn.lock +node_modules/ + +# Development +**/TODO diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..305a4542 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +## 🛠️ Contributing to shinigami 🛠️ + +Welcome, contributing to `shinigami` is easy! + +1. Submit or comment your intent on an issue +1. We will try to respond quickly +1. Fork this repo +1. Submit your PR against `main` +1. Address PR Reviews + +### Issue + +Project tracking is done via GitHub [issues](https://github.com/keep-starknet-strange/shinigami/issues) +First look at open issues to see if your request is already submitted. +If it is comment on the issue requesting assignment, if not open an issue. + +We use 3 issue labels for development: + +- `feat` -> suggest new feature +- `bug` -> create a reproducible bug report +- `dev` -> non-functional repository changes + +These labels are used as prefixes as follows for `issue`, `branch name`, `pr title`: + +- `[feat]` -> `feat/{issue #}-{issue name}` -> `feat:` +- `[bug]` -> `bug/{issue #}-{issue name}` -> `bug:` +- `[dev]` -> `dev/{issue #}-{issue name}` -> `dev:` + +#### TODO + +If your PR includes a `TODO` comment please open an issue and comment the relevant +code with `TODO(#ISSUE_NUM):`. + +### Submit PR + +Ensure your code is well formatted, well tested and well documented. A core contributor +will review your work. Address changes, ensure ci passes, +and voilà you're a `shinigami` contributor. + +Markdown [linter](https://github.com/markdownlint/markdownlint?tab=readme-ov-file#markdown-lint-tool): + +```bash +mdl -s .github/linter/readme_style.rb README.md +``` + +Scarb linter: + +```bash +scarb fmt +``` + +### Additional Resources + +- [Cairo Book](https://book.cairo-lang.org/) +- [Starknet Book](https://book.starknet.io/) +- [Starknet Foundry Book](https://foundry-rs.github.io/starknet-foundry/) +- [Starknet By Example](https://starknet-by-example.voyager.online/) +- [Starkli Book](https://book.starkli.rs/) +- [Bitcoin Wiki](https://en.bitcoin.it/wiki/Script) +- [Golang Bitcoin Implementation](https://github.com/btcsuite/btcd) +- [Syncing a Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) + +## + +Thank you for your contribution! diff --git a/README.md b/README.md new file mode 100644 index 00000000..ffa320ad --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +
+ shinigami-logo + + ***Bitcoin Script VM in Cairo*** + + [![Check Workflow Status](https://github.com/keep-starknet-strange/shinigami/actions/workflows/check.yml/badge.svg)](https://github.com/keep-starknet-strange/shinigami/actions/workflows/check.yml) + [![Build Workflow Status](https://github.com/keep-starknet-strange/shinigami/actions/workflows/build.yml/badge.svg)](https://github.com/keep-starknet-strange/shinigami/actions/workflows/build.yml) + + [![Exploration_Team](https://img.shields.io/badge/Exploration_Team-29296E.svg?&style=for-the-badge&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJhIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxODEgMTgxIj48ZGVmcz48c3R5bGU+LmJ7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iYiIgZD0iTTE3Ni43Niw4OC4xOGwtMzYtMzcuNDNjLTEuMzMtMS40OC0zLjQxLTIuMDQtNS4zMS0xLjQybC0xMC42MiwyLjk4LTEyLjk1LDMuNjNoLjc4YzUuMTQtNC41Nyw5LjktOS41NSwxNC4yNS0xNC44OSwxLjY4LTEuNjgsMS44MS0yLjcyLDAtNC4yN0w5Mi40NSwuNzZxLTEuOTQtMS4wNC00LjAxLC4xM2MtMTIuMDQsMTIuNDMtMjMuODMsMjQuNzQtMzYsMzcuNjktMS4yLDEuNDUtMS41LDMuNDQtLjc4LDUuMThsNC4yNywxNi41OGMwLDIuNzIsMS40Miw1LjU3LDIuMDcsOC4yOS00LjczLTUuNjEtOS43NC0xMC45Ny0xNS4wMi0xNi4wNi0xLjY4LTEuODEtMi41OS0xLjgxLTQuNCwwTDQuMzksODguMDVjLTEuNjgsMi4zMy0xLjgxLDIuMzMsMCw0LjUzbDM1Ljg3LDM3LjNjMS4zNiwxLjUzLDMuNSwyLjEsNS40NCwxLjQybDExLjQtMy4xMSwxMi45NS0zLjYzdi45MWMtNS4yOSw0LjE3LTEwLjIyLDguNzYtMTQuNzYsMTMuNzNxLTMuNjMsMi45OC0uNzgsNS4zMWwzMy40MSwzNC44NGMyLjIsMi4yLDIuOTgsMi4yLDUuMTgsMGwzNS40OC0zNy4xN2MxLjU5LTEuMzgsMi4xNi0zLjYsMS40Mi01LjU3LTEuNjgtNi4wOS0zLjI0LTEyLjMtNC43OS0xOC4zOS0uNzQtMi4yNy0xLjIyLTQuNjItMS40Mi02Ljk5LDQuMyw1LjkzLDkuMDcsMTEuNTIsMTQuMjUsMTYuNzEsMS42OCwxLjY4LDIuNzIsMS42OCw0LjQsMGwzNC4zMi0zNS43NHExLjU1LTEuODEsMC00LjAxWm0tNzIuMjYsMTUuMTVjLTMuMTEtLjc4LTYuMDktMS41NS05LjE5LTIuNTktMS43OC0uMzQtMy42MSwuMy00Ljc5LDEuNjhsLTEyLjk1LDEzLjg2Yy0uNzYsLjg1LTEuNDUsMS43Ni0yLjA3LDIuNzJoLS42NWMxLjMtNS4zMSwyLjcyLTEwLjYyLDQuMDEtMTUuOGwxLjY4LTYuNzNjLjg0LTIuMTgsLjE1LTQuNjUtMS42OC02LjA5bC0xMi45NS0xNC4xMmMtLjY0LS40NS0xLjE0LTEuMDgtMS40Mi0xLjgxbDE5LjA0LDUuMTgsMi41OSwuNzhjMi4wNCwuNzYsNC4zMywuMTQsNS43LTEuNTVsMTIuOTUtMTQuMzhzLjc4LTEuMDQsMS42OC0xLjE3Yy0xLjgxLDYuNi0yLjk4LDE0LjEyLTUuNDQsMjAuNDYtMS4wOCwyLjk2LS4wOCw2LjI4LDIuNDYsOC4xNiw0LjI3LDQuMTQsOC4yOSw4LjU1LDEyLjk1LDEyLjk1LDAsMCwxLjMsLjkxLDEuNDIsMi4wN2wtMTMuMzQtMy42M1oiLz48L3N2Zz4=)](https://github.com/keep-starknet-strange) + +
+ +## Overview + +`shinigami` is a library enabling Bitcoin Script VM execution in Cairo, thus allowing the generation of STARK proofs of generic Bitcoin Script computation. + +Key features : + - Bitcoin script interpretation and execution + - Easily configurable VM ( enable different op-codes ) + +### Running + +```bash +scarb cairo-run --available-gas=200000000 +``` + +This will run the provided Bitcoin Script in Cairo. + +### Building + +```bash +scarb build +``` + +This will compile all the components. + +### Testing + +```bash +scarb test +``` + +This will run the test-suite for all op-codes, integration, and test Bitcoin Scripts. + +## References + +- [Bitcoin Script Wiki](https://en.bitcoin.it/wiki/Script) +- [Telegram]() +- [OnlyDust]() + +## Contributors ✨ + +Thanks goes to these wonderful people. Follow the [contributors guide](https://github.com/keep-starknet-strange/art-peace/blob/main/CONTRIBUTING.md) if you'd like to take part. diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 00000000..42bea520 --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "shinigami" +version = "0.1.0" diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 00000000..f2f161c9 --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "shinigami" +version = "0.1.0" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] diff --git a/resources/how-to-add-an-opcode.md b/resources/how-to-add-an-opcode.md new file mode 100644 index 00000000..466124e1 --- /dev/null +++ b/resources/how-to-add-an-opcode.md @@ -0,0 +1,13 @@ +## How to add an opcode to "Shinigami" + +1. Understand how the opcode works by looking at documentation: + - [Bitcoin Script Wiki](https://en.bitcoin.it/wiki/Script) + - [Reference implementation in BTCD](https://github.com/btcsuite/btcd/blob/b161cd6a199b4e35acec66afc5aad221f05fe1e3/txscript/opcode.go#L312) + - In BTCD find the function that matches the last element in `opcodeArray` for the specified opcode, that will be the reference implementation. +1. Add the Opcode to `src/opcodes/opcodes.cairo` + - Add the Opcode byte const like `pub const OP_ADD: u8 = 147;` + - Create the function implementing the opcode like `fn opcode_add(ref engine: Engine) {` + - Add the function to the `execute` method like `147 => opcode_add(ref engine),` +1. Add the Opcode to the compiler dict at `src/compiler.cairo` like `opcodes.insert('OP_ADD', Opcode::OP_ADD);`. +1. Create a test for your opcode at `src/opcodes/tests/test_opcodes.cairo` and ensure all the logic works as expected. +1. Create a PR, ensure CI passes, and await review. diff --git a/resources/shinigami-logo/shinigami.png b/resources/shinigami-logo/shinigami.png new file mode 100644 index 00000000..c2e77f71 Binary files /dev/null and b/resources/shinigami-logo/shinigami.png differ diff --git a/src/compiler.cairo b/src/compiler.cairo new file mode 100644 index 00000000..9cff19a8 --- /dev/null +++ b/src/compiler.cairo @@ -0,0 +1,73 @@ +use shinigami::opcodes::Opcode; + +// Compiler that takes a Bitcoin Script program and compiles it into a bytecode +#[derive(Destruct)] +pub struct Compiler { + // Dict containing opcode names to their bytecode representation + opcodes: Felt252Dict +} + +pub trait CompilerTrait { + // Create a compiler, initializing the opcode dict + fn new() -> Compiler; + // Compiles a program like "OP_1 OP_2 OP_ADD" into a bytecode run by the Engine. + fn compile(self: Compiler, script: ByteArray) -> ByteArray; +} + +pub impl CompilerTraitImpl of CompilerTrait { + fn new() -> Compiler { + let mut opcodes = Default::default(); + // Add the opcodes to the dict + opcodes.insert('OP_0', Opcode::OP_0); + opcodes.insert('OP_1', Opcode::OP_1); + opcodes.insert('OP_ADD', Opcode::OP_ADD); + Compiler { opcodes } + } + + // TODO: Why self is mutable? + fn compile(mut self: Compiler, script: ByteArray) -> ByteArray { + let mut bytecode = ""; + let seperator = ' '; + let byte_shift = 256; + let max_word_size = 31; + let mut current: felt252 = ''; + let mut i = 0; + let mut word_len = 0; + let mut current_word: felt252 = ''; + while i < script.len() { + let char = script[i].into(); + if char == seperator { + let opcode = self.opcodes.get(current); + current_word = current_word * byte_shift + opcode.into(); + word_len += 1; + if word_len >= max_word_size { + // Add the current word to the bytecode representation + bytecode.append_word(current_word, max_word_size); + word_len = 0; + } + current = ''; + } else { + // Add the char to the bytecode representation + current = current * byte_shift + char; + } + i += 1; + }; + // Handle the last opcode + if current != '' { + let opcode = self.opcodes.get(current); + current_word = current_word * byte_shift + opcode.into(); + word_len += 1; + if word_len >= max_word_size { + // Add the current word to the bytecode representation + bytecode.append_word(current_word, max_word_size); + word_len = 0; + } + } + if word_len > 0 { + // Add the current word to the bytecode representation + bytecode.append_word(current_word, word_len); + } + + bytecode + } +} diff --git a/src/engine.cairo b/src/engine.cairo new file mode 100644 index 00000000..c96d5810 --- /dev/null +++ b/src/engine.cairo @@ -0,0 +1,94 @@ +use shinigami::stack::{ScriptStack, ScriptStackImpl}; +use shinigami::opcodes::opcodes::Opcode::execute; + +// Represents the VM that executes Bitcoin scripts +#[derive(Destruct)] +pub struct Engine { + // The script to execute + script: @ByteArray, + // Program counter within the current script + opcode_idx: usize, + // Primary data stack + pub dstack: ScriptStack, + // Alternate data stack + pub astack: ScriptStack, + // TODO + // ... +} + +pub trait EngineTrait { + // Create a new Engine with the given script + fn new(script: ByteArray) -> Engine; + fn get_dstack(ref self: Engine) -> Span; + fn get_astack(ref self: Engine) -> Span; + // Executes the next instruction in the script + fn step(ref self: Engine) -> bool; // TODO return type w/ error handling + // Executes the entire script and returns top of stack or error if script fails + fn execute(ref self: Engine) -> Result; +} + +pub impl EngineTraitImpl of EngineTrait { + fn new(script: ByteArray) -> Engine { + Engine { + script: @script, + opcode_idx: 0, + dstack: ScriptStackImpl::new(), + astack: ScriptStackImpl::new(), + } + } + + fn get_dstack(ref self: Engine) -> Span { + return self.dstack.stack_to_span(); + } + + fn get_astack(ref self: Engine) -> Span { + return self.astack.stack_to_span(); + } + + fn step(ref self: Engine) -> bool { + if self.opcode_idx >= self.script.len() { + return false; + } + + let opcode = self.script[self.opcode_idx]; + execute(opcode, ref self); + self.opcode_idx += 1; + return true; + } + + fn execute(ref self: Engine) -> Result { + while self.opcode_idx < self.script.len() { + let opcode = self.script[self.opcode_idx]; + execute(opcode, ref self); + self.opcode_idx += 1; + // TODO: remove debug + // self.dstack.print(); + // println!("=================="); + }; + + // TODO: CheckErrorCondition + if self.dstack.len() < 1 { + return Result::Err('Stack empty at end of script'); + } else if self.dstack.len() > 1 { + return Result::Err('Stack must contain item'); + } else { + // TODO: pop bool + let top_stack = self.dstack.pop_byte_array(); + let ret_val = top_stack.clone(); + let mut is_ok = false; + let mut i = 0; + while i < top_stack.len() { + if top_stack[i] != 0 { + is_ok = true; + break; + } + i += 1; + }; + if is_ok { + return Result::Ok(ret_val); + } else { + return Result::Err('Script failed'); + } + } + } +} diff --git a/src/lib.cairo b/src/lib.cairo new file mode 100644 index 00000000..04a4e86b --- /dev/null +++ b/src/lib.cairo @@ -0,0 +1,13 @@ +pub mod compiler; +pub mod opcodes { + pub mod opcodes; + mod tests { + #[cfg(test)] + mod test_opcodes; + } + pub(crate) use opcodes::Opcode; +} +pub mod engine; +pub mod stack; + +mod main; diff --git a/src/main.cairo b/src/main.cairo new file mode 100644 index 00000000..3fea314b --- /dev/null +++ b/src/main.cairo @@ -0,0 +1,16 @@ +use shinigami::compiler::CompilerTraitImpl; +use shinigami::engine::EngineTraitImpl; + +fn main() { + let program = "OP_0 OP_1 OP_ADD"; + println!("Running Bitcoin Script: {}", program); + let mut compiler = CompilerTraitImpl::new(); + let bytecode = compiler.compile(program); + let mut engine = EngineTraitImpl::new(bytecode); + let res = engine.execute(); + if res.is_ok() { + println!("Execution successful"); + } else { + println!("Execution failed"); + } +} diff --git a/src/opcodes/opcodes.cairo b/src/opcodes/opcodes.cairo new file mode 100644 index 00000000..94383b58 --- /dev/null +++ b/src/opcodes/opcodes.cairo @@ -0,0 +1,180 @@ +pub mod Opcode { + pub const OP_0: u8 = 0; + pub const OP_1: u8 = 81; + pub const OP_ADD: u8 = 147; + + use shinigami::engine::Engine; + use shinigami::stack::ScriptStackTrait; + pub fn execute(opcode: u8, ref engine: Engine) { + match opcode { + 0 => opcode_false(ref engine), + 1 => not_implemented(ref engine), + 2 => not_implemented(ref engine), + 3 => not_implemented(ref engine), + 4 => not_implemented(ref engine), + 5 => not_implemented(ref engine), + 6 => not_implemented(ref engine), + 7 => not_implemented(ref engine), + 8 => not_implemented(ref engine), + 9 => not_implemented(ref engine), + 10 => not_implemented(ref engine), + 11 => not_implemented(ref engine), + 12 => not_implemented(ref engine), + 13 => not_implemented(ref engine), + 14 => not_implemented(ref engine), + 15 => not_implemented(ref engine), + 16 => not_implemented(ref engine), + 17 => not_implemented(ref engine), + 18 => not_implemented(ref engine), + 19 => not_implemented(ref engine), + 20 => not_implemented(ref engine), + 21 => not_implemented(ref engine), + 22 => not_implemented(ref engine), + 23 => not_implemented(ref engine), + 24 => not_implemented(ref engine), + 25 => not_implemented(ref engine), + 26 => not_implemented(ref engine), + 27 => not_implemented(ref engine), + 28 => not_implemented(ref engine), + 29 => not_implemented(ref engine), + 30 => not_implemented(ref engine), + 31 => not_implemented(ref engine), + 32 => not_implemented(ref engine), + 33 => not_implemented(ref engine), + 34 => not_implemented(ref engine), + 35 => not_implemented(ref engine), + 36 => not_implemented(ref engine), + 37 => not_implemented(ref engine), + 38 => not_implemented(ref engine), + 39 => not_implemented(ref engine), + 40 => not_implemented(ref engine), + 41 => not_implemented(ref engine), + 42 => not_implemented(ref engine), + 43 => not_implemented(ref engine), + 44 => not_implemented(ref engine), + 45 => not_implemented(ref engine), + 46 => not_implemented(ref engine), + 47 => not_implemented(ref engine), + 48 => not_implemented(ref engine), + 49 => not_implemented(ref engine), + 50 => not_implemented(ref engine), + 51 => not_implemented(ref engine), + 52 => not_implemented(ref engine), + 53 => not_implemented(ref engine), + 54 => not_implemented(ref engine), + 55 => not_implemented(ref engine), + 56 => not_implemented(ref engine), + 57 => not_implemented(ref engine), + 58 => not_implemented(ref engine), + 59 => not_implemented(ref engine), + 60 => not_implemented(ref engine), + 61 => not_implemented(ref engine), + 62 => not_implemented(ref engine), + 63 => not_implemented(ref engine), + 64 => not_implemented(ref engine), + 65 => not_implemented(ref engine), + 66 => not_implemented(ref engine), + 67 => not_implemented(ref engine), + 68 => not_implemented(ref engine), + 69 => not_implemented(ref engine), + 70 => not_implemented(ref engine), + 71 => not_implemented(ref engine), + 72 => not_implemented(ref engine), + 73 => not_implemented(ref engine), + 74 => not_implemented(ref engine), + 75 => not_implemented(ref engine), + 76 => not_implemented(ref engine), + 77 => not_implemented(ref engine), + 78 => not_implemented(ref engine), + 79 => not_implemented(ref engine), + 80 => not_implemented(ref engine), + 81 => opcode_n(1, ref engine), + 82 => not_implemented(ref engine), + 83 => not_implemented(ref engine), + 84 => not_implemented(ref engine), + 85 => not_implemented(ref engine), + 86 => not_implemented(ref engine), + 87 => not_implemented(ref engine), + 88 => not_implemented(ref engine), + 89 => not_implemented(ref engine), + 90 => not_implemented(ref engine), + 91 => not_implemented(ref engine), + 92 => not_implemented(ref engine), + 93 => not_implemented(ref engine), + 94 => not_implemented(ref engine), + 95 => not_implemented(ref engine), + 96 => not_implemented(ref engine), + 97 => not_implemented(ref engine), + 98 => not_implemented(ref engine), + 99 => not_implemented(ref engine), + 100 => not_implemented(ref engine), + 101 => not_implemented(ref engine), + 102 => not_implemented(ref engine), + 103 => not_implemented(ref engine), + 104 => not_implemented(ref engine), + 105 => not_implemented(ref engine), + 106 => not_implemented(ref engine), + 107 => not_implemented(ref engine), + 108 => not_implemented(ref engine), + 109 => not_implemented(ref engine), + 110 => not_implemented(ref engine), + 111 => not_implemented(ref engine), + 112 => not_implemented(ref engine), + 113 => not_implemented(ref engine), + 114 => not_implemented(ref engine), + 115 => not_implemented(ref engine), + 116 => not_implemented(ref engine), + 117 => not_implemented(ref engine), + 118 => not_implemented(ref engine), + 119 => not_implemented(ref engine), + 120 => not_implemented(ref engine), + 121 => not_implemented(ref engine), + 122 => not_implemented(ref engine), + 123 => not_implemented(ref engine), + 124 => not_implemented(ref engine), + 125 => not_implemented(ref engine), + 126 => not_implemented(ref engine), + 127 => not_implemented(ref engine), + 128 => not_implemented(ref engine), + 129 => not_implemented(ref engine), + 130 => not_implemented(ref engine), + 131 => not_implemented(ref engine), + 132 => not_implemented(ref engine), + 133 => not_implemented(ref engine), + 134 => not_implemented(ref engine), + 135 => not_implemented(ref engine), + 136 => not_implemented(ref engine), + 137 => not_implemented(ref engine), + 138 => not_implemented(ref engine), + 139 => not_implemented(ref engine), + 140 => not_implemented(ref engine), + 141 => not_implemented(ref engine), + 142 => not_implemented(ref engine), + 143 => not_implemented(ref engine), + 144 => not_implemented(ref engine), + 145 => not_implemented(ref engine), + 146 => not_implemented(ref engine), + 147 => opcode_add(ref engine), + _ => not_implemented(ref engine) + } + } + + fn opcode_false(ref engine: Engine) { + engine.dstack.push_byte_array(""); + } + + fn opcode_n(n: i64, ref engine: Engine) { + engine.dstack.push_int(n); + } + + fn opcode_add(ref engine: Engine) { + // TODO: Error handling + let a = engine.dstack.pop_int(); + let b = engine.dstack.pop_int(); + engine.dstack.push_int(a + b); + } + + fn not_implemented(ref engine: Engine) { + panic!("Opcode not implemented"); + } +} diff --git a/src/opcodes/tests/test_opcodes.cairo b/src/opcodes/tests/test_opcodes.cairo new file mode 100644 index 00000000..74df5b84 --- /dev/null +++ b/src/opcodes/tests/test_opcodes.cairo @@ -0,0 +1,53 @@ +use shinigami::compiler::CompilerTraitImpl; +use shinigami::engine::EngineTraitImpl; + +#[test] +fn test_op_0() { + let program = "OP_0"; + let mut compiler = CompilerTraitImpl::new(); + let bytecode = compiler.compile(program); + let mut engine = EngineTraitImpl::new(bytecode); + let res = engine.step(); + assert!(res, "Execution of step failed"); + + let dstack = engine.get_dstack(); + assert_eq!(dstack.len(), 1, "Stack length is not 1"); + + let expected_stack = array![""]; + assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected"); +} + +#[test] +fn test_op_1() { + let program = "OP_1"; + let mut compiler = CompilerTraitImpl::new(); + let bytecode = compiler.compile(program); + let mut engine = EngineTraitImpl::new(bytecode); + let res = engine.step(); + assert!(res, "Execution of step failed"); + + let dstack = engine.get_dstack(); + assert_eq!(dstack.len(), 1, "Stack length is not 1"); + + // TODO: Is this the correct representation of 1? + let expected_stack = array!["\0\0\0\0\0\0\0\x01"]; + assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected"); +} + +#[test] +fn test_op_add() { + let program = "OP_1 OP_1 OP_ADD"; + let mut compiler = CompilerTraitImpl::new(); + let bytecode = compiler.compile(program); + let mut engine = EngineTraitImpl::new(bytecode); + let _ = engine.step(); + let _ = engine.step(); + let res = engine.step(); + assert!(res, "Execution of run failed"); + + let dstack = engine.get_dstack(); + assert_eq!(dstack.len(), 1, "Stack length is not 1"); + + let expected_stack = array!["\0\0\0\0\0\0\0\x02"]; + assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected"); +} diff --git a/src/stack.cairo b/src/stack.cairo new file mode 100644 index 00000000..a3ce5bdb --- /dev/null +++ b/src/stack.cairo @@ -0,0 +1,105 @@ +use core::nullable::NullableTrait; +use core::dict::Felt252DictEntryTrait; + +#[derive(Destruct)] +pub struct ScriptStack { + data: Felt252Dict>, + len: usize, +} + +#[generate_trait()] +pub impl ScriptStackImpl of ScriptStackTrait { + fn new() -> ScriptStack { + ScriptStack { + data: Default::default(), + len: 0, + } + } + + fn push_byte_array(ref self: ScriptStack, value: ByteArray) { + self.data.insert(self.len.into(), NullableTrait::new(value)); + self.len += 1; + } + + fn push_int(ref self: ScriptStack, value: i64) { + let mut bytes = ""; + bytes.append_word(value.into(), 8); + self.push_byte_array(bytes); + } + + fn pop_byte_array(ref self: ScriptStack) -> ByteArray { + if self.len == 0 { + // TODO + panic!("pop_byte_array: stack underflow"); + } + self.len -= 1; + let (entry, bytes) = self.data.entry(self.len.into()); + self.data = entry.finalize(NullableTrait::new("")); + bytes.deref() + } + + fn pop_int(ref self: ScriptStack) -> i64 { + let bytes = self.pop_byte_array(); + // TODO: Error handling & MakeScriptNum + let bytes_len = bytes.len(); + if bytes_len == 0 { + return 0; + } + let mut value: i64 = 0; + let mut i = 0; + if bytes_len < 8 { + while i < bytes_len { + value = value * 256 + bytes.at(i).unwrap().into(); + i += 1; + }; + return value; + } else { + while i < 8 { + value = value * 256 + bytes.at(bytes_len - 8 + i).unwrap().into(); + i += 1; + }; + return value; + } + } + + fn len(ref self: ScriptStack) -> usize { + self.len + } + + fn depth(ref self: ScriptStack) -> usize { + self.len + } + + fn print_element(ref self: ScriptStack, idx: usize) { + let (entry, arr) = self.data.entry(idx.into()); + let arr = arr.deref(); + if arr.len() == 0 { + println!("stack[{}]: null", idx); + } else { + println!("stack[{}]: {}", idx, arr); + } + self.data = entry.finalize(NullableTrait::new(arr)); + } + + fn print(ref self: ScriptStack) { + let mut i = self.len; + while i > 0 { + i -= 1; + self.print_element(i.into()); + } + } + + fn stack_to_span(ref self: ScriptStack) -> Span { + let mut result = array![]; + let mut i = self.len; + while i > 0 { + i -= 1; + let (entry, arr) = self.data.entry(i.into()); + let arr = arr.deref(); + result.append(arr.clone()); + self.data = entry.finalize(NullableTrait::new(arr)); + }; + + return result.span(); + } +} diff --git a/tests/test.cairo b/tests/test.cairo new file mode 100644 index 00000000..1d15337c --- /dev/null +++ b/tests/test.cairo @@ -0,0 +1,12 @@ +use shinigami::compiler::CompilerTraitImpl; +use shinigami::engine::EngineTraitImpl; + +#[test] +fn execution_test() { + let program = "OP_0 OP_1 OP_ADD"; + let mut compiler = CompilerTraitImpl::new(); + let bytecode = compiler.compile(program); + let mut engine = EngineTraitImpl::new(bytecode); + let res = engine.execute(); + assert!(res.is_ok(), "Execution failed"); +}