From 26d54a5dd54581eab0dd6a8aa27e94b7e47f5949 Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Thu, 2 Apr 2026 12:33:47 +0300 Subject: [PATCH 1/3] unit tests for `extract_pst` --- .../sdk/src/transaction/final_transaction.rs | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 5150ccd..e952c62 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -218,3 +218,181 @@ impl FinalTransaction { pst } } +#[cfg(test)] +mod tests { + use bitcoin_hashes::Hash; + use simplicityhl::elements::{OutPoint, Script, TxOut, TxOutWitness, Txid, confidential}; + + use crate::transaction::UTXO; + + use super::*; + + fn dummy_asset_id(byte: u8) -> AssetId { + AssetId::from_slice(&[byte; 32]).unwrap() + } + + fn dummy_txid(byte: u8) -> Txid { + Txid::from_slice(&[byte; 32]).unwrap() + } + + fn explicit_utxo(txid_byte: u8, vout: u32, amount: u64, asset: AssetId) -> UTXO { + UTXO { + outpoint: OutPoint::new(dummy_txid(txid_byte), vout), + txout: TxOut::new_fee(amount, asset), + secrets: None, + } + } + + fn confidential_utxo(txid_byte: u8, vout: u32, asset: AssetId, value: u64) -> UTXO { + UTXO { + outpoint: OutPoint::new(dummy_txid(txid_byte), vout), + txout: TxOut { + asset: confidential::Asset::Null, + value: confidential::Value::Null, + nonce: confidential::Nonce::Null, + script_pubkey: Script::new(), + witness: TxOutWitness::default(), + }, + secrets: Some(TxOutSecrets::new( + asset, + AssetBlindingFactor::zero(), + value, + ValueBlindingFactor::zero(), + )), + } + } + // Manually construct PST and check extract_pst correctness based on it + #[test] + fn extract_pst_single_explicit_input_single_output() { + let policy = dummy_asset_id(0xAA); + + let utxo = explicit_utxo(0x01, 0, 5000, policy); + let partial_input = PartialInput::new(utxo); + let partial_output = PartialOutput::new(Script::new(), 4000, policy); + + let mut ft = FinalTransaction::new(); + ft.add_input(partial_input.clone(), RequiredSignature::None); + ft.add_output(partial_output.clone()); + + let mut expected_pst = PartiallySignedTransaction::new_v2(); + expected_pst.add_input(partial_input.to_input()); + expected_pst.add_output(partial_output.to_output()); + + let expected_secrets: HashMap = HashMap::from([( + 0, + TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()), + )]); + + let (pst, secrets) = ft.extract_pst(); + + assert_eq!(pst, expected_pst); + assert_eq!(secrets, expected_secrets); + } + + #[test] + fn extract_pst_single_confidential_input() { + let policy = dummy_asset_id(0xAA); + + let utxo = confidential_utxo(0x01, 0, policy, 3000); + let partial_input = PartialInput::new(utxo); + let partial_output = PartialOutput::new(Script::new(), 2000, policy); + + let mut ft = FinalTransaction::new(); + ft.add_input(partial_input.clone(), RequiredSignature::None); + ft.add_output(partial_output.clone()); + + let mut expected_pst = PartiallySignedTransaction::new_v2(); + expected_pst.add_input(partial_input.to_input()); + expected_pst.add_output(partial_output.to_output()); + + let expected_secrets = HashMap::from([( + 0, + TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 3000, ValueBlindingFactor::zero()), + )]); + + let (pst, secrets) = ft.extract_pst(); + + assert_eq!(pst, expected_pst); + assert_eq!(secrets, expected_secrets); + } + + #[test] + fn extract_pst_mixed_inputs_multiple_outputs() { + let policy = dummy_asset_id(0xAA); + let other = dummy_asset_id(0xBB); + + let explicit_utxo = explicit_utxo(0x01, 0, 5000, policy); + let conf_utxo = confidential_utxo(0x02, 1, other, 1000); + + let explicit_partial = PartialInput::new(explicit_utxo); + let conf_partial = PartialInput::new(conf_utxo); + + let output_a = PartialOutput::new(Script::new(), 3000, policy); + let output_b = PartialOutput::new(Script::new(), 800, other); + + let mut ft = FinalTransaction::new(); + ft.add_input(explicit_partial.clone(), RequiredSignature::None); + ft.add_input(conf_partial.clone(), RequiredSignature::None); + ft.add_output(output_a.clone()); + ft.add_output(output_b.clone()); + + let mut expected_pst = PartiallySignedTransaction::new_v2(); + expected_pst.add_input(explicit_partial.to_input()); + expected_pst.add_input(conf_partial.to_input()); + expected_pst.add_output(output_a.to_output()); + expected_pst.add_output(output_b.to_output()); + + let expected_secrets = HashMap::from([ + ( + 0, + TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()), + ), + ( + 1, + TxOutSecrets::new(other, AssetBlindingFactor::zero(), 1000, ValueBlindingFactor::zero()), + ), + ]); + + let (pst, secrets) = ft.extract_pst(); + + assert_eq!(pst, expected_pst); + assert_eq!(secrets, expected_secrets); + } + + #[test] + fn extract_pst_with_issuance_input() { + let policy = dummy_asset_id(0xAA); + let entropy = [0x42u8; 32]; + let issuance_amount = 1_000_000u64; + + let utxo = explicit_utxo(0x01, 0, 5000, policy); + let partial_input = PartialInput::new(utxo); + let issuance = IssuanceInput::new(issuance_amount, entropy); + let partial_output = PartialOutput::new(Script::new(), 4000, policy); + + let mut ft = FinalTransaction::new(); + ft.add_issuance_input(partial_input.clone(), issuance.clone(), RequiredSignature::None); + ft.add_output(partial_output.clone()); + + // build expected pst, merge partial_input and issuance manually + let mut expected_pst = PartiallySignedTransaction::new_v2(); + let mut expected_input = partial_input.to_input(); + let issuance_input = issuance.to_input(); + expected_input.issuance_value_amount = issuance_input.issuance_value_amount; + expected_input.issuance_asset_entropy = issuance_input.issuance_asset_entropy; + expected_input.issuance_inflation_keys = issuance_input.issuance_inflation_keys; + expected_input.blinded_issuance = issuance_input.blinded_issuance; + expected_pst.add_input(expected_input); + expected_pst.add_output(partial_output.to_output()); + + let expected_secrets = HashMap::from([( + 0, + TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()), + )]); + + let (pst, secrets) = ft.extract_pst(); + + assert_eq!(pst, expected_pst); + assert_eq!(secrets, expected_secrets); + } +} From 7f80eff188dfa86357854fbddd83d5f09fd4fc9a Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Fri, 3 Apr 2026 11:19:17 +0300 Subject: [PATCH 2/3] unit test for program's `get_env()` --- crates/sdk/src/program/core.rs | 98 ++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 9aea929..e2a1f94 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -205,3 +205,101 @@ impl Program { Ok(info.control_block(&script_ver).expect("control block should exist")) } } +#[cfg(test)] +mod tests { + + use simplicityhl::{ + Arguments, + elements::{AssetId, TxOutWitness, confidential, pset::Input}, + }; + + use super::*; + + // simplicityhl/examples/cat.simf + const DUMMY_PROGRAM: &str = r#" + fn main() { + let ab: u16 = <(u8, u8)>::into((0x10, 0x01)); + let c: u16 = 0x1001; + assert!(jet::eq_16(ab, c)); + let ab: u8 = <(u4, u4)>::into((0b1011, 0b1101)); + let c: u8 = 0b10111101; + assert!(jet::eq_8(ab, c)); + } + "#; + + #[derive(Clone)] + struct EmptyArguments; + + impl ArgumentsTrait for EmptyArguments { + fn build_arguments(&self) -> Arguments { + Arguments::default() + } + } + + fn dummy_pubkey(seed: u64) -> XOnlyPublicKey { + let mut rng = ::seed_from_u64(seed); + secp256k1::Keypair::new_global(&mut rng).x_only_public_key().0 + } + + fn dummy_program() -> Program { + Program::new(DUMMY_PROGRAM, dummy_pubkey(0), Box::new(EmptyArguments)) + } + + fn dummy_network() -> SimplicityNetwork { + SimplicityNetwork::default_regtest() + } + + fn make_pst_with_script(script: Script) -> PartiallySignedTransaction { + let txout = TxOut { + asset: confidential::Asset::Explicit(dummy_asset_id(0xAA)), + value: confidential::Value::Explicit(1000), + nonce: confidential::Nonce::Null, + script_pubkey: script, + witness: TxOutWitness::default(), + }; + + let input = Input { + witness_utxo: Some(txout), + ..Default::default() + }; + + let mut pst = PartiallySignedTransaction::new_v2(); + pst.add_input(input); + + pst + } + + fn dummy_asset_id(byte: u8) -> AssetId { + AssetId::from_slice(&[byte; 32]).unwrap() + } + + #[test] + fn test_get_env_idx() { + let program = dummy_program(); + let network = dummy_network(); + + let correct_script = program.get_script_pubkey(&network); + let wrong_script = Script::new(); + + let mut pst = make_pst_with_script(wrong_script); + let correct_txout = TxOut { + asset: confidential::Asset::Explicit(dummy_asset_id(0xAA)), + value: confidential::Value::Explicit(1000), + nonce: confidential::Nonce::Null, + script_pubkey: correct_script, + witness: TxOutWitness::default(), + }; + pst.add_input(Input { + witness_utxo: Some(correct_txout), + ..Default::default() + }); + + // take script with wrong pubkey + assert!(matches!( + program.get_env(&pst, 0, &network).unwrap_err(), + ProgramError::ScriptPubkeyMismatch { .. } + )); + + assert!(program.get_env(&pst, 1, &network).is_ok()); + } +} From 0d4f925f651ebb4cffd6a33fd5381d7d6b927ec5 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Wed, 8 Apr 2026 15:40:08 +0300 Subject: [PATCH 3/3] quick cleanup --- crates/sdk/src/program/core.rs | 24 +++++++++---------- .../sdk/src/transaction/final_transaction.rs | 13 ++++------ examples/basic/Cargo.lock | 12 +++++----- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 2f08f8b..3399147 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -207,12 +207,12 @@ impl Program { Ok(info.control_block(&script_ver).expect("control block should exist")) } } + #[cfg(test)] mod tests { - use simplicityhl::{ Arguments, - elements::{AssetId, TxOutWitness, confidential, pset::Input}, + elements::{AssetId, confidential, pset::Input}, }; use super::*; @@ -238,6 +238,10 @@ mod tests { } } + fn dummy_asset_id(byte: u8) -> AssetId { + AssetId::from_slice(&[byte; 32]).unwrap() + } + fn dummy_pubkey(seed: u64) -> XOnlyPublicKey { let mut rng = ::seed_from_u64(seed); secp256k1::Keypair::new_global(&mut rng).x_only_public_key().0 @@ -255,26 +259,21 @@ mod tests { let txout = TxOut { asset: confidential::Asset::Explicit(dummy_asset_id(0xAA)), value: confidential::Value::Explicit(1000), - nonce: confidential::Nonce::Null, script_pubkey: script, - witness: TxOutWitness::default(), + ..Default::default() }; - let input = Input { witness_utxo: Some(txout), ..Default::default() }; let mut pst = PartiallySignedTransaction::new_v2(); + pst.add_input(input); pst } - fn dummy_asset_id(byte: u8) -> AssetId { - AssetId::from_slice(&[byte; 32]).unwrap() - } - #[test] fn test_get_env_idx() { let program = dummy_program(); @@ -284,19 +283,20 @@ mod tests { let wrong_script = Script::new(); let mut pst = make_pst_with_script(wrong_script); + let correct_txout = TxOut { asset: confidential::Asset::Explicit(dummy_asset_id(0xAA)), value: confidential::Value::Explicit(1000), - nonce: confidential::Nonce::Null, script_pubkey: correct_script, - witness: TxOutWitness::default(), + ..Default::default() }; + pst.add_input(Input { witness_utxo: Some(correct_txout), ..Default::default() }); - // take script with wrong pubkey + // take a script with a wrong pubkey assert!(matches!( program.get_env(&pst, 0, &network).unwrap_err(), ProgramError::ScriptPubkeyMismatch { .. } diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index bef73cc..06294ab 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -239,10 +239,12 @@ impl FinalTransaction { (pst, input_secrets) } } + #[cfg(test)] mod tests { use bitcoin_hashes::Hash; - use simplicityhl::elements::{OutPoint, Script, TxOut, TxOutWitness, Txid, confidential}; + + use simplicityhl::elements::{OutPoint, Script, TxOut, Txid}; use crate::transaction::UTXO; @@ -267,13 +269,7 @@ mod tests { fn confidential_utxo(txid_byte: u8, vout: u32, asset: AssetId, value: u64) -> UTXO { UTXO { outpoint: OutPoint::new(dummy_txid(txid_byte), vout), - txout: TxOut { - asset: confidential::Asset::Null, - value: confidential::Value::Null, - nonce: confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: TxOutWitness::default(), - }, + txout: TxOut::default(), secrets: Some(TxOutSecrets::new( asset, AssetBlindingFactor::zero(), @@ -282,6 +278,7 @@ mod tests { )), } } + // Manually construct PST and check extract_pst correctness based on it #[test] fn extract_pst_single_explicit_input_single_output() { diff --git a/examples/basic/Cargo.lock b/examples/basic/Cargo.lock index 30cc944..54ff1a6 100644 --- a/examples/basic/Cargo.lock +++ b/examples/basic/Cargo.lock @@ -1209,7 +1209,7 @@ dependencies = [ [[package]] name = "smplx-build" -version = "0.0.2" +version = "0.0.3" dependencies = [ "glob", "globwalk", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "smplx-macros" -version = "0.0.2" +version = "0.0.3" dependencies = [ "smplx-build", "smplx-test", @@ -1235,7 +1235,7 @@ dependencies = [ [[package]] name = "smplx-regtest" -version = "0.0.2" +version = "0.0.3" dependencies = [ "electrsd", "serde", @@ -1246,7 +1246,7 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.2" +version = "0.0.3" dependencies = [ "bip39", "bitcoin_hashes", @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.2" +version = "0.0.3" dependencies = [ "either", "serde", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.2" +version = "0.0.3" dependencies = [ "electrsd", "proc-macro2",