From fe6d825f8942817aed1fce6662c1338e32291dc6 Mon Sep 17 00:00:00 2001 From: Oleh Komendant Date: Thu, 12 Nov 2026 14:35:33 +0200 Subject: [PATCH] Add script hash asserts functions --- CHANGELOG.md | 1 + README.md | 6 +- WISHLIST.md | 2 +- docs/stdlib.json | 21 ++ simf/lib/utxo/asserts/script_hash.simf | 30 +++ simf/tests/utxo/asserts/script_hash.simf | 37 ++++ tests/stdlib/utxo/asserts/mod.rs | 1 + tests/stdlib/utxo/asserts/script_hash.rs | 268 +++++++++++++++++++++++ 8 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 simf/lib/utxo/asserts/script_hash.simf create mode 100644 simf/tests/utxo/asserts/script_hash.simf create mode 100644 tests/stdlib/utxo/asserts/script_hash.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd7eb5..9b54eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Extended functions for retrieving information from `OP_RETURN`. - Conversion and bit functions for `u2` and `u4`. +- Functions for asserting script hashes. ### Changed - Bumped Simplex to 0.2.0. diff --git a/README.md b/README.md index c8e12e9..79db4f9 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,10 @@ simf/lib │ │ │ └── Functions for asserting `Amount` in inputs and outputs. │ │ ├── asset_id.simf │ │ │ └── Functions for asserting `AssetId` in inputs and outputs. -│ │ └── asset_id_with_amount.simf -│ │ └── Functions for asserting `Amount` and `AssetId` in inputs and outputs. +│ │ ├── asset_id_with_amount.simf +│ │ │ └── Functions for asserting `Amount` and `AssetId` in inputs and outputs. +│ │ └── script_hash.simf +│ │ └── Functions for asserting hash of the scriptPubKey in inputs and outputs. │ ├── getters │ │ ├── amount.simf │ │ │ └── Functions for retrieving `Amount` in inputs and outputs. diff --git a/WISHLIST.md b/WISHLIST.md index 8f0db3e..62ed2ad 100644 --- a/WISHLIST.md +++ b/WISHLIST.md @@ -65,7 +65,7 @@ Q. Aren't the first three directly implemented by jets? Do we just need to `unwr Input/output by index. * Get script hash (implemented: `utxo::getters::script_hash`) -* Check that the script hash is equal to the passed value +* Check that the script hash is equal to the passed value (implemented: `utxo::asserts::script_hash`) * Check that the input script hash equals the output script hash * Same as prior function, but also check that the values are equal to the passed value diff --git a/docs/stdlib.json b/docs/stdlib.json index f72a997..2ec57e2 100644 --- a/docs/stdlib.json +++ b/docs/stdlib.json @@ -440,6 +440,27 @@ "output_type": "Confidential1", "description": "Returns the confidential input asset_id at the current_index.\n\n## Panics\nPanics if asset_id is explicit." }, + { + "simplicityhl_name": "assert_script_hash", + "section": "Script hash asserts", + "input_type": "u32, bool, u256", + "output_type": "()", + "description": "Asserts that the SHA256 hash of the scriptPubKey at the specified input or output index equals the expected `u256` value.\n\n## Panics\nThe assertion fails." + }, + { + "simplicityhl_name": "assert_input_script_hash", + "section": "Script hash asserts", + "input_type": "u32, u256", + "output_type": "()", + "description": "Asserts that the SHA256 hash of the scriptPubKey of the input at the given index equals the expected `u256` value.\n\n## Panics\nThe assertion fails." + }, + { + "simplicityhl_name": "assert_output_script_hash", + "section": "Script hash asserts", + "input_type": "u32, u256", + "output_type": "()", + "description": "Asserts that the SHA256 hash of the scriptPubKey of the output at the given index equals the expected `u256` value.\n\n## Panics\nThe assertion fails." + }, { "simplicityhl_name": "get_script_hash", "section": "Script hash getters", diff --git a/simf/lib/utxo/asserts/script_hash.simf b/simf/lib/utxo/asserts/script_hash.simf new file mode 100644 index 0000000..87c8d77 --- /dev/null +++ b/simf/lib/utxo/asserts/script_hash.simf @@ -0,0 +1,30 @@ +use crate::lib::utxo::getters::script_hash::{ + get_script_hash, + get_input_script_hash, + get_output_script_hash +}; +use crate::lib::asserts::assert_eq_256; + +/// Asserts that the SHA256 hash of the scriptPubKey at the specified input or output equals the expected u256 value +/// +/// Warning: using this incurs overhead, and should be used only when the input/output selection +/// needs to be determined at runtime; otherwise, use the input- or output-specific assert directly. +pub fn assert_script_hash(index: u32, is_input_index: bool, expected: u256) { + let script_hash: u256 = get_script_hash(index, is_input_index); + + assert_eq_256(script_hash, expected); +} + +/// Asserts that the SHA256 hash of the scriptPubKey of the input at the given index equals the expected u256 value +pub fn assert_input_script_hash(index: u32, expected: u256) { + let script_hash: u256 = get_input_script_hash(index); + + assert_eq_256(script_hash, expected); +} + +/// Asserts that the SHA256 hash of the scriptPubKey of the output at the given index equals the expected u256 value +pub fn assert_output_script_hash(index: u32, expected: u256) { + let script_hash: u256 = get_output_script_hash(index); + + assert_eq_256(script_hash, expected); +} diff --git a/simf/tests/utxo/asserts/script_hash.simf b/simf/tests/utxo/asserts/script_hash.simf new file mode 100644 index 0000000..4f10f6e --- /dev/null +++ b/simf/tests/utxo/asserts/script_hash.simf @@ -0,0 +1,37 @@ +use crate::lib::utxo::asserts::script_hash::{ + assert_script_hash, + assert_input_script_hash, + assert_output_script_hash +}; + +use crate::tests::support::dispatch::is_selected; + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + let index: u32 = witness::INDEX; + let is_input_index: bool = witness::IS_INPUT_INDEX; + + let expected_hash: u256 = witness::EXPECTED; + + match is_selected(0, fn_idx) { + true => { + assert_script_hash(index, is_input_index, expected_hash); + }, + false => (), + }; + + match is_selected(1, fn_idx) { + true => { + assert_input_script_hash(index, expected_hash); + }, + false => (), + }; + + match is_selected(2, fn_idx) { + true => { + assert_output_script_hash(index, expected_hash); + }, + false => (), + }; +} diff --git a/tests/stdlib/utxo/asserts/mod.rs b/tests/stdlib/utxo/asserts/mod.rs index 6cbabb2..c2cea35 100644 --- a/tests/stdlib/utxo/asserts/mod.rs +++ b/tests/stdlib/utxo/asserts/mod.rs @@ -1,3 +1,4 @@ mod amount; mod asset_id; mod asset_id_with_amount; +mod script_hash; diff --git a/tests/stdlib/utxo/asserts/script_hash.rs b/tests/stdlib/utxo/asserts/script_hash.rs new file mode 100644 index 0000000..d87cbdc --- /dev/null +++ b/tests/stdlib/utxo/asserts/script_hash.rs @@ -0,0 +1,268 @@ +use primitive_types::U256; +use rand::Rng; +use simplex::simplicityhl::elements::Txid; + +use crate::common::core::Expect; +use crate::common::core::run_with_inputs_outputs; +use crate::common::helper::generate_u256; +use crate::common::utxo_helper::create_utxos_and_return_script_hash; + +use simplicityhl_std::artifacts::tests::utxo::asserts::script_hash::ScriptHashProgram as TestScriptHashProgram; +use simplicityhl_std::artifacts::tests::utxo::asserts::script_hash::derived_script_hash::{ + ScriptHashArguments as TestScriptHashArguments, ScriptHashWitness as TestScriptHashWitness, +}; + +use simplicityhl_std::artifacts::tests::mock::MockProgram as TestMockProgram; +use simplicityhl_std::artifacts::tests::mock::derived_mock::MockArguments as TestMockArguments; + +use FunctionToTest::*; + +enum FunctionToTest { + Script, + InputScript, + OutputScript, +} + +#[derive(PartialEq, Eq)] +enum IndexType { + Input, + Output, +} + +fn is_input(o: IndexType) -> bool { + o == IndexType::Input +} + +fn program() -> TestScriptHashProgram { + TestScriptHashProgram::new(&TestScriptHashArguments {}) +} + +fn mock() -> TestMockProgram { + TestMockProgram::new(&TestMockArguments {}) +} + +/// One dispatch arm of the contract, plus the witness it reads. +struct Case { + witness: TestScriptHashWitness, +} + +fn case(function: FunctionToTest) -> Case { + Case { + witness: TestScriptHashWitness { + function_index: function as u8, + index: 0, + is_input_index: false, + expected: [0; 32], + }, + } +} + +impl Case { + /// `index`: the input/output the arm inspects. + fn index(mut self, index: u32) -> Self { + self.witness.index = index; + self + } + + /// `is_input`: whether the arm inspects an input. + fn index_type(mut self, index_type: IndexType) -> Self { + self.witness.is_input_index = is_input(index_type); + self + } + + /// `expected`: value the arm should assert. + fn expect(mut self, expected: [u8; 32]) -> Self { + self.witness.expected = expected; + self + } + + /// Fund, spend, and expect the spend to succeed. + fn run(self, context: &simplex::TestContext, txids: Vec) -> anyhow::Result<()> { + self.expecting(context, Expect::Ok, txids) + } + + /// Fund, spend, and expect `expect`. + fn expecting( + self, + context: &simplex::TestContext, + expect: Expect, + txids: Vec, + ) -> anyhow::Result<()> { + run_with_inputs_outputs( + context, + program(), + self.witness, + expect, + txids, + Some(mock().as_ref().clone()), + ) + } +} + +fn create_utxos_wrapper( + context: &simplex::TestContext, + index: u32, + is_input: bool, +) -> anyhow::Result<([u8; 32], Vec)> { + let last_script = &mock().as_ref().get_script_pubkey(context.get_network()); + let (hash, txids) = + create_utxos_and_return_script_hash(context, index, is_input, last_script, program())?; + + Ok((hash, txids)) +} + +#[simplex::test] +fn assert_script_hash_for_random_input(context: simplex::TestContext) -> anyhow::Result<()> { + let index = rand::thread_rng().gen_range(0..=20) as u32; // not a big value to not to slow down tests + let (expected_hash, txids) = create_utxos_wrapper(&context, index, is_input(IndexType::Input))?; + + case(Script) + .index(index) + .index_type(IndexType::Input) + .expect(expected_hash) + .run(&context, txids) +} + +#[simplex::test] +fn assert_script_hash_for_random_output(context: simplex::TestContext) -> anyhow::Result<()> { + let index = rand::thread_rng().gen_range(0..=20) as u32; // not a big value to not to slow down tests + let (expected_hash, txids) = + create_utxos_wrapper(&context, index, is_input(IndexType::Output))?; + + case(Script) + .index(index) + .index_type(IndexType::Output) + .expect(expected_hash) + .run(&context, txids) +} + +#[simplex::test] +fn assert_false_script_hash_for_input(context: simplex::TestContext) -> anyhow::Result<()> { + let index = 2; + let (_, txids) = create_utxos_wrapper(&context, index, is_input(IndexType::Input))?; + let false_hash = generate_u256(U256::zero(), U256::MAX).to_big_endian(); + + case(Script) + .index(index) + .index_type(IndexType::Input) + .expect(false_hash) + .expecting(&context, Expect::AssertFailed, txids) +} + +#[simplex::test] +fn assert_false_script_hash_for_output(context: simplex::TestContext) -> anyhow::Result<()> { + let index = 2; + let (_, txids) = create_utxos_wrapper(&context, index, is_input(IndexType::Output))?; + let false_hash = generate_u256(U256::zero(), U256::MAX).to_big_endian(); + + case(Script) + .index(index) + .index_type(IndexType::Output) + .expect(false_hash) + .expecting(&context, Expect::AssertFailed, txids) +} + +/// The output script at the same index is tweaked, so the input's hash must not match the output. +#[simplex::test] +fn assert_input_script_hash_against_output_fail( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let index = 1; + let (input_hash, txids) = create_utxos_wrapper(&context, index, is_input(IndexType::Input))?; + + case(Script) + .index(index) + .index_type(IndexType::Output) + .expect(input_hash) + .expecting(&context, Expect::AssertFailed, txids) +} + +#[simplex::test] +fn assert_script_hash_for_missing_input_fail(context: simplex::TestContext) -> anyhow::Result<()> { + let (_, txids) = create_utxos_wrapper(&context, 0, is_input(IndexType::Input))?; + + case(Script) + .index(u32::MAX) + .index_type(IndexType::Input) + .expecting(&context, Expect::PrunedBranch, txids) +} + +#[simplex::test] +fn assert_script_hash_for_missing_output_fail(context: simplex::TestContext) -> anyhow::Result<()> { + let (_, txids) = create_utxos_wrapper(&context, 0, is_input(IndexType::Output))?; + + case(Script) + .index(u32::MAX) + .index_type(IndexType::Output) + .expecting(&context, Expect::PrunedBranch, txids) +} + +#[simplex::test] +fn assert_input_script_hash(context: simplex::TestContext) -> anyhow::Result<()> { + let index = rand::thread_rng().gen_range(0..=3) as u32; + let (expected_hash, txids) = create_utxos_wrapper(&context, index, is_input(IndexType::Input))?; + + case(InputScript) + .index(index) + .expect(expected_hash) + .run(&context, txids) +} + +#[simplex::test] +fn assert_false_input_script_hash(context: simplex::TestContext) -> anyhow::Result<()> { + let index = 0; + let (_, txids) = create_utxos_wrapper(&context, index, is_input(IndexType::Input))?; + let false_hash = generate_u256(U256::zero(), U256::MAX).to_big_endian(); + + case(InputScript).index(index).expect(false_hash).expecting( + &context, + Expect::AssertFailed, + txids, + ) +} + +#[simplex::test] +fn assert_input_script_hash_missing_input_fail( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let (_, txids) = create_utxos_wrapper(&context, 0, is_input(IndexType::Input))?; + + case(InputScript) + .index(u32::MAX) + .expecting(&context, Expect::PrunedBranch, txids) +} + +#[simplex::test] +fn assert_output_script_hash(context: simplex::TestContext) -> anyhow::Result<()> { + let index = rand::thread_rng().gen_range(0..=3) as u32; + let (expected_hash, txids) = + create_utxos_wrapper(&context, index, is_input(IndexType::Output))?; + + case(OutputScript) + .index(index) + .expect(expected_hash) + .run(&context, txids) +} + +#[simplex::test] +fn assert_false_output_script_hash(context: simplex::TestContext) -> anyhow::Result<()> { + let index = 3; + let (_, txids) = create_utxos_wrapper(&context, index, is_input(IndexType::Output))?; + let false_hash = generate_u256(U256::zero(), U256::MAX).to_big_endian(); + + case(OutputScript) + .index(index) + .expect(false_hash) + .expecting(&context, Expect::AssertFailed, txids) +} + +#[simplex::test] +fn assert_output_script_hash_missing_output_fail( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let (_, txids) = create_utxos_wrapper(&context, 0, is_input(IndexType::Output))?; + + case(OutputScript) + .index(u32::MAX) + .expecting(&context, Expect::PrunedBranch, txids) +}