From 20a025917ccfd01d431c1b97f84b4d02deb50836 Mon Sep 17 00:00:00 2001 From: aritkulova Date: Mon, 2 Nov 2026 16:39:09 +0200 Subject: [PATCH 1/3] functions for retrieving information from output_null_datum --- CHANGELOG.md | 6 + README.md | 26 +- docs/stdlib.json | 70 +++ simf/lib/op_return.simf | 12 - simf/lib/utxo/op_return.simf | 119 +++++ simf/tests/op_return.simf | 24 -- simf/tests/utxo/op_return.simf | 240 +++++++++++ tests/stdlib/common/core.rs | 30 +- tests/stdlib/main.rs | 1 - tests/stdlib/op_return.rs | 131 ------ tests/stdlib/utxo/mod.rs | 1 + tests/stdlib/utxo/op_return.rs | 762 +++++++++++++++++++++++++++++++++ 12 files changed, 1237 insertions(+), 185 deletions(-) delete mode 100644 simf/lib/op_return.simf create mode 100644 simf/lib/utxo/op_return.simf delete mode 100644 simf/tests/op_return.simf create mode 100644 simf/tests/utxo/op_return.simf delete mode 100644 tests/stdlib/op_return.rs create mode 100644 tests/stdlib/utxo/op_return.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f33290d..78e5fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added +- Extended functions for retrieving information from `OP_RETURN`. + +### Changed - Bumped Simplex to 0.2.0. - Aligned existing testing API with changes in Simplex. diff --git a/README.md b/README.md index 9c764da..cb1e5b8 100644 --- a/README.md +++ b/README.md @@ -48,21 +48,21 @@ simf/lib │ │ │ └── Functions for asserting `AssetId` in inputs and outputs. │ │ └── asset_id_with_amount.simf │ │ └── Functions for asserting `Amount` and `AssetId` in inputs and outputs. -│ └── getters -│ ├── amount.simf -│ │ └── Functions for retrieving `Amount` in inputs and outputs. -│ ├── asset_id.simf -│ │ └── Functions for retrieving `AssetId` in inputs and outputs. -│ ├── asset_id_with_amount.simf -│ │ └── Functions for retrieving `Amount` and `AssetId` in inputs and outputs. -│ └── script_hash.simf -│ └── Functions for retrieving hash of the scriptPubKey in inputs and outputs. +│ ├── getters +│ │ ├── amount.simf +│ │ │ └── Functions for retrieving `Amount` in inputs and outputs. +│ │ ├── asset_id.simf +│ │ │ └── Functions for retrieving `AssetId` in inputs and outputs. +│ │ ├── asset_id_with_amount.simf +│ │ │ └── Functions for retrieving `Amount` and `AssetId` in inputs and outputs. +│ │ └── script_hash.simf +│ │ └── Functions for retrieving hash of the scriptPubKey in inputs and outputs. +│ └── op_return.simf +│ └── Utilities for detecting and enforcing `OP_RETURN` (null data) outputs. ├── asserts.simf │ └── Assertion helpers: `assert_eq_*` for uints and `bool`, plus `assert_none_*` for `Option`. -├── binary.simf -│ └── Basic binary logic operations: `and`, `or`, `not`, `xor`. -└── op_return.simf - └── Utilities for detecting and enforcing `OP_RETURN` (null data) outputs. +└── binary.simf + └── Basic binary logic operations: `and`, `or`, `not`, `xor`. ``` ## Installation diff --git a/docs/stdlib.json b/docs/stdlib.json index 133053e..7afb04c 100644 --- a/docs/stdlib.json +++ b/docs/stdlib.json @@ -489,6 +489,62 @@ "output_type": "bool", "description": "Return the logical XOR of the two given values." }, + { + "simplicityhl_name": "get_entry_option", + "section": "OP_RETURN", + "input_type": "u32, u32", + "output_type": "Option", + "description": "Return specified entry at the given OP_RETURN output and given index. Return `None` if the output doesn't exist, isn't null data, or has no such entry." + }, + { + "simplicityhl_name": "pushed_data_from_entry", + "section": "OP_RETURN", + "input_type": "Entry", + "output_type": "Option<(u2, u256)>", + "description": "Return `(x, hash)` from the given entry if it is pushed data and `None` otherwise. `hash` is the SHA256 hash of the pushed data and `x` indicates how the data was pushed: 0 is an immediate 0 to 75 bytes, 1 is an OP_PUSHDATA1, 2 is an OP_PUSHDATA2 and 3 is an OP_PUSHDATA4." + }, + { + "simplicityhl_name": "get_pushed_data", + "section": "OP_RETURN", + "input_type": "u32, u32", + "output_type": "Option<(u2, u256)>", + "description": "Return `(x, hash)` if the entry at the given index of the given output is pushed data. `hash` is the SHA256 hash of the pushed data and `x` indicates how the data was pushed: 0 is an immediate 0 to 75 bytes, 1 is an OP_PUSHDATA1, 2 is an OP_PUSHDATA2 and 3 is an OP_PUSHDATA4." + }, + { + "simplicityhl_name": "op_x_minus_1_from_entry", + "section": "OP_RETURN", + "input_type": "Entry", + "output_type": "Option", + "description": "Return `x - 1` from the given entry if it is OP_x for x in the range 1..=16 and `None` otherwise." + }, + { + "simplicityhl_name": "get_op_x_minus_1", + "section": "OP_RETURN", + "input_type": "u32, u32", + "output_type": "Option", + "description": "Return `x - 1` if the given entry at the given output is OP_x for x in the range 1..=16 and `None` otherwise." + }, + { + "simplicityhl_name": "negate_or_reserved_from_entry", + "section": "OP_RETURN", + "input_type": "Entry", + "output_type": "Option", + "description": "Return `Some(0)` if the given entry is OP_1NEGATE, `Some(1)` if the entry is OP_RESERVED and `None` otherwise." + }, + { + "simplicityhl_name": "get_negate_or_reserved", + "section": "OP_RETURN", + "input_type": "u32, u32", + "output_type": "Option", + "description": "Return `Some(0)` if the the entry at the given entry of the given output is OP_1NEGATE, `Some(1)` if the entry is OP_RESERVED and `None` otherwise." + }, + { + "simplicityhl_name": "kind_of_entry", + "section": "OP_RETURN", + "input_type": "Entry", + "output_type": "u8", + "description": "Return the kind of the entry. 0 is pushed data, 1 is OP_1..OP_16 and 2 is OP_1NEGATE or OP_RESERVED." + }, { "simplicityhl_name": "is_output_op_return", "section": "OP_RETURN", @@ -496,6 +552,13 @@ "output_type": "bool", "description": "Return `true` if the output at the given index is an OP_RETURN (null data) output, `false` otherwise (including if the output does not exist)." }, + { + "simplicityhl_name": "has_entry", + "section": "OP_RETURN", + "input_type": "u32, u32", + "output_type": "bool", + "description": "Return true if the given output is an OP_RETURN output that has an entry at `entry_index`." + }, { "simplicityhl_name": "assert_output_is_op_return", "section": "OP_RETURN", @@ -503,6 +566,13 @@ "output_type": "()", "description": "Assert that the output at the given index is an OP_RETURN (null data) output.\n\n## Panics\nThe assertion fails." }, + { + "simplicityhl_name": "assert_has_entry", + "section": "OP_RETURN", + "input_type": "u32, u32", + "output_type": "()", + "description": "Assert that the given output is an OP_RETURN that has an entry at `entry_index`.\n\n## Panics\nThe assertion fails." + }, { "simplicityhl_name": "ge_to_point", "section": "secp256k1 operations", diff --git a/simf/lib/op_return.simf b/simf/lib/op_return.simf deleted file mode 100644 index d6e1d85..0000000 --- a/simf/lib/op_return.simf +++ /dev/null @@ -1,12 +0,0 @@ -/// Returns true if the specified output is an OP_RETURN (null data) output -pub fn is_output_op_return(output_index: u32) -> bool { - match jet::output_null_datum(output_index, 0) { - Some(entry: Option>>) => true, - None => false, - } -} - -/// Asserts that the specified output is an OP_RETURN (null data) output -pub fn assert_output_is_op_return(output_index: u32) { - assert!(is_output_op_return(output_index)); -} diff --git a/simf/lib/utxo/op_return.simf b/simf/lib/utxo/op_return.simf new file mode 100644 index 0000000..ec3f2fb --- /dev/null +++ b/simf/lib/utxo/op_return.simf @@ -0,0 +1,119 @@ +pub type Entry = Either<(u2, u256), Either>; + +/// Returns specified entry at the specified OP_RETURN output and entry index. +/// Returns `None` if the output doesn't exist, isn't null data, or has no such entry. +pub fn get_entry_option(output_index: u32, entry_index: u32) -> Option { + match jet::output_null_datum(output_index, entry_index) { + Some(entry_option: Option) => entry_option, + None => None, + } +} + +/// Returns `(x, hash)` from the specified entry if it is pushed data and `None` otherwise. +/// +/// `hash` is the SHA256 hash of the pushed data and `x` indicates how the data was pushed: +/// +/// 0 is an immediate 0 to 75 bytes, 1 is an OP_PUSHDATA1, +/// 2 is an OP_PUSHDATA2 and 3 is an OP_PUSHDATA4. +pub fn pushed_data_from_entry(entry: Entry) -> Option<(u2, u256)> { + match entry { + Left(pushed_data: (u2, u256)) => Some(pushed_data), + Right(_other: Either) => None, + } +} + +/// Returns `(x, hash)` if the the entry at entry_index of the specified output is pushed data. +/// +/// `hash` is the SHA256 hash of the pushed data and `x` indicates how the data was pushed: +/// +/// 0 is an immediate 0 to 75 bytes, 1 is an OP_PUSHDATA1, +/// 2 is an OP_PUSHDATA2 and 3 is an OP_PUSHDATA4. +pub fn get_pushed_data(output_index: u32, entry_index: u32) -> Option<(u2, u256)> { + let entry: Option = get_entry_option(output_index, entry_index); + + match entry { + Some(entry: Entry) => pushed_data_from_entry(entry), + None => None, + } +} + +/// Returns `x - 1` from the specified entry if it is `OP_x` for x in the range 1..=16 and `None` otherwise +pub fn op_x_minus_1_from_entry(entry: Entry) -> Option { + match entry { + Left(_pushed_data: (u2, u256)) => None, + Right(right: Either) => match right { + Left(_negate_or_reserved: u1) => None, + Right(op_x_minus_1: u4) => Some(op_x_minus_1), + }, + } +} + +/// Returns `x - 1` if the specified entry at the specified output is `OP_x` for x in the range 1..=16 and `None` otherwise +pub fn get_op_x_minus_1(output_index: u32, entry_index: u32) -> Option { + let entry: Option = get_entry_option(output_index, entry_index); + + match entry { + Some(entry: Entry) => op_x_minus_1_from_entry(entry), + None => None, + } +} + +/// Returns Some(0) if the specified entry is OP_1NEGATE, Some(1) if the entry is OP_RESERVED and `None` otherwise +pub fn negate_or_reserved_from_entry(entry: Entry) -> Option { + match entry { + Left(_pushed_data: (u2, u256)) => None, + Right(right: Either) => match right { + Left(negate_or_reserved: u1) => Some(negate_or_reserved), + Right(_op_x_minus_1: u4) => None, + }, + } +} + +/// Returns Some(0) if the the entry at entry_index of the specified output is OP_1NEGATE, +/// Some(1) if the entry is OP_RESERVED and `None` otherwise +pub fn get_negate_or_reserved(output_index: u32, entry_index: u32) -> Option { + let entry: Option = get_entry_option(output_index, entry_index); + + match entry { + Some(entry: Entry) => negate_or_reserved_from_entry(entry), + None => None, + } +} + +/// Returns the kind of the entry. +/// 0 is pushed data, 1 is OP_1..OP_16 and 2 is OP_1NEGATE or OP_RESERVED +pub fn kind_of_entry(entry: Entry) -> u8 { + match entry { + Left(_pushed_data: (u2, u256)) => 0, + Right(right_branch: Either) => match right_branch { + Left(_negate_or_reserved: u1) => 2, + Right(_op_x_minus_1: u4) => 1, + }, + } +} + +/// Returns true if the specified output is an OP_RETURN (null data) output. +pub fn is_output_op_return(output_index: u32) -> bool { + match jet::output_null_datum(output_index, 0) { + Some(_entry_option: Option) => true, + None => false, + } +} + +/// Returns true if the specified output is an OP_RETURN output that has an entry at entry_index +pub fn has_entry(output_index: u32, entry_index: u32) -> bool { + match get_entry_option(output_index, entry_index) { + Some(_entry: Entry) => true, + None => false, + } +} + +/// Asserts that the specified output is an OP_RETURN (null data) output +pub fn assert_output_is_op_return(output_index: u32) { + assert!(is_output_op_return(output_index)); +} + +/// Asserts that the specified output is an OP_RETURN output that has an entry at entry_index +pub fn assert_has_entry(output_index: u32, entry_index: u32) { + assert!(has_entry(output_index, entry_index)); +} diff --git a/simf/tests/op_return.simf b/simf/tests/op_return.simf deleted file mode 100644 index d30a1d0..0000000 --- a/simf/tests/op_return.simf +++ /dev/null @@ -1,24 +0,0 @@ -use crate::lib::op_return::{is_output_op_return, assert_output_is_op_return}; - -use crate::tests::support::dispatch::is_selected; -use crate::tests::support::expect::assert_bool; - -fn main() { - let fn_idx: u8 = witness::FUNCTION_INDEX; - - let index: u32 = witness::INDEX; - let expected: bool = witness::EXPECTED; - - match is_selected(0, fn_idx) { - true => { - assert_bool(is_output_op_return(index), expected); - }, - false => (), - }; - match is_selected(1, fn_idx) { - true => { - assert_output_is_op_return(index); - }, - false => (), - }; -} diff --git a/simf/tests/utxo/op_return.simf b/simf/tests/utxo/op_return.simf new file mode 100644 index 0000000..3f8813d --- /dev/null +++ b/simf/tests/utxo/op_return.simf @@ -0,0 +1,240 @@ +use crate::lib::utxo::op_return::{ + Entry, + get_entry_option, + pushed_data_from_entry, + get_pushed_data, + op_x_minus_1_from_entry, + get_op_x_minus_1, + negate_or_reserved_from_entry, + get_negate_or_reserved, + kind_of_entry, + is_output_op_return, + has_entry, + assert_output_is_op_return, + assert_has_entry +}; + +use crate::lib::binary::and; +use crate::lib::asserts::assert_eq_8; +use crate::lib::u8::convert::safe_u8_to_u1; + +use crate::tests::support::dispatch::is_selected; +use crate::tests::support::expect::assert_bool; + +// todo: move to lib +fn eq_2(a: u2, b: u2) -> bool { + let (a_high, a_low): (u1, u1) = ::into(a); + let (b_high, b_low): (u1, u1) = ::into(b); + + and(jet::eq_1(a_high, b_high), jet::eq_1(a_low, b_low)) +} + +// todo: move to lib +fn eq_4(a: u4, b: u4) -> bool { + let a_u8: u8 = <(u4, u4)>::into((0, a)); + let b_u8: u8 = <(u4, u4)>::into((0, b)); + + jet::eq_8(a_u8, b_u8) +} + +fn eq_pushed_data(a: (u2, u256), b: (u2, u256)) -> bool { + let (indicator_a, hash_a): (u2, u256) = a; + let (indicator_b, hash_b): (u2, u256) = b; + + match eq_2(indicator_a, indicator_b) { + true => jet::eq_256(hash_a, hash_b), + false => false, + } +} + +fn eq_entry(a: Entry, b: Entry) -> bool { + match a { + Left(push_a: (u2, u256)) => match b { + Left(push_b: (u2, u256)) => eq_pushed_data(push_a, push_b), + Right(_other: Either) => false, + }, + Right(right_a: Either) => match b { + Left(_push_b: (u2, u256)) => false, + Right(right_b: Either) => match right_a { + Left(neg_or_res_a: u1) => match right_b { + Left(neg_or_res_b: u1) => jet::eq_1(neg_or_res_a, neg_or_res_b), + Right(_op_x_minus_1: u4) => false, + }, + Right(op_x_minus_1_a: u4) => match right_b { + Left(_neg_or_res: u1) => false, + Right(op_x_minus_1_b: u4) => eq_4(op_x_minus_1_a, op_x_minus_1_b), + }, + }, + }, + } +} + +fn eq_entry_option(a: Option, b: Option) -> bool { + match a { + Some(entry_a: Entry) => match b { + Some(entry_b: Entry) => eq_entry(entry_a, entry_b), + None => false, + }, + None => match b { + Some(_b: Entry) => false, + None => true, + }, + } +} + +fn assert_pushed_data( + expected_value: Option, + expected_hash: u256, + actual_pushed_data: Option<(u2, u256)> +) { + match expected_value { + Some(expected: u8) => { + let (_, _, _, expected_indicator): (u2, u2, u2, u2) = ::into(expected); + + assert!( + eq_pushed_data(unwrap(actual_pushed_data), (expected_indicator, expected_hash)) + ); + }, + None => { + assert!(is_none::<(u2, u256)>(actual_pushed_data)); + }, + } +} + +fn assert_op_x_minus_1(expected_value: Option, actual_op_x_minus_1: Option) { + match expected_value { + Some(expected: u8) => { + let (_, expected): (u4, u4) = ::into(expected); + assert!(eq_4(unwrap(actual_op_x_minus_1), expected)); + }, + None => { + assert!(is_none::(actual_op_x_minus_1)); + }, + } +} + +fn assert_neg_or_res(expected_value: Option, actual_neg_or_res: Option) { + match expected_value { + Some(expected: u8) => { + let expected: u1 = safe_u8_to_u1(expected); + assert!(jet::eq_1(unwrap(actual_neg_or_res), expected)); + }, + None => { + assert!(is_none::(actual_neg_or_res)); + }, + } +} + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + let output_index: u32 = witness::OUTPUT_INDEX; + let entry_index: u32 = witness::ENTRY_INDEX; + let entry: Entry = witness::ENTRY; + + let expected_entry: Option = witness::EXPECTED_ENTRY; + let expected_bool: bool = witness::EXPECTED_BOOL; + + let expected_value: Option = witness::expected_value; + let expected_hash: u256 = witness::EXPECTED_HASH; + + match is_selected(0, fn_idx) { + true => { + let result_entry: Option = get_entry_option(output_index, entry_index); + + assert!(eq_entry_option(result_entry, expected_entry)); + }, + false => (), + }; + + match is_selected(1, fn_idx) { + true => { + let pushed_data: Option<(u2, u256)> = pushed_data_from_entry(entry); + + assert_pushed_data(expected_value, expected_hash, pushed_data); + }, + false => (), + }; + + match is_selected(2, fn_idx) { + true => { + let pushed_data: Option<(u2, u256)> = get_pushed_data(output_index, entry_index); + + assert_pushed_data(expected_value, expected_hash, pushed_data); + }, + false => (), + }; + + match is_selected(3, fn_idx) { + true => { + let op_x_minus_1: Option = op_x_minus_1_from_entry(entry); + + assert_op_x_minus_1(expected_value, op_x_minus_1); + }, + false => (), + }; + + match is_selected(4, fn_idx) { + true => { + let op_x_minus_1: Option = get_op_x_minus_1(output_index, entry_index); + + assert_op_x_minus_1(expected_value, op_x_minus_1); + }, + false => (), + }; + + match is_selected(5, fn_idx) { + true => { + let neg_or_res: Option = negate_or_reserved_from_entry(entry); + + assert_neg_or_res(expected_value, neg_or_res); + }, + false => (), + }; + + match is_selected(6, fn_idx) { + true => { + let neg_or_res: Option = get_negate_or_reserved(output_index, entry_index); + + assert_neg_or_res(expected_value, neg_or_res); + }, + false => (), + }; + + match is_selected(7, fn_idx) { + true => { + let kind: u8 = kind_of_entry(entry); + + assert_eq_8(kind, unwrap(expected_value)); + }, + false => (), + }; + + match is_selected(8, fn_idx) { + true => { + assert_bool(is_output_op_return(output_index), expected_bool); + }, + false => (), + }; + + match is_selected(9, fn_idx) { + true => { + assert_bool(has_entry(output_index, entry_index), expected_bool); + }, + false => (), + }; + + match is_selected(10, fn_idx) { + true => { + assert_output_is_op_return(output_index); + }, + false => (), + }; + + match is_selected(11, fn_idx) { + true => { + assert_has_entry(output_index, entry_index); + }, + false => (), + }; +} diff --git a/tests/stdlib/common/core.rs b/tests/stdlib/common/core.rs index 495a3ea..b596c81 100644 --- a/tests/stdlib/common/core.rs +++ b/tests/stdlib/common/core.rs @@ -8,6 +8,8 @@ use simplex::program::Program; use simplex::signer::Signer; use simplex::simplicityhl::WitnessValues; use simplex::simplicityhl::elements::bitcoin::PublicKey; +use simplex::simplicityhl::elements::opcodes::{self, All}; +use simplex::simplicityhl::elements::script::Builder; use simplex::simplicityhl::elements::{AssetId, Script, Txid}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{ @@ -209,12 +211,32 @@ where } if let Some(data) = data { - ft.add_output(PartialOutput::new_metadata(data)) + ft.add_output(PartialOutput::new( + create_op_return_script(data), + 0, + AssetId::default(), + )); }; Ok(ft) } +/// Create an op return script from bytes, where the first byte indicates whether +/// the bytes are an immediate push or raw opcodes +fn create_op_return_script(data: &[u8]) -> Script { + let mut builder = Builder::new().push_opcode(opcodes::all::OP_RETURN); + + if data[0] == 0x00 { + builder = builder.push_slice(&data[1..]); + } else { + for i in &data[1..] { + builder = builder.push_opcode(All::from(*i)); + } + } + + builder.into_script() +} + /// Fetch the UTXO for script, unblind if confidential. fn prepare_utxo( context: &simplex::TestContext, @@ -249,11 +271,11 @@ fn unblind(signer: &Signer, mut utxo: UTXO) -> anyhow::Result { Ok(utxo) } -// Add an output mirroring utxo's asset with amount - 1 and the tweaked script. +/// Add an output mirroring utxo's asset with amount - 1 and the tweaked script. fn add_mirror_output( mut ft: FinalTransaction, utxo: &UTXO, - binding_key: PublicKey, + blinding_key: PublicKey, policy_asset: AssetId, ) -> anyhow::Result<(FinalTransaction, Option)> { let mut output = PartialOutput::new( @@ -265,7 +287,7 @@ fn add_mirror_output( ); if utxo.txout.value.is_confidential() { - output = output.with_blinding_key(binding_key) + output = output.with_blinding_key(blinding_key) } ft.add_output(output); diff --git a/tests/stdlib/main.rs b/tests/stdlib/main.rs index 04a23cb..f988ca3 100644 --- a/tests/stdlib/main.rs +++ b/tests/stdlib/main.rs @@ -2,7 +2,6 @@ mod common; mod asserts; mod binary; -mod op_return; mod secp256k1; mod u1; mod u128; diff --git a/tests/stdlib/op_return.rs b/tests/stdlib/op_return.rs deleted file mode 100644 index 2b83226..0000000 --- a/tests/stdlib/op_return.rs +++ /dev/null @@ -1,131 +0,0 @@ -use rand::Rng; - -use crate::common::core::{Expect, run, run_with_op_return}; - -use simplicityhl_std::artifacts::tests::op_return::OpReturnProgram as OpReturnTestProgram; -use simplicityhl_std::artifacts::tests::op_return::derived_op_return::{ - OpReturnArguments as OpReturnTestArguments, OpReturnWitness as OpReturnTestWitness, -}; - -use FunctionToTest::*; - -enum FunctionToTest { - IsOpReturn, - AssertOutputIsOpReturn, -} - -const DEFAULT_DATA: &[u8; 1] = &[1]; - -fn program() -> OpReturnTestProgram { - OpReturnTestProgram::new(&OpReturnTestArguments {}) -} - -/// One dispatch arm of the contract, plus the witness it reads and whether the -/// spend carries an `OP_RETURN` output. -struct Case { - witness: OpReturnTestWitness, - data: Option<&'static [u8]>, -} - -fn case(function: FunctionToTest) -> Case { - Case { - witness: OpReturnTestWitness { - function_index: function as u8, - index: 0, - expected: false, - }, - data: None, - } -} - -impl Case { - /// `index`: the output the arm inspects. - fn index(mut self, index: u32) -> Self { - self.witness.index = index; - self - } - - /// `expected`: the boolean the arm should report. - fn flag(mut self, flag: bool) -> Self { - self.witness.expected = flag; - self - } - - /// Add an `OP_RETURN` output carrying `data` to the spend. - fn op_return(mut self, data: &'static [u8]) -> Self { - self.data = Some(data); - self - } - - /// Fund, spend, and expect the spend to succeed. - fn run(self, context: &simplex::TestContext) -> anyhow::Result<()> { - self.expecting(context, Expect::Ok) - } - - /// Fund, spend, and expect `expect`. - fn expecting(self, context: &simplex::TestContext, expect: Expect) -> anyhow::Result<()> { - match self.data { - Some(data) => run_with_op_return(context, program(), self.witness, expect, data), - None => run(context, program(), self.witness, expect), - } - } -} - -#[simplex::test] -fn is_output_op_return_true(context: simplex::TestContext) -> anyhow::Result<()> { - let index = 0; - - case(IsOpReturn) - .index(index) - .flag(true) - .op_return(DEFAULT_DATA) - .run(&context) -} - -#[simplex::test] -fn is_output_op_return_empty_index_false(context: simplex::TestContext) -> anyhow::Result<()> { - let index = rand::thread_rng().gen_range(1..=u32::MAX); - - case(IsOpReturn) - .index(index) - .op_return(DEFAULT_DATA) - .run(&context) -} - -#[simplex::test] -fn is_output_op_return_false(context: simplex::TestContext) -> anyhow::Result<()> { - let index = 0; - - case(IsOpReturn).index(index).run(&context) -} - -#[simplex::test] -fn assert_output_is_op_return_pass(context: simplex::TestContext) -> anyhow::Result<()> { - let index = 0; - - case(AssertOutputIsOpReturn) - .index(index) - .op_return(DEFAULT_DATA) - .run(&context) -} - -#[simplex::test] -fn assert_output_is_op_return_empty_index_fail( - context: simplex::TestContext, -) -> anyhow::Result<()> { - let index = rand::thread_rng().gen_range(1..=u32::MAX); - - case(AssertOutputIsOpReturn) - .index(index) - .op_return(DEFAULT_DATA) - .expecting(&context, Expect::AssertFailed) -} - -#[simplex::test] -fn assert_output_is_op_return_fail(context: simplex::TestContext) -> anyhow::Result<()> { - let index = 0; - - case(AssertOutputIsOpReturn) - .index(index) - .expecting(&context, Expect::AssertFailed) -} diff --git a/tests/stdlib/utxo/mod.rs b/tests/stdlib/utxo/mod.rs index 9c68058..18d33bf 100644 --- a/tests/stdlib/utxo/mod.rs +++ b/tests/stdlib/utxo/mod.rs @@ -1,2 +1,3 @@ mod asserts; mod getters; +mod op_return; diff --git a/tests/stdlib/utxo/op_return.rs b/tests/stdlib/utxo/op_return.rs new file mode 100644 index 0000000..ebaf2c2 --- /dev/null +++ b/tests/stdlib/utxo/op_return.rs @@ -0,0 +1,762 @@ +use rand::Rng; +use secp256k1_zkp::hashes::Hash; + +use simplex::either::Either; +use simplex::simplicityhl::elements::Script; + +use crate::common::core::{Expect, run, run_with_op_return}; + +use simplicityhl_std::artifacts::tests::utxo::op_return::OpReturnProgram as OpReturnTestProgram; +use simplicityhl_std::artifacts::tests::utxo::op_return::derived_op_return::{ + OpReturnArguments as OpReturnTestArguments, OpReturnWitness as OpReturnTestWitness, +}; + +use FunctionToTest::*; + +enum FunctionToTest { + GetEntryOption, + PushedDataFromEntry, + GetPushedData, + OpXMinus1FromEntry, + GetOpXMinus1, + NegateOrReservedFromEntry, + GetNegateOrReserved, + KindOfEntry, + IsOutputOpReturn, + HasEntry, + AssertOutputIsOpReturn, + AssertHasEntry, +} + +type Entry = Either<(u8, [u8; 32]), Either>; + +const DEFAULT_DATA: &[u8; 2] = &[0, 1]; +const OP_PUSHNUM_NEG1: &[u8] = &[0x01, 0x4f]; +const OP_RESERVED: &[u8] = &[0x01, 0x50]; + +fn program() -> OpReturnTestProgram { + OpReturnTestProgram::new(&OpReturnTestArguments {}) +} + +/// One dispatch arm of the contract, plus the witness it reads and whether the +/// spend carries an `OP_RETURN` output. +struct Case { + witness: OpReturnTestWitness, + data: Option<&'static [u8]>, +} + +fn case(function: FunctionToTest) -> Case { + Case { + witness: OpReturnTestWitness { + function_index: function as u8, + output_index: 0, + entry_index: 0, + entry: Either::Right(Either::Right(0)), + expected_entry: None, + expected_value: None, + expected_bool: false, + expected_hash: [0; 32], + }, + data: None, + } +} + +impl Case { + /// `output_index`: the output the arm inspects. + fn output_index(mut self, output_index: u32) -> Self { + self.witness.output_index = output_index; + self + } + + /// `entry_index`: the entry index the arm inspects. + fn entry_index(mut self, entry_index: u32) -> Self { + self.witness.entry_index = entry_index; + self + } + + /// `entry`:the entry passed to the arm. + fn entry(mut self, entry: Entry) -> Self { + self.witness.entry = entry; + self + } + + /// `expected_entry`: the entry the arm expects. + fn expected_entry(mut self, expected_entry: Option) -> Self { + self.witness.expected_entry = expected_entry; + self + } + + /// `expected_value`: the value the arm expects. + fn expected_value(mut self, expected_value: Option) -> Self { + self.witness.expected_value = expected_value; + self + } + + /// `expected_bool`: the boolean the arm should report. + fn expected_bool(mut self, expected_bool: bool) -> Self { + self.witness.expected_bool = expected_bool; + self + } + + /// `expected_hash`: the hash of the pushed data the arm inspects. + fn expected_hash(mut self, expected_hash: [u8; 32]) -> Self { + self.witness.expected_hash = expected_hash; + self + } + + /// Add an `OP_RETURN` output carrying `data` to the spend. + /// First byte is a build indicator (0 means single data push, 1 means raw opcode push) + fn op_return(mut self, data: &'static [u8]) -> Self { + self.data = Some(data); + self + } + + /// Fund, spend, and expect the spend to succeed. + fn run(self, context: &simplex::TestContext) -> anyhow::Result<()> { + self.expecting(context, Expect::Ok) + } + + /// Fund, spend, and expect `expect`. + fn expecting(self, context: &simplex::TestContext, expect: Expect) -> anyhow::Result<()> { + match self.data { + Some(data) => run_with_op_return(context, program(), self.witness, expect, data), + None => run(context, program(), self.witness, expect), + } + } +} + +pub fn hash_of_pushed_data(data: &[u8]) -> [u8; 32] { + Script::from(data.to_vec()).wscript_hash().to_byte_array() +} + +#[simplex::test] +fn get_entry_option(context: simplex::TestContext) -> anyhow::Result<()> { + /// 0 = push data (0x12, 0x23), 1 = OP_1, 2 = OP_1NEGATE, 3 = OP_RESERVED. + const MIXED_ENTRIES: &[u8] = &[1, 0x02, 0x12, 0x23, 0x51, 0x4f, 0x50]; + const PUSHED_DATA: &[u8] = &[0x12, 0x23]; + + let cases: [(u32, Entry); 4] = [ + (0, Either::Left((0, hash_of_pushed_data(PUSHED_DATA)))), + (1, Either::Right(Either::Right(0))), + (2, Either::Right(Either::Left(0))), + (3, Either::Right(Either::Left(1))), + ]; + + for (entry_index, entry) in cases { + case(GetEntryOption) + .entry_index(entry_index) + .expected_entry(Some(entry)) + .op_return(MIXED_ENTRIES) + .run(&context) + .unwrap_or_else(|_| panic!("entry index = {entry_index}")); + } + + Ok(()) +} + +#[simplex::test] +fn get_entry_option_nonexistent_output(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = u32::MAX; + let entry_index = 0; + let expected_entry: Option = None; + + case(GetEntryOption) + .output_index(output_index) + .entry_index(entry_index) + .expected_entry(expected_entry) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn get_entry_option_not_op_return_output(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + let expected_entry: Option = None; + + case(GetEntryOption) + .output_index(output_index) + .entry_index(entry_index) + .expected_entry(expected_entry) + .run(&context) +} + +#[simplex::test] +fn get_entry_option_out_of_range_entry(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = u32::MAX; + let expected_entry: Option = None; + + case(GetEntryOption) + .output_index(output_index) + .entry_index(entry_index) + .expected_entry(expected_entry) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn pushed_data_from_entry(context: simplex::TestContext) -> anyhow::Result<()> { + let indicator = 0_u8; + let hash = hash_of_pushed_data(&DEFAULT_DATA[1..]); + + let entry: Entry = Either::Left((indicator, hash)); + + case(PushedDataFromEntry) + .entry(entry) + .expected_value(Some(indicator)) + .expected_hash(hash) + .run(&context) +} + +#[simplex::test] +fn pushed_data_from_entry_op_x_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { + case(PushedDataFromEntry) + .entry(Either::Right(Either::Right(0))) + .expected_value(None) + .run(&context) +} + +#[simplex::test] +fn get_pushdata0(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + let indicator = 0_u8; + let hash = hash_of_pushed_data(&DEFAULT_DATA[1..]); + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(Some(indicator)) + .expected_hash(hash) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn get_pushdata1(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + const DATA: [u8; 77] = [0; 77]; // a 76-byte push, which needs OP_PUSHDATA1 + + let indicator = 1_u8; + let hash = hash_of_pushed_data(&DATA[1..]); + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(Some(indicator)) + .expected_hash(hash) + .op_return(&DATA) + .run(&context) +} + +#[simplex::test] +fn get_pushdata2(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + const DATA: [u8; 7] = [1, 0x4d, 0x03, 0x00, 0xde, 0xad, 0xbe]; // the last 3 bytes are the pushed data + + let indicator = 2_u8; + let hash = hash_of_pushed_data(&DATA[4..]); + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(Some(indicator)) + .expected_hash(hash) + .op_return(&DATA) + .run(&context) +} + +#[simplex::test] +fn get_pushdata4(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + const DATA: [u8; 9] = [1, 0x4e, 0x03, 0x00, 0x00, 0x00, 0xde, 0xad, 0xbe]; // the last 3 bytes are the pushed data + + let indicator = 3_u8; + let hash = hash_of_pushed_data(&DATA[6..]); + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(Some(indicator)) + .expected_hash(hash) + .op_return(&DATA) + .run(&context) +} + +#[simplex::test] +fn get_pushed_data_nonexistent_output(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = u32::MAX; + let entry_index = 0; + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn get_pushed_data_not_op_return_output(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .run(&context) +} + +#[simplex::test] +fn get_pushed_data_out_of_range_entry(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = u32::MAX; + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn get_pushed_data_neg1_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(GetPushedData) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(OP_PUSHNUM_NEG1) + .run(&context) +} + +#[simplex::test] +fn op_x_minus_1_from_entry(context: simplex::TestContext) -> anyhow::Result<()> { + for i in 0..16 { + let entry: Entry = Either::Right(Either::Right(i)); + + case(OpXMinus1FromEntry) + .entry(entry) + .expected_value(Some(i)) + .run(&context) + .unwrap_or_else(|_| panic!("entry index = {i}")); + } + + Ok(()) +} + +#[simplex::test] +fn op_x_minus_1_from_entry_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { + let entry: Entry = Either::Right(Either::Left(1)); // OP_RESERVED + + case(OpXMinus1FromEntry) + .entry(entry) + .expected_value(None) + .run(&context) +} + +#[simplex::test] +fn get_op_x_minus_1(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + + // the first byte is the build indicator + const OP_1_TO_16: &[u8] = &[ + 0x01, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, + 0x5f, 0x60, + ]; + + for i in 0..16 { + case(GetOpXMinus1) + .output_index(output_index) + .entry_index(i) + .expected_value(Some(i as u8)) + .op_return(OP_1_TO_16) + .run(&context) + .unwrap_or_else(|err| panic!("{err}, entry index = {i}")); + } + + Ok(()) +} + +#[simplex::test] +fn get_op_x_minus_1_nonexistent_output(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = u32::MAX; + let entry_index = 0; + + case(GetOpXMinus1) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn get_op_x_minus_1_not_op_return_output(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(GetOpXMinus1) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .run(&context) +} + +#[simplex::test] +fn get_op_x_minus_1_out_of_range_entry(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = u32::MAX; + const OP_1: &[u8] = &[0x01, 0x51]; + + case(GetOpXMinus1) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(OP_1) + .run(&context) +} + +#[simplex::test] +fn get_op_x_minus_1_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(GetOpXMinus1) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(OP_RESERVED) + .run(&context) +} + +#[simplex::test] +fn negate_or_reserved_from_entry_negate(context: simplex::TestContext) -> anyhow::Result<()> { + let indicator = 0_u8; + + let entry: Entry = Either::Right(Either::Left(indicator)); + + case(NegateOrReservedFromEntry) + .entry(entry) + .expected_value(Some(indicator)) + .run(&context) +} + +#[simplex::test] +fn negate_or_reserved_from_entry_reserved(context: simplex::TestContext) -> anyhow::Result<()> { + let indicator = 1_u8; + + let entry: Entry = Either::Right(Either::Left(indicator)); + + case(NegateOrReservedFromEntry) + .entry(entry) + .expected_value(Some(indicator)) + .run(&context) +} + +#[simplex::test] +fn negate_or_reserved_from_entry_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { + let entry: Entry = Either::Left((0, hash_of_pushed_data(&[1]))); + + case(NegateOrReservedFromEntry) + .entry(entry) + .expected_value(None) + .run(&context) +} + +#[simplex::test] +fn get_negate_or_reserved_negate(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + let indicator = 0_u8; + + case(GetNegateOrReserved) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(Some(indicator)) + .op_return(OP_PUSHNUM_NEG1) + .run(&context) +} + +#[simplex::test] +fn get_negate_or_reserved_reserved(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + let indicator = 1_u8; + + case(GetNegateOrReserved) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(Some(indicator)) + .op_return(OP_RESERVED) + .run(&context) +} + +#[simplex::test] +fn get_negate_or_reserved_nonexistent_output(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = u32::MAX; + let entry_index = 0; + + case(GetNegateOrReserved) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn get_negate_or_reserved_not_op_return_output( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(GetNegateOrReserved) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .run(&context) +} + +#[simplex::test] +fn get_negate_or_reserved_out_of_range_entry(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = u32::MAX; + + case(GetNegateOrReserved) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(OP_RESERVED) + .run(&context) +} + +#[simplex::test] +fn get_negate_or_reserved_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(GetNegateOrReserved) + .output_index(output_index) + .entry_index(entry_index) + .expected_value(None) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn kind_of_entry_pushed_data(context: simplex::TestContext) -> anyhow::Result<()> { + let entry: Entry = Either::Left((0, hash_of_pushed_data(&[1]))); + + case(KindOfEntry) + .entry(entry) + .expected_value(Some(0)) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn kind_of_entry_op_x_minus_1(context: simplex::TestContext) -> anyhow::Result<()> { + let op_x_minus_1 = rand::thread_rng().gen_range(0..16); + let entry: Entry = Either::Right(Either::Right(op_x_minus_1)); + + case(KindOfEntry) + .entry(entry) + .expected_value(Some(1)) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn kind_of_entry_negate_or_reserved_negate(context: simplex::TestContext) -> anyhow::Result<()> { + let indicator = 0_u8; + let entry: Entry = Either::Right(Either::Left(indicator)); + + case(KindOfEntry) + .entry(entry) + .expected_value(Some(2)) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn kind_of_entry_negate_or_reserved_reserved(context: simplex::TestContext) -> anyhow::Result<()> { + let indicator = 1_u8; + let entry: Entry = Either::Right(Either::Left(indicator)); + + case(KindOfEntry) + .entry(entry) + .expected_value(Some(2)) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn is_output_op_return_true(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(IsOutputOpReturn) + .output_index(output_index) + .entry_index(entry_index) + .expected_bool(true) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn is_output_op_returns_nonexistent_output_false( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let output_index = u32::MAX; + + case(IsOutputOpReturn) + .output_index(output_index) + .expected_bool(false) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn is_output_op_return_false(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + + case(IsOutputOpReturn) + .output_index(output_index) + .expected_bool(false) + .run(&context) +} + +#[simplex::test] +fn has_entry_true(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = 0; + + case(HasEntry) + .output_index(output_index) + .entry_index(entry_index) + .expected_bool(true) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn has_entry_nonexistent_output_false(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = u32::MAX; + + case(HasEntry) + .output_index(output_index) + .expected_bool(false) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn has_entry_out_of_range_entry_false(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = u32::MAX; + + case(HasEntry) + .output_index(output_index) + .entry_index(entry_index) + .expected_bool(false) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn has_entry_output_not_op_return_false(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + + case(HasEntry) + .output_index(output_index) + .expected_bool(false) + .run(&context) +} + +#[simplex::test] +fn assert_output_is_op_return_pass(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + + case(AssertOutputIsOpReturn) + .output_index(output_index) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn assert_output_is_op_returns_nonexistent_output_fails( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let output_index = u32::MAX; + + case(AssertOutputIsOpReturn) + .output_index(output_index) + .op_return(DEFAULT_DATA) + .expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn assert_output_is_op_return_fail(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + + case(AssertOutputIsOpReturn) + .output_index(output_index) + .expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn assert_has_entry_pass(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + + case(AssertHasEntry) + .output_index(output_index) + .op_return(DEFAULT_DATA) + .run(&context) +} + +#[simplex::test] +fn assert_has_entry_nonexistent_output_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = u32::MAX; + + case(AssertHasEntry) + .output_index(output_index) + .op_return(DEFAULT_DATA) + .expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn assert_has_entry_output_not_op_return_fails( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let output_index = 0; + + case(AssertHasEntry) + .output_index(output_index) + .expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn assert_has_entry_out_of_range_entry_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let output_index = 0; + let entry_index = u32::MAX; + + case(AssertHasEntry) + .output_index(output_index) + .entry_index(entry_index) + .op_return(DEFAULT_DATA) + .expecting(&context, Expect::AssertFailed) +} From 5dd067bf4b2344283dc002db5894e91ebc5f9d50 Mon Sep 17 00:00:00 2001 From: aritkulova Date: Thu, 5 Nov 2026 12:45:45 +0200 Subject: [PATCH 2/3] renamed Entry to OpReturnEntry --- docs/stdlib.json | 10 +++++----- simf/lib/utxo/op_return.simf | 30 +++++++++++++++--------------- simf/tests/utxo/op_return.simf | 18 +++++++++--------- tests/stdlib/utxo/op_return.rs | 34 +++++++++++++++++----------------- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/docs/stdlib.json b/docs/stdlib.json index 7afb04c..0276c56 100644 --- a/docs/stdlib.json +++ b/docs/stdlib.json @@ -493,13 +493,13 @@ "simplicityhl_name": "get_entry_option", "section": "OP_RETURN", "input_type": "u32, u32", - "output_type": "Option", + "output_type": "Option", "description": "Return specified entry at the given OP_RETURN output and given index. Return `None` if the output doesn't exist, isn't null data, or has no such entry." }, { "simplicityhl_name": "pushed_data_from_entry", "section": "OP_RETURN", - "input_type": "Entry", + "input_type": "OpReturnEntry", "output_type": "Option<(u2, u256)>", "description": "Return `(x, hash)` from the given entry if it is pushed data and `None` otherwise. `hash` is the SHA256 hash of the pushed data and `x` indicates how the data was pushed: 0 is an immediate 0 to 75 bytes, 1 is an OP_PUSHDATA1, 2 is an OP_PUSHDATA2 and 3 is an OP_PUSHDATA4." }, @@ -513,7 +513,7 @@ { "simplicityhl_name": "op_x_minus_1_from_entry", "section": "OP_RETURN", - "input_type": "Entry", + "input_type": "OpReturnEntry", "output_type": "Option", "description": "Return `x - 1` from the given entry if it is OP_x for x in the range 1..=16 and `None` otherwise." }, @@ -527,7 +527,7 @@ { "simplicityhl_name": "negate_or_reserved_from_entry", "section": "OP_RETURN", - "input_type": "Entry", + "input_type": "OpReturnEntry", "output_type": "Option", "description": "Return `Some(0)` if the given entry is OP_1NEGATE, `Some(1)` if the entry is OP_RESERVED and `None` otherwise." }, @@ -541,7 +541,7 @@ { "simplicityhl_name": "kind_of_entry", "section": "OP_RETURN", - "input_type": "Entry", + "input_type": "OpReturnEntry", "output_type": "u8", "description": "Return the kind of the entry. 0 is pushed data, 1 is OP_1..OP_16 and 2 is OP_1NEGATE or OP_RESERVED." }, diff --git a/simf/lib/utxo/op_return.simf b/simf/lib/utxo/op_return.simf index ec3f2fb..d308178 100644 --- a/simf/lib/utxo/op_return.simf +++ b/simf/lib/utxo/op_return.simf @@ -1,10 +1,10 @@ -pub type Entry = Either<(u2, u256), Either>; +pub type OpReturnEntry = Either<(u2, u256), Either>; /// Returns specified entry at the specified OP_RETURN output and entry index. /// Returns `None` if the output doesn't exist, isn't null data, or has no such entry. -pub fn get_entry_option(output_index: u32, entry_index: u32) -> Option { +pub fn get_entry_option(output_index: u32, entry_index: u32) -> Option { match jet::output_null_datum(output_index, entry_index) { - Some(entry_option: Option) => entry_option, + Some(entry_option: Option) => entry_option, None => None, } } @@ -15,7 +15,7 @@ pub fn get_entry_option(output_index: u32, entry_index: u32) -> Option { /// /// 0 is an immediate 0 to 75 bytes, 1 is an OP_PUSHDATA1, /// 2 is an OP_PUSHDATA2 and 3 is an OP_PUSHDATA4. -pub fn pushed_data_from_entry(entry: Entry) -> Option<(u2, u256)> { +pub fn pushed_data_from_entry(entry: OpReturnEntry) -> Option<(u2, u256)> { match entry { Left(pushed_data: (u2, u256)) => Some(pushed_data), Right(_other: Either) => None, @@ -29,16 +29,16 @@ pub fn pushed_data_from_entry(entry: Entry) -> Option<(u2, u256)> { /// 0 is an immediate 0 to 75 bytes, 1 is an OP_PUSHDATA1, /// 2 is an OP_PUSHDATA2 and 3 is an OP_PUSHDATA4. pub fn get_pushed_data(output_index: u32, entry_index: u32) -> Option<(u2, u256)> { - let entry: Option = get_entry_option(output_index, entry_index); + let entry: Option = get_entry_option(output_index, entry_index); match entry { - Some(entry: Entry) => pushed_data_from_entry(entry), + Some(entry: OpReturnEntry) => pushed_data_from_entry(entry), None => None, } } /// Returns `x - 1` from the specified entry if it is `OP_x` for x in the range 1..=16 and `None` otherwise -pub fn op_x_minus_1_from_entry(entry: Entry) -> Option { +pub fn op_x_minus_1_from_entry(entry: OpReturnEntry) -> Option { match entry { Left(_pushed_data: (u2, u256)) => None, Right(right: Either) => match right { @@ -50,16 +50,16 @@ pub fn op_x_minus_1_from_entry(entry: Entry) -> Option { /// Returns `x - 1` if the specified entry at the specified output is `OP_x` for x in the range 1..=16 and `None` otherwise pub fn get_op_x_minus_1(output_index: u32, entry_index: u32) -> Option { - let entry: Option = get_entry_option(output_index, entry_index); + let entry: Option = get_entry_option(output_index, entry_index); match entry { - Some(entry: Entry) => op_x_minus_1_from_entry(entry), + Some(entry: OpReturnEntry) => op_x_minus_1_from_entry(entry), None => None, } } /// Returns Some(0) if the specified entry is OP_1NEGATE, Some(1) if the entry is OP_RESERVED and `None` otherwise -pub fn negate_or_reserved_from_entry(entry: Entry) -> Option { +pub fn negate_or_reserved_from_entry(entry: OpReturnEntry) -> Option { match entry { Left(_pushed_data: (u2, u256)) => None, Right(right: Either) => match right { @@ -72,17 +72,17 @@ pub fn negate_or_reserved_from_entry(entry: Entry) -> Option { /// Returns Some(0) if the the entry at entry_index of the specified output is OP_1NEGATE, /// Some(1) if the entry is OP_RESERVED and `None` otherwise pub fn get_negate_or_reserved(output_index: u32, entry_index: u32) -> Option { - let entry: Option = get_entry_option(output_index, entry_index); + let entry: Option = get_entry_option(output_index, entry_index); match entry { - Some(entry: Entry) => negate_or_reserved_from_entry(entry), + Some(entry: OpReturnEntry) => negate_or_reserved_from_entry(entry), None => None, } } /// Returns the kind of the entry. /// 0 is pushed data, 1 is OP_1..OP_16 and 2 is OP_1NEGATE or OP_RESERVED -pub fn kind_of_entry(entry: Entry) -> u8 { +pub fn kind_of_entry(entry: OpReturnEntry) -> u8 { match entry { Left(_pushed_data: (u2, u256)) => 0, Right(right_branch: Either) => match right_branch { @@ -95,7 +95,7 @@ pub fn kind_of_entry(entry: Entry) -> u8 { /// Returns true if the specified output is an OP_RETURN (null data) output. pub fn is_output_op_return(output_index: u32) -> bool { match jet::output_null_datum(output_index, 0) { - Some(_entry_option: Option) => true, + Some(_entry_option: Option) => true, None => false, } } @@ -103,7 +103,7 @@ pub fn is_output_op_return(output_index: u32) -> bool { /// Returns true if the specified output is an OP_RETURN output that has an entry at entry_index pub fn has_entry(output_index: u32, entry_index: u32) -> bool { match get_entry_option(output_index, entry_index) { - Some(_entry: Entry) => true, + Some(_entry: OpReturnEntry) => true, None => false, } } diff --git a/simf/tests/utxo/op_return.simf b/simf/tests/utxo/op_return.simf index 3f8813d..f5c9702 100644 --- a/simf/tests/utxo/op_return.simf +++ b/simf/tests/utxo/op_return.simf @@ -1,5 +1,5 @@ use crate::lib::utxo::op_return::{ - Entry, + OpReturnEntry, get_entry_option, pushed_data_from_entry, get_pushed_data, @@ -47,7 +47,7 @@ fn eq_pushed_data(a: (u2, u256), b: (u2, u256)) -> bool { } } -fn eq_entry(a: Entry, b: Entry) -> bool { +fn eq_entry(a: OpReturnEntry, b: OpReturnEntry) -> bool { match a { Left(push_a: (u2, u256)) => match b { Left(push_b: (u2, u256)) => eq_pushed_data(push_a, push_b), @@ -69,14 +69,14 @@ fn eq_entry(a: Entry, b: Entry) -> bool { } } -fn eq_entry_option(a: Option, b: Option) -> bool { +fn eq_entry_option(a: Option, b: Option) -> bool { match a { - Some(entry_a: Entry) => match b { - Some(entry_b: Entry) => eq_entry(entry_a, entry_b), + Some(entry_a: OpReturnEntry) => match b { + Some(entry_b: OpReturnEntry) => eq_entry(entry_a, entry_b), None => false, }, None => match b { - Some(_b: Entry) => false, + Some(_b: OpReturnEntry) => false, None => true, }, } @@ -130,9 +130,9 @@ fn main() { let output_index: u32 = witness::OUTPUT_INDEX; let entry_index: u32 = witness::ENTRY_INDEX; - let entry: Entry = witness::ENTRY; + let entry: OpReturnEntry = witness::ENTRY; - let expected_entry: Option = witness::EXPECTED_ENTRY; + let expected_entry: Option = witness::EXPECTED_ENTRY; let expected_bool: bool = witness::EXPECTED_BOOL; let expected_value: Option = witness::expected_value; @@ -140,7 +140,7 @@ fn main() { match is_selected(0, fn_idx) { true => { - let result_entry: Option = get_entry_option(output_index, entry_index); + let result_entry: Option = get_entry_option(output_index, entry_index); assert!(eq_entry_option(result_entry, expected_entry)); }, diff --git a/tests/stdlib/utxo/op_return.rs b/tests/stdlib/utxo/op_return.rs index ebaf2c2..db557da 100644 --- a/tests/stdlib/utxo/op_return.rs +++ b/tests/stdlib/utxo/op_return.rs @@ -28,7 +28,7 @@ enum FunctionToTest { AssertHasEntry, } -type Entry = Either<(u8, [u8; 32]), Either>; +type OpReturnEntry = Either<(u8, [u8; 32]), Either>; const DEFAULT_DATA: &[u8; 2] = &[0, 1]; const OP_PUSHNUM_NEG1: &[u8] = &[0x01, 0x4f]; @@ -75,13 +75,13 @@ impl Case { } /// `entry`:the entry passed to the arm. - fn entry(mut self, entry: Entry) -> Self { + fn entry(mut self, entry: OpReturnEntry) -> Self { self.witness.entry = entry; self } /// `expected_entry`: the entry the arm expects. - fn expected_entry(mut self, expected_entry: Option) -> Self { + fn expected_entry(mut self, expected_entry: Option) -> Self { self.witness.expected_entry = expected_entry; self } @@ -135,7 +135,7 @@ fn get_entry_option(context: simplex::TestContext) -> anyhow::Result<()> { const MIXED_ENTRIES: &[u8] = &[1, 0x02, 0x12, 0x23, 0x51, 0x4f, 0x50]; const PUSHED_DATA: &[u8] = &[0x12, 0x23]; - let cases: [(u32, Entry); 4] = [ + let cases: [(u32, OpReturnEntry); 4] = [ (0, Either::Left((0, hash_of_pushed_data(PUSHED_DATA)))), (1, Either::Right(Either::Right(0))), (2, Either::Right(Either::Left(0))), @@ -158,7 +158,7 @@ fn get_entry_option(context: simplex::TestContext) -> anyhow::Result<()> { fn get_entry_option_nonexistent_output(context: simplex::TestContext) -> anyhow::Result<()> { let output_index = u32::MAX; let entry_index = 0; - let expected_entry: Option = None; + let expected_entry: Option = None; case(GetEntryOption) .output_index(output_index) @@ -172,7 +172,7 @@ fn get_entry_option_nonexistent_output(context: simplex::TestContext) -> anyhow: fn get_entry_option_not_op_return_output(context: simplex::TestContext) -> anyhow::Result<()> { let output_index = 0; let entry_index = 0; - let expected_entry: Option = None; + let expected_entry: Option = None; case(GetEntryOption) .output_index(output_index) @@ -185,7 +185,7 @@ fn get_entry_option_not_op_return_output(context: simplex::TestContext) -> anyho fn get_entry_option_out_of_range_entry(context: simplex::TestContext) -> anyhow::Result<()> { let output_index = 0; let entry_index = u32::MAX; - let expected_entry: Option = None; + let expected_entry: Option = None; case(GetEntryOption) .output_index(output_index) @@ -200,7 +200,7 @@ fn pushed_data_from_entry(context: simplex::TestContext) -> anyhow::Result<()> { let indicator = 0_u8; let hash = hash_of_pushed_data(&DEFAULT_DATA[1..]); - let entry: Entry = Either::Left((indicator, hash)); + let entry: OpReturnEntry = Either::Left((indicator, hash)); case(PushedDataFromEntry) .entry(entry) @@ -345,7 +345,7 @@ fn get_pushed_data_neg1_returns_none(context: simplex::TestContext) -> anyhow::R #[simplex::test] fn op_x_minus_1_from_entry(context: simplex::TestContext) -> anyhow::Result<()> { for i in 0..16 { - let entry: Entry = Either::Right(Either::Right(i)); + let entry: OpReturnEntry = Either::Right(Either::Right(i)); case(OpXMinus1FromEntry) .entry(entry) @@ -359,7 +359,7 @@ fn op_x_minus_1_from_entry(context: simplex::TestContext) -> anyhow::Result<()> #[simplex::test] fn op_x_minus_1_from_entry_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { - let entry: Entry = Either::Right(Either::Left(1)); // OP_RESERVED + let entry: OpReturnEntry = Either::Right(Either::Left(1)); // OP_RESERVED case(OpXMinus1FromEntry) .entry(entry) @@ -446,7 +446,7 @@ fn get_op_x_minus_1_returns_none(context: simplex::TestContext) -> anyhow::Resul fn negate_or_reserved_from_entry_negate(context: simplex::TestContext) -> anyhow::Result<()> { let indicator = 0_u8; - let entry: Entry = Either::Right(Either::Left(indicator)); + let entry: OpReturnEntry = Either::Right(Either::Left(indicator)); case(NegateOrReservedFromEntry) .entry(entry) @@ -458,7 +458,7 @@ fn negate_or_reserved_from_entry_negate(context: simplex::TestContext) -> anyhow fn negate_or_reserved_from_entry_reserved(context: simplex::TestContext) -> anyhow::Result<()> { let indicator = 1_u8; - let entry: Entry = Either::Right(Either::Left(indicator)); + let entry: OpReturnEntry = Either::Right(Either::Left(indicator)); case(NegateOrReservedFromEntry) .entry(entry) @@ -468,7 +468,7 @@ fn negate_or_reserved_from_entry_reserved(context: simplex::TestContext) -> anyh #[simplex::test] fn negate_or_reserved_from_entry_returns_none(context: simplex::TestContext) -> anyhow::Result<()> { - let entry: Entry = Either::Left((0, hash_of_pushed_data(&[1]))); + let entry: OpReturnEntry = Either::Left((0, hash_of_pushed_data(&[1]))); case(NegateOrReservedFromEntry) .entry(entry) @@ -559,7 +559,7 @@ fn get_negate_or_reserved_returns_none(context: simplex::TestContext) -> anyhow: #[simplex::test] fn kind_of_entry_pushed_data(context: simplex::TestContext) -> anyhow::Result<()> { - let entry: Entry = Either::Left((0, hash_of_pushed_data(&[1]))); + let entry: OpReturnEntry = Either::Left((0, hash_of_pushed_data(&[1]))); case(KindOfEntry) .entry(entry) @@ -571,7 +571,7 @@ fn kind_of_entry_pushed_data(context: simplex::TestContext) -> anyhow::Result<() #[simplex::test] fn kind_of_entry_op_x_minus_1(context: simplex::TestContext) -> anyhow::Result<()> { let op_x_minus_1 = rand::thread_rng().gen_range(0..16); - let entry: Entry = Either::Right(Either::Right(op_x_minus_1)); + let entry: OpReturnEntry = Either::Right(Either::Right(op_x_minus_1)); case(KindOfEntry) .entry(entry) @@ -583,7 +583,7 @@ fn kind_of_entry_op_x_minus_1(context: simplex::TestContext) -> anyhow::Result<( #[simplex::test] fn kind_of_entry_negate_or_reserved_negate(context: simplex::TestContext) -> anyhow::Result<()> { let indicator = 0_u8; - let entry: Entry = Either::Right(Either::Left(indicator)); + let entry: OpReturnEntry = Either::Right(Either::Left(indicator)); case(KindOfEntry) .entry(entry) @@ -595,7 +595,7 @@ fn kind_of_entry_negate_or_reserved_negate(context: simplex::TestContext) -> any #[simplex::test] fn kind_of_entry_negate_or_reserved_reserved(context: simplex::TestContext) -> anyhow::Result<()> { let indicator = 1_u8; - let entry: Entry = Either::Right(Either::Left(indicator)); + let entry: OpReturnEntry = Either::Right(Either::Left(indicator)); case(KindOfEntry) .entry(entry) From 325b3aaa52203f01043936055e6bf68c350d1cd3 Mon Sep 17 00:00:00 2001 From: aritkulova Date: Thu, 5 Nov 2026 13:49:44 +0200 Subject: [PATCH 3/3] fix in according to simplex bump --- tests/stdlib/utxo/op_return.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/stdlib/utxo/op_return.rs b/tests/stdlib/utxo/op_return.rs index db557da..98e4164 100644 --- a/tests/stdlib/utxo/op_return.rs +++ b/tests/stdlib/utxo/op_return.rs @@ -1,5 +1,4 @@ use rand::Rng; -use secp256k1_zkp::hashes::Hash; use simplex::either::Either; use simplex::simplicityhl::elements::Script;