From d6078a45bb7529185d26801bb92b18edebd9f38e Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Tue, 7 Apr 2026 11:47:42 +0300 Subject: [PATCH 1/5] add nested sig parser --- crates/sdk/src/program/core.rs | 8 +- crates/sdk/src/signer/core.rs | 84 ++++++++++++- crates/sdk/src/signer/error.rs | 12 ++ .../sdk/src/transaction/final_transaction.rs | 4 +- crates/sdk/src/transaction/partial_input.rs | 2 +- examples/basic/Cargo.lock | 12 +- examples/basic/simf/nested_sig.simf | 26 ++++ examples/basic/tests/nested_sig.rs | 113 ++++++++++++++++++ 8 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 examples/basic/simf/nested_sig.simf create mode 100644 examples/basic/tests/nested_sig.rs diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 45c1304..3625355 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -41,6 +41,8 @@ pub trait ProgramTrait: DynClone { input_index: usize, network: &SimplicityNetwork, ) -> Result>, ProgramError>; + + fn load(&self) -> Result; } #[derive(Clone)] @@ -142,6 +144,10 @@ impl ProgramTrait for Program { self.control_block()?.serialize(), ]) } + + fn load(&self) -> Result { + self.load() + } } impl Program { @@ -173,7 +179,7 @@ impl Program { hash_script(&self.get_script_pubkey(network)) } - fn load(&self) -> Result { + pub fn load(&self) -> Result { let compiled = CompiledProgram::new(self.source, self.arguments.build_arguments(), true) .map_err(ProgramError::Compilation)?; Ok(compiled) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 045fb15..dda50e9 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; -use simplicityhl::Value; use simplicityhl::WitnessValues; use simplicityhl::elements::pset::PartiallySignedTransaction; use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr}; @@ -9,7 +8,9 @@ use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Transaction, Tx use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; use simplicityhl::simplicity::hashes::Hash; use simplicityhl::str::WitnessName; +use simplicityhl::types::{TypeInner, UIntType}; use simplicityhl::value::ValueConstructible; +use simplicityhl::{ResolvedType, Value}; use bip39::Mnemonic; use bip39::rand::thread_rng; @@ -420,11 +421,12 @@ impl Signer { if let Some(program_input) = &input_i.program_input { let signed_witness: Result = match &input_i.required_sig { // sign the program and insert the signature into the witness - RequiredSignature::Witness(witness_name) => Ok(self.get_signed_program_witness( + RequiredSignature::Witness(witness_name, sig_path) => Ok(self.get_signed_program_witness( &pst, program_input.program.as_ref(), &program_input.witness.build_witness(), witness_name, + sig_path, index, )?), // just build the passed witness @@ -455,20 +457,35 @@ impl Signer { program: &dyn ProgramTrait, witness: &WitnessValues, witness_name: &str, + sig_path: &Option, index: usize, ) -> Result { let signature = self.sign_program(pst, program, index, &self.network)?; + // put signature right after wtns field name if path is not provided + let sig_val = match sig_path { + Some(path) => { + let parsed_path = parse_sig_path(path)?; + let compiled = program.load().map_err(SignerError::Program)?; + + let abi_meta = compiled.generate_abi_meta().map_err(SignerError::ProgramGenAbiMeta)?; + + let witness_type = abi_meta + .witness_types + .get(&WitnessName::from_str_unchecked(witness_name)) + .ok_or(SignerError::WtnsFieldNotFound(witness_name.to_string()))?; + + wrap_signature_along_path(witness_type, Value::byte_array(signature.serialize()), &parsed_path)? + } + None => Value::byte_array(signature.serialize()), + }; let mut hm = HashMap::new(); witness.iter().for_each(|el| { hm.insert(el.0.clone(), el.1.clone()); }); - hm.insert( - WitnessName::from_str_unchecked(witness_name), - Value::byte_array(signature.serialize()), - ); + hm.insert(WitnessName::from_str_unchecked(witness_name), sig_val); Ok(WitnessValues::from(hm)) } @@ -524,6 +541,61 @@ impl Signer { } } +enum EitherDirection { + Left, + Right, +} + +fn parse_sig_path(path: &str) -> Result, SignerError> { + let mut res = Vec::new(); + + for dir in path.split_ascii_whitespace() { + match dir { + "Left" | "L" | "0" => res.push(EitherDirection::Left), + "Right" | "R" | "1" => res.push(EitherDirection::Right), + _ => return Err(SignerError::WtnsSigParse), + } + } + Ok(res) +} + +fn wrap_signature_along_path(ty: &ResolvedType, sig: Value, path: &[EitherDirection]) -> Result { + let mut stack = Vec::new(); + let mut current_ty = ty; + + for direction in path { + match current_ty.as_inner() { + TypeInner::Either(left_ty, right_ty) => match direction { + EitherDirection::Left => { + stack.push((EitherDirection::Left, (**right_ty).clone())); + current_ty = left_ty; + } + EitherDirection::Right => { + stack.push((EitherDirection::Right, (**left_ty).clone())); + current_ty = right_ty; + } + }, + _ => return Err(SignerError::InvalidSigPath), + } + } + + match current_ty.as_inner() { + TypeInner::Array(inner, 64) if matches!(inner.as_inner(), TypeInner::UInt(UIntType::U8)) => {} + _ => return Err(SignerError::InvalidSigPath), + } + + let mut value = sig; + + for (direction, sibling_ty) in stack.into_iter().rev() { + value = match direction { + EitherDirection::Left => Value::left(value, sibling_ty), + EitherDirection::Right => Value::right(sibling_ty, value), + }; + } + + Ok(value) +} + #[cfg(test)] mod tests { use crate::provider::EsploraProvider; diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index 7293936..face702 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -53,4 +53,16 @@ pub enum SignerError { #[error("Failed to construct a wpkh address: {0}")] WpkhAddressConstruction(#[from] elements_miniscript::Error), + + #[error("Failed to obtain program witness types: {0}")] + ProgramGenAbiMeta(String), + + #[error("Missing such witness field: {0}")] + WtnsFieldNotFound(String), + + #[error("Invalid witness signature path")] + InvalidSigPath, + + #[error("Failed to parse witness signature path")] + WtnsSigParse, } diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 83610ba..af5882a 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -38,7 +38,7 @@ impl FinalTransaction { } pub fn add_input(&mut self, partial_input: PartialInput, required_sig: RequiredSignature) { - if let RequiredSignature::Witness(_) = required_sig { + if let RequiredSignature::Witness(_, _) = required_sig { panic!("Requested signature is not NativeEcdsa or None"); } @@ -74,7 +74,7 @@ impl FinalTransaction { issuance_input: IssuanceInput, required_sig: RequiredSignature, ) -> AssetId { - if let RequiredSignature::Witness(_) = required_sig { + if let RequiredSignature::Witness(_, _) = required_sig { panic!("Requested signature is not NativeEcdsa or None"); } diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index e3122e3..a4b8fb8 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -11,7 +11,7 @@ use super::UTXO; pub enum RequiredSignature { None, NativeEcdsa, - Witness(String), + Witness(String, Option), } #[derive(Debug, Clone)] 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", diff --git a/examples/basic/simf/nested_sig.simf b/examples/basic/simf/nested_sig.simf new file mode 100644 index 0000000..90c7c5b --- /dev/null +++ b/examples/basic/simf/nested_sig.simf @@ -0,0 +1,26 @@ +fn checksig(pk: Pubkey, sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((pk, msg), sig); +} + +fn inherit_spend(inheritor_sig: Signature) { + checksig(param::PUBLIC_KEY, inheritor_sig); +} + +fn cold_spend(cold_sig: Signature) { + checksig(param::PUBLIC_KEY, cold_sig); +} + +fn hot_spend(hot_sig: Signature) { + checksig(param::PUBLIC_KEY, hot_sig); +} + +fn main() { + match witness::INHERIT_OR_NOT { + Left(inheritor_sig: Signature) => inherit_spend(inheritor_sig), + Right(cold_or_hot: Either) => match cold_or_hot { + Left(cold_sig: Signature) => cold_spend(cold_sig), + Right(hot_sig: Signature) => hot_spend(hot_sig), + }, + } +} \ No newline at end of file diff --git a/examples/basic/tests/nested_sig.rs b/examples/basic/tests/nested_sig.rs new file mode 100644 index 0000000..d33e37a --- /dev/null +++ b/examples/basic/tests/nested_sig.rs @@ -0,0 +1,113 @@ +use simplex::constants::DUMMY_SIGNATURE; +use simplex::simplicityhl::elements::{Script, Txid}; +use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature}; +use simplex::utils::tr_unspendable_key; + +use simplex_example::artifacts::nested_sig::NestedSigProgram; +use simplex_example::artifacts::nested_sig::derived_nested_sig::{NestedSigArguments, NestedSigWitness}; + +fn get_nested_sig(context: &simplex::TestContext) -> (NestedSigProgram, Script) { + let signer = context.get_default_signer(); + + let arguments = NestedSigArguments { + public_key: signer.get_schnorr_public_key().serialize(), + }; + + let program = NestedSigProgram::new(tr_unspendable_key(), arguments); + let script = program.get_program().get_script_pubkey(context.get_network()); + + (program, script) +} +fn fund_nested_sig(context: &simplex::TestContext) -> anyhow::Result { + let signer = context.get_default_signer(); + let (_, script) = get_nested_sig(context); + + let txid = signer.send(script, 50_000)?; + println!("Funded: {}", txid); + + Ok(txid) +} + +fn spend_nested_sig( + context: &simplex::TestContext, + witness: NestedSigWitness, + sig_path: Option, +) -> anyhow::Result { + let signer = context.get_default_signer(); + let provider = context.get_default_provider(); + + let (program, script) = get_nested_sig(context); + + let mut utxos = provider.fetch_scripthash_utxos(&script)?; + utxos.retain(|utxo| utxo.explicit_asset() == context.get_network().policy_asset()); + + let mut ft = FinalTransaction::new(); + + ft.add_program_input( + PartialInput::new(utxos[0].clone()), + ProgramInput::new(Box::new(program.get_program().clone()), Box::new(witness)), + RequiredSignature::Witness("INHERIT_OR_NOT".to_string(), sig_path), + ); + + let txid = signer.broadcast(&ft)?; + println!("Broadcast: {}", txid); + + Ok(txid) +} + +#[simplex::test] +fn test_inherit_spend(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let fund_tx = fund_nested_sig(&context)?; + provider.wait(&fund_tx)?; + + // Left — inheritor sig injected by signer at path L + let witness = NestedSigWitness { + inherit_or_not: simplex::either::Either::Left(DUMMY_SIGNATURE), + }; + + let spend_tx = spend_nested_sig(&context, witness, Some("Left".to_string()))?; + provider.wait(&spend_tx)?; + println!("Inherit spend confirmed"); + + Ok(()) +} + +#[simplex::test] +fn test_cold_spend(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let fund_tx = fund_nested_sig(&context)?; + provider.wait(&fund_tx)?; + + // Right Left — cold sig injected by signer at path R L + let witness = NestedSigWitness { + inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Left(DUMMY_SIGNATURE)), + }; + + let spend_tx = spend_nested_sig(&context, witness, Some("Right Left".to_string()))?; + provider.wait(&spend_tx)?; + println!("Cold spend confirmed"); + + Ok(()) +} + +#[simplex::test] +fn test_hot_spend(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let fund_tx = fund_nested_sig(&context)?; + provider.wait(&fund_tx)?; + + // Right Right — hot sig injected by signer at path R R + let witness = NestedSigWitness { + inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Right(DUMMY_SIGNATURE)), + }; + + let spend_tx = spend_nested_sig(&context, witness, Some("Right Right".to_string()))?; + provider.wait(&spend_tx)?; + println!("Hot spend confirmed"); + + Ok(()) +} From 28312750632dc7073b58849bbf5ebecbc202222c Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Thu, 9 Apr 2026 15:38:13 +0300 Subject: [PATCH 2/5] wip: add support for tuple and array paths --- crates/sdk/src/signer/core.rs | 59 +------ crates/sdk/src/signer/mod.rs | 1 + crates/sdk/src/signer/wtns_parser.rs | 225 +++++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 57 deletions(-) create mode 100644 crates/sdk/src/signer/wtns_parser.rs diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index dda50e9..65c28ac 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; +use simplicityhl::Value; use simplicityhl::WitnessValues; use simplicityhl::elements::pset::PartiallySignedTransaction; use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr}; @@ -8,9 +9,7 @@ use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Transaction, Tx use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; use simplicityhl::simplicity::hashes::Hash; use simplicityhl::str::WitnessName; -use simplicityhl::types::{TypeInner, UIntType}; use simplicityhl::value::ValueConstructible; -use simplicityhl::{ResolvedType, Value}; use bip39::Mnemonic; use bip39::rand::thread_rng; @@ -32,6 +31,7 @@ use crate::constants::MIN_FEE; use crate::program::ProgramTrait; use crate::provider::ProviderTrait; use crate::provider::SimplicityNetwork; +use crate::signer::wtns_parser::{parse_sig_path, wrap_signature_along_path}; use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO}; use super::error::SignerError; @@ -541,61 +541,6 @@ impl Signer { } } -enum EitherDirection { - Left, - Right, -} - -fn parse_sig_path(path: &str) -> Result, SignerError> { - let mut res = Vec::new(); - - for dir in path.split_ascii_whitespace() { - match dir { - "Left" | "L" | "0" => res.push(EitherDirection::Left), - "Right" | "R" | "1" => res.push(EitherDirection::Right), - _ => return Err(SignerError::WtnsSigParse), - } - } - Ok(res) -} - -fn wrap_signature_along_path(ty: &ResolvedType, sig: Value, path: &[EitherDirection]) -> Result { - let mut stack = Vec::new(); - let mut current_ty = ty; - - for direction in path { - match current_ty.as_inner() { - TypeInner::Either(left_ty, right_ty) => match direction { - EitherDirection::Left => { - stack.push((EitherDirection::Left, (**right_ty).clone())); - current_ty = left_ty; - } - EitherDirection::Right => { - stack.push((EitherDirection::Right, (**left_ty).clone())); - current_ty = right_ty; - } - }, - _ => return Err(SignerError::InvalidSigPath), - } - } - - match current_ty.as_inner() { - TypeInner::Array(inner, 64) if matches!(inner.as_inner(), TypeInner::UInt(UIntType::U8)) => {} - _ => return Err(SignerError::InvalidSigPath), - } - - let mut value = sig; - - for (direction, sibling_ty) in stack.into_iter().rev() { - value = match direction { - EitherDirection::Left => Value::left(value, sibling_ty), - EitherDirection::Right => Value::right(sibling_ty, value), - }; - } - - Ok(value) -} - #[cfg(test)] mod tests { use crate::provider::EsploraProvider; diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs index 97c19b1..27ce237 100644 --- a/crates/sdk/src/signer/mod.rs +++ b/crates/sdk/src/signer/mod.rs @@ -1,5 +1,6 @@ pub mod core; pub mod error; +mod wtns_parser; pub use core::{Signer, SignerTrait}; pub use error::SignerError; diff --git a/crates/sdk/src/signer/wtns_parser.rs b/crates/sdk/src/signer/wtns_parser.rs new file mode 100644 index 0000000..ca9b8e3 --- /dev/null +++ b/crates/sdk/src/signer/wtns_parser.rs @@ -0,0 +1,225 @@ +use std::sync::Arc; + +use simplicityhl::{ + ResolvedType, Value, + types::{TypeInner, UIntType}, + value::{ValueConstructible, ValueInner}, +}; + +use crate::signer::SignerError; + +#[derive(Clone, Copy, Debug)] +pub enum WtnsPathRoute { + Either(EitherRoute), + Tuple(EnumerableRoute), +} + +impl TryInto for WtnsPathRoute { + type Error = WtnsPathRoute; + + fn try_into(self) -> Result { + match self { + Self::Either(direction) => Ok(direction), + _ => Err(self), + } + } +} + +impl TryInto for WtnsPathRoute { + type Error = WtnsPathRoute; + + fn try_into(self) -> Result { + match self { + Self::Tuple(tuple) => Ok(tuple), + _ => Err(self), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum EitherRoute { + Left, + Right, +} + +#[derive(Clone, Copy, Debug)] +pub struct EnumerableRoute(usize); + +pub fn parse_sig_path(path: &str) -> Result, SignerError> { + let mut res = Vec::new(); + + for dir in path.split_ascii_whitespace() { + match dir { + "Left" | "L" | "0" => res.push(EitherRoute::Left), + "Right" | "R" | "1" => res.push(EitherRoute::Right), + _ => return Err(SignerError::WtnsSigParse), + } + } + Ok(res) +} + +pub enum WtnsWrappingError { + UnsupportedPathType, + TupleOutOfBounds, + RootTypeMismatch, +} + +pub fn wrap_value_along_path( + existing_witness: &Arc, + ty: &ResolvedType, + injected_val: Value, + path: &[WtnsPathRoute], +) -> Result { + enum StackItem { + Either(EitherRoute, Arc), + Array(EnumerableRoute, Arc, Arc<[Value]>), + Tuple(EnumerableRoute, Arc<[Arc]>, Arc<[Value]>), + } + + fn downcast_either(val: &Value, direction: EitherRoute) -> Arc { + match (direction, val.inner()) { + (EitherRoute::Left, ValueInner::Either(either)) => Arc::clone(either.as_ref().unwrap_left()), + (EitherRoute::Right, ValueInner::Either(either)) => Arc::clone(either.as_ref().unwrap_right()), + _ => unreachable!(), + } + } + + fn downcast_array(val: &Value) -> Arc<[Value]> { + match val.inner() { + ValueInner::Array(arr) => Arc::clone(arr), + _ => unreachable!(), + } + } + + fn downcast_tuple(val: &Value) -> Arc<[Value]> { + match val.inner() { + ValueInner::Tuple(arr) => Arc::clone(arr), + _ => unreachable!(), + } + } + + let mut stack = Vec::new(); + let mut current_val = Arc::clone(existing_witness); + let mut current_ty = ty; + + for route in path { + if !matches!( + (route, current_ty.as_inner()), + (WtnsPathRoute::Tuple(_), TypeInner::Array(_, _)) + | (WtnsPathRoute::Tuple(_), TypeInner::Tuple(_)) + | (WtnsPathRoute::Either(_), TypeInner::Either(_, _)) + ) { + return Err(WtnsWrappingError::UnsupportedPathType); + } + + match current_ty.as_inner() { + TypeInner::Either(left_ty, right_ty) => { + let direction: EitherRoute = (*route).try_into().expect("Checked in matches! above"); + match direction { + EitherRoute::Left => { + stack.push(StackItem::Either(direction, Arc::clone(right_ty))); + current_ty = left_ty; + } + EitherRoute::Right => { + stack.push(StackItem::Either(direction, Arc::clone(left_ty))); + current_ty = right_ty; + } + } + current_val = downcast_either(¤t_val, direction); + } + TypeInner::Array(ty, len) => { + let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); + + if idx.0 >= *len { + return Err(WtnsWrappingError::TupleOutOfBounds); + } + + let arr_val = downcast_array(¤t_val); + + stack.push(StackItem::Array(idx, Arc::clone(ty), Arc::clone(&arr_val))); + + current_ty = ty; + current_val = Arc::new(arr_val[idx.0].clone()); + } + TypeInner::Tuple(tuple) => { + let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); + + if idx.0 >= tuple.len() { + return Err(WtnsWrappingError::TupleOutOfBounds); + } + + let tuple_val = downcast_tuple(¤t_val); + + stack.push(StackItem::Tuple(idx, Arc::clone(tuple), Arc::clone(&tuple_val))); + + current_ty = &tuple[idx.0]; + current_val = Arc::new(tuple_val[idx.0].clone()); + } + _ => return Err(WtnsWrappingError::UnsupportedPathType), + } + } + + if injected_val.ty() != current_ty { + return Err(WtnsWrappingError::RootTypeMismatch); + } + + let mut value = injected_val; + + for item in stack.into_iter().rev() { + value = match item { + StackItem::Either(direction, sibling_ty) => match direction { + EitherRoute::Left => Value::left(value, (*sibling_ty).clone()), + EitherRoute::Right => Value::right((*sibling_ty).clone(), value), + }, + StackItem::Array(idx, elem_ty, arr) => { + let mut elements = arr.to_vec(); + elements[idx.0] = value; + Value::array(elements, (*elem_ty).clone()) + } + StackItem::Tuple(idx, _, tuple_vals) => { + let mut elements = tuple_vals.to_vec(); + elements[idx.0] = value; + Value::tuple(elements) + } + }; + } + + Ok(value) +} + +pub fn wrap_signature_along_path(ty: &ResolvedType, sig: Value, path: &[EitherRoute]) -> Result { + let mut stack = Vec::new(); + let mut current_ty = ty; + + for direction in path { + match current_ty.as_inner() { + TypeInner::Either(left_ty, right_ty) => match direction { + EitherRoute::Left => { + stack.push((EitherRoute::Left, (**right_ty).clone())); + current_ty = left_ty; + } + EitherRoute::Right => { + stack.push((EitherRoute::Right, (**left_ty).clone())); + current_ty = right_ty; + } + }, + _ => return Err(SignerError::InvalidSigPath), + } + } + + match current_ty.as_inner() { + TypeInner::Array(inner, 64) if matches!(inner.as_inner(), TypeInner::UInt(UIntType::U8)) => {} + _ => return Err(SignerError::InvalidSigPath), + } + + let mut value = sig; + + for (direction, sibling_ty) in stack.into_iter().rev() { + value = match direction { + EitherRoute::Left => Value::left(value, sibling_ty), + EitherRoute::Right => Value::right(sibling_ty, value), + }; + } + + Ok(value) +} From d44eba01e16c7709aff4e09f4c1d762a1a3d54af Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Fri, 10 Apr 2026 11:54:43 +0300 Subject: [PATCH 3/5] wip: store changes --- crates/sdk/src/signer/core.rs | 21 ++++- crates/sdk/src/signer/error.rs | 6 +- crates/sdk/src/signer/wtns_parser.rs | 86 +++++++++------------ crates/sdk/src/transaction/partial_input.rs | 2 +- examples/basic/tests/nested_sig.rs | 6 +- 5 files changed, 60 insertions(+), 61 deletions(-) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 65c28ac..7dbdbbf 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; +use std::sync::Arc; use simplicityhl::Value; use simplicityhl::WitnessValues; @@ -31,7 +32,7 @@ use crate::constants::MIN_FEE; use crate::program::ProgramTrait; use crate::provider::ProviderTrait; use crate::provider::SimplicityNetwork; -use crate::signer::wtns_parser::{parse_sig_path, wrap_signature_along_path}; +use crate::signer::wtns_parser::{parse_sig_path, wrap_value_along_path}; use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO}; use super::error::SignerError; @@ -457,7 +458,7 @@ impl Signer { program: &dyn ProgramTrait, witness: &WitnessValues, witness_name: &str, - sig_path: &Option, + sig_path: &Option>, index: usize, ) -> Result { let signature = self.sign_program(pst, program, index, &self.network)?; @@ -465,7 +466,7 @@ impl Signer { // put signature right after wtns field name if path is not provided let sig_val = match sig_path { Some(path) => { - let parsed_path = parse_sig_path(path)?; + let parsed_path = parse_sig_path(path.as_ref())?; let compiled = program.load().map_err(SignerError::Program)?; let abi_meta = compiled.generate_abi_meta().map_err(SignerError::ProgramGenAbiMeta)?; @@ -475,7 +476,19 @@ impl Signer { .get(&WitnessName::from_str_unchecked(witness_name)) .ok_or(SignerError::WtnsFieldNotFound(witness_name.to_string()))?; - wrap_signature_along_path(witness_type, Value::byte_array(signature.serialize()), &parsed_path)? + let existing_witness = Arc::new( + witness + .get(&WitnessName::from_str_unchecked(witness_name)) + .expect("checked above") + .clone(), + ); + + wrap_value_along_path( + &existing_witness, + witness_type, + Value::byte_array(signature.serialize()), + &parsed_path, + )? } None => Value::byte_array(signature.serialize()), }; diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index face702..b97d428 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -60,9 +60,9 @@ pub enum SignerError { #[error("Missing such witness field: {0}")] WtnsFieldNotFound(String), - #[error("Invalid witness signature path")] - InvalidSigPath, - #[error("Failed to parse witness signature path")] WtnsSigParse, + + #[error("Failed to inject value into witness: {0}")] + WtnsInjectError(String), } diff --git a/crates/sdk/src/signer/wtns_parser.rs b/crates/sdk/src/signer/wtns_parser.rs index ca9b8e3..75b2b6c 100644 --- a/crates/sdk/src/signer/wtns_parser.rs +++ b/crates/sdk/src/signer/wtns_parser.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use simplicityhl::{ ResolvedType, Value, - types::{TypeInner, UIntType}, + types::TypeInner, value::{ValueConstructible, ValueInner}, }; @@ -45,25 +45,48 @@ pub enum EitherRoute { #[derive(Clone, Copy, Debug)] pub struct EnumerableRoute(usize); -pub fn parse_sig_path(path: &str) -> Result, SignerError> { +pub fn parse_sig_path(path: &Vec) -> Result, SignerError> { let mut res = Vec::new(); - for dir in path.split_ascii_whitespace() { - match dir { - "Left" | "L" | "0" => res.push(EitherRoute::Left), - "Right" | "R" | "1" => res.push(EitherRoute::Right), + for route in path { + let parsed = match route.as_str() { + "Left" => WtnsPathRoute::Either(EitherRoute::Left), + "Right" => WtnsPathRoute::Either(EitherRoute::Right), + _ if route.parse::().is_ok() => { + WtnsPathRoute::Tuple(EnumerableRoute(route.parse::().unwrap())) + } _ => return Err(SignerError::WtnsSigParse), - } + }; + res.push(parsed); } Ok(res) } pub enum WtnsWrappingError { UnsupportedPathType, - TupleOutOfBounds, + TupleOutOfBounds(usize, usize), RootTypeMismatch, } +impl std::fmt::Display for WtnsWrappingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RootTypeMismatch => write!(f, "injected value's type does not match with type declared in witness"), + Self::UnsupportedPathType => write!(f, "unsupported path type; only Either, Array and Tuple are available"), + Self::TupleOutOfBounds(expected, input) => { + let msg = format!("index out of bound; length is {}, got {}", expected, input); + write!(f, "{}", msg) + } + } + } +} + +impl From for SignerError { + fn from(value: WtnsWrappingError) -> Self { + Self::WtnsInjectError(value.to_string()) + } +} + pub fn wrap_value_along_path( existing_witness: &Arc, ty: &ResolvedType, @@ -73,7 +96,7 @@ pub fn wrap_value_along_path( enum StackItem { Either(EitherRoute, Arc), Array(EnumerableRoute, Arc, Arc<[Value]>), - Tuple(EnumerableRoute, Arc<[Arc]>, Arc<[Value]>), + Tuple(EnumerableRoute, Arc<[Value]>), } fn downcast_either(val: &Value, direction: EitherRoute) -> Arc { @@ -131,7 +154,7 @@ pub fn wrap_value_along_path( let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); if idx.0 >= *len { - return Err(WtnsWrappingError::TupleOutOfBounds); + return Err(WtnsWrappingError::TupleOutOfBounds(*len, idx.0)); } let arr_val = downcast_array(¤t_val); @@ -145,12 +168,12 @@ pub fn wrap_value_along_path( let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); if idx.0 >= tuple.len() { - return Err(WtnsWrappingError::TupleOutOfBounds); + return Err(WtnsWrappingError::TupleOutOfBounds(tuple.len(), idx.0)); } let tuple_val = downcast_tuple(¤t_val); - stack.push(StackItem::Tuple(idx, Arc::clone(tuple), Arc::clone(&tuple_val))); + stack.push(StackItem::Tuple(idx, Arc::clone(&tuple_val))); current_ty = &tuple[idx.0]; current_val = Arc::new(tuple_val[idx.0].clone()); @@ -176,7 +199,7 @@ pub fn wrap_value_along_path( elements[idx.0] = value; Value::array(elements, (*elem_ty).clone()) } - StackItem::Tuple(idx, _, tuple_vals) => { + StackItem::Tuple(idx, tuple_vals) => { let mut elements = tuple_vals.to_vec(); elements[idx.0] = value; Value::tuple(elements) @@ -186,40 +209,3 @@ pub fn wrap_value_along_path( Ok(value) } - -pub fn wrap_signature_along_path(ty: &ResolvedType, sig: Value, path: &[EitherRoute]) -> Result { - let mut stack = Vec::new(); - let mut current_ty = ty; - - for direction in path { - match current_ty.as_inner() { - TypeInner::Either(left_ty, right_ty) => match direction { - EitherRoute::Left => { - stack.push((EitherRoute::Left, (**right_ty).clone())); - current_ty = left_ty; - } - EitherRoute::Right => { - stack.push((EitherRoute::Right, (**left_ty).clone())); - current_ty = right_ty; - } - }, - _ => return Err(SignerError::InvalidSigPath), - } - } - - match current_ty.as_inner() { - TypeInner::Array(inner, 64) if matches!(inner.as_inner(), TypeInner::UInt(UIntType::U8)) => {} - _ => return Err(SignerError::InvalidSigPath), - } - - let mut value = sig; - - for (direction, sibling_ty) in stack.into_iter().rev() { - value = match direction { - EitherRoute::Left => Value::left(value, sibling_ty), - EitherRoute::Right => Value::right(sibling_ty, value), - }; - } - - Ok(value) -} diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index a4b8fb8..7d23f31 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -11,7 +11,7 @@ use super::UTXO; pub enum RequiredSignature { None, NativeEcdsa, - Witness(String, Option), + Witness(String, Option>), } #[derive(Debug, Clone)] diff --git a/examples/basic/tests/nested_sig.rs b/examples/basic/tests/nested_sig.rs index d33e37a..85df7fb 100644 --- a/examples/basic/tests/nested_sig.rs +++ b/examples/basic/tests/nested_sig.rs @@ -67,7 +67,7 @@ fn test_inherit_spend(context: simplex::TestContext) -> anyhow::Result<()> { inherit_or_not: simplex::either::Either::Left(DUMMY_SIGNATURE), }; - let spend_tx = spend_nested_sig(&context, witness, Some("Left".to_string()))?; + let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Left"]))?; provider.wait(&spend_tx)?; println!("Inherit spend confirmed"); @@ -86,7 +86,7 @@ fn test_cold_spend(context: simplex::TestContext) -> anyhow::Result<()> { inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Left(DUMMY_SIGNATURE)), }; - let spend_tx = spend_nested_sig(&context, witness, Some("Right Left".to_string()))?; + let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Right", "Left"]))?; provider.wait(&spend_tx)?; println!("Cold spend confirmed"); @@ -105,7 +105,7 @@ fn test_hot_spend(context: simplex::TestContext) -> anyhow::Result<()> { inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Right(DUMMY_SIGNATURE)), }; - let spend_tx = spend_nested_sig(&context, witness, Some("Right Right".to_string()))?; + let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Right", "Right"]))?; provider.wait(&spend_tx)?; println!("Hot spend confirmed"); From 1640fbed945d5ea3d6cdea1d8fc8c4a26df4b46e Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Fri, 10 Apr 2026 19:26:02 +0300 Subject: [PATCH 4/5] fix tests and few docs --- crates/sdk/src/signer/core.rs | 2 +- crates/sdk/src/signer/wtns_parser.rs | 37 +++++++++++++++++++++------- examples/basic/simf/nested_sig.simf | 14 ++++++----- examples/basic/tests/nested_sig.rs | 17 +++++++------ 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 7dbdbbf..1e0a924 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -466,7 +466,7 @@ impl Signer { // put signature right after wtns field name if path is not provided let sig_val = match sig_path { Some(path) => { - let parsed_path = parse_sig_path(path.as_ref())?; + let parsed_path = parse_sig_path(path)?; let compiled = program.load().map_err(SignerError::Program)?; let abi_meta = compiled.generate_abi_meta().map_err(SignerError::ProgramGenAbiMeta)?; diff --git a/crates/sdk/src/signer/wtns_parser.rs b/crates/sdk/src/signer/wtns_parser.rs index 75b2b6c..19978a9 100644 --- a/crates/sdk/src/signer/wtns_parser.rs +++ b/crates/sdk/src/signer/wtns_parser.rs @@ -11,7 +11,7 @@ use crate::signer::SignerError; #[derive(Clone, Copy, Debug)] pub enum WtnsPathRoute { Either(EitherRoute), - Tuple(EnumerableRoute), + Enumerable(EnumerableRoute), } impl TryInto for WtnsPathRoute { @@ -30,7 +30,7 @@ impl TryInto for WtnsPathRoute { fn try_into(self) -> Result { match self { - Self::Tuple(tuple) => Ok(tuple), + Self::Enumerable(tuple) => Ok(tuple), _ => Err(self), } } @@ -45,6 +45,18 @@ pub enum EitherRoute { #[derive(Clone, Copy, Debug)] pub struct EnumerableRoute(usize); +/// ## Usage +/// ```rust,ignore +/// // .simf script +/// match witness::SOMETHING { +/// Left(x: u64) => ..., +/// Right([y, z]: [u64, u64]) => ... +/// } +/// // path for each variable +/// vec!["Left"] // for x +/// vec!["Right", "0"] // for y +/// vec!["Right", "1"] // for z +/// ``` pub fn parse_sig_path(path: &Vec) -> Result, SignerError> { let mut res = Vec::new(); @@ -53,7 +65,7 @@ pub fn parse_sig_path(path: &Vec) -> Result, SignerEr "Left" => WtnsPathRoute::Either(EitherRoute::Left), "Right" => WtnsPathRoute::Either(EitherRoute::Right), _ if route.parse::().is_ok() => { - WtnsPathRoute::Tuple(EnumerableRoute(route.parse::().unwrap())) + WtnsPathRoute::Enumerable(EnumerableRoute(route.parse::().unwrap())) } _ => return Err(SignerError::WtnsSigParse), }; @@ -64,7 +76,7 @@ pub fn parse_sig_path(path: &Vec) -> Result, SignerEr pub enum WtnsWrappingError { UnsupportedPathType, - TupleOutOfBounds(usize, usize), + IdxOutOfBounds(usize, usize), RootTypeMismatch, } @@ -73,7 +85,7 @@ impl std::fmt::Display for WtnsWrappingError { match self { Self::RootTypeMismatch => write!(f, "injected value's type does not match with type declared in witness"), Self::UnsupportedPathType => write!(f, "unsupported path type; only Either, Array and Tuple are available"), - Self::TupleOutOfBounds(expected, input) => { + Self::IdxOutOfBounds(expected, input) => { let msg = format!("index out of bound; length is {}, got {}", expected, input); write!(f, "{}", msg) } @@ -87,6 +99,11 @@ impl From for SignerError { } } +/// Injects `injected_val` into `existing_witness` at the position described by `path`. +/// +/// `existing_witness` and `ty` must be consistent — `ty` must be the declared +/// `ResolvedType` of `existing_witness`. The existing witness values at non-injected +/// positions are preserved during tuple and array reconstruction. pub fn wrap_value_along_path( existing_witness: &Arc, ty: &ResolvedType, @@ -99,6 +116,8 @@ pub fn wrap_value_along_path( Tuple(EnumerableRoute, Arc<[Value]>), } + // invocations of these functions below determined from types during traversal + // matches! guard at top of loop guarantees that types and routes are fn downcast_either(val: &Value, direction: EitherRoute) -> Arc { match (direction, val.inner()) { (EitherRoute::Left, ValueInner::Either(either)) => Arc::clone(either.as_ref().unwrap_left()), @@ -128,8 +147,8 @@ pub fn wrap_value_along_path( for route in path { if !matches!( (route, current_ty.as_inner()), - (WtnsPathRoute::Tuple(_), TypeInner::Array(_, _)) - | (WtnsPathRoute::Tuple(_), TypeInner::Tuple(_)) + (WtnsPathRoute::Enumerable(_), TypeInner::Array(_, _)) + | (WtnsPathRoute::Enumerable(_), TypeInner::Tuple(_)) | (WtnsPathRoute::Either(_), TypeInner::Either(_, _)) ) { return Err(WtnsWrappingError::UnsupportedPathType); @@ -154,7 +173,7 @@ pub fn wrap_value_along_path( let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); if idx.0 >= *len { - return Err(WtnsWrappingError::TupleOutOfBounds(*len, idx.0)); + return Err(WtnsWrappingError::IdxOutOfBounds(*len, idx.0)); } let arr_val = downcast_array(¤t_val); @@ -168,7 +187,7 @@ pub fn wrap_value_along_path( let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); if idx.0 >= tuple.len() { - return Err(WtnsWrappingError::TupleOutOfBounds(tuple.len(), idx.0)); + return Err(WtnsWrappingError::IdxOutOfBounds(tuple.len(), idx.0)); } let tuple_val = downcast_tuple(¤t_val); diff --git a/examples/basic/simf/nested_sig.simf b/examples/basic/simf/nested_sig.simf index 90c7c5b..e70d467 100644 --- a/examples/basic/simf/nested_sig.simf +++ b/examples/basic/simf/nested_sig.simf @@ -3,7 +3,8 @@ fn checksig(pk: Pubkey, sig: Signature) { jet::bip_0340_verify((pk, msg), sig); } -fn inherit_spend(inheritor_sig: Signature) { +fn inherit_spend(inherit_data: (Signature, u256)) { + let (inheritor_sig, nonce): (Signature, u256) = inherit_data; checksig(param::PUBLIC_KEY, inheritor_sig); } @@ -11,16 +12,17 @@ fn cold_spend(cold_sig: Signature) { checksig(param::PUBLIC_KEY, cold_sig); } -fn hot_spend(hot_sig: Signature) { - checksig(param::PUBLIC_KEY, hot_sig); +fn hot_spend(hot_sigs: [Signature; 2]) { + let [sig1, sig2]: [Signature; 2] = hot_sigs; + checksig(param::PUBLIC_KEY, sig1); } fn main() { match witness::INHERIT_OR_NOT { - Left(inheritor_sig: Signature) => inherit_spend(inheritor_sig), - Right(cold_or_hot: Either) => match cold_or_hot { + Left(inherit_data: (Signature, u256)) => inherit_spend(inherit_data), + Right(cold_or_hot: Either) => match cold_or_hot { Left(cold_sig: Signature) => cold_spend(cold_sig), - Right(hot_sig: Signature) => hot_spend(hot_sig), + Right(hot_sigs: [Signature; 2]) => hot_spend(hot_sigs), }, } } \ No newline at end of file diff --git a/examples/basic/tests/nested_sig.rs b/examples/basic/tests/nested_sig.rs index 85df7fb..24de285 100644 --- a/examples/basic/tests/nested_sig.rs +++ b/examples/basic/tests/nested_sig.rs @@ -28,10 +28,10 @@ fn fund_nested_sig(context: &simplex::TestContext) -> anyhow::Result { Ok(txid) } -fn spend_nested_sig( +fn spend_nested_sig<'a>( context: &simplex::TestContext, witness: NestedSigWitness, - sig_path: Option, + sig_path: Option>, ) -> anyhow::Result { let signer = context.get_default_signer(); let provider = context.get_default_provider(); @@ -46,7 +46,10 @@ fn spend_nested_sig( ft.add_program_input( PartialInput::new(utxos[0].clone()), ProgramInput::new(Box::new(program.get_program().clone()), Box::new(witness)), - RequiredSignature::Witness("INHERIT_OR_NOT".to_string(), sig_path), + RequiredSignature::Witness( + "INHERIT_OR_NOT".to_string(), + sig_path.map(|vec| vec.iter().map(|s| s.to_string()).collect()), + ), ); let txid = signer.broadcast(&ft)?; @@ -64,10 +67,10 @@ fn test_inherit_spend(context: simplex::TestContext) -> anyhow::Result<()> { // Left — inheritor sig injected by signer at path L let witness = NestedSigWitness { - inherit_or_not: simplex::either::Either::Left(DUMMY_SIGNATURE), + inherit_or_not: simplex::either::Either::Left((DUMMY_SIGNATURE, [0; 32])), }; - let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Left"]))?; + let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Left", "0"]))?; provider.wait(&spend_tx)?; println!("Inherit spend confirmed"); @@ -102,10 +105,10 @@ fn test_hot_spend(context: simplex::TestContext) -> anyhow::Result<()> { // Right Right — hot sig injected by signer at path R R let witness = NestedSigWitness { - inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Right(DUMMY_SIGNATURE)), + inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Right([DUMMY_SIGNATURE, [0; 64]])), }; - let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Right", "Right"]))?; + let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Right", "Right", "0"]))?; provider.wait(&spend_tx)?; println!("Hot spend confirmed"); From f374cb79affd9bc8b742c58bb04832ff0a4ae8ac Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Wed, 15 Apr 2026 09:41:53 +0300 Subject: [PATCH 5/5] refactor injecting functions into structure --- crates/sdk/src/signer/core.rs | 62 ++--- crates/sdk/src/signer/error.rs | 17 +- crates/sdk/src/signer/mod.rs | 2 +- crates/sdk/src/signer/wtns_injector.rs | 212 ++++++++++++++++ crates/sdk/src/signer/wtns_parser.rs | 230 ------------------ .../sdk/src/transaction/final_transaction.rs | 18 +- crates/sdk/src/transaction/partial_input.rs | 3 +- examples/basic/tests/nested_sig.rs | 14 +- 8 files changed, 278 insertions(+), 280 deletions(-) create mode 100644 crates/sdk/src/signer/wtns_injector.rs delete mode 100644 crates/sdk/src/signer/wtns_parser.rs diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 1e0a924..741d7df 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -32,7 +32,7 @@ use crate::constants::MIN_FEE; use crate::program::ProgramTrait; use crate::provider::ProviderTrait; use crate::provider::SimplicityNetwork; -use crate::signer::wtns_parser::{parse_sig_path, wrap_value_along_path}; +use crate::signer::wtns_injector::WtnsInjector; use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO}; use super::error::SignerError; @@ -420,9 +420,14 @@ impl Signer { for (index, input_i) in inputs.iter().enumerate() { // we need to prune the program if let Some(program_input) = &input_i.program_input { - let signed_witness: Result = match &input_i.required_sig { - // sign the program and insert the signature into the witness - RequiredSignature::Witness(witness_name, sig_path) => Ok(self.get_signed_program_witness( + let signing_info: Option<(&String, &[String])> = match &input_i.required_sig { + RequiredSignature::Witness(wnts_name) => Some((wnts_name, &[])), + RequiredSignature::WitnessWithPath(wnts_name, sig_path) => Some((wnts_name, sig_path)), + _ => None, + }; + + let signed_witness: Result = match signing_info { + Some((witness_name, sig_path)) => Ok(self.get_signed_program_witness( &pst, program_input.program.as_ref(), &program_input.witness.build_witness(), @@ -430,9 +435,9 @@ impl Signer { sig_path, index, )?), - // just build the passed witness - _ => Ok(program_input.witness.build_witness()), + None => Ok(program_input.witness.build_witness()), }; + let pruned_witness = program_input .program .finalize(&pst, &signed_witness.unwrap(), index, &self.network) @@ -458,39 +463,34 @@ impl Signer { program: &dyn ProgramTrait, witness: &WitnessValues, witness_name: &str, - sig_path: &Option>, + sig_path: &[String], index: usize, ) -> Result { let signature = self.sign_program(pst, program, index, &self.network)?; // put signature right after wtns field name if path is not provided - let sig_val = match sig_path { - Some(path) => { - let parsed_path = parse_sig_path(path)?; - let compiled = program.load().map_err(SignerError::Program)?; + let sig_val = if !sig_path.is_empty() { + let wtns_injector = WtnsInjector::new(sig_path)?; + + let compiled = program.load().map_err(SignerError::Program)?; - let abi_meta = compiled.generate_abi_meta().map_err(SignerError::ProgramGenAbiMeta)?; + let abi_meta = compiled.generate_abi_meta().map_err(SignerError::ProgramGenAbiMeta)?; - let witness_type = abi_meta - .witness_types + let witness_types = abi_meta + .witness_types + .get(&WitnessName::from_str_unchecked(witness_name)) + .ok_or(SignerError::WtnsFieldNotFound(witness_name.to_string()))?; + + let local_wtns = Arc::new( + witness .get(&WitnessName::from_str_unchecked(witness_name)) - .ok_or(SignerError::WtnsFieldNotFound(witness_name.to_string()))?; - - let existing_witness = Arc::new( - witness - .get(&WitnessName::from_str_unchecked(witness_name)) - .expect("checked above") - .clone(), - ); - - wrap_value_along_path( - &existing_witness, - witness_type, - Value::byte_array(signature.serialize()), - &parsed_path, - )? - } - None => Value::byte_array(signature.serialize()), + .expect("checked above") + .clone(), + ); + + wtns_injector.inject_value(&local_wtns, witness_types, Value::byte_array(signature.serialize()))? + } else { + Value::byte_array(signature.serialize()) }; let mut hm = HashMap::new(); diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index b97d428..d119780 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -60,9 +60,18 @@ pub enum SignerError { #[error("Missing such witness field: {0}")] WtnsFieldNotFound(String), - #[error("Failed to parse witness signature path")] - WtnsSigParse, + #[error(transparent)] + WtnsInjectError(#[from] WtnsWrappingError), +} - #[error("Failed to inject value into witness: {0}")] - WtnsInjectError(String), +#[derive(Debug, thiserror::Error)] +pub enum WtnsWrappingError { + #[error("Failed to parse path")] + ParsingError, + #[error("Unsupported path type: {0}")] + UnsupportedPathType(String), + #[error("Path index out of bounds: len is {0}, got {1}")] + IdxOutOfBounds(usize, usize), + #[error("Root type mismatch: expected {0}, got {1}")] + RootTypeMismatch(String, String), } diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs index 27ce237..9cdcfcd 100644 --- a/crates/sdk/src/signer/mod.rs +++ b/crates/sdk/src/signer/mod.rs @@ -1,6 +1,6 @@ pub mod core; pub mod error; -mod wtns_parser; +mod wtns_injector; pub use core::{Signer, SignerTrait}; pub use error::SignerError; diff --git a/crates/sdk/src/signer/wtns_injector.rs b/crates/sdk/src/signer/wtns_injector.rs new file mode 100644 index 0000000..aa619c6 --- /dev/null +++ b/crates/sdk/src/signer/wtns_injector.rs @@ -0,0 +1,212 @@ +use std::sync::Arc; + +use simplicityhl::{ + ResolvedType, Value, + types::TypeInner, + value::{ValueConstructible, ValueInner}, +}; + +use crate::signer::error::WtnsWrappingError; + +/// Struct for injecting specific value by given path into witness value +#[derive(Clone)] +pub struct WtnsInjector { + path: Vec, +} + +impl WtnsInjector { + /// ## Usage + /// ```rust,ignore + /// // .simf script + /// match witness::SOMETHING { + /// Left(x: u64) => ..., + /// Right([y, z]: [u64, u64]) => ... + /// } + /// // path for each variable + /// vec!["Left"] // for x + /// vec!["Right", "0"] // for y + /// vec!["Right", "1"] // for z + /// ``` + pub fn new(path: &[String]) -> Result { + let parsed_path = path + .iter() + .map(|route| match route.as_str() { + "Left" => Ok(WtnsPathRoute::Either(EitherRoute::Left)), + "Right" => Ok(WtnsPathRoute::Either(EitherRoute::Right)), + s => s + .parse::() + .map(|n| WtnsPathRoute::Enumerable(EnumerableRoute(n))) + .map_err(|_| WtnsWrappingError::ParsingError), + }) + .collect::, _>>()?; + + Ok(Self { path: parsed_path }) + } + + /// Constructs new value by intjecting given value into witness at the position described by `path`. + /// Consistency between `witness` and `witness_types` should be guaranteed by caller. + pub fn inject_value( + &self, + witness: &Arc, + witness_types: &ResolvedType, + value: Value, + ) -> Result { + enum StackItem { + Either(EitherRoute, Arc), + Array(EnumerableRoute, Arc, Arc<[Value]>), + Tuple(EnumerableRoute, Arc<[Value]>), + } + + // invocations of these functions below determined from types during traversal + // matches! guard at top of loop guarantees that types and routes are consistent + fn downcast_either(val: &Value, direction: EitherRoute) -> Arc { + match (direction, val.inner()) { + (EitherRoute::Left, ValueInner::Either(either)) => Arc::clone(either.as_ref().unwrap_left()), + (EitherRoute::Right, ValueInner::Either(either)) => Arc::clone(either.as_ref().unwrap_right()), + _ => unreachable!(), + } + } + + fn downcast_array(val: &Value) -> Arc<[Value]> { + match val.inner() { + ValueInner::Array(arr) => Arc::clone(arr), + _ => unreachable!(), + } + } + + fn downcast_tuple(val: &Value) -> Arc<[Value]> { + match val.inner() { + ValueInner::Tuple(arr) => Arc::clone(arr), + _ => unreachable!(), + } + } + + let mut stack = Vec::new(); + let mut current_val = Arc::clone(witness); + let mut current_ty = witness_types; + + for route in self.path.iter() { + if !matches!( + (route, current_ty.as_inner()), + (WtnsPathRoute::Enumerable(_), TypeInner::Array(_, _)) + | (WtnsPathRoute::Enumerable(_), TypeInner::Tuple(_)) + | (WtnsPathRoute::Either(_), TypeInner::Either(_, _)) + ) { + return Err(WtnsWrappingError::UnsupportedPathType(current_ty.to_string())); + } + + match current_ty.as_inner() { + TypeInner::Either(left_ty, right_ty) => { + let direction: EitherRoute = (*route).try_into().expect("Checked in matches! above"); + match direction { + EitherRoute::Left => { + stack.push(StackItem::Either(direction, Arc::clone(right_ty))); + current_ty = left_ty; + } + EitherRoute::Right => { + stack.push(StackItem::Either(direction, Arc::clone(left_ty))); + current_ty = right_ty; + } + } + current_val = downcast_either(¤t_val, direction); + } + TypeInner::Array(ty, len) => { + let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); + + if idx.0 >= *len { + return Err(WtnsWrappingError::IdxOutOfBounds(*len, idx.0)); + } + + let arr_val = downcast_array(¤t_val); + + stack.push(StackItem::Array(idx, Arc::clone(ty), Arc::clone(&arr_val))); + + current_ty = ty; + current_val = Arc::new(arr_val[idx.0].clone()); + } + TypeInner::Tuple(tuple) => { + let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); + + if idx.0 >= tuple.len() { + return Err(WtnsWrappingError::IdxOutOfBounds(tuple.len(), idx.0)); + } + + let tuple_val = downcast_tuple(¤t_val); + + stack.push(StackItem::Tuple(idx, Arc::clone(&tuple_val))); + + current_ty = &tuple[idx.0]; + current_val = Arc::new(tuple_val[idx.0].clone()); + } + _ => unreachable!("checked at the top of loop"), + } + } + + if value.ty() != current_ty { + return Err(WtnsWrappingError::RootTypeMismatch( + current_ty.to_string(), + value.ty().to_string(), + )); + } + + let mut value = value; + + for item in stack.into_iter().rev() { + value = match item { + StackItem::Either(direction, sibling_ty) => match direction { + EitherRoute::Left => Value::left(value, (*sibling_ty).clone()), + EitherRoute::Right => Value::right((*sibling_ty).clone(), value), + }, + StackItem::Array(idx, elem_ty, arr) => { + let mut elements = arr.to_vec(); + elements[idx.0] = value; + Value::array(elements, (*elem_ty).clone()) + } + StackItem::Tuple(idx, tuple_vals) => { + let mut elements = tuple_vals.to_vec(); + elements[idx.0] = value; + Value::tuple(elements) + } + }; + } + + Ok(value) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum WtnsPathRoute { + Either(EitherRoute), + Enumerable(EnumerableRoute), +} + +impl TryInto for WtnsPathRoute { + type Error = WtnsPathRoute; + + fn try_into(self) -> Result { + match self { + Self::Either(direction) => Ok(direction), + _ => Err(self), + } + } +} + +impl TryInto for WtnsPathRoute { + type Error = WtnsPathRoute; + + fn try_into(self) -> Result { + match self { + Self::Enumerable(tuple) => Ok(tuple), + _ => Err(self), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum EitherRoute { + Left, + Right, +} + +#[derive(Clone, Copy, Debug)] +pub struct EnumerableRoute(usize); diff --git a/crates/sdk/src/signer/wtns_parser.rs b/crates/sdk/src/signer/wtns_parser.rs deleted file mode 100644 index 19978a9..0000000 --- a/crates/sdk/src/signer/wtns_parser.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::sync::Arc; - -use simplicityhl::{ - ResolvedType, Value, - types::TypeInner, - value::{ValueConstructible, ValueInner}, -}; - -use crate::signer::SignerError; - -#[derive(Clone, Copy, Debug)] -pub enum WtnsPathRoute { - Either(EitherRoute), - Enumerable(EnumerableRoute), -} - -impl TryInto for WtnsPathRoute { - type Error = WtnsPathRoute; - - fn try_into(self) -> Result { - match self { - Self::Either(direction) => Ok(direction), - _ => Err(self), - } - } -} - -impl TryInto for WtnsPathRoute { - type Error = WtnsPathRoute; - - fn try_into(self) -> Result { - match self { - Self::Enumerable(tuple) => Ok(tuple), - _ => Err(self), - } - } -} - -#[derive(Clone, Copy, Debug)] -pub enum EitherRoute { - Left, - Right, -} - -#[derive(Clone, Copy, Debug)] -pub struct EnumerableRoute(usize); - -/// ## Usage -/// ```rust,ignore -/// // .simf script -/// match witness::SOMETHING { -/// Left(x: u64) => ..., -/// Right([y, z]: [u64, u64]) => ... -/// } -/// // path for each variable -/// vec!["Left"] // for x -/// vec!["Right", "0"] // for y -/// vec!["Right", "1"] // for z -/// ``` -pub fn parse_sig_path(path: &Vec) -> Result, SignerError> { - let mut res = Vec::new(); - - for route in path { - let parsed = match route.as_str() { - "Left" => WtnsPathRoute::Either(EitherRoute::Left), - "Right" => WtnsPathRoute::Either(EitherRoute::Right), - _ if route.parse::().is_ok() => { - WtnsPathRoute::Enumerable(EnumerableRoute(route.parse::().unwrap())) - } - _ => return Err(SignerError::WtnsSigParse), - }; - res.push(parsed); - } - Ok(res) -} - -pub enum WtnsWrappingError { - UnsupportedPathType, - IdxOutOfBounds(usize, usize), - RootTypeMismatch, -} - -impl std::fmt::Display for WtnsWrappingError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::RootTypeMismatch => write!(f, "injected value's type does not match with type declared in witness"), - Self::UnsupportedPathType => write!(f, "unsupported path type; only Either, Array and Tuple are available"), - Self::IdxOutOfBounds(expected, input) => { - let msg = format!("index out of bound; length is {}, got {}", expected, input); - write!(f, "{}", msg) - } - } - } -} - -impl From for SignerError { - fn from(value: WtnsWrappingError) -> Self { - Self::WtnsInjectError(value.to_string()) - } -} - -/// Injects `injected_val` into `existing_witness` at the position described by `path`. -/// -/// `existing_witness` and `ty` must be consistent — `ty` must be the declared -/// `ResolvedType` of `existing_witness`. The existing witness values at non-injected -/// positions are preserved during tuple and array reconstruction. -pub fn wrap_value_along_path( - existing_witness: &Arc, - ty: &ResolvedType, - injected_val: Value, - path: &[WtnsPathRoute], -) -> Result { - enum StackItem { - Either(EitherRoute, Arc), - Array(EnumerableRoute, Arc, Arc<[Value]>), - Tuple(EnumerableRoute, Arc<[Value]>), - } - - // invocations of these functions below determined from types during traversal - // matches! guard at top of loop guarantees that types and routes are - fn downcast_either(val: &Value, direction: EitherRoute) -> Arc { - match (direction, val.inner()) { - (EitherRoute::Left, ValueInner::Either(either)) => Arc::clone(either.as_ref().unwrap_left()), - (EitherRoute::Right, ValueInner::Either(either)) => Arc::clone(either.as_ref().unwrap_right()), - _ => unreachable!(), - } - } - - fn downcast_array(val: &Value) -> Arc<[Value]> { - match val.inner() { - ValueInner::Array(arr) => Arc::clone(arr), - _ => unreachable!(), - } - } - - fn downcast_tuple(val: &Value) -> Arc<[Value]> { - match val.inner() { - ValueInner::Tuple(arr) => Arc::clone(arr), - _ => unreachable!(), - } - } - - let mut stack = Vec::new(); - let mut current_val = Arc::clone(existing_witness); - let mut current_ty = ty; - - for route in path { - if !matches!( - (route, current_ty.as_inner()), - (WtnsPathRoute::Enumerable(_), TypeInner::Array(_, _)) - | (WtnsPathRoute::Enumerable(_), TypeInner::Tuple(_)) - | (WtnsPathRoute::Either(_), TypeInner::Either(_, _)) - ) { - return Err(WtnsWrappingError::UnsupportedPathType); - } - - match current_ty.as_inner() { - TypeInner::Either(left_ty, right_ty) => { - let direction: EitherRoute = (*route).try_into().expect("Checked in matches! above"); - match direction { - EitherRoute::Left => { - stack.push(StackItem::Either(direction, Arc::clone(right_ty))); - current_ty = left_ty; - } - EitherRoute::Right => { - stack.push(StackItem::Either(direction, Arc::clone(left_ty))); - current_ty = right_ty; - } - } - current_val = downcast_either(¤t_val, direction); - } - TypeInner::Array(ty, len) => { - let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); - - if idx.0 >= *len { - return Err(WtnsWrappingError::IdxOutOfBounds(*len, idx.0)); - } - - let arr_val = downcast_array(¤t_val); - - stack.push(StackItem::Array(idx, Arc::clone(ty), Arc::clone(&arr_val))); - - current_ty = ty; - current_val = Arc::new(arr_val[idx.0].clone()); - } - TypeInner::Tuple(tuple) => { - let idx: EnumerableRoute = (*route).try_into().expect("Checked in matches! above"); - - if idx.0 >= tuple.len() { - return Err(WtnsWrappingError::IdxOutOfBounds(tuple.len(), idx.0)); - } - - let tuple_val = downcast_tuple(¤t_val); - - stack.push(StackItem::Tuple(idx, Arc::clone(&tuple_val))); - - current_ty = &tuple[idx.0]; - current_val = Arc::new(tuple_val[idx.0].clone()); - } - _ => return Err(WtnsWrappingError::UnsupportedPathType), - } - } - - if injected_val.ty() != current_ty { - return Err(WtnsWrappingError::RootTypeMismatch); - } - - let mut value = injected_val; - - for item in stack.into_iter().rev() { - value = match item { - StackItem::Either(direction, sibling_ty) => match direction { - EitherRoute::Left => Value::left(value, (*sibling_ty).clone()), - EitherRoute::Right => Value::right((*sibling_ty).clone(), value), - }, - StackItem::Array(idx, elem_ty, arr) => { - let mut elements = arr.to_vec(); - elements[idx.0] = value; - Value::array(elements, (*elem_ty).clone()) - } - StackItem::Tuple(idx, tuple_vals) => { - let mut elements = tuple_vals.to_vec(); - elements[idx.0] = value; - Value::tuple(elements) - } - }; - } - - Ok(value) -} diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index af5882a..a21df4d 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -38,9 +38,12 @@ impl FinalTransaction { } pub fn add_input(&mut self, partial_input: PartialInput, required_sig: RequiredSignature) { - if let RequiredSignature::Witness(_, _) = required_sig { - panic!("Requested signature is not NativeEcdsa or None"); - } + match required_sig { + RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => { + panic!("Requested signature is not NativeEcdsa or None") + } + _ => {} + }; self.inputs.push(FinalInput { partial_input, @@ -74,9 +77,12 @@ impl FinalTransaction { issuance_input: IssuanceInput, required_sig: RequiredSignature, ) -> AssetId { - if let RequiredSignature::Witness(_, _) = required_sig { - panic!("Requested signature is not NativeEcdsa or None"); - } + match required_sig { + RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => { + panic!("Requested signature is not NativeEcdsa or None") + } + _ => {} + }; let asset_id = AssetId::from_entropy(asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy)); diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index 7d23f31..08538ee 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -11,7 +11,8 @@ use super::UTXO; pub enum RequiredSignature { None, NativeEcdsa, - Witness(String, Option>), + Witness(String), + WitnessWithPath(String, Vec), } #[derive(Debug, Clone)] diff --git a/examples/basic/tests/nested_sig.rs b/examples/basic/tests/nested_sig.rs index 24de285..94fc76a 100644 --- a/examples/basic/tests/nested_sig.rs +++ b/examples/basic/tests/nested_sig.rs @@ -28,10 +28,10 @@ fn fund_nested_sig(context: &simplex::TestContext) -> anyhow::Result { Ok(txid) } -fn spend_nested_sig<'a>( +fn spend_nested_sig( context: &simplex::TestContext, witness: NestedSigWitness, - sig_path: Option>, + sig_path: Vec<&str>, ) -> anyhow::Result { let signer = context.get_default_signer(); let provider = context.get_default_provider(); @@ -46,9 +46,9 @@ fn spend_nested_sig<'a>( ft.add_program_input( PartialInput::new(utxos[0].clone()), ProgramInput::new(Box::new(program.get_program().clone()), Box::new(witness)), - RequiredSignature::Witness( + RequiredSignature::WitnessWithPath( "INHERIT_OR_NOT".to_string(), - sig_path.map(|vec| vec.iter().map(|s| s.to_string()).collect()), + sig_path.iter().map(ToString::to_string).collect(), ), ); @@ -70,7 +70,7 @@ fn test_inherit_spend(context: simplex::TestContext) -> anyhow::Result<()> { inherit_or_not: simplex::either::Either::Left((DUMMY_SIGNATURE, [0; 32])), }; - let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Left", "0"]))?; + let spend_tx = spend_nested_sig(&context, witness, vec!["Left", "0"])?; provider.wait(&spend_tx)?; println!("Inherit spend confirmed"); @@ -89,7 +89,7 @@ fn test_cold_spend(context: simplex::TestContext) -> anyhow::Result<()> { inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Left(DUMMY_SIGNATURE)), }; - let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Right", "Left"]))?; + let spend_tx = spend_nested_sig(&context, witness, vec!["Right", "Left"])?; provider.wait(&spend_tx)?; println!("Cold spend confirmed"); @@ -108,7 +108,7 @@ fn test_hot_spend(context: simplex::TestContext) -> anyhow::Result<()> { inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Right([DUMMY_SIGNATURE, [0; 64]])), }; - let spend_tx = spend_nested_sig(&context, witness, Some(vec!["Right", "Right", "0"]))?; + let spend_tx = spend_nested_sig(&context, witness, vec!["Right", "Right", "0"])?; provider.wait(&spend_tx)?; println!("Hot spend confirmed");