Skip to content

Commit

Permalink
feat: add scriptnum (#41)
Browse files Browse the repository at this point in the history
<!-- enter the gh issue after hash -->

- [X] follows contribution
[guide](https://github.com/keep-starknet-strange/shinigami/blob/main/CONTRIBUTING.md)
- [X] code change includes tests

<!-- PR description below -->

- Add module to convert integer to Bitcoin 'sign-magnitude'
representation in a ByteArray.
- Add conversion tests
- Edit some tests of opcode using pop_int()

---------

Co-authored-by: j1mbo64 <j1mbo64@none.dev>
Co-authored-by: Brandon Roberts <brandonjroberts22@gmail.com>
  • Loading branch information
3 people authored Jul 30, 2024
1 parent b7a6b29 commit 02839b2
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 102 deletions.
20 changes: 10 additions & 10 deletions resources/how-to-add-an-opcode.md
Original file line number Diff line number Diff line change
@@ -1,13 +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.
2. 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),`
3. Add the Opcode to the compiler dict at `src/compiler.cairo` like `opcodes.insert('OP_ADD', Opcode::OP_ADD);`.
4. Create a test for your opcode at `src/opcodes/tests/test_opcodes.cairo` and ensure all the logic works as expected.
5. Create a PR, ensure CI passes, and await review.
* [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.
13 changes: 10 additions & 3 deletions src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub mod compiler;
pub mod engine;
pub mod stack;
pub mod cond_stack;
pub mod opcodes {
pub mod opcodes;
Expand All @@ -8,8 +10,13 @@ pub mod opcodes {
}
pub(crate) use opcodes::Opcode;
}
pub mod engine;
pub mod stack;
pub mod utils;
pub mod scriptnum {
pub mod scriptnum;
mod tests {
#[cfg(test)]
mod test_scriptnum;
}
pub(crate) use scriptnum::ScriptNum;
}

mod main;
19 changes: 10 additions & 9 deletions src/opcodes/tests/test_opcodes.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use shinigami::compiler::CompilerTraitImpl;
use shinigami::engine::EngineTraitImpl;
use shinigami::utils::int_to_bytes;
use shinigami::scriptnum::ScriptNum;

fn test_op_n(value: u8) {
let program = format!("OP_{}", value);
Expand All @@ -12,7 +12,7 @@ fn test_op_n(value: u8) {

let dstack = engine.get_dstack();
assert_eq!(dstack.len(), 1, "Stack length is not 1");
let expected_stack = array![int_to_bytes(value.into())];
let expected_stack = array![ScriptNum::wrap(value.into())];
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected");
}

Expand Down Expand Up @@ -112,7 +112,7 @@ fn test_op_negate_1() {
let dstack = engine.get_dstack();
assert_eq!(dstack.len(), 1, "Stack length is not 1");

let expected_stack = array![int_to_bytes(-1)];
let expected_stack = array![ScriptNum::wrap(-1)];
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected");
}

Expand Down Expand Up @@ -141,7 +141,7 @@ fn test_op_negate_negative() {
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![int_to_bytes(1)];
let expected_stack = array![ScriptNum::wrap(1)];
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected");
}

Expand Down Expand Up @@ -207,7 +207,7 @@ fn test_op_sub() {
let dstack = engine.get_dstack();
assert_eq!(dstack.len(), 1, "Stack length is not 1");

let expected_stack: Array<ByteArray> = array![int_to_bytes(-1)];
let expected_stack: Array<ByteArray> = array![ScriptNum::wrap(-1)];
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected");
}

Expand Down Expand Up @@ -378,6 +378,7 @@ fn test_op_within_false() {
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected");
}

#[test]
fn test_op_depth_empty_stack() {
let program = "OP_DEPTH";
let mut compiler = CompilerTraitImpl::new();
Expand All @@ -390,7 +391,7 @@ fn test_op_depth_empty_stack() {
let dstack = engine.get_dstack();
assert_eq!(dstack.len(), 1, "Stack length is not 1");

let expected_stack = array!["\0"];
let expected_stack = array![""];
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected for empty stack");
}

Expand Down Expand Up @@ -451,7 +452,7 @@ fn test_op_depth_multiple_items() {
}

#[test]
fn test_op_TRUE() {
fn test_op_true() {
let program = "OP_TRUE";
let mut compiler = CompilerTraitImpl::new();
let bytecode = compiler.compile(program);
Expand Down Expand Up @@ -507,6 +508,7 @@ fn test_op_else_false() {
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected");
}

#[test]
fn test_op_1add() {
let program = "OP_1 OP_1ADD";
let mut compiler = CompilerTraitImpl::new();
Expand Down Expand Up @@ -774,7 +776,6 @@ fn test_op_1negate() {

assert_eq!(dstack.len(), 1, "Stack length is not 1");

let expected_stack = array![int_to_bytes(-1)];
let expected_stack = array![ScriptNum::wrap(-1)];
assert_eq!(dstack, expected_stack.span(), "Stack is not equal to expected");
}

98 changes: 98 additions & 0 deletions src/scriptnum/scriptnum.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Wrapper around Bitcoin Script 'sign-magnitude' 4 byte integer.
pub mod ScriptNum {
const BYTESHIFT: i64 = 256;
const MAX_BYTE_LEN: usize = 4;

// Wrap i64 with a maximum size of 4 bytes. Can result in 5 byte array.
pub fn wrap(mut input: i64) -> ByteArray {
if input == 0 {
return "";
}

let mut result: ByteArray = Default::default();
let is_negative = {
if input < 0 {
input *= -1;
true
} else {
false
}
};
// Unwrap cannot fail because input is set to positive above.
let unsigned: u64 = input.try_into().unwrap();
let bytes_len: usize = integer_bytes_len(unsigned.into());
if bytes_len > MAX_BYTE_LEN {
panic!("scriptnum(wrap): number more than {} bytes long", MAX_BYTE_LEN);
}
result.append_word_rev(unsigned.into(), bytes_len - 1);
// Compute 'sign-magnitude' byte.
let sign_byte: u8 = get_last_byte_of_uint(unsigned);
if is_negative {
if (sign_byte > 127) {
result.append_byte(sign_byte);
result.append_byte(128);
} else {
result.append_byte(sign_byte + 128);
}
} else {
if (sign_byte > 127) {
result.append_byte(sign_byte);
result.append_byte(0);
} else {
result.append_byte(sign_byte);
}
}
result
}

// Unwrap sign-magnitude encoded ByteArray into a 4 byte int maximum.
pub fn unwrap(input: ByteArray) -> i64 {
let mut result: i64 = 0;
let mut i: u32 = 0;
let mut multiplier: i64 = 1;
if input.len() == 0 {
return 0;
}
let snap_input = @input;
while i < snap_input.len()
- 1 {
result += snap_input.at(i).unwrap().into() * multiplier;
multiplier *= BYTESHIFT;
i += 1;
};
// Recover value and sign from 'sign-magnitude' byte.
let sign_byte: i64 = input.at(i).unwrap().into();
if sign_byte >= 128 {
result = (multiplier * (sign_byte - 128) * -1) - result;
} else {
result += sign_byte * multiplier;
}
if integer_bytes_len(result.into()) > MAX_BYTE_LEN {
panic!("scriptnum(unwrap): number more than {} bytes long", MAX_BYTE_LEN);
}
result
}

// Return the minimal number of byte to represent 'value'.
fn integer_bytes_len(mut value: i128) -> usize {
if value < 0 {
value *= -1;
}
let mut power_byte = BYTESHIFT.try_into().unwrap();
let mut bytes_len: usize = 1;
while value >= power_byte {
bytes_len += 1;
power_byte *= 256;
};
bytes_len
}

// Return the value of the last byte of 'value'.
fn get_last_byte_of_uint(mut value: u64) -> u8 {
let byteshift = BYTESHIFT.try_into().unwrap();
while value > byteshift {
value = value / byteshift;
};
value.try_into().unwrap()
}
}
74 changes: 74 additions & 0 deletions src/scriptnum/tests/test_scriptnum.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use core::byte_array::ByteArray;
use shinigami::scriptnum::ScriptNum;
#[test]
fn test_scriptnum_wrap_unwrap() {
let mut int = 0;
let mut returned_int = ScriptNum::unwrap(ScriptNum::wrap(int));
assert!(int == returned_int, "Wrap/unwrap 0 not equal");

int = 1;
returned_int = ScriptNum::unwrap(ScriptNum::wrap(int));
assert!(int == returned_int, "Wrap/unwrap 1 not equal");

int = -1;
returned_int = ScriptNum::unwrap(ScriptNum::wrap(int));
assert!(int == returned_int, "Wrap/unwrap -1 not equal");

int = 32767;
returned_int = ScriptNum::unwrap(ScriptNum::wrap(int));
assert!(int == returned_int, "Wrap/unwrap 32767 not equal");

int = -452354;
returned_int = ScriptNum::unwrap(ScriptNum::wrap(int));
assert!(int == returned_int, "Wrap/unwrap 32767 not equal");

int = 4294967295; // 0xFFFFFFFF
returned_int = ScriptNum::unwrap(ScriptNum::wrap(int));
assert!(int == returned_int, "Wrap/unwrap 4294967295 not equal");
}

#[test]
fn test_scriptnum_bytes_wrap() {
let mut bytes: ByteArray = Default::default();
bytes.append_byte(42); // 0x2A
let mut returned_int = ScriptNum::unwrap(bytes);
assert!(returned_int == 42, "Unwrap 0x2A not equal to 42");

let mut bytes: ByteArray = "";
returned_int = ScriptNum::unwrap(bytes);
assert!(returned_int == 0, "Unwrap empty bytes not equal to 0");

let mut bytes: ByteArray = Default::default();
bytes.append_byte(129); // 0x81
bytes.append_byte(128); // 0x80
returned_int = ScriptNum::unwrap(bytes);
assert!(returned_int == -129, "Unwrap 0x8180 not equal to -129");

let mut bytes: ByteArray = Default::default();
bytes.append_byte(255); // 0xFF
bytes.append_byte(127); // 0x7F
returned_int = ScriptNum::unwrap(bytes);
assert!(returned_int == 32767, "0xFF7F not equal to 32767");

let mut bytes: ByteArray = Default::default();
bytes.append_byte(0); // 0x00
bytes.append_byte(128); // 0x80
bytes.append_byte(128); // 0x80
returned_int = ScriptNum::unwrap(bytes);
assert!(returned_int == -32768, "0x008080 not equal to -32768");
}

#[test]
#[should_panic]
fn test_scriptnum_too_big_unwrap_panic() {
let mut bytes: ByteArray = Default::default();
bytes.append_word_rev(4294967295 + 1, 5); // 0xFFFFFFFF + 1
ScriptNum::unwrap(bytes);
}

#[test]
#[should_panic]
fn test_scriptnum_too_big_wrap_panic() {
let value: i64 = 4294967295 + 1; // 0xFFFFFFFF + 1
ScriptNum::wrap(value);
}
10 changes: 5 additions & 5 deletions src/stack.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use core::dict::Felt252DictEntryTrait;
use shinigami::utils;
use shinigami::scriptnum::ScriptNum;

#[derive(Destruct)]
pub struct ScriptStack {
Expand All @@ -19,7 +19,7 @@ pub impl ScriptStackImpl of ScriptStackTrait {
}

fn push_int(ref self: ScriptStack, value: i64) {
let mut bytes = utils::int_to_bytes(value);
let bytes = ScriptNum::wrap(value);
self.push_byte_array(bytes);
}

Expand All @@ -35,9 +35,9 @@ pub impl ScriptStackImpl of ScriptStackTrait {
}

fn pop_int(ref self: ScriptStack) -> i64 {
//TODO Error Handling
let bytes = self.pop_byte_array();
// TODO: Error handling & MakeScriptNum
return utils::bytes_to_int(bytes);
ScriptNum::unwrap(bytes)
}

fn pop_bool(ref self: ScriptStack) -> bool {
Expand Down Expand Up @@ -74,7 +74,7 @@ pub impl ScriptStackImpl of ScriptStackTrait {

fn peek_int(ref self: ScriptStack, idx: usize) -> i64 {
let bytes = self.peek_byte_array(idx);
return utils::bytes_to_int(bytes);
ScriptNum::unwrap(bytes)
}

fn peek_bool(ref self: ScriptStack, idx: usize) -> bool {
Expand Down
Loading

0 comments on commit 02839b2

Please sign in to comment.