diff --git a/Scarb.toml b/Scarb.toml index 045fc46..80bcd5b 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -10,6 +10,8 @@ starknet = "2.8.5" openzeppelin = "0.18.0" utils = { git = "https://github.com/keep-starknet-strange/raito.git", rev = "02a13045b7074ae2b3247431cd91f1ad76263fb2" } consensus = { git = "https://github.com/keep-starknet-strange/raito.git", rev = "02a13045b7074ae2b3247431cd91f1ad76263fb2" } +alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.git" } +alexandria_data_structures = { git = "https://github.com/keep-starknet-strange/alexandria.git" } [dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.31.0" } diff --git a/src/constants.cairo b/src/constants.cairo new file mode 100644 index 0000000..f5c1878 --- /dev/null +++ b/src/constants.cairo @@ -0,0 +1,4 @@ +pub const OP_RETURN: u8 = 0x6a; +pub const OP_13: u8 = 0x5d; +pub const ETCHING_MAX_DIVISIBILITY: u8 = 38; +pub const ETCHING_MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; diff --git a/src/lib.cairo b/src/lib.cairo index 2633fd0..76fb20f 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,7 +1,26 @@ pub mod parser; +pub mod types; +pub mod constants; +pub mod runestone { + pub mod flag; + pub mod tag; + pub mod message; +} + +pub mod utils { + pub mod varint; + pub mod fields; + pub mod char; +} #[cfg(test)] mod tests { mod parser; + mod opcodes; + mod cenotaph; + mod utils; + mod varint; + mod flag; + mod transactions; } diff --git a/src/parser.cairo b/src/parser.cairo index 3f142aa..4a55dbb 100644 --- a/src/parser.cairo +++ b/src/parser.cairo @@ -1,9 +1,18 @@ -use consensus::{types::transaction::{Transaction}}; +use alexandria_data_structures::{span_ext::SpanTraitExt, byte_array_ext::ByteArrayIntoArrayU8}; +use core::num::traits::Zero; +use core::traits::Into; +use core::dict::Felt252Dict; +use consensus::types::transaction::Transaction; +use utils::bytearray::ByteArraySnapSerde; -pub const OP_RETURN: u8 = 0x6a; -pub const OP_13: u8 = 0x8c; +use runes_lib::runestone::{tag::{Tag, TagTrait}, message::extract_etching}; +use runes_lib::utils::{ + varint::decode_integers, fields::{append_value, store_field_key, has_even_tag} +}; +use runes_lib::types::{Edict, RuneId, Runestone, Artifact, Cenotaph, Payload, Rune}; +use runes_lib::constants::{OP_RETURN, OP_13}; -pub fn extract_runestone(tx: Transaction) -> Option> { +pub fn extract_runestone(tx: Transaction) -> Option { let mut outputs = tx.outputs; let mut runestone_output = Option::None; loop { @@ -11,7 +20,50 @@ pub fn extract_runestone(tx: Transaction) -> Option> { Option::None => { break; }, Option::Some(output) => { let pubscript = *output.pk_script; + if pubscript.len() < 2 { + continue; + } if pubscript[0] == OP_RETURN && pubscript[1] == OP_13 { + // load payload + let payload = match load_payload(pubscript.clone()) { + Option::Some(payload) => { + match payload { + Payload::Valid(p) => p, + Payload::Invalid(flaw) => { + runestone_output = + Option::Some( + Artifact::Cenotaph( + Cenotaph { + flaw: Option::Some(flaw), ..Default::default() + } + ) + ); + break; + }, + } + }, + Option::None => { break; }, + }; + + // decode integers + let decoded = match decode_integers(payload) { + Result::Ok(decoded) => decoded, + Result::Err(flaw) => { + runestone_output = + Option::Some( + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some(flaw), ..Default::default() } + ) + ); + break; + }, + }; + + // parse message + let (edicts, fields, flaws, fields_keys) = parse_message(tx, decoded.span()); + + // get runestone + runestone_output = get_runestone(tx, edicts, fields, fields_keys, flaws); break; }; }, @@ -22,80 +74,270 @@ pub fn extract_runestone(tx: Transaction) -> Option> { } // would load bitcoin script instructions to add element in stack and panic on other instruction -fn load_payload(pubscript: ByteArray, mut index: usize) -> ByteArray { +pub fn load_payload(pubscript: ByteArray) -> Option { let mut output: ByteArray = Default::default(); - let mut current_index: u32 = index; + let mut flaw: ByteArray = Default::default(); + let pubscript_arr: Array = pubscript.into(); + let mut pubscript_span: Span = pubscript_arr.span(); + pubscript_span.remove_front_n(2); + loop { - let opcode: u8 = pubscript[current_index]; - current_index += 1; - - // Handle direct size (1-75 bytes) - if opcode > 0 && opcode <= 0x4b { - let size: u32 = opcode.into(); - // Copy the next 'size' bytes to output - let mut i: usize = 0; - loop { - if i >= size { - break; + match pubscript_span.pop_front() { + Option::Some(opcode) => { + let opcode: u8 = *opcode; + + if opcode == 0 { + continue; } - output.append_byte(pubscript[current_index + i]); - i += 1; - }; - current_index += size; - continue; - } - // Handle OP_PUSHDATA1 (76-255 bytes) - if opcode == 0x4c { - let size: u32 = pubscript[current_index].into(); - current_index += 1; - // Copy the next 'size' bytes to output - let mut i = 0; - loop { - if i >= size { - break; + // Handle direct size (1-75 bytes) + if opcode > 0 && opcode <= 0x4b { + let size: u32 = opcode.into(); + let mut i: usize = 0; + loop { + if i == size { + break; + } + match pubscript_span.pop_front() { + Option::Some(data) => { output.append_byte(*data); }, + Option::None => { + flaw = "invalid script in OP_RETURN"; + break; + }, + }; + i += 1; + }; + continue; } - output.append_byte(pubscript[current_index + i]); - i += 1; - }; - current_index += size; - continue; - } - // Handle OP_PUSHDATA2 (256-65535 bytes) - if opcode == 0x4d { - // * 256 is << 8z - let size: u32 = ((pubscript[current_index]).into() - // so it doesn't exceed the max value - | ((pubscript[current_index + 1]).into() * 256) - & 0xFF00); - - current_index += 2; - // Copy the next 'size' bytes to output - let mut i: usize = 0; - loop { - if i >= size { - break; + // Handle OP_PUSHDATA1 (76-255 bytes) + if opcode == 0x4c { + let size: u32 = match pubscript_span.pop_front() { + Option::Some(data) => (*data).into(), + Option::None => { + flaw = "invalid script in OP_RETURN"; + break; + }, + }; + let mut i: usize = 0; + loop { + if i == size { + break; + } + match pubscript_span.pop_front() { + Option::Some(data) => { output.append_byte(*data); }, + Option::None => { + flaw = "invalid script in OP_RETURN"; + break; + }, + }; + i += 1; + }; + continue; + } + + // Handle OP_PUSHDATA2 (256-65535 bytes) + if opcode == 0x4d { + let size_arr = pubscript_span.pop_front_n(2); + if size_arr.len() < 2 { + flaw = "Not enough data for OP_PUSHDATA2 size field"; + break; + } + let size: u32 = ((*size_arr[0]).into() // so it doesn't exceed the max value + | ((*size_arr[1]).into() * 256_u32) + & 0xFF00); + + let mut i: usize = 0; + loop { + if i == size { + break; + } + match pubscript_span.pop_front() { + Option::Some(data) => { output.append_byte(*data); }, + Option::None => { + flaw = "Not enough data for OP_PUSHDATA2 size field"; + break; + }, + }; + i += 1; + }; + continue; + } + + // Handle OP_PUSHDATA4 (256-65535 bytes) + if opcode == 0x4e { + let size_arr = pubscript_span.pop_front_n(4); + if size_arr.len() < 2 { + flaw = "Not enough data for OP_PUSHDATA4 size field"; + break; + } + let size: u32 = ((*size_arr[0]).into() // so it doesn't exceed the max value + | ((*size_arr[1]).into() * 256_u32) + & 0xFF00); + + let mut i: usize = 0; + loop { + if i == size { + break; + } + match pubscript_span.pop_front() { + Option::Some(data) => { output.append_byte(*data); }, + Option::None => { + flaw = "Not enough data for OP_PUSHDATA4 size field"; + break; + }, + }; + i += 1; + }; + continue; } - output.append_byte(pubscript[current_index + i]); - i += 1; - }; - current_index += size; - continue; + + flaw = "non-pushdata opcode in OP_RETURN"; + break; + }, + Option::None => { break; }, } + }; + + if flaw.len() > 0 { + return Option::Some(Payload::Invalid(flaw)); + } + + Option::Some(Payload::Valid(output)) +} + +fn build_edict(tx: Transaction, id: RuneId, amount: u128, output: u128) -> Option { + let output: u32 = match output.try_into() { + Option::None => { return Option::None; }, + Option::Some(o) => o, + }; - // // Handle small integers (OP_0 to OP_16) - // if opcode == 0 { - // output.append_byte(0); - // continue; - // } - // // if opcode >= 0x51 && opcode <= 0x60 { - // // output.append_byte(opcode - 0x50); - // // continue; - // // } - - break; // Unknown opcode + let output_len: u32 = tx.outputs.len().try_into()?; + + if output > output_len { + return Option::None; + } + + Option::Some(Edict { id, amount, output }) +} + +pub fn parse_message( + tx: Transaction, mut payload: Span +) -> (Array, Felt252Dict>>, Option, Array) { + let mut edicts: Array = ArrayTrait::new(); + let mut fields: Felt252Dict>> = Default::default(); + let mut fields_keys: Array = ArrayTrait::new(); + let mut flaw: ByteArray = Default::default(); + + loop { + match payload.pop_front() { + Option::Some(tag) => { + if *tag == Tag::Body.get() { + let mut id: RuneId = Default::default(); + + loop { + if payload.len() == 0 { + break; + } + + // Build chunks + let mut chunk: Array = ArrayTrait::new(); + + loop { + if chunk.len() == 4 { + break; + } + + match payload.pop_front() { + Option::Some(data) => { chunk.append(*data); }, + Option::None => { break; }, + } + }; + + if chunk.len() != 4 { + flaw = "trailing integers in body"; + break; + } + + // Process chunk + let next = id.next(*chunk[0], *chunk[1]); + match next { + Option::Some(next_id) => { + match build_edict(tx, next_id, *chunk[2], *chunk[3]) { + Option::Some(edict) => { + id = next_id; + edicts.append(edict); + }, + Option::None => { + flaw = "edict output greater than transaction output count"; + break; + }, + }; + }, + Option::None => { + flaw = "invalid rune ID in edict"; + break; + } + } + }; + } else { + // value + match payload.pop_front() { + Option::Some(v) => { + append_value(ref fields, (*tag).into(), v.deref()); + store_field_key(ref fields_keys, *tag); + }, + Option::None => { + flaw = "field with missing value"; + break; + }, + }; + } + }, + Option::None => { break; }, + }; }; - output + (edicts, fields, Option::Some(flaw), fields_keys) +} + +pub fn get_runestone( + tx: Transaction, + edicts: Array, + mut fields: Felt252Dict>>, + mut fields_keys: Array, + flaw: Option +) -> Option { + let mut flaw: ByteArray = flaw.unwrap_or_default(); + let mut flags = match Tag::Flags.take(ref fields, ref fields_keys, 1) { + Option::None => { 0 }, + Option::Some(flags) => { *flags.at(0) }, + }; + + let (etching, supply_check, rune) = extract_etching(ref flags, ref fields, ref fields_keys); + + let mint: Option = Tag::Mint.take_mint(ref fields, ref fields_keys); + + let pointer: Option = Tag::Pointer + .take_pointer(ref fields, ref fields_keys, tx.outputs.len().into()); + + if etching.is_some() && supply_check.is_none() { + flaw = "supply overflows u128"; + } + + if !flags.is_zero() { + flaw = "unrecognized field"; + } + + if has_even_tag(fields_keys.span()) { + flaw = "unrecognized even tag"; + } + + if flaw.len() > 0 { + return Option::Some( + Artifact::Cenotaph(Cenotaph { flaw: Option::Some(flaw), mint, etching: rune }) + ); + } + + Option::Some(Artifact::Runestone(Runestone { edicts, mint, etching, pointer })) } diff --git a/src/runestone/flag.cairo b/src/runestone/flag.cairo new file mode 100644 index 0000000..d68f044 --- /dev/null +++ b/src/runestone/flag.cairo @@ -0,0 +1,56 @@ +use alexandria_math::pow; + +#[derive(Drop)] +pub enum Flag { + Etching, + Terms, + Turbo, + Cenotaph, +} + +pub trait FlagTrait { + fn get(self: Flag) -> u128; + fn mask(self: Flag) -> u128; + fn take(self: Flag, ref flags: u128) -> bool; + fn set(self: Flag, ref flags: u128); +} + +impl FlagImpl of FlagTrait { + fn get(self: Flag) -> u128 { + match self { + Flag::Etching => 0, + Flag::Terms => 1, + Flag::Turbo => 2, + Flag::Cenotaph => 127, + } + } + + fn mask(self: Flag) -> u128 { + pow(2_u128, self.get()) + } + + // Takes the flag: Checks if it is set, then clears it + fn take(self: Flag, ref flags: u128) -> bool { + let mask = self.mask(); + + let is_set = flags & mask != 0; + + // Clear the flag if set + flags = if is_set { + flags - mask + } else { + flags + }; + is_set + } + + fn set(self: Flag, ref flags: u128) { + let mask = self.mask(); + let divider: NonZero = mask.try_into().unwrap(); + let (q, _) = DivRem::::div_rem(flags, divider); + + if q % 2 == 0 { + flags += mask // Set the flag if not already set + } + } +} diff --git a/src/runestone/message.cairo b/src/runestone/message.cairo new file mode 100644 index 0000000..8fba3e4 --- /dev/null +++ b/src/runestone/message.cairo @@ -0,0 +1,121 @@ +use core::dict::Felt252Dict; +use core::num::traits::CheckedMul; +use core::num::traits::CheckedAdd; + +use super::{tag::{Tag, TagTrait}, flag::{Flag, FlagTrait}}; +use runes_lib::{ + types::{Etching, Terms}, utils::char::from_u32_to_char, + constants::{ETCHING_MAX_DIVISIBILITY, ETCHING_MAX_SPACERS} +}; + +pub fn extract_etching( + ref flags: u128, ref fields: Felt252Dict>>, ref fields_keys: Array +) -> (Option, Option, Option) { + if !Flag::Etching.take(ref flags) { + return (Option::None, Option::None, Option::None); + } + + let divisibility: Option = get_divisibility(ref fields, ref fields_keys); + let premine = extract_single(Tag::Premine, ref fields, ref fields_keys); + let rune = extract_single(Tag::Rune, ref fields, ref fields_keys); + let spacers = get_spacers(ref fields, ref fields_keys); + let symbol = from_u32_to_char( + extract_and_convert_to_u32(Tag::Symbol, ref fields, ref fields_keys) + ); + let terms = get_terms(ref flags, ref fields, ref fields_keys); + let turbo = Flag::Turbo.take(ref flags); + + let supply_check = check_supply(@premine, @terms); + + ( + Option::Some(Etching { divisibility, premine, rune, spacers, symbol, terms, turbo, }), + supply_check, + rune + ) +} + +fn get_divisibility( + ref fields: Felt252Dict>>, ref fields_keys: Array +) -> Option { + match Tag::Divisibility.take(ref fields, ref fields_keys, 1) { + Option::Some(span) => { + let divisibility: u8 = (*span.at(0)).try_into()?; + if divisibility <= ETCHING_MAX_DIVISIBILITY { + Option::Some(divisibility) + } else { + Option::None + } + }, + Option::None => Option::None, + } +} + +fn get_spacers( + ref fields: Felt252Dict>>, ref fields_keys: Array +) -> Option { + match Tag::Spacers.take(ref fields, ref fields_keys, 1) { + Option::Some(span) => { + let spacers: u32 = (*span.at(0)).try_into()?; + if spacers <= ETCHING_MAX_SPACERS { + Option::Some(spacers) + } else { + Option::None + } + }, + Option::None => Option::None, + } +} + +pub fn get_terms( + ref flags: u128, ref fields: Felt252Dict>>, ref fields_keys: Array +) -> Option { + if !Flag::Terms.take(ref flags) { + return Option::None; + } + + let cap = extract_single(Tag::Cap, ref fields, ref fields_keys); + let height = ( + extract_and_convert_to_u64(Tag::HeightStart, ref fields, ref fields_keys), + extract_and_convert_to_u64(Tag::HeightEnd, ref fields, ref fields_keys) + ); + let amount = extract_single(Tag::Amount, ref fields, ref fields_keys); + let offset = ( + extract_and_convert_to_u64(Tag::OffsetStart, ref fields, ref fields_keys), + extract_and_convert_to_u64(Tag::OffsetEnd, ref fields, ref fields_keys) + ); + + Option::Some(Terms { cap, amount, height, offset, }) +} + +fn check_supply(premine: @Option, terms: @Option) -> Option { + let premine = (*premine).unwrap_or_default(); + let terms = (*terms).unwrap_or_default(); + let cap = terms.cap.unwrap_or_default(); + let amount = terms.amount.unwrap_or_default(); + + premine.checked_add(cap.checked_mul(amount)?) +} + +fn extract_single( + tag: Tag, ref fields: Felt252Dict>>, ref fields_keys: Array +) -> Option { + match tag.take(ref fields, ref fields_keys, 1) { + Option::Some(span) => Option::Some(*span.at(0)), + Option::None => Option::None, + } +} + +fn extract_and_convert_to_u64( + tag: Tag, ref fields: Felt252Dict>>, ref fields_keys: Array +) -> Option { + let val: u64 = extract_single(tag, ref fields, ref fields_keys)?.try_into()?; + Option::Some(val) +} + +fn extract_and_convert_to_u32( + tag: Tag, ref fields: Felt252Dict>>, ref fields_keys: Array +) -> Option { + let val: u32 = extract_single(tag, ref fields, ref fields_keys)?.try_into()?; + Option::Some(val) +} + diff --git a/src/runestone/tag.cairo b/src/runestone/tag.cairo new file mode 100644 index 0000000..39949c3 --- /dev/null +++ b/src/runestone/tag.cairo @@ -0,0 +1,163 @@ +use core::dict::{Felt252Dict, Felt252DictEntryTrait}; +use runes_lib::{ + types::{RuneId, Rune}, utils::fields::{get_array_entry, get_new_field_val, remove_field_key} +}; + +#[derive(Drop, Copy)] +pub enum Tag { + Body, + Flags, + Rune, + Premine, + Cap, + Amount, + HeightStart, + HeightEnd, + OffsetStart, + OffsetEnd, + Mint, + Pointer, + Cenotaph, + Divisibility, + Spacers, + Symbol, + Nop, +} + +pub trait TagTrait { + fn get(self: Tag) -> u128; + fn take( + self: Tag, + ref fields: Felt252Dict>>, + ref field_key: Array, + n: usize, + ) -> Option>; + fn take_mint( + self: Tag, ref fields: Felt252Dict>>, ref field_key: Array, + ) -> Option; + fn take_pointer( + self: Tag, + ref fields: Felt252Dict>>, + ref field_key: Array, + output_len: u64 + ) -> Option; +} + +impl TagImpl of TagTrait { + // Maps each tag to its corresponding value + fn get(self: Tag) -> u128 { + match self { + Tag::Body => 0, + Tag::Flags => 2, + Tag::Rune => 4, + Tag::Premine => 6, + Tag::Cap => 8, + Tag::Amount => 10, + Tag::HeightStart => 12, + Tag::HeightEnd => 14, + Tag::OffsetStart => 16, + Tag::OffsetEnd => 18, + Tag::Mint => 20, + Tag::Pointer => 22, + Tag::Cenotaph => 126, + Tag::Divisibility => 1, + Tag::Spacers => 3, + Tag::Symbol => 5, + Tag::Nop => 127, + } + } + + fn take( + self: Tag, + ref fields: Felt252Dict>>, + ref field_key: Array, + n: usize, + ) -> Option> { + let index = self.get(); + let field = get_array_entry(ref fields, index.into()); + + if field.len() < n { + return Option::None; + } + + let res = field.slice(0, n); + + // Update fields value + let (entry, _) = fields.entry(index.into()); + if field.len() == n { + // We remove the field & field key + fields = entry.finalize(NullableTrait::new(Default::default())); + remove_field_key(ref field_key, index); + } else { + let new_field = field.slice(n, field.len() - 1); + fields = entry.finalize(NullableTrait::new(get_new_field_val(new_field))); + } + + // Return the first `n` elements + Option::Some(res) + } + + fn take_mint( + self: Tag, ref fields: Felt252Dict>>, ref field_key: Array, + ) -> Option { + let index = self.get(); + let field = get_array_entry(ref fields, index.into()); + + if field.len() < 2 { + return Option::None; + } + + let res = field.slice(0, 2); + + let block: u64 = (*res.at(0)).try_into()?; + let tx: u32 = (*res.at(1)).try_into()?; + let rune = Rune::new(block, tx)?; + + // Update fields value + let (entry, _) = fields.entry(index.into()); + if field.len() == 2 { + // We remove the field & field key + fields = entry.finalize(NullableTrait::new(Default::default())); + remove_field_key(ref field_key, index); + } else { + let new_field = field.slice(2, field.len() - 1); + fields = entry.finalize(NullableTrait::new(get_new_field_val(new_field))); + } + + Option::Some(rune) + } + + fn take_pointer( + self: Tag, + ref fields: Felt252Dict>>, + ref field_key: Array, + output_len: u64 + ) -> Option { + let index = self.get(); + let field = get_array_entry(ref fields, index.into()); + + if field.len() < 1 { + return Option::None; + } + + let res = field.slice(0, 1); + let pointer: u32 = (*res.at(0)).try_into()?; + if pointer.into() >= output_len { + return Option::None; + } + + // Update fields value + let (entry, _) = fields.entry(index.into()); + if field.len() == 1 { + // We remove the field & field key + fields = entry.finalize(NullableTrait::new(Default::default())); + remove_field_key(ref field_key, index); + } else { + let new_field = field.slice(1, field.len() - 1); + fields = entry.finalize(NullableTrait::new(get_new_field_val(new_field))); + } + + Option::Some(pointer) + } +} + diff --git a/src/tests/cenotaph.cairo b/src/tests/cenotaph.cairo new file mode 100644 index 0000000..8bcb8b3 --- /dev/null +++ b/src/tests/cenotaph.cairo @@ -0,0 +1,982 @@ +use runes_lib::parser::extract_runestone; +use runes_lib::constants::{OP_RETURN, OP_13}; +use runes_lib::types::{Runestone, Artifact, Cenotaph, Edict, RuneId}; +use runes_lib::runestone::{tag::{Tag, TagTrait}, flag::{Flag, FlagTrait}}; +use super::utils::{transaction, OP_PUSHBYTES_4, append_arr, build_output, CenotaphFlaw}; + +#[test] +fn test_deciphering_valid_runestone_with_invalid_script_postfix_returns_invalid_payload() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + op_return_script.append_byte(OP_PUSHBYTES_4); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::INVALID_SCRIPT(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_terms_flag_without_etching_flag_produces_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Terms.mask().try_into().unwrap(), + Tag::Body.get().try_into().unwrap(), + 0, + 0, + 0, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_FLAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_recognized_fields_without_flag_produces_cenotaph() { + let mut cases = array![ + array![Tag::Premine.get().try_into().unwrap(), 0], + array![Tag::Rune.get().try_into().unwrap(), 0], + array![Tag::Cap.get().try_into().unwrap(), 0], + array![Tag::Amount.get().try_into().unwrap(), 0], + array![Tag::OffsetStart.get().try_into().unwrap(), 0], + array![Tag::OffsetEnd.get().try_into().unwrap(), 0], + array![Tag::HeightStart.get().try_into().unwrap(), 0], + array![Tag::HeightEnd.get().try_into().unwrap(), 0], + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.get().try_into().unwrap(), + Tag::Cap.get().try_into().unwrap(), + 0 + ], + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.get().try_into().unwrap(), + Tag::Amount.get().try_into().unwrap(), + 0 + ], + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.get().try_into().unwrap(), + Tag::OffsetStart.get().try_into().unwrap(), + 0 + ], + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.get().try_into().unwrap(), + Tag::OffsetEnd.get().try_into().unwrap(), + 0 + ], + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.get().try_into().unwrap(), + Tag::HeightStart.get().try_into().unwrap(), + 0 + ], + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.get().try_into().unwrap(), + Tag::HeightEnd.get().try_into().unwrap(), + 0 + ], + ] + .span(); + let mut i = 0; + + loop { + let case = match cases.pop_front() { + Option::Some(value) => value, + Option::None => { break; } + }; + let op_return_script: ByteArray = build_output(case.clone()); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } + + i += 1; + } +} + +#[test] +fn test_invalid_varint_produces_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + // push number 128 + op_return_script.append_byte(0x01); // Push size prefix (1 byte) + op_return_script.append_byte(128); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::VARINT(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_duplicate_even_tags_produce_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 4, + Tag::Rune.get().try_into().unwrap(), + 5, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Cenotaph( + Cenotaph { + flaw: Option::Some("unrecognized even tag"), + etching: Option::Some(4), + ..Default::default() + } + ); + assert(output == expected, 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestone_with_unrecognized_even_tag_is_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Cenotaph.get().try_into().unwrap(), + 0, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestone_with_unrecognized_flag_is_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 2, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_FLAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestone_with_edict_id_with_zero_block_and_nonzero_tx_is_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, array![Tag::Body.get().try_into().unwrap(), 0, 1, 2, 0].span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::EDICT_RUNE_ID(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestone_with_overflowing_edict_id_delta_is_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Body.get().try_into().unwrap(), + 1, + 0, + 0, + 0, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 1, + 0, + 0, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::EDICT_RUNE_ID(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestone_with_overflowing_edict_id_delta_is_cenotaph_2() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 0, + 0, + 0, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 1, + 0, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::EDICT_RUNE_ID(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestone_with_output_over_max_is_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, array![Tag::Body.get().try_into().unwrap(), 1, 1, 2, 2].span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::EDICT_OUTPUT(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_tag_with_no_value_is_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![Tag::Flags.get().try_into().unwrap(), 1, Tag::Flags.get().try_into().unwrap()].span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::TRUNCATED_FIELD(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_trailing_integers_in_body_is_cenotaph() { + let mut cases = array![ + array![Tag::Body.get().try_into().unwrap(), 1, 1, 2, 0], + array![Tag::Body.get().try_into().unwrap(), 1, 1, 2, 0, 0], + array![Tag::Body.get().try_into().unwrap(), 1, 1, 2, 0, 0, 0], + array![Tag::Body.get().try_into().unwrap(), 1, 1, 2, 0, 0, 0, 0], + ] + .span(); + let mut i = 0; + + loop { + let case = match cases.pop_front() { + Option::Some(value) => value, + Option::None => { break; } + }; + + let op_return_script: ByteArray = build_output(case.clone()); + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + if i == 0 { + let expected = Artifact::Runestone( + Runestone { + edicts: array![ + Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 } + ], + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } else { + assert(output == CenotaphFlaw::TRAILING_INTEGERS(), 'wrong cenotaph value'); + } + } + } + i += 1; + } +} + +#[test] +fn test_recognized_even_etching_fields_produce_cenotaph_if_etching_flag_is_not_set() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr(ref op_return_script, array![Tag::Rune.get().try_into().unwrap(), 4,].span()); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestones_with_invalid_rune_id_blocks_are_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3, + 1, + 0, + 0, + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::EDICT_RUNE_ID(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_runestones_with_invalid_rune_id_txs_are_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0, + 1, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3, + 0, + 0, + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::EDICT_RUNE_ID(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_etching_with_term_greater_than_maximum_is_still_an_etching() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::OffsetEnd.get().try_into().unwrap(), + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 2 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_edict_output_greater_than_32_max_produces_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 1, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 2 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::EDICT_OUTPUT(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_partial_mint_produces_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr(ref op_return_script, array![Tag::Mint.get().try_into().unwrap(), 1,].span()); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_mint_produces_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![Tag::Mint.get().try_into().unwrap(), 0, Tag::Mint.get().try_into().unwrap(), 1,] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_deadline_produces_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::OffsetEnd.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_default_output_produces_cenotaph_1() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr(ref op_return_script, array![Tag::Pointer.get().try_into().unwrap(), 1].span()); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_default_output_produces_cenotaph_2() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Pointer.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_term_produces_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::OffsetEnd.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::UNRECOGNIZED_EVEN_TAG(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_supply_produces_cenotaph() { + let op_return_script = build_output( + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap() | Flag::Terms.mask().try_into().unwrap(), + Tag::Cap.get().try_into().unwrap(), + 2, + Tag::Amount.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::SUPPLY_OVERFLOW(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_supply_produces_cenotaph_2() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap() | Flag::Terms.mask().try_into().unwrap(), + Tag::Cap.get().try_into().unwrap(), + 2, + Tag::Amount.get().try_into().unwrap(), + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 2 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::SUPPLY_OVERFLOW(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_supply_produces_cenotaph_3() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap() | Flag::Terms.mask().try_into().unwrap(), + Tag::Premine.get().try_into().unwrap(), + 1, + Tag::Cap.get().try_into().unwrap(), + 1, + Tag::Amount.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::SUPPLY_OVERFLOW(), 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_invalid_scripts_in_op_returns_with_magic_number_produce_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + op_return_script.append_byte(OP_PUSHBYTES_4); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + assert(output == CenotaphFlaw::INVALID_SCRIPT(), 'wrong cenotaph value'); + } + } +} diff --git a/src/tests/flag.cairo b/src/tests/flag.cairo new file mode 100644 index 0000000..55a19cf --- /dev/null +++ b/src/tests/flag.cairo @@ -0,0 +1,26 @@ +use runes_lib::runestone::flag::{Flag, FlagTrait}; +use alexandria_math::pow; + +#[test] +fn test_mask() { + assert_eq!(Flag::Etching.mask(), 0b1); + assert_eq!(Flag::Cenotaph.mask(), pow(2_u128, 127)); +} + +#[test] +fn test_take() { + let mut flags = 1; + assert!(Flag::Etching.take(ref flags)); + assert_eq!(flags, 0); + + let mut flags = 0; + assert!(!Flag::Etching.take(ref flags)); + assert_eq!(flags, 0); +} + +#[test] +fn test_set() { + let mut flags = 0; + Flag::Etching.set(ref flags); + assert_eq!(flags, 1); +} diff --git a/src/tests/opcodes.cairo b/src/tests/opcodes.cairo new file mode 100644 index 0000000..7cee801 --- /dev/null +++ b/src/tests/opcodes.cairo @@ -0,0 +1,168 @@ +use runes_lib::parser::extract_runestone; +use runes_lib::constants::{OP_RETURN, OP_13}; +use runes_lib::types::{Runestone, Artifact, Cenotaph}; +use super::utils::{transaction, OP_VERIFY, OP_PUSHNUM_1}; + +#[test] +fn test_all_pushdata_opcodes_are_valid() { + let mut i: u8 = 0; + + loop { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + op_return_script.append_byte(i); + + if i <= 75 { + let mut j: u8 = 0; + loop { + if j >= i { + break; + } + let val: u8 = if j % 2 == 0 { + 1_u8 + } else { + 0_u8 + }; + op_return_script.append_byte(val); + j += 1; + }; + + if i % 2 == 1 { + op_return_script.append_byte(1); + op_return_script.append_byte(1); + } + } else if i == 76 { + op_return_script.append_byte(0); + } else if i == 77 { + op_return_script.append_byte(0); + op_return_script.append_byte(0); + } else if i == 78 { + op_return_script.append_byte(0); + op_return_script.append_byte(0); + op_return_script.append_byte(0); + op_return_script.append_byte(0); + } + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone(Runestone { ..Default::default() }); + assert(output == expected, 'wrong runestone value'); + } + } + + if i == 78 { + break; + } + + i += 1; + }; +} + +#[test] +fn test_all_non_pushdata_opcodes_are_invalid() { + let mut i: u8 = 79; + + loop { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + op_return_script.append_byte(i); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Cenotaph( + Cenotaph { + flaw: Option::Some("non-pushdata opcode in OP_RETURN"), ..Default::default() + } + ); + assert(output == expected, 'wrong cenotaph value'); + } + } + + if i == 255 { + break; + } + i += 1; + }; +} + +#[test] +fn test_outputs_with_non_pushdata_opcodes_are_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + op_return_script.append_byte(OP_VERIFY); + // Add single byte [0] + op_return_script.append_byte(0x01); // Push size prefix (1 byte) + op_return_script.append_byte(0x00); // The actual value [0] + // Add varint::encode(1) twice (each as a single byte) + op_return_script.append_byte(0x01); // Push size prefix (1 byte) + op_return_script.append_byte(0x01); // First varint + op_return_script.append_byte(0x01); // Push size prefix (1 byte) + op_return_script.append_byte(0x01); // Second varint + // Add [2, 0] + op_return_script.append_byte(0x02); // Push size prefix (2 bytes) + op_return_script.append_byte(0x02); // First byte + op_return_script.append_byte(0x00); // Second byte + + let mut op_return_script_2: ByteArray = Default::default(); + op_return_script_2.append_byte(OP_RETURN); + op_return_script_2.append_byte(OP_13); + // Add single byte [0] + op_return_script_2.append_byte(0x01); // Push size prefix (1 byte) + op_return_script_2.append_byte(0x00); // The actual value [0] + // Add varint::encode(1) + op_return_script_2.append_byte(0x01); // Push size prefix (1 byte) + op_return_script_2.append_byte(0x01); // First varint + // Add varint::encode(2) + op_return_script_2.append_byte(0x01); // Push size prefix (1 byte) + op_return_script_2.append_byte(0x02); // Second varint + // Add [3, 0] + op_return_script_2.append_byte(0x02); // Push size prefix (2 bytes) + op_return_script_2.append_byte(0x03); // First byte + op_return_script_2.append_byte(0x00); // Second byte + + let tx = transaction(array![op_return_script, op_return_script_2].span()); + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Cenotaph( + Cenotaph { + flaw: Option::Some("non-pushdata opcode in OP_RETURN"), ..Default::default() + } + ); + assert(output == expected, 'wrong cenotaph value'); + } + } +} + +#[test] +fn test_pushnum_opcodes_in_runestone_produce_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + op_return_script.append_byte(OP_VERIFY); + op_return_script.append_byte(OP_PUSHNUM_1); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Cenotaph( + Cenotaph { + flaw: Option::Some("non-pushdata opcode in OP_RETURN"), ..Default::default() + } + ); + assert(output == expected, 'wrong cenotaph value'); + } + } +} diff --git a/src/tests/parser.cairo b/src/tests/parser.cairo index 323c5f6..6688f4a 100644 --- a/src/tests/parser.cairo +++ b/src/tests/parser.cairo @@ -1,26 +1,1150 @@ -use runes_lib::parser::{extract_runestone, OP_RETURN, OP_13}; -use utils::hex::from_hex; -use consensus::types::transaction::{Transaction, TxOut}; +use runes_lib::parser::{extract_runestone}; +use runes_lib::constants::{OP_RETURN, OP_13, ETCHING_MAX_DIVISIBILITY}; +use runes_lib::types::{Runestone, RuneId, Edict, Artifact, Etching, Terms}; +use runes_lib::runestone::{tag::{Tag, TagTrait}, flag::{Flag, FlagTrait}}; +use super::utils::{ + append_string, append_arr, transaction, OP_PUSHBYTES_4, OP_PUSHBYTES_9, MINT, U128_MAX +}; +#[test] +fn test_decipher_returns_none_if_first_opcode_is_malformed() { + // Create a transaction with an OP_PUSHBYTES_4 output containing encoded values + let mut op_return_script: ByteArray = "4"; + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + assert(values.is_none(), 'values found'); +} + +#[test] +fn test_deciphering_transaction_with_no_outputs_returns_none() { + let tx = transaction(array![].span()); + let values = extract_runestone(tx); + assert(values.is_none(), 'values found'); +} + +#[test] +fn test_deciphering_transaction_with_non_op_return_output_returns_none() { + let mut op_return_script: ByteArray = Default::default(); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + assert(values.is_none(), 'should return None'); +} + +#[test] +fn test_deciphering_transaction_with_bare_op_return_returns_none() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + assert(values.is_none(), 'should return None'); +} + +#[test] +fn test_deciphering_transaction_with_non_matching_op_return_returns_none() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + append_string("FOOO", ref op_return_script); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + assert(values.is_none(), 'values found'); +} + +#[test] +fn test_deciphering_runestone_with_truncated_varint_succeeds() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + // push number 128 + op_return_script.append_byte(0x01); + op_return_script.append_byte(128); + + let tx = transaction(array![op_return_script].span()); + + let values = extract_runestone(tx); + assert(values.is_some(), 'should return Some'); +} + +#[test] +fn test_deciphering_empty_runestone_is_successful() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone(Runestone { ..Default::default() }); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_invalid_input_scripts_are_skipped_when_searching_for_runestone() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_PUSHBYTES_9); + op_return_script.append_byte(OP_13); + op_return_script.append_byte(OP_PUSHBYTES_4); + + let mut op_return_script_2: ByteArray = Default::default(); + op_return_script_2.append_byte(OP_RETURN); + op_return_script_2.append_byte(OP_13); + // Add [MINT, 1, MINT, 1]; + op_return_script_2.append_byte(OP_PUSHBYTES_4); + op_return_script_2.append_byte(MINT); + op_return_script_2.append_byte(1); + op_return_script_2.append_byte(MINT); + op_return_script_2.append_byte(1); + + let tx = transaction(array![op_return_script, op_return_script_2].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone( + Runestone { mint: Option::Some(RuneId { block: 1, tx: 1 }), ..Default::default() } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_deciphering_non_empty_runestone_is_successful() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, array![Tag::Body.get().try_into().unwrap(), 1, 1, 2, 0].span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone( + Runestone { + mint: Option::None, + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::None, + pointer: Option::None, + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Default::default(); + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching_with_rune() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 4, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { rune: Option::Some(4), ..Default::default() }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching_with_term() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap() | Flag::Terms.mask().try_into().unwrap(), + Tag::OffsetEnd.get().try_into().unwrap(), + 4, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + terms: Option::Some( + Terms { offset: (Option::None, Option::Some(4)), ..Default::default() } + ), + ..Default::default() + }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching_with_amount() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap() | Flag::Terms.mask().try_into().unwrap(), + Tag::Amount.get().try_into().unwrap(), + 4, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + terms: Option::Some(Terms { amount: Option::Some(4), ..Default::default() }), + ..Default::default() + }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_duplicate_odd_tags_are_ignored() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Divisibility.get().try_into().unwrap(), + 4, + Tag::Divisibility.get().try_into().unwrap(), + 5, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + rune: Option::None, divisibility: Option::Some(4), ..Default::default() + }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_unrecognized_odd_tag_is_ignored() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Nop.get().try_into().unwrap(), 100, Tag::Body.get().try_into().unwrap(), 1, 1, 2, 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching_with_divisibility() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 4, + Tag::Divisibility.get().try_into().unwrap(), + 5, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + rune: Option::Some(4), divisibility: Option::Some(5), ..Default::default() + }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_divisibility_above_max_is_ignored() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 4, + Tag::Divisibility.get().try_into().unwrap(), + (ETCHING_MAX_DIVISIBILITY + 1).try_into().unwrap(), + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { rune: Option::Some(4), ..Default::default() }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} #[test] -fn test_extract_runestone() { - // Create a transaction with an OP_RETURN output containing encoded values - let mut op_return_script: ByteArray = from_hex("6a5d0714c5953514df23"); +fn test_symbol_above_max_is_ignored() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Symbol.get().try_into().unwrap(), + 128, + 128, + 68, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); - let tx = Transaction { - version: 0, - inputs: array![].span(), - outputs: array![TxOut { value: 0, pk_script: @op_return_script.into(), cached: false }] - .span(), - lock_time: 0, - is_segwit: false - }; + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { ..Default::default() }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching_with_symbol() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 4, + Tag::Symbol.get().try_into().unwrap(), + 'a'.try_into().unwrap(), + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + rune: Option::Some(4), symbol: Option::Some("a"), ..Default::default() + }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching_with_all_etching_tags() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap() + | Flag::Terms.mask().try_into().unwrap() + | Flag::Turbo.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 4, + Tag::Divisibility.get().try_into().unwrap(), + 1, + Tag::Spacers.get().try_into().unwrap(), + 5, + Tag::Symbol.get().try_into().unwrap(), + 'a'.try_into().unwrap(), + Tag::OffsetEnd.get().try_into().unwrap(), + 2, + Tag::Amount.get().try_into().unwrap(), + 3, + Tag::Premine.get().try_into().unwrap(), + 8, + Tag::Cap.get().try_into().unwrap(), + 9, + Tag::Pointer.get().try_into().unwrap(), + 0, + Tag::Mint.get().try_into().unwrap(), + 1, + Tag::Mint.get().try_into().unwrap(), + 1, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + divisibility: Option::Some(1), + premine: Option::Some(8), + rune: Option::Some(4), + spacers: Option::Some(5), + symbol: Option::Some("a"), + terms: Option::Some( + Terms { + amount: Option::Some(3), + height: (Option::None, Option::None), + cap: Option::Some(9), + offset: (Option::None, Option::Some(2)), + } + ), + turbo: true, + }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + pointer: Option::Some(0), + mint: Option::Some(RuneId { block: 1, tx: 1 }), + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_decipher_etching_with_divisibility_and_symbol() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 4, + Tag::Divisibility.get().try_into().unwrap(), + 1, + Tag::Symbol.get().try_into().unwrap(), + 'a'.try_into().unwrap(), + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + divisibility: Option::Some(1), + rune: Option::Some(4), + symbol: Option::Some("a"), + ..Default::default() + }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_tag_values_are_not_parsed_as_tags() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Divisibility.get().try_into().unwrap(), + Tag::Body.get().try_into().unwrap(), + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { divisibility: Option::Some(0), ..Default::default() }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_runestone_may_contain_multiple_edicts() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![Tag::Body.get().try_into().unwrap(), 1, 1, 2, 0, 0, 3, 5, 0].span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone( + Runestone { + edicts: array![ + Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 }, + Edict { id: RuneId { block: 1, tx: 3 }, amount: 5, output: 0 }, + ], + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_payload_pushes_are_concatenated() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Divisibility.get().try_into().unwrap(), + 5, + Tag::Body.get().try_into().unwrap(), + 1, + 1, + 2, + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { divisibility: Option::Some(5), ..Default::default() }; + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 },], + etching: Option::Some(etching), + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_runestone_may_be_in_second_output() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr(ref op_return_script, array![0, 1, 1, 2, 0].span()); + + let tx = transaction(array![Default::default(), op_return_script.into(),].span()); + + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 },], + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_runestone_may_be_after_non_matching_op_return() { + let mut op_return_script_1: ByteArray = Default::default(); + op_return_script_1.append_byte(OP_RETURN); + append_string("FOO", ref op_return_script_1); + + let mut op_return_script_2: ByteArray = Default::default(); + op_return_script_2.append_byte(OP_RETURN); + op_return_script_2.append_byte(OP_13); + append_arr(ref op_return_script_2, array![0, 1, 1, 2, 0].span()); - // Extract values and verify + let tx = transaction(array![op_return_script_1, op_return_script_2].span()); let values = extract_runestone(tx); - println!("values: {:?}", values); - assert(values.len() == 2, 'wrong number of values'); - assert(*values[0] == 1, 'wrong value at index 0'); - assert(*values[1] == 2, 'wrong value at index 1'); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone( + Runestone { + edicts: array![Edict { id: RuneId { block: 1, tx: 1 }, amount: 2, output: 0 },], + ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_min_runes_is_not_cenotaphs() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 0 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { rune: Option::Some(0), ..Default::default() }; + let expected = Artifact::Runestone( + Runestone { etching: Option::Some(etching), ..Default::default() } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} +#[test] +fn test_max_runes_is_not_cenotaphs() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap(), + Tag::Rune.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { rune: Option::Some(U128_MAX), ..Default::default() }; + let expected = Artifact::Runestone( + Runestone { etching: Option::Some(etching), ..Default::default() } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_invalid_spacers_does_not_produce_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Spacers.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone(Runestone { ..Default::default() }); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_invalid_symbol_does_not_produce_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Symbol.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone(Runestone { ..Default::default() }); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_amount_does_not_produce_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Flags.get().try_into().unwrap(), + Flag::Etching.mask().try_into().unwrap() | Flag::Terms.mask().try_into().unwrap(), + Tag::Cap.get().try_into().unwrap(), + 1, + Tag::Amount.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let etching: Etching = Etching { + terms: Option::Some( + Terms { + cap: Option::Some(1), + amount: Option::Some(U128_MAX), + height: (Option::None, Option::None), + offset: (Option::None, Option::None), + } + ), + ..Default::default() + }; + let expected = Artifact::Runestone( + Runestone { etching: Option::Some(etching), ..Default::default() } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_invalid_scripts_in_op_returns_without_magic_number_are_ignored_1() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_PUSHBYTES_4); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + assert(values.is_none(), 'should not return output'); +} + +#[test] +fn test_invalid_scripts_in_op_returns_without_magic_number_are_ignored_2() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + + let tx = transaction(array![op_return_script].span()); + + let values = extract_runestone(tx); + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone(Runestone { ..Default::default() }); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_invalid_divisibility_does_not_produce_cenotaph() { + let mut op_return_script: ByteArray = Default::default(); + op_return_script.append_byte(OP_RETURN); + op_return_script.append_byte(OP_13); + append_arr( + ref op_return_script, + array![ + Tag::Divisibility.get().try_into().unwrap(), + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 3 + ] + .span() + ); + + let tx = transaction(array![op_return_script].span()); + let values = extract_runestone(tx); + + match values { + Option::None => { panic!("Should not return None"); }, + Option::Some(output) => { + let expected = Artifact::Runestone(Runestone { ..Default::default() }); + assert(output == expected, 'wrong runestone value'); + } + } } diff --git a/src/tests/transactions.cairo b/src/tests/transactions.cairo new file mode 100644 index 0000000..8b93205 --- /dev/null +++ b/src/tests/transactions.cairo @@ -0,0 +1,208 @@ +use runes_lib::parser::{extract_runestone}; +use runes_lib::types::{Runestone, RuneId, Artifact}; +use consensus::types::transaction::{Transaction, TxOut, TxIn, OutPoint}; +use utils::hex::{from_hex, hex_to_hash_rev}; + +#[test] +fn test_tx_mint_1() { + // a795ede3bec4b9095eb207bff4abacdbcdd1de065788d4ffb53b1ea3fe5d67fb + let outputs: Array = array![ + TxOut { value: 0, pk_script: @from_hex("6a5d0714f6a73514d50d",), cached: false }, + TxOut { + value: 546, + pk_script: @from_hex( + "5120f6c5b32ab6926f66fc96eaf5bcacb968c93317b10700e43e779589ab3e59db2e", + ), + cached: false + }, + TxOut { + value: 1500, + pk_script: @from_hex("001452dfd69700e8e1a2a3256799e5b7c30a221f6568",), + cached: false + }, + TxOut { + value: 50419, + pk_script: @from_hex( + "5120f63388b3e7360898c655d3fb062654b4dc39f86f0e93649df425770147f03f66", + ), + cached: false + } + ]; + + let tx = Transaction { + version: 2, inputs: array![].span(), outputs: outputs.into(), lock_time: 0, is_segwit: false + }; + + let values = extract_runestone(tx); + match values { + Option::None => { println!("Should not return None"); }, + Option::Some(output) => { + println!("{:?}", output); + let expected: Artifact = Artifact::Runestone( + Runestone { + mint: Option::Some(RuneId { block: 873462, tx: 1749 }), ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_tx_mint_2() { + // 8f47e81d3777d442f45a1984bbc391b951b4808f3ea44e5c35aaa348e95e71e4 + let outputs: Array = array![ + TxOut { + value: 1596, + pk_script: @from_hex( + "5120eb0dfe9f2b5c7744a2497b8c5f8b68e5f6fbd8f3d1d4dfcd208c2ea12b1a61ce", + ), + cached: false + }, + TxOut { value: 0, pk_script: @from_hex("6a5d0714bce43314c101",), cached: false } + ]; + + let tx = Transaction { + version: 2, inputs: array![].span(), outputs: outputs.into(), lock_time: 0, is_segwit: false + }; + + let values = extract_runestone(tx); + match values { + Option::None => { println!("Should not return None"); }, + Option::Some(output) => { + let expected: Artifact = Artifact::Runestone( + Runestone { + mint: Option::Some(RuneId { block: 848444, tx: 193 }), ..Default::default() + } + ); + assert(output == expected, 'wrong runestone value'); + } + } +} + +#[test] +fn test_tx_etching() { + // 7781853fab708e7dbe2086449fdb6c917bb3ee94b062934d3397cb8b744a44d2 + let outputs: Array = array![ + TxOut { + value: 546, + pk_script: @from_hex( + "51208d50fca97fc75365e648e7b68d4710a3920df6e54086a0a067a128a233140826", + ), + cached: false + }, + TxOut { + value: 546, + pk_script: @from_hex( + "5120f6c5b32ab6926f66fc96eaf5bcacb968c93317b10700e43e779589ab3e59db2e", + ), + cached: false + }, + TxOut { + value: 546, + pk_script: @from_hex( + "51208d50fca97fc75365e648e7b68d4710a3920df6e54086a0a067a128a233140826", + ), + cached: false + }, + TxOut { + value: 0, + pk_script: @from_hex( + "6a5d240207049b8cb7fceeb6fbeae1bbe20b010003884205cc4e0698e3060ae8070881ab011601", + ), + cached: false + } + ]; + + let tx = Transaction { + version: 2, inputs: array![].span(), outputs: outputs.into(), lock_time: 0, is_segwit: false + }; + + let values = extract_runestone(tx); + match values { + Option::None => { println!("Should not return None"); }, + Option::Some(output) => { + match output { + Artifact::Runestone(runestone) => { + let etching = runestone.etching.unwrap(); + let terms = etching.terms.unwrap(); + assert(etching.premine == Option::Some(111000), 'wrong etching value'); + assert(etching.divisibility == Option::Some(0), 'wrong pointer value'); + assert( + etching.rune == Option::Some(1778522209552757531985435), + 'wrong etching value' + ); + assert(etching.spacers == Option::Some(8456), 'wrong etching value'); + assert(terms.amount == Option::Some(1000), 'wrong terms value'); + assert(terms.cap == Option::Some(21889), 'wrong terms value'); + assert( + terms.height == (Option::None(()), Option::None(())), 'wrong terms value' + ); + assert( + terms.offset == (Option::None(()), Option::None(())), 'wrong terms value' + ); + assert(etching.turbo == true, 'wrong etching value'); + assert(runestone.pointer == Option::Some(1), 'wrong pointer value'); + assert(runestone.mint.is_none(), 'wrong mint value'); + assert(runestone.edicts.len() == 0, 'wrong edicts value'); + }, + _ => { panic!("Expected Runestone"); } + } + } + } +} + +#[test] +fn test_tx_send() { + // 34c0a5f659aadc9da4fc87b8b2179174979f9297a1b9d8a51d295b5ea9ccf878 + let outputs: Array = array![ + TxOut { + value: 2121, + pk_script: @from_hex( + "512079a2aa2c82cd13dadc5e3c38338406b291a2c26c39feb5a65f08e498535c4109", + ), + cached: false + }, + TxOut { value: 0, pk_script: @from_hex("6a5d0714c0a23314b802",), cached: false }, + ]; + + let inputs: Array = array![ + TxIn { + previous_output: OutPoint { + txid: hex_to_hash_rev( + "3f6c165571e4202c2c2b67577a725b09bb0c2cc92d53e87596b0a74de4606e2f" + ), + vout: 0, + block_height: 873484, + median_time_past: 1733472864, + is_coinbase: false, + data: TxOut { + value: 1596, + pk_script: @from_hex( + "512079a2aa2c82cd13dadc5e3c38338406b291a2c26c39feb5a65f08e498535c4109", + ), + cached: false + }, + }, + script: Default::default(), + sequence: 4294967295, + witness: array![ + from_hex( + "b2d374d3f0ec5018dcf7121cdfa11ba9e3a8f8a57f93d911ee3005747c779f6ce4929bac2cd65dcecc2d42636ae9edc5a2eb5967b37845db9172924091ee4ed8" + ) + ] + .span() + } + ]; + + let tx = Transaction { + version: 2, inputs: inputs.span(), outputs: outputs.into(), lock_time: 0, is_segwit: false + }; + + let values = extract_runestone(tx); + match values { + Option::None => { println!("Should not return None"); }, + Option::Some(output) => { println!("output: {:?}", output); } + } +} + diff --git a/src/tests/utils.cairo b/src/tests/utils.cairo new file mode 100644 index 0000000..81a9f42 --- /dev/null +++ b/src/tests/utils.cairo @@ -0,0 +1,134 @@ +use consensus::types::transaction::{Transaction, TxOut}; +use runes_lib::types::{Runestone, RuneId, Edict, Artifact, Etching}; +use runes_lib::constants::{OP_RETURN, OP_13}; + +const POW_256_1: u128 = 0x100; +pub const OP_VERIFY: u8 = 0x69; +pub const OP_PUSHNUM_1: u8 = 0x51; +pub const OP_PUSHBYTES_4: u8 = 0x04; +pub const OP_PUSHBYTES_9: u8 = 0x09; +pub const MINT: u8 = 20; +pub const U128_MAX: u128 = 340282366920938463463374607431768211455; + +pub fn transaction(mut op_return_scripts: Span) -> Transaction { + let mut outputs: Array = ArrayTrait::new(); + loop { + match op_return_scripts.pop_front() { + Option::Some(script) => { + outputs.append(TxOut { value: 0, pk_script: script.into(), cached: false }); + }, + Option::None => { break; } + } + }; + Transaction { + version: 2, inputs: array![].span(), outputs: outputs.span(), lock_time: 0, is_segwit: false + } +} + +pub fn append_string(string: ByteArray, ref array: ByteArray) { + let mut index = 0; + loop { + if index >= string.len() { + break; + } + let foo_byte: u8 = string[index]; + index += 1; + array.append_byte(foo_byte); + }; +} + +pub fn get_rune( + mint: Option, edicts: Array, etching: Option, pointer: Option +) -> Artifact { + let runestone: Runestone = Runestone { + mint: mint, edicts: edicts, etching: etching, pointer: pointer, + }; + Artifact::Runestone(runestone) +} + +pub fn append_arr(ref op_return_script: ByteArray, mut arr: Span) { + let mut index = 0; + let arr_len = arr.len(); + op_return_script.append_byte(arr_len.try_into().unwrap()); + loop { + if index >= arr_len { + break; + } + let value = arr.pop_front().unwrap(); + op_return_script.append_byte(*value); + index += 1; + } +} + +pub fn build_output(data: Array) -> ByteArray { + let mut output: ByteArray = Default::default(); + output.append_byte(OP_RETURN); + output.append_byte(OP_13); + append_arr(ref output, data.span()); + + output +} + +pub mod CenotaphFlaw { + use runes_lib::types::{Artifact, Cenotaph}; + + pub fn EDICT_OUTPUT() -> Artifact { + Artifact::Cenotaph( + Cenotaph { + flaw: Option::Some("edict output greater than transaction output count"), + ..Default::default() + } + ) + } + + pub fn EDICT_RUNE_ID() -> Artifact { + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some("invalid rune ID in edict"), ..Default::default() } + ) + } + + pub fn INVALID_SCRIPT() -> Artifact { + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some("invalid script in OP_RETURN"), ..Default::default() } + ) + } + + pub fn OPCODE() -> Artifact { + Artifact::Cenotaph( + Cenotaph { + flaw: Option::Some("non-pushdata opcode in OP_RETURN"), ..Default::default() + } + ) + } + + pub fn SUPPLY_OVERFLOW() -> Artifact { + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some("supply overflows u128"), ..Default::default() } + ) + } + + pub fn TRAILING_INTEGERS() -> Artifact { + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some("trailing integers in body"), ..Default::default() } + ) + } + pub fn TRUNCATED_FIELD() -> Artifact { + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some("field with missing value"), ..Default::default() } + ) + } + pub fn UNRECOGNIZED_EVEN_TAG() -> Artifact { + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some("unrecognized even tag"), ..Default::default() } + ) + } + pub fn UNRECOGNIZED_FLAG() -> Artifact { + Artifact::Cenotaph( + Cenotaph { flaw: Option::Some("unrecognized field"), ..Default::default() } + ) + } + pub fn VARINT() -> Artifact { + Artifact::Cenotaph(Cenotaph { flaw: Option::Some("invalid varint"), ..Default::default() }) + } +} + diff --git a/src/tests/varint.cairo b/src/tests/varint.cairo new file mode 100644 index 0000000..5a92fd1 --- /dev/null +++ b/src/tests/varint.cairo @@ -0,0 +1,370 @@ +use runes_lib::utils::varint::{decode_integer, decode_integers}; +use alexandria_math::pow; +use alexandria_data_structures::byte_array_ext::SpanU8IntoBytearray; + +fn encode_to_vec(mut n: u128) -> Array { + let mut result: Array = Default::default(); + + loop { + // Extract the lower 7 bits of `n` + let (q, r) = DivRem::::div_rem(n, 128); + let value: u8 = r.try_into().unwrap(); + + if q > 0 { + // Add the lower 7 bits with the MSB set (0b1000_0000) + result.append(value + 128_u8); + n = q; + } else { + // Add the last byte without setting the MSB + result.append(value); + break; + } + }; + + result +} + +// gas: ~2 +#[test] +fn test_zero_round_trips_successfully() { + let n = 0; + let encoded: Array = array![0]; + let (decoded, length) = match decode_integer(encoded.clone(), 0) { + Result::Ok((integer, length)) => (integer, length), + Result::Err(error) => panic!("Error: {:?}", error), + }; + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); +} + +// gas: ~113 +#[test] +fn test_u128_max_round_trips_successfully() { + let n = 340282366920938463463374607431768211455; + let encoded = array![ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 3 + ]; + let (decoded, length) = match decode_integer(encoded.clone(), 0) { + Result::Ok((integer, length)) => (integer, length), + Result::Err(error) => panic!("Error: {:?}", error), + }; + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); +} + +// gas: ~7364 +#[test] +fn test_powers_of_two_round_trip_successfully() { + let mut i = 0; + + loop { + let n = pow(2_u128, i); + let encoded: Array = encode_to_vec(n); + let (decoded, length) = match decode_integer(encoded.clone(), 0) { + Result::Ok((integer, length)) => (integer, length), + Result::Err(error) => panic!("Error: {:?}", error), + }; + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); + + i += 1; + + if i == 128 { + break; + } + }; +} + +// gas: ~6737 +#[test] +fn test_alternating_bit_strings_round_trip_successfully() { + let mut i = 0; + let mut n = 0; + + loop { + let shifted_n = n * 2_u128; + n = shifted_n + (i % 2); + + let encoded = encode_to_vec(n); + let (decoded, length) = match decode_integer(encoded.clone(), 0) { + Result::Ok((integer, length)) => (integer, length), + Result::Err(error) => panic!("Error: {:?}", error), + }; + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); + + i += 1; + + if i == 128 { + break; + } + }; +} + +// gas: ~113 +#[test] +fn test_varints_may_not_be_longer_than_19_bytes() { + let VALID: Array = array![ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 0, + ]; + + let (decoded, length) = match decode_integer(VALID.clone(), 0) { + Result::Ok((integer, length)) => (integer, length), + Result::Err(error) => panic!("Error: {:?}", error), + }; + assert_eq!(decoded, 0); + assert_eq!(length, 19); +} + +// gas: ~114 +#[test] +#[should_panic(expected: "Overlong error")] +fn test_varints_may_not_be_longer_than_19_bytes_fails() { + let INVALID: Array = array![ + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 0, + ]; + match decode_integer(INVALID.clone(), 0) { + Result::Ok(_) => {}, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +// gas: ~106 +#[test] +#[should_panic(expected: "Overflow error")] +fn test_varints_may_not_overflow_u128_fails_1() { + let INVALID: Array = array![ + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 64, + ]; + match decode_integer(INVALID.clone(), 0) { + Result::Ok(_) => {}, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +// gas: ~106 +#[test] +#[should_panic(expected: "Overflow error")] +fn test_varints_may_not_overflow_u128_fails_2() { + let INVALID: Array = array![ + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 32, + ]; + match decode_integer(INVALID.clone(), 0) { + Result::Ok(_) => {}, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +#[test] +#[should_panic(expected: "Overflow error")] +fn test_varints_may_not_overflow_u128_fails_3() { + let INVALID: Array = array![ + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 16, + ]; + match decode_integer(INVALID.clone(), 0) { + Result::Ok(_) => {}, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +#[test] +#[should_panic(expected: "Overflow error")] +fn test_varints_may_not_overflow_u128_fails_4() { + let INVALID: Array = array![ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 8, + ]; + match decode_integer(INVALID.clone(), 0) { + Result::Ok(_) => {}, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +#[test] +#[should_panic(expected: "Overflow error")] +fn test_varints_may_not_overflow_u128_fails_5() { + let INVALID: Array = array![ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 4, + ]; + match decode_integer(INVALID.clone(), 0) { + Result::Ok(_) => {}, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +// gas: ~119 +#[test] +fn test_varints_may_not_overflow_u128() { + let VALID: Array = array![ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 2, + ]; + let (decoded, length) = match decode_integer(VALID.clone(), 0) { + Result::Ok((integer, length)) => (integer, length), + Result::Err(error) => panic!("Error: {:?}", error), + }; + let expected = pow(2_u128, 127); + assert_eq!(decoded, expected); + assert_eq!(length, 19); +} + +#[test] +#[should_panic(expected: "invalid varint")] +fn test_varints_must_be_terminated() { + let data: Array = array![128]; + match decode_integer(data.clone(), 0) { + Result::Ok(_) => {}, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +// gas: ~37 +#[test] +fn test_decode_integers_tx_mint() { + let payload_u8: Array = array![20, 188, 228, 51, 20, 193, 1]; + let payload: ByteArray = payload_u8.span().into(); + let expected_integers: Array = array![20, 848444, 20, 193]; + match decode_integers(payload) { + Result::Ok(res) => { assert_eq!(res, expected_integers); }, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} + +// gas: ~226 +#[test] +fn test_decode_integers_tx_etching() { + let payload_u8: Array = array![ + 2, + 7, + 4, + 155, + 140, + 183, + 252, + 238, + 182, + 251, + 234, + 225, + 187, + 226, + 11, + 1, + 0, + 3, + 136, + 66, + 5, + 204, + 78, + 6, + 152, + 227, + 6, + 10, + 232, + 7, + 8, + 129, + 171, + 1, + 22, + 1 + ]; + let payload: ByteArray = payload_u8.span().into(); + let expected_integers: Array = array![ + 2, + 7, + 4, + 1778522209552757531985435, + 1, + 0, + 3, + 8456, + 5, + 10060, + 6, + 111000, + 10, + 1000, + 8, + 21889, + 22, + 1 + ]; + match decode_integers(payload) { + Result::Ok(res) => { assert_eq!(res, expected_integers); }, + Result::Err(error) => panic!("Error: {:?}", error), + }; +} diff --git a/src/types.cairo b/src/types.cairo new file mode 100644 index 0000000..708e620 --- /dev/null +++ b/src/types.cairo @@ -0,0 +1,116 @@ +use core::num::traits::CheckedSub; +use core::num::traits::CheckedAdd; + +#[derive(Copy, Drop, Default, PartialEq, Debug)] +pub struct RuneId { + pub block: u64, + pub tx: u32, +} + +#[derive(Copy, Drop, PartialEq, Default, Debug)] +pub struct Edict { + pub id: RuneId, + pub amount: u128, + pub output: u32, +} + +#[derive(Copy, Drop, PartialEq, Default, Debug, Serde)] +pub struct Terms { + pub amount: Option, + pub cap: Option, + pub height: (Option, Option), + pub offset: (Option, Option), +} + +#[derive(Drop, PartialEq, Default, Debug, Serde)] +pub struct Etching { + pub divisibility: Option, + pub premine: Option, + pub rune: Option, + pub spacers: Option, + pub symbol: Option, + pub terms: Option, + pub turbo: bool, +} + +#[derive(Default, Drop, PartialEq, Debug)] +pub struct Runestone { + pub edicts: Array, + pub etching: Option, + pub mint: Option, + pub pointer: Option, +} + +#[derive(Default, Drop, PartialEq, Debug)] +pub struct Cenotaph { + pub etching: Option, + pub flaw: Option, + pub mint: Option, +} + +#[derive(Drop, PartialEq, Debug)] +pub enum Artifact { + Cenotaph: Cenotaph, + Runestone: Runestone, +} + +#[derive(Debug, PartialEq)] +pub enum Payload { + Valid: ByteArray, + Invalid: ByteArray +} + +pub trait Rune { + fn new(block: u64, tx: u32) -> Option; + fn delta(self: RuneId, next: RuneId) -> Option<(u128, u128)>; + fn next(self: RuneId, block: u128, tx: u128) -> Option; +} + +impl RuneImpl of Rune { + fn new(block: u64, tx: u32) -> Option { + let id = RuneId { block, tx, }; + + if id.block == 0 && id.tx > 0 { + return Option::None; + } + + Option::Some(id) + } + + fn delta(self: RuneId, next: RuneId) -> Option<(u128, u128)> { + let block = match next.block.checked_sub(self.block) { + Option::None => { return Option::None; }, + Option::Some(b) => b, + }; + + let tx = if block == 0 { + match next.tx.checked_sub(self.tx) { + Option::None => { return Option::None; }, + Option::Some(t) => t, + } + } else { + next.tx + }; + + return Option::Some((block.into(), tx.into())); + } + + fn next(self: RuneId, block: u128, tx: u128) -> Option { + let block: u64 = match self.block.checked_add(block.try_into()?) { + Option::None => { return Option::None; }, + Option::Some(b) => b, + }; + + let tx: u32 = if block == 0 { + match self.tx.checked_add(tx.try_into()?) { + Option::None => { return Option::None; }, + Option::Some(t) => t.try_into()?, + } + } else { + tx.try_into()? + }; + + return Self::new(block, tx); + } +} + diff --git a/src/utils/char.cairo b/src/utils/char.cairo new file mode 100644 index 0000000..c1455d4 --- /dev/null +++ b/src/utils/char.cairo @@ -0,0 +1,42 @@ +use alexandria_data_structures::byte_array_ext::SpanU8IntoBytearray; + +const SHIFT_6: u32 = 64; // 2^6 +const SHIFT_12: u32 = 4096; // 2^12 +const SHIFT_18: u32 = 262144; // 2^18 + +pub fn from_u32_to_char(symbol: Option) -> Option { + if symbol.is_none() { + return Option::None; + } + + let symbol = symbol.unwrap(); + if symbol > 0x10FFFF || (symbol >= 0xD800 && symbol <= 0xDFFF) { + return Option::None; // Invalid Unicode scalar value + } + + // Convert the u32 to a single-character UTF-8 string + let buffer: Span = encode_utf8(symbol); + + // Create a single-character string from the buffer + Option::Some(buffer.into()) +} + +fn encode_utf8(code_point: u32) -> Span { + let mut buffer: Array = Default::default(); + if code_point <= 0x7F { + buffer.append(code_point.try_into().unwrap()); + } else if code_point <= 0x7FF { + buffer.append((0xC0 | (code_point / SHIFT_6)).try_into().unwrap()); + buffer.append((0x80 | (code_point % SHIFT_6)).try_into().unwrap()); + } else if code_point <= 0xFFFF { + buffer.append((0xE0 | (code_point / SHIFT_12)).try_into().unwrap()); + buffer.append((0x80 | ((code_point / SHIFT_6) % SHIFT_6)).try_into().unwrap()); + buffer.append((0x80 | (code_point % SHIFT_6)).try_into().unwrap()); + } else { + buffer.append((0xF0 | (code_point / SHIFT_18)).try_into().unwrap()); + buffer.append((0x80 | ((code_point / SHIFT_12) % SHIFT_6)).try_into().unwrap()); + buffer.append((0x80 | ((code_point / SHIFT_6) % SHIFT_6)).try_into().unwrap()); + buffer.append((0x80 | (code_point % SHIFT_6)).try_into().unwrap()); + } + buffer.span() +} diff --git a/src/utils/fields.cairo b/src/utils/fields.cairo new file mode 100644 index 0000000..844881d --- /dev/null +++ b/src/utils/fields.cairo @@ -0,0 +1,88 @@ +use core::nullable::NullableTrait; +use core::dict::{Felt252Dict, Felt252DictEntryTrait}; +use alexandria_data_structures::byte_array_ext::SpanU8IntoBytearray; + +pub fn store_field_key(ref fields_keys: Array, key: u128) { + let mut i = 0; + let mut found = false; + loop { + if i >= fields_keys.len() { + break; + } + if *fields_keys[i] == key { + found = true; + break; + } + i += 1; + }; + + if !found { + fields_keys.append(key); + } +} + +pub fn append_value(ref dict: Felt252Dict>>, index: felt252, value: u128) { + let (entry, arr) = dict.entry(index); + let mut unboxed_val = arr.deref_or(array![]); + unboxed_val.append(value); + dict = entry.finalize(NullableTrait::new(unboxed_val)); +} + +pub fn get_array_entry(ref dict: Felt252Dict>>, index: felt252) -> Span { + let (entry, _arr) = dict.entry(index); + let mut arr = _arr.deref_or(array![]); + let span = arr.span(); + dict = entry.finalize(NullableTrait::new(arr)); + span +} + +pub fn remove_field_key(ref fields_keys: Array, key: u128) { + let mut new_fields_keys: Array = array![]; + let mut i = 0; + + loop { + if i >= fields_keys.len() { + break; + } + + let value = *fields_keys[i]; + if value != key { + new_fields_keys.append(value); + } + + i += 1; + }; + fields_keys = new_fields_keys; +} + +pub fn get_new_field_val(mut new_field: Span) -> Array { + let mut new_field_arr = array![]; + let mut index = 0; + loop { + if index >= new_field.len() { + break; + } + let value = new_field.pop_front().unwrap(); + new_field_arr.append(*value); + index += 1; + }; + new_field_arr +} + +pub fn has_even_tag(mut fields_keys: Span) -> bool { + let mut i = 0; + let mut is_err = false; + loop { + if i >= fields_keys.len() { + break; + } + let key = fields_keys.pop_front().unwrap(); + let (_, r) = DivRem::::div_rem(*key, 2); + if r == 0 { + is_err = true; + break; + } + i += 1; + }; + is_err +} diff --git a/src/utils/varint.cairo b/src/utils/varint.cairo new file mode 100644 index 0000000..e7d0480 --- /dev/null +++ b/src/utils/varint.cairo @@ -0,0 +1,90 @@ +use alexandria_math::pow; +use alexandria_data_structures::byte_array_ext::ByteArrayIntoArrayU8; + +pub fn decode_integers(payload: ByteArray) -> Result, ByteArray> { + let payload: Array = payload.into(); + let mut integers: Array = ArrayTrait::new(); + let mut i = 0; + let mut err: ByteArray = Default::default(); + + loop { + if i == payload.len() { + break; + } + + // Decode the integer starting at the current index + let (integer, length) = match decode_integer(payload.clone(), i) { + Result::Ok((integer, length)) => (integer, length), + Result::Err(error) => { + err = error; + break; + } + }; + + // Append the decoded integer to the result array + integers.append(integer); + + // Move the index forward by the length of the decoded integer + i += length; + }; + + if err.len() > 0 { + return Result::Err(err); + } + + Result::Ok(integers) +} + +pub fn decode_integer(buffer: Array, start: usize) -> Result<(u128, usize), ByteArray> { + let mut n: u128 = 0; + let mut length: usize = 0; + let mut i: usize = 0; + let mut is_valid: bool = false; + + let mut err: ByteArray = Default::default(); + + loop { + if i >= buffer.len() - start { + break; + } + + let byte = buffer[start + i]; + // byte & 0b0111_1111; + let (_, value) = DivRem::::div_rem((*byte).into(), 128); + + // Overflow check + if i > 18 { + err = "Overlong error"; + break; + } + + if i == 18 && value & 0b0111_1100 != 0 { + err = "Overflow error"; + break; + } + + let shift: u128 = 7_u128 * i.into(); + n += (value.into() * pow(2_u128, shift)); + + length += 1; + + // Check if it's the last byte of the varint + let (q, _) = DivRem::::div_rem((*byte).into(), 128); + if q == 0 { + is_valid = true; + break; + } + + i += 1; + }; + + if err.len() > 0 { + return Result::Err(err); + } + + if !is_valid { + return Result::Err("invalid varint"); + } + + Result::Ok((n, length)) +}