From 659fd5e9a8b166aa366dd1df5c4b4b919558c799 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Thu, 26 Mar 2026 19:01:41 +0200 Subject: [PATCH 01/10] add confidential descriptors --- crates/sdk/src/signer/core.rs | 71 +++++++++++++++++++++++++++------- crates/sdk/src/signer/error.rs | 3 ++ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 9439b85..e121a37 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -1,9 +1,8 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; -use elements_miniscript::Descriptor; use elements_miniscript::bitcoin::PublicKey; -use elements_miniscript::descriptor::Wpkh; +use elements_miniscript::{ConfidentialDescriptor, Descriptor}; use simplicityhl::Value; use simplicityhl::WitnessValues; @@ -27,6 +26,7 @@ use elements_miniscript::{ }, elementssig_to_rawsig, psbt::PsbtExt, + slip77::MasterBlindingKey, }; use super::error::SignerError; @@ -58,6 +58,7 @@ pub trait SignerTrait { } pub struct Signer { + mnemonic: Mnemonic, xprv: Xpriv, provider: Box, network: SimplicityNetwork, @@ -121,6 +122,7 @@ impl Signer { let network = *provider.get_network(); Ok(Self { + mnemonic, xprv, provider, network, @@ -213,17 +215,23 @@ impl Signer { self.provider.as_ref() } - pub fn get_wpkh_address(&self) -> Result { - let fingerprint = self.fingerprint()?; - let path = self.get_derivation_path()?; - let xpub = self.derive_xpub(&path)?; + pub fn get_wpkh_confidential_address(&self) -> Result { + let mut descriptor = ConfidentialDescriptor::::from_str(&self.get_slip77_descriptor()?) + .map_err(|e| SignerError::Slip77Descriptor(e.to_string()))?; - let desc = format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)"); + // confidential descriptor doesn't support multipath + descriptor.descriptor = descriptor.descriptor.into_single_descriptors()?[0].clone(); - let descriptor: Descriptor = - Descriptor::Wpkh(Wpkh::from_str(&desc).map_err(|e| SignerError::WpkhDescriptor(e.to_string()))?); + Ok(descriptor + .at_derivation_index(1)? + .address(&self.secp, self.network.address_params())?) + } + + pub fn get_wpkh_address(&self) -> Result { + let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor()?) + .map_err(|e| SignerError::WpkhDescriptor(e.to_string()))?; - Ok(descriptor.clone().into_single_descriptors()?[0] + Ok(descriptor.into_single_descriptors()?[0] .at_derivation_index(1)? .address(self.network.address_params())?) } @@ -393,6 +401,12 @@ impl Signer { Ok(WitnessValues::from(hm)) } + fn master_slip77(&self) -> Result { + let seed = self.mnemonic.to_seed(""); + + Ok(MasterBlindingKey::from_seed(&seed[..])) + } + fn derive_xpriv(&self, path: &DerivationPath) -> Result { Ok(self.xprv.derive_priv(&self.secp, &path)?) } @@ -415,6 +429,21 @@ impl Signer { Ok(self.master_xpub()?.fingerprint()) } + fn get_slip77_descriptor(&self) -> Result { + let wpkh_descriptor = self.get_wpkh_descriptor()?; + let blinding_key = self.master_slip77()?; + + Ok(format!("ct(slip77({blinding_key}),{wpkh_descriptor})")) + } + + fn get_wpkh_descriptor(&self) -> Result { + let fingerprint = self.fingerprint()?; + let path = self.get_derivation_path()?; + let xpub = self.derive_xpub(&path)?; + + Ok(format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)")) + } + fn get_derivation_path(&self) -> Result { let coin_type = if self.network.is_mainnet() { 1776 } else { 1 }; let path = format!("84h/{coin_type}h/0h"); @@ -429,22 +458,34 @@ mod tests { use super::*; - #[test] - fn keys_correspond_to_address() { + fn create_signer() -> Signer { let url = "https://blockstream.info/liquidtestnet/api".to_string(); let network = SimplicityNetwork::LiquidTestnet; - let signer = Signer::new( + Signer::new( "exist carry drive collect lend cereal occur much tiger just involve mean", Box::new(EsploraProvider::new(url, network)), ) - .unwrap(); + .unwrap() + } + + #[test] + fn keys_correspond_to_address() { + let signer = create_signer(); let address = signer.get_wpkh_address().unwrap(); let pubkey = signer.get_ecdsa_public_key().unwrap(); - let derived_addr = Address::p2wpkh(&pubkey, None, network.address_params()); + let derived_addr = Address::p2wpkh(&pubkey, None, signer.get_provider().get_network().address_params()); assert_eq!(derived_addr.to_string(), address.to_string()); } + + #[test] + fn descriptors() { + let signer = create_signer(); + + println!("{}", signer.get_wpkh_address().unwrap()); + println!("{}", signer.get_wpkh_confidential_address().unwrap()); + } } diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index 6f91f07..b7fdf64 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -43,6 +43,9 @@ pub enum SignerError { #[error("Failed to construct a wpkh descriptor: {0}")] WpkhDescriptor(String), + #[error("Failed to construct a slip77 descriptor: {0}")] + Slip77Descriptor(String), + #[error("Failed to convert a descriptor: {0}")] DescriptorConversion(#[from] elements_miniscript::descriptor::ConversionError), From b6b82ed45c6eb2d24a6bd0e3d3ff90123f669040 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Fri, 27 Mar 2026 15:18:46 +0200 Subject: [PATCH 02/10] fix partial input interface --- crates/sdk/src/transaction/partial_input.rs | 34 +++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index ce597c5..37acbe5 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -1,6 +1,6 @@ use simplicityhl::elements::confidential::{Asset, Value}; use simplicityhl::elements::pset::Input; -use simplicityhl::elements::{AssetId, OutPoint, Sequence, TxOut, Txid}; +use simplicityhl::elements::{AssetId, LockTime, OutPoint, Sequence, TxOut, Txid}; use crate::program::ProgramTrait; use crate::program::WitnessTrait; @@ -18,6 +18,7 @@ pub struct PartialInput { pub witness_output_index: u32, pub witness_utxo: TxOut, pub sequence: Sequence, + pub locktime: LockTime, pub amount: Option, pub asset: Option, } @@ -36,10 +37,6 @@ pub struct IssuanceInput { impl PartialInput { pub fn new(utxo: (OutPoint, TxOut)) -> Self { - Self::new_sequence(utxo, Default::default()) - } - - pub fn new_sequence(utxo: (OutPoint, TxOut), sequence: Sequence) -> Self { let amount = match utxo.1.value { Value::Explicit(value) => Some(value), _ => None, @@ -53,12 +50,25 @@ impl PartialInput { witness_txid: utxo.0.txid, witness_output_index: utxo.0.vout, witness_utxo: utxo.1, - sequence, + sequence: Sequence::default(), + locktime: LockTime::ZERO, amount, asset, } } + pub fn with_sequence(mut self, sequence: Sequence) -> Self { + self.sequence = sequence; + + self + } + + pub fn with_locktime(mut self, locktime: LockTime) -> Self { + self.locktime = locktime; + + self + } + pub fn outpoint(&self) -> OutPoint { OutPoint { txid: self.witness_txid, @@ -67,11 +77,23 @@ impl PartialInput { } pub fn input(&self) -> Input { + let time_locktime = match self.locktime { + LockTime::Seconds(value) => Some(value), + _ => None, + }; + // zero height locktime is essentially ignored + let height_locktime = match self.locktime { + LockTime::Blocks(value) => Some(value), + _ => None, + }; + Input { previous_txid: self.witness_txid, previous_output_index: self.witness_output_index, witness_utxo: Some(self.witness_utxo.clone()), sequence: Some(self.sequence), + required_time_locktime: time_locktime, + required_height_locktime: height_locktime, amount: self.amount, asset: self.asset, ..Default::default() From e0819dfac85bd6e64f2049174e36886e3bf6c47e Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Fri, 27 Mar 2026 15:40:33 +0200 Subject: [PATCH 03/10] quick utxo refactoring --- crates/sdk/src/provider/core.rs | 7 +++--- crates/sdk/src/provider/esplora.rs | 15 ++++++------ crates/sdk/src/provider/simplex.rs | 8 +++---- crates/sdk/src/signer/core.rs | 26 ++++++++++----------- crates/sdk/src/transaction/mod.rs | 2 ++ crates/sdk/src/transaction/partial_input.rs | 14 ++++++----- crates/sdk/src/transaction/utxo.rs | 12 ++++++++++ crates/test/src/config.rs | 1 - 8 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 crates/sdk/src/transaction/utxo.rs diff --git a/crates/sdk/src/provider/core.rs b/crates/sdk/src/provider/core.rs index fbea454..2b76ee7 100644 --- a/crates/sdk/src/provider/core.rs +++ b/crates/sdk/src/provider/core.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; -use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; +use simplicityhl::elements::{Address, Script, Transaction, Txid}; use crate::provider::SimplicityNetwork; +use crate::transaction::UTXO; use super::error::ProviderError; @@ -22,9 +23,9 @@ pub trait ProviderTrait { fn fetch_transaction(&self, txid: &Txid) -> Result; - fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError>; + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError>; - fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError>; + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError>; fn fetch_fee_estimates(&self) -> Result, ProviderError>; diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index 62563a9..e7c2010 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -11,6 +11,7 @@ use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid use serde::Deserialize; use crate::provider::SimplicityNetwork; +use crate::transaction::UTXO; use super::core::{DEFAULT_ESPLORA_TIMEOUT_SECS, ProviderTrait}; use super::error::ProviderError; @@ -73,7 +74,7 @@ impl EsploraProvider { Ok(OutPoint::new(txid, utxo.vout)) } - fn populate_txouts_from_outpoints(&self, outpoints: &[OutPoint]) -> Result, ProviderError> { + fn populate_txouts_from_outpoints(&self, outpoints: &[OutPoint]) -> Result, ProviderError> { let set: HashSet<_> = outpoints.iter().collect(); let mut map = HashMap::new(); @@ -86,11 +87,9 @@ impl EsploraProvider { // populate TxOuts Ok(outpoints .iter() - .map(|point| { - ( - *point, - map.get(&point.txid).unwrap().output[point.vout as usize].clone(), - ) + .map(|point| UTXO { + outpoint: *point, + txout: map.get(&point.txid).unwrap().output[point.vout as usize].clone(), }) .collect()) } @@ -250,7 +249,7 @@ impl ProviderTrait for EsploraProvider { Ok(tx) } - fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { let url = format!("{}/address/{}/utxo", self.esplora_url, address); let timeout_secs = self.timeout.as_secs(); @@ -275,7 +274,7 @@ impl ProviderTrait for EsploraProvider { self.populate_txouts_from_outpoints(&outpoints) } - fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { let hash = sha256::Hash::hash(script.as_bytes()); let hash_bytes = hash.to_byte_array(); let scripthash = hex::encode(hash_bytes); diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs index d94bf8b..2189b40 100644 --- a/crates/sdk/src/provider/simplex.rs +++ b/crates/sdk/src/provider/simplex.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; use electrsd::bitcoind::bitcoincore_rpc::Auth; -use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; +use simplicityhl::elements::{Address, Script, Transaction, Txid}; use crate::provider::SimplicityNetwork; +use crate::transaction::UTXO; use super::core::ProviderTrait; use super::error::ProviderError; - use super::{ElementsRpc, EsploraProvider}; pub struct SimplexProvider { @@ -59,11 +59,11 @@ impl ProviderTrait for SimplexProvider { self.esplora.fetch_transaction(txid) } - fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { self.esplora.fetch_address_utxos(address) } - fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { self.esplora.fetch_scripthash_utxos(script) } diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index e121a37..f490731 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -29,15 +29,13 @@ use elements_miniscript::{ slip77::MasterBlindingKey, }; -use super::error::SignerError; use crate::constants::MIN_FEE; use crate::program::ProgramTrait; use crate::provider::ProviderTrait; use crate::provider::SimplicityNetwork; -use crate::transaction::FinalTransaction; -use crate::transaction::PartialInput; -use crate::transaction::PartialOutput; -use crate::transaction::RequiredSignature; +use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO}; + +use super::error::SignerError; pub const PLACEHOLDER_FEE: u64 = 1; @@ -158,8 +156,8 @@ impl Signer { }); } - signer_utxos.retain(|(outpoint, _)| !set.contains(outpoint)); - signer_utxos.sort_by(|a, b| b.1.value.cmp(&a.1.value)); + signer_utxos.retain(|utxo| !set.contains(&utxo.outpoint)); + signer_utxos.sort_by(|a, b| b.txout.value.cmp(&a.txout.value)); let mut fee_tx = tx.clone(); let mut curr_fee = MIN_FEE; @@ -236,22 +234,22 @@ impl Signer { .address(self.network.address_params())?) } - pub fn get_wpkh_utxos(&self) -> Result, SignerError> { + pub fn get_wpkh_utxos(&self) -> Result, SignerError> { self.get_wpkh_utxos_filter(|_| true) } - pub fn get_wpkh_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { - self.get_wpkh_utxos_filter(|(_, txout)| txout.asset.explicit().unwrap() == asset) + pub fn get_wpkh_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { + self.get_wpkh_utxos_filter(|utxo| utxo.txout.asset.explicit().unwrap() == asset) } // TODO: can this be optimized to not populate TxOuts that are filtered out? - pub fn get_wpkh_utxos_txid(&self, txid: Txid) -> Result, SignerError> { - self.get_wpkh_utxos_filter(|(outpoint, _)| outpoint.txid == txid) + pub fn get_wpkh_utxos_txid(&self, txid: Txid) -> Result, SignerError> { + self.get_wpkh_utxos_filter(|utxo| utxo.outpoint.txid == txid) } - pub fn get_wpkh_utxos_filter(&self, filter: F) -> Result, SignerError> + pub fn get_wpkh_utxos_filter(&self, filter: F) -> Result, SignerError> where - F: FnMut(&(OutPoint, TxOut)) -> bool, + F: FnMut(&UTXO) -> bool, { let mut utxos = self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?; diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs index 41c0ba3..2816cd8 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -2,8 +2,10 @@ pub mod error; pub mod final_transaction; pub mod partial_input; pub mod partial_output; +pub mod utxo; pub use error::TransactionError; pub use final_transaction::{FinalInput, FinalTransaction}; pub use partial_input::{PartialInput, ProgramInput, RequiredSignature}; pub use partial_output::PartialOutput; +pub use utxo::UTXO; diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index 37acbe5..5223e00 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -5,6 +5,8 @@ use simplicityhl::elements::{AssetId, LockTime, OutPoint, Sequence, TxOut, Txid} use crate::program::ProgramTrait; use crate::program::WitnessTrait; +use super::UTXO; + #[derive(Debug, Clone)] pub enum RequiredSignature { None, @@ -36,20 +38,20 @@ pub struct IssuanceInput { } impl PartialInput { - pub fn new(utxo: (OutPoint, TxOut)) -> Self { - let amount = match utxo.1.value { + pub fn new(utxo: UTXO) -> Self { + let amount = match utxo.txout.value { Value::Explicit(value) => Some(value), _ => None, }; - let asset = match utxo.1.asset { + let asset = match utxo.txout.asset { Asset::Explicit(asset) => Some(asset), _ => None, }; Self { - witness_txid: utxo.0.txid, - witness_output_index: utxo.0.vout, - witness_utxo: utxo.1, + witness_txid: utxo.outpoint.txid, + witness_output_index: utxo.outpoint.vout, + witness_utxo: utxo.txout, sequence: Sequence::default(), locktime: LockTime::ZERO, amount, diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs new file mode 100644 index 0000000..fd7b979 --- /dev/null +++ b/crates/sdk/src/transaction/utxo.rs @@ -0,0 +1,12 @@ +use simplicityhl::elements::{OutPoint, TxOut, TxOutSecrets}; + +pub struct UTXO { + pub outpoint: OutPoint, + pub txout: TxOut, +} + +pub struct CTXO { + pub outpoint: OutPoint, + pub txout: TxOut, + pub secrets: TxOutSecrets, +} diff --git a/crates/test/src/config.rs b/crates/test/src/config.rs index 929e1af..14516e6 100644 --- a/crates/test/src/config.rs +++ b/crates/test/src/config.rs @@ -43,7 +43,6 @@ impl TestConfig { file.read_to_string(&mut content)?; - // TODO: check that network name is correct Ok(toml::from_str(&content)?) } From 1c35b6ba2c154b5940cb7f260d2d07f106164406 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Fri, 27 Mar 2026 19:01:30 +0200 Subject: [PATCH 04/10] WIP confidential inputs --- crates/sdk/src/provider/esplora.rs | 3 +- crates/sdk/src/signer/core.rs | 54 +++++++++++++++---- crates/sdk/src/signer/error.rs | 3 ++ .../sdk/src/transaction/final_transaction.rs | 1 + crates/sdk/src/transaction/partial_input.rs | 3 +- crates/sdk/src/transaction/utxo.rs | 8 +-- 6 files changed, 53 insertions(+), 19 deletions(-) diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index e7c2010..886c51e 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -6,7 +6,7 @@ use std::time::Duration; use simplicityhl::elements::hashes::{Hash, sha256}; use simplicityhl::elements::encode; -use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, Txid}; use serde::Deserialize; @@ -90,6 +90,7 @@ impl EsploraProvider { .map(|point| UTXO { outpoint: *point, txout: map.get(&point.txid).unwrap().output[point.vout as usize].clone(), + secrets: None, }) .collect()) } diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index f490731..ca0b756 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -8,7 +8,7 @@ use simplicityhl::Value; use simplicityhl::WitnessValues; use simplicityhl::elements::pset::PartiallySignedTransaction; use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr}; -use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Transaction, TxOut, Txid}; +use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Transaction, Txid}; use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; use simplicityhl::simplicity::hashes::Hash; use simplicityhl::str::WitnessName; @@ -157,6 +157,7 @@ impl Signer { } signer_utxos.retain(|utxo| !set.contains(&utxo.outpoint)); + // FIXME: sorting should account confidential utxos signer_utxos.sort_by(|a, b| b.txout.value.cmp(&a.txout.value)); let mut fee_tx = tx.clone(); @@ -235,27 +236,43 @@ impl Signer { } pub fn get_wpkh_utxos(&self) -> Result, SignerError> { - self.get_wpkh_utxos_filter(|_| true) + self.get_wpkh_utxos_filter(&|_| true, &|_| true) } pub fn get_wpkh_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { - self.get_wpkh_utxos_filter(|utxo| utxo.txout.asset.explicit().unwrap() == asset) + self.get_wpkh_utxos_filter(&|utxo| utxo.txout.asset.explicit().unwrap() == asset, &|utxo| { + utxo.secrets.unwrap().asset == asset + }) } // TODO: can this be optimized to not populate TxOuts that are filtered out? pub fn get_wpkh_utxos_txid(&self, txid: Txid) -> Result, SignerError> { - self.get_wpkh_utxos_filter(|utxo| utxo.outpoint.txid == txid) + self.get_wpkh_utxos_filter(&|utxo| utxo.outpoint.txid == txid, &|utxo| utxo.outpoint.txid == txid) } - pub fn get_wpkh_utxos_filter(&self, filter: F) -> Result, SignerError> - where - F: FnMut(&UTXO) -> bool, - { - let mut utxos = self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?; + pub fn get_wpkh_utxos_filter( + &self, + explicit_filter: &dyn Fn(&UTXO) -> bool, + confidential_filter: &dyn Fn(&UTXO) -> bool, + ) -> Result, SignerError> { + // fetch explicit utxos + let mut explicit_utxos = self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?; + // fetch confidential utxos + let confidential_utxos = self + .provider + .fetch_address_utxos(&self.get_wpkh_confidential_address()?)?; + + // unblind + let mut confidential_utxos = self.unblind(confidential_utxos)?; - utxos.retain(filter); + // filter out + explicit_utxos.retain(explicit_filter); + confidential_utxos.retain(confidential_filter); - Ok(utxos) + // push unblinded utxos to explicit ones + explicit_utxos.extend(confidential_utxos); + + Ok(explicit_utxos) } pub fn get_schnorr_public_key(&self) -> Result { @@ -281,6 +298,21 @@ impl Signer { Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test)) } + fn unblind(&self, utxos: Vec) -> Result, SignerError> { + let mut unblinded: Vec = Vec::new(); + + for mut utxo in utxos { + let blinding_key = self.master_slip77()?.blinding_private_key(&utxo.txout.script_pubkey); + let secrets = utxo.txout.unblind(&self.secp, blinding_key)?; + + utxo.secrets = Some(secrets); + + unblinded.push(utxo); + } + + Ok(unblinded) + } + fn estimate_tx( &self, mut fee_tx: FinalTransaction, diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index b7fdf64..13cdb88 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -19,6 +19,9 @@ pub enum SignerError { #[error("Failed to extract tx from pst: {0}")] TxExtraction(#[from] simplicityhl::elements::pset::Error), + #[error("Failed to unblind txout: {0}")] + Unblind(#[from] simplicityhl::elements::UnblindError), + #[error("Failed to construct a message for the input spending: {0}")] SighashConstruction(#[from] elements_miniscript::psbt::SighashError), diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 5150ccd..f23ca57 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -170,6 +170,7 @@ impl FinalTransaction { self.outputs.len() } + // FIXME: confidential inputs pub fn calculate_fee_delta(&self) -> i64 { let available_amount = self .inputs diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index 5223e00..fb6659b 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -1,6 +1,6 @@ use simplicityhl::elements::confidential::{Asset, Value}; use simplicityhl::elements::pset::Input; -use simplicityhl::elements::{AssetId, LockTime, OutPoint, Sequence, TxOut, Txid}; +use simplicityhl::elements::{AssetId, LockTime, OutPoint, Sequence, TxOut, TxOutSecrets, Txid}; use crate::program::ProgramTrait; use crate::program::WitnessTrait; @@ -23,6 +23,7 @@ pub struct PartialInput { pub locktime: LockTime, pub amount: Option, pub asset: Option, + pub secrets: Option, } #[derive(Clone)] diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs index fd7b979..387bdb3 100644 --- a/crates/sdk/src/transaction/utxo.rs +++ b/crates/sdk/src/transaction/utxo.rs @@ -1,12 +1,8 @@ use simplicityhl::elements::{OutPoint, TxOut, TxOutSecrets}; +#[derive(Debug, Clone)] pub struct UTXO { pub outpoint: OutPoint, pub txout: TxOut, -} - -pub struct CTXO { - pub outpoint: OutPoint, - pub txout: TxOut, - pub secrets: TxOutSecrets, + pub secrets: Option, } From c89894df085d0245969c6b268617e9b18150db1b Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Mon, 30 Mar 2026 18:52:33 +0300 Subject: [PATCH 05/10] confidential impl --- crates/sdk/src/signer/core.rs | 55 +++++++++---- crates/sdk/src/signer/error.rs | 3 + .../sdk/src/transaction/final_transaction.rs | 80 +++++++++++++------ crates/sdk/src/transaction/partial_input.rs | 7 +- crates/sdk/src/transaction/partial_output.rs | 19 ++++- 5 files changed, 121 insertions(+), 43 deletions(-) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index ca0b756..a49ec02 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -1,9 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; -use elements_miniscript::bitcoin::PublicKey; -use elements_miniscript::{ConfidentialDescriptor, Descriptor}; - use simplicityhl::Value; use simplicityhl::WitnessValues; use simplicityhl::elements::pset::PartiallySignedTransaction; @@ -15,10 +12,11 @@ use simplicityhl::str::WitnessName; use simplicityhl::value::ValueConstructible; use bip39::Mnemonic; +use bip39::rand::thread_rng; use elements_miniscript::{ - DescriptorPublicKey, - bitcoin::{NetworkKind, PrivateKey, bip32::DerivationPath}, + ConfidentialDescriptor, Descriptor, DescriptorPublicKey, + bitcoin::{NetworkKind, PrivateKey, PublicKey, bip32::DerivationPath}, elements::{ EcdsaSighashType, bitcoin::bip32::{Fingerprint, Xpriv, Xpub}, @@ -130,7 +128,7 @@ impl Signer { // TODO: add an ability to send arbitrary assets pub fn send(&self, to: Script, amount: u64) -> Result { - let mut ft = FinalTransaction::new(self.network); + let mut ft = FinalTransaction::new(); ft.add_output(PartialOutput::new(to, amount, self.network.policy_asset())); @@ -157,15 +155,27 @@ impl Signer { } signer_utxos.retain(|utxo| !set.contains(&utxo.outpoint)); - // FIXME: sorting should account confidential utxos - signer_utxos.sort_by(|a, b| b.txout.value.cmp(&a.txout.value)); + + // descending sort of both confidential and explicit utxos + signer_utxos.sort_by(|a, b| { + let a_value = match a.secrets { + Some(secrets) => secrets.value, + None => a.txout.value.explicit().unwrap(), + }; + let b_value = match b.secrets { + Some(secrets) => secrets.value, + None => b.txout.value.explicit().unwrap(), + }; + + b_value.cmp(&a_value) + }); let mut fee_tx = tx.clone(); let mut curr_fee = MIN_FEE; let fee_rate = self.provider.fetch_fee_rate(1)?; for utxo in signer_utxos { - let policy_amount_delta = fee_tx.calculate_fee_delta(); + let policy_amount_delta = fee_tx.calculate_fee_delta(&self.network); if policy_amount_delta >= curr_fee as i64 { match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? { @@ -178,7 +188,7 @@ impl Signer { } // need to try one more time after the loop - let policy_amount_delta = fee_tx.calculate_fee_delta(); + let policy_amount_delta = fee_tx.calculate_fee_delta(&self.network); if policy_amount_delta >= curr_fee as i64 { match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? { @@ -195,7 +205,7 @@ impl Signer { tx: &FinalTransaction, target_blocks: u32, ) -> Result<(Transaction, u64), SignerError> { - let policy_amount_delta = tx.calculate_fee_delta(); + let policy_amount_delta = tx.calculate_fee_delta(&self.network); if policy_amount_delta < MIN_FEE as i64 { return Err(SignerError::DustAmount(policy_amount_delta)); @@ -286,6 +296,10 @@ impl Signer { Ok(self.get_private_key()?.public_key(&self.secp)) } + pub fn get_blinding_public_key(&self, script_pubkey: &Script) -> Result { + Ok(self.get_blinding_private_key(script_pubkey)?.public_key(&self.secp)) + } + pub fn get_private_key(&self) -> Result { let master_xprv = self.master_xpriv()?; let full_path = self.get_derivation_path()?; @@ -298,12 +312,18 @@ impl Signer { Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test)) } + pub fn get_blinding_private_key(&self, script_pubkey: &Script) -> Result { + let blinding_key = self.master_slip77()?.blinding_private_key(script_pubkey); + + Ok(PrivateKey::new(blinding_key, NetworkKind::Test)) + } + fn unblind(&self, utxos: Vec) -> Result, SignerError> { let mut unblinded: Vec = Vec::new(); for mut utxo in utxos { - let blinding_key = self.master_slip77()?.blinding_private_key(&utxo.txout.script_pubkey); - let secrets = utxo.txout.unblind(&self.secp, blinding_key)?; + let blinding_key = self.get_blinding_private_key(&utxo.txout.script_pubkey)?; + let secrets = utxo.txout.unblind(&self.secp, blinding_key.inner)?; utxo.secrets = Some(secrets); @@ -321,6 +341,7 @@ impl Signer { ) -> Result { // estimate the tx fee with the change // use this wpkh address as a change script + // TODO: should this be confidential? fee_tx.add_output(PartialOutput::new( self.get_wpkh_address()?.script_pubkey(), PLACEHOLDER_FEE, @@ -334,7 +355,7 @@ impl Signer { )); let final_tx = self.sign_tx(&fee_tx)?; - let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate); + let fee = fee_tx.calculate_fee(final_tx.discount_weight(), fee_rate); if available_delta > fee && available_delta - fee >= MIN_FEE { // we have enough funds to cover the change UTXO @@ -352,7 +373,7 @@ impl Signer { fee_tx.remove_output(fee_tx.n_outputs() - 2); let final_tx = self.sign_tx(&fee_tx)?; - let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate); + let fee = fee_tx.calculate_fee(final_tx.discount_weight(), fee_rate); if available_delta < fee { return Ok(Estimate::Failure(fee)); @@ -370,7 +391,7 @@ impl Signer { } fn sign_tx(&self, tx: &FinalTransaction) -> Result { - let mut pst = tx.extract_pst(); + let (mut pst, secrets) = tx.extract_pst(); let inputs = tx.inputs(); for (index, input_i) in inputs.iter().enumerate() { @@ -404,6 +425,8 @@ impl Signer { } } + pst.blind_last(&mut thread_rng(), &self.secp, &secrets)?; + Ok(pst.extract_tx()?) } diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index 13cdb88..c0c6c48 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -22,6 +22,9 @@ pub enum SignerError { #[error("Failed to unblind txout: {0}")] Unblind(#[from] simplicityhl::elements::UnblindError), + #[error("Failed to blind a PST: {0}")] + PsetBlind(#[from] simplicityhl::elements::pset::PsetBlindError), + #[error("Failed to construct a message for the input spending: {0}")] SighashConstruction(#[from] elements_miniscript::psbt::SighashError), diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index f23ca57..0e9f0bf 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -1,5 +1,10 @@ -use simplicityhl::elements::AssetId; +use std::collections::HashMap; + use simplicityhl::elements::pset::PartiallySignedTransaction; +use simplicityhl::elements::{ + AssetId, TxOutSecrets, + confidential::{AssetBlindingFactor, ValueBlindingFactor}, +}; use crate::provider::SimplicityNetwork; use crate::utils::asset_entropy; @@ -20,15 +25,13 @@ pub struct FinalInput { #[derive(Clone)] pub struct FinalTransaction { - pub network: SimplicityNetwork, inputs: Vec, outputs: Vec, } impl FinalTransaction { - pub fn new(network: SimplicityNetwork) -> Self { + pub fn new() -> Self { Self { - network, inputs: Vec::new(), outputs: Vec::new(), } @@ -170,18 +173,30 @@ impl FinalTransaction { self.outputs.len() } - // FIXME: confidential inputs - pub fn calculate_fee_delta(&self) -> i64 { - let available_amount = self - .inputs - .iter() - .filter(|input| input.partial_input.asset.unwrap() == self.network.policy_asset()) - .fold(0_u64, |acc, input| acc + input.partial_input.amount.unwrap()); + pub fn calculate_fee_delta(&self, network: &SimplicityNetwork) -> i64 { + let mut available_amount = 0; + + for input in &self.inputs { + match input.partial_input.secrets { + // this is an unblinded confidential input + Some(secrets) => { + if secrets.asset == network.policy_asset() { + available_amount += secrets.value; + } + } + // this is an explicit input + None => { + if input.partial_input.asset.unwrap() == network.policy_asset() { + available_amount += input.partial_input.amount.unwrap(); + } + } + } + } let consumed_amount = self .outputs .iter() - .filter(|output| output.asset == self.network.policy_asset()) + .filter(|output| output.asset == network.policy_asset()) .fold(0_u64, |acc, output| acc + output.amount); available_amount as i64 - consumed_amount as i64 @@ -193,29 +208,46 @@ impl FinalTransaction { (vsize as f32 * fee_rate / 1000.0).ceil() as u64 } - pub fn extract_pst(&self) -> PartiallySignedTransaction { + pub fn extract_pst(&self) -> (PartiallySignedTransaction, HashMap) { + let mut input_secrets = HashMap::new(); let mut pst = PartiallySignedTransaction::new_v2(); - self.inputs.iter().for_each(|el| { - let mut input = el.partial_input.input(); + for i in 0..self.inputs.len() { + let final_input = &self.inputs[i]; + let mut pst_input = final_input.partial_input.to_input(); // populate the input manually since `input.merge` is private - if el.issuance_input.is_some() { - let issue = el.issuance_input.clone().unwrap().input(); + if final_input.issuance_input.is_some() { + let issue = final_input.issuance_input.clone().unwrap().to_input(); - input.issuance_value_amount = issue.issuance_value_amount; - input.issuance_asset_entropy = issue.issuance_asset_entropy; - input.issuance_inflation_keys = issue.issuance_inflation_keys; - input.blinded_issuance = issue.blinded_issuance; + pst_input.issuance_value_amount = issue.issuance_value_amount; + pst_input.issuance_asset_entropy = issue.issuance_asset_entropy; + pst_input.issuance_inflation_keys = issue.issuance_inflation_keys; + pst_input.blinded_issuance = issue.blinded_issuance; } - pst.add_input(input); - }); + match final_input.partial_input.secrets.clone() { + // insert input secrets if present + Some(secrets) => input_secrets.insert(i, secrets), + // else populate input secrets with "explicit" amounts + None => input_secrets.insert( + i, + TxOutSecrets { + asset: pst_input.asset.unwrap(), + asset_bf: AssetBlindingFactor::zero(), + value: pst_input.amount.unwrap(), + value_bf: ValueBlindingFactor::zero(), + }, + ), + }; + + pst.add_input(pst_input); + } self.outputs.iter().for_each(|el| { pst.add_output(el.to_output()); }); - pst + (pst, input_secrets) } } diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index fb6659b..e3122e3 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -21,8 +21,10 @@ pub struct PartialInput { pub witness_utxo: TxOut, pub sequence: Sequence, pub locktime: LockTime, + // if utxo is explicit, amount and asset are Some pub amount: Option, pub asset: Option, + // if utxo is confidential, secrets are Some pub secrets: Option, } @@ -57,6 +59,7 @@ impl PartialInput { locktime: LockTime::ZERO, amount, asset, + secrets: utxo.secrets, } } @@ -79,7 +82,7 @@ impl PartialInput { } } - pub fn input(&self) -> Input { + pub fn to_input(&self) -> Input { let time_locktime = match self.locktime { LockTime::Seconds(value) => Some(value), _ => None, @@ -118,7 +121,7 @@ impl IssuanceInput { } } - pub fn input(&self) -> Input { + pub fn to_input(&self) -> Input { Input { issuance_value_amount: Some(self.issuance_amount), issuance_asset_entropy: Some(self.asset_entropy), diff --git a/crates/sdk/src/transaction/partial_output.rs b/crates/sdk/src/transaction/partial_output.rs index 31fca32..57a93a8 100644 --- a/crates/sdk/src/transaction/partial_output.rs +++ b/crates/sdk/src/transaction/partial_output.rs @@ -1,3 +1,5 @@ +use elements_miniscript::bitcoin::PublicKey; + use simplicityhl::elements::pset::Output; use simplicityhl::elements::{AssetId, Script}; @@ -6,6 +8,7 @@ pub struct PartialOutput { pub script_pubkey: Script, pub amount: u64, pub asset: AssetId, + pub blinding_key: Option, } impl PartialOutput { @@ -14,10 +17,24 @@ impl PartialOutput { script_pubkey: script, amount, asset, + blinding_key: None, } } + pub fn with_blinding_key(mut self, blinding_key: PublicKey) -> Self { + self.blinding_key = Some(blinding_key); + + self + } + pub fn to_output(&self) -> Output { - Output::new_explicit(self.script_pubkey.clone(), self.amount, self.asset, None) + let mut output = Output::new_explicit(self.script_pubkey.clone(), self.amount, self.asset, self.blinding_key); + + // the index doesn't really matter as we are the only signer + if self.blinding_key.is_some() { + output.blinder_index = Some(0); + } + + output } } From 31ef0efc7545dd4de3aa80a76a67f72b3323f063 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Mon, 30 Mar 2026 19:36:39 +0300 Subject: [PATCH 06/10] fix example --- crates/sdk/src/signer/core.rs | 31 ++++++++++++------- .../sdk/src/transaction/final_transaction.rs | 12 ++++++- examples/basic/tests/example_test.rs | 4 +-- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index a49ec02..2b3479a 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -265,24 +265,29 @@ impl Signer { explicit_filter: &dyn Fn(&UTXO) -> bool, confidential_filter: &dyn Fn(&UTXO) -> bool, ) -> Result, SignerError> { - // fetch explicit utxos - let mut explicit_utxos = self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?; - // fetch confidential utxos - let confidential_utxos = self + // fetch explicit and confidential utxos + let mut all_utxos = self .provider .fetch_address_utxos(&self.get_wpkh_confidential_address()?)?; - // unblind - let mut confidential_utxos = self.unblind(confidential_utxos)?; - - // filter out - explicit_utxos.retain(explicit_filter); + // filter out only confidential utxos and unblind them + let mut confidential_utxos = self.unblind( + all_utxos + .iter() + .filter(|utxo| utxo.txout.value.is_confidential()) + .cloned() + .collect(), + )?; + // leave only explicit utxos + all_utxos.retain(|utxo| !utxo.txout.value.is_confidential()); + + all_utxos.retain(explicit_filter); confidential_utxos.retain(confidential_filter); // push unblinded utxos to explicit ones - explicit_utxos.extend(confidential_utxos); + all_utxos.extend(confidential_utxos); - Ok(explicit_utxos) + Ok(all_utxos) } pub fn get_schnorr_public_key(&self) -> Result { @@ -425,7 +430,9 @@ impl Signer { } } - pst.blind_last(&mut thread_rng(), &self.secp, &secrets)?; + if tx.needs_blinding() { + pst.blind_last(&mut thread_rng(), &self.secp, &secrets)?; + } Ok(pst.extract_tx()?) } diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 0e9f0bf..223b2a8 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -29,6 +29,12 @@ pub struct FinalTransaction { outputs: Vec, } +impl Default for FinalTransaction { + fn default() -> Self { + Self::new() + } +} + impl FinalTransaction { pub fn new() -> Self { Self { @@ -173,6 +179,10 @@ impl FinalTransaction { self.outputs.len() } + pub fn needs_blinding(&self) -> bool { + self.outputs.iter().any(|el| el.blinding_key.is_some()) + } + pub fn calculate_fee_delta(&self, network: &SimplicityNetwork) -> i64 { let mut available_amount = 0; @@ -226,7 +236,7 @@ impl FinalTransaction { pst_input.blinded_issuance = issue.blinded_issuance; } - match final_input.partial_input.secrets.clone() { + match final_input.partial_input.secrets { // insert input secrets if present Some(secrets) => input_secrets.insert(i, secrets), // else populate input secrets with "explicit" amounts diff --git a/examples/basic/tests/example_test.rs b/examples/basic/tests/example_test.rs index 9a7ab3c..9224e63 100644 --- a/examples/basic/tests/example_test.rs +++ b/examples/basic/tests/example_test.rs @@ -40,9 +40,9 @@ fn spend_p2pk(context: &simplex::TestContext) -> Txid { let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script).unwrap(); - p2pk_utxos.retain(|el| el.1.asset.explicit().unwrap() == context.get_network().policy_asset()); + p2pk_utxos.retain(|utxo| utxo.txout.asset.explicit().unwrap() == context.get_network().policy_asset()); - let mut ft = FinalTransaction::new(*context.get_network()); + let mut ft = FinalTransaction::new(); let witness = P2pkWitness { signature: DUMMY_SIGNATURE, From 49acd2c154e75d90ec1b763594043dfd89732540 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 31 Mar 2026 20:56:11 +0300 Subject: [PATCH 07/10] fix confidential --- crates/cli/src/commands/regtest.rs | 2 +- crates/regtest/src/regtest.rs | 4 +- crates/sdk/src/lib.rs | 1 - crates/sdk/src/presets/mod.rs | 4 - crates/sdk/src/presets/p2pk.rs | 66 -- crates/sdk/src/presets/simf/p2pk.simf | 3 - crates/sdk/src/program/core.rs | 10 +- crates/sdk/src/provider/core.rs | 9 + crates/sdk/src/provider/mod.rs | 2 +- crates/sdk/src/signer/core.rs | 54 +- .../sdk/src/transaction/final_transaction.rs | 7 +- crates/test/src/context.rs | 67 +- examples/basic/README.md | 2 - .../another_module/bytes32_tr_storage.simf | 66 -- .../another_module/dual_currency_deposit.simf | 592 ------------------ .../simf/another_dir/array_tr_storage.simf | 81 --- examples/basic/simf/module/option_offer.simf | 213 ------- examples/basic/simf/options.simf | 395 ------------ .../tests/{example_test.rs => basic_test.rs} | 14 +- examples/basic/tests/confidential_test.rs | 36 ++ 20 files changed, 142 insertions(+), 1486 deletions(-) delete mode 100644 crates/sdk/src/presets/mod.rs delete mode 100644 crates/sdk/src/presets/p2pk.rs delete mode 100644 crates/sdk/src/presets/simf/p2pk.simf delete mode 100644 examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf delete mode 100644 examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf delete mode 100644 examples/basic/simf/another_dir/array_tr_storage.simf delete mode 100644 examples/basic/simf/module/option_offer.simf delete mode 100644 examples/basic/simf/options.simf rename examples/basic/tests/{example_test.rs => basic_test.rs} (86%) create mode 100644 examples/basic/tests/confidential_test.rs diff --git a/crates/cli/src/commands/regtest.rs b/crates/cli/src/commands/regtest.rs index 4c44eea..83a355e 100644 --- a/crates/cli/src/commands/regtest.rs +++ b/crates/cli/src/commands/regtest.rs @@ -29,7 +29,7 @@ impl Regtest { println!("Esplora: {}", client.esplora_url()); println!("User: {:?}, Password: {:?}", auth.0.unwrap(), auth.1.unwrap()); println!(); - println!("Signer: {:?}", signer.get_wpkh_address()?); + println!("Signer: {:?}", signer.get_address()?); println!("======================================"); while running.load(Ordering::SeqCst) {} diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index c99386a..1c8ccb0 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -38,13 +38,13 @@ impl Regtest { rpc_provider.sweep_initialfreecoins()?; rpc_provider.generate_blocks(100)?; - rpc_provider.send_to_address(&signer.get_wpkh_address()?, btc2sat(bitcoins), None)?; + rpc_provider.send_to_address(&signer.get_address()?, btc2sat(bitcoins), None)?; // wait for electrs to index let mut attempts = 0; loop { - if !(signer.get_wpkh_utxos()?).is_empty() { + if !(signer.get_utxos()?).is_empty() { break; } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 5dc4e44..0849165 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,5 +1,4 @@ pub mod constants; -pub mod presets; pub mod program; pub mod provider; pub mod signer; diff --git a/crates/sdk/src/presets/mod.rs b/crates/sdk/src/presets/mod.rs deleted file mode 100644 index 90c5352..0000000 --- a/crates/sdk/src/presets/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod p2pk; - -pub use p2pk::P2PK; -pub use p2pk::p2pk_build::{P2PKArguments, P2PKWitness}; diff --git a/crates/sdk/src/presets/p2pk.rs b/crates/sdk/src/presets/p2pk.rs deleted file mode 100644 index 7f70a04..0000000 --- a/crates/sdk/src/presets/p2pk.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::program::ArgumentsTrait; -use crate::program::Program; - -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; - -// TODO macro -pub struct P2PK { - program: Program, -} - -impl P2PK { - pub const SOURCE: &'static str = include_str!("./simf/p2pk.simf"); - - pub fn new(public_key: XOnlyPublicKey, arguments: impl ArgumentsTrait + 'static) -> Self { - Self { - program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), - } - } - - pub fn get_program(&self) -> &Program { - &self.program - } - - pub fn get_program_mut(&mut self) -> &mut Program { - &mut self.program - } -} - -pub mod p2pk_build { - use crate::program::ArgumentsTrait; - use crate::program::WitnessTrait; - use simplicityhl::num::U256; - use simplicityhl::str::WitnessName; - use simplicityhl::value::UIntValue; - use simplicityhl::value::ValueConstructible; - use simplicityhl::{Arguments, Value, WitnessValues}; - use std::collections::HashMap; - - #[derive(Clone)] - pub struct P2PKWitness { - pub signature: [u8; 64usize], - } - - #[derive(Clone)] - pub struct P2PKArguments { - pub public_key: [u8; 32], - } - - impl WitnessTrait for P2PKWitness { - fn build_witness(&self) -> WitnessValues { - WitnessValues::from(HashMap::from([( - WitnessName::from_str_unchecked("SIGNATURE"), - Value::byte_array(self.signature), - )])) - } - } - - impl ArgumentsTrait for P2PKArguments { - fn build_arguments(&self) -> Arguments { - Arguments::from(HashMap::from([( - WitnessName::from_str_unchecked("PUBLIC_KEY"), - Value::from(UIntValue::U256(U256::from_byte_array(self.public_key))), - )])) - } - } -} diff --git a/crates/sdk/src/presets/simf/p2pk.simf b/crates/sdk/src/presets/simf/p2pk.simf deleted file mode 100644 index f6a75e6..0000000 --- a/crates/sdk/src/presets/simf/p2pk.simf +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) -} diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 9aea929..a45a622 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -5,11 +5,11 @@ use dyn_clone::DynClone; use simplicityhl::CompiledProgram; use simplicityhl::WitnessValues; use simplicityhl::elements::pset::PartiallySignedTransaction; -use simplicityhl::elements::{Address, Script, Transaction, TxOut, script, taproot}; +use simplicityhl::elements::{Address, Script, Transaction, TxOut, taproot}; use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; use simplicityhl::simplicity::jet::Elements; use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; -use simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; +use simplicityhl::simplicity::{BitMachine, RedeemNode, Value, leaf_version}; use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; use super::arguments::ArgumentsTrait; @@ -110,6 +110,7 @@ impl ProgramTrait for Program { .satisfy(witness.clone()) .map_err(ProgramError::WitnessSatisfaction)?; + // TODO: global config for TrackerLogLevel let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(TrackerLogLevel::Debug); let env = self.get_env(pst, input_index, network)?; @@ -180,11 +181,12 @@ impl Program { fn script_version(&self) -> Result<(Script, taproot::LeafVersion), ProgramError> { let cmr = self.load()?.commit().cmr(); - let script = script::Script::from(cmr.as_ref().to_vec()); + let script = Script::from(cmr.as_ref().to_vec()); - Ok((script, simplicityhl::simplicity::leaf_version())) + Ok((script, leaf_version())) } + // TODO: taproot storage fn taproot_spending_info(&self) -> Result { let builder = taproot::TaprootBuilder::new(); let (script, version) = self.script_version()?; diff --git a/crates/sdk/src/provider/core.rs b/crates/sdk/src/provider/core.rs index 2b76ee7..a5c76da 100644 --- a/crates/sdk/src/provider/core.rs +++ b/crates/sdk/src/provider/core.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use electrsd::bitcoind::bitcoincore_rpc::Auth; + use simplicityhl::elements::{Address, Script, Transaction, Txid}; use crate::provider::SimplicityNetwork; @@ -10,6 +12,13 @@ use super::error::ProviderError; pub const DEFAULT_FEE_RATE: f32 = 100.0; pub const DEFAULT_ESPLORA_TIMEOUT_SECS: u64 = 10; +#[derive(Debug, Clone)] +pub struct ProviderInfo { + pub esplora_url: String, + pub elements_url: Option, + pub auth: Option, +} + pub trait ProviderTrait { fn get_network(&self) -> &SimplicityNetwork; diff --git a/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs index 9cae962..112aa05 100644 --- a/crates/sdk/src/provider/mod.rs +++ b/crates/sdk/src/provider/mod.rs @@ -5,7 +5,7 @@ pub mod network; pub mod rpc; pub mod simplex; -pub use core::ProviderTrait; +pub use core::{ProviderTrait, ProviderInfo}; pub use esplora::EsploraProvider; pub use rpc::elements::ElementsRpc; pub use simplex::SimplexProvider; diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 2b3479a..26657c9 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -144,7 +144,7 @@ impl Signer { } pub fn finalize(&self, tx: &FinalTransaction) -> Result<(Transaction, u64), SignerError> { - let mut signer_utxos = self.get_wpkh_utxos_asset(self.network.policy_asset())?; + let mut signer_utxos = self.get_utxos_asset(self.network.policy_asset())?; let mut set = HashSet::new(); for input in tx.inputs() { @@ -224,7 +224,7 @@ impl Signer { self.provider.as_ref() } - pub fn get_wpkh_confidential_address(&self) -> Result { + pub fn get_confidential_address(&self) -> Result { let mut descriptor = ConfidentialDescriptor::::from_str(&self.get_slip77_descriptor()?) .map_err(|e| SignerError::Slip77Descriptor(e.to_string()))?; @@ -236,7 +236,7 @@ impl Signer { .address(&self.secp, self.network.address_params())?) } - pub fn get_wpkh_address(&self) -> Result { + pub fn get_address(&self) -> Result { let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor()?) .map_err(|e| SignerError::WpkhDescriptor(e.to_string()))?; @@ -245,30 +245,28 @@ impl Signer { .address(self.network.address_params())?) } - pub fn get_wpkh_utxos(&self) -> Result, SignerError> { - self.get_wpkh_utxos_filter(&|_| true, &|_| true) + pub fn get_utxos(&self) -> Result, SignerError> { + self.get_utxos_filter(&|_| true, &|_| true) } - pub fn get_wpkh_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { - self.get_wpkh_utxos_filter(&|utxo| utxo.txout.asset.explicit().unwrap() == asset, &|utxo| { + pub fn get_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { + self.get_utxos_filter(&|utxo| utxo.txout.asset.explicit().unwrap() == asset, &|utxo| { utxo.secrets.unwrap().asset == asset }) } // TODO: can this be optimized to not populate TxOuts that are filtered out? - pub fn get_wpkh_utxos_txid(&self, txid: Txid) -> Result, SignerError> { - self.get_wpkh_utxos_filter(&|utxo| utxo.outpoint.txid == txid, &|utxo| utxo.outpoint.txid == txid) + pub fn get_utxos_txid(&self, txid: Txid) -> Result, SignerError> { + self.get_utxos_filter(&|utxo| utxo.outpoint.txid == txid, &|utxo| utxo.outpoint.txid == txid) } - pub fn get_wpkh_utxos_filter( + pub fn get_utxos_filter( &self, explicit_filter: &dyn Fn(&UTXO) -> bool, confidential_filter: &dyn Fn(&UTXO) -> bool, ) -> Result, SignerError> { // fetch explicit and confidential utxos - let mut all_utxos = self - .provider - .fetch_address_utxos(&self.get_wpkh_confidential_address()?)?; + let mut all_utxos = self.provider.fetch_address_utxos(&self.get_confidential_address()?)?; // filter out only confidential utxos and unblind them let mut confidential_utxos = self.unblind( @@ -301,8 +299,8 @@ impl Signer { Ok(self.get_private_key()?.public_key(&self.secp)) } - pub fn get_blinding_public_key(&self, script_pubkey: &Script) -> Result { - Ok(self.get_blinding_private_key(script_pubkey)?.public_key(&self.secp)) + pub fn get_blinding_public_key(&self) -> Result { + Ok(self.get_blinding_private_key()?.public_key(&self.secp)) } pub fn get_private_key(&self) -> Result { @@ -317,8 +315,10 @@ impl Signer { Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test)) } - pub fn get_blinding_private_key(&self, script_pubkey: &Script) -> Result { - let blinding_key = self.master_slip77()?.blinding_private_key(script_pubkey); + pub fn get_blinding_private_key(&self) -> Result { + let blinding_key = self + .master_slip77()? + .blinding_private_key(&self.get_address()?.script_pubkey()); Ok(PrivateKey::new(blinding_key, NetworkKind::Test)) } @@ -327,7 +327,7 @@ impl Signer { let mut unblinded: Vec = Vec::new(); for mut utxo in utxos { - let blinding_key = self.get_blinding_private_key(&utxo.txout.script_pubkey)?; + let blinding_key = self.get_blinding_private_key()?; let secrets = utxo.txout.unblind(&self.secp, blinding_key.inner)?; utxo.secrets = Some(secrets); @@ -346,9 +346,9 @@ impl Signer { ) -> Result { // estimate the tx fee with the change // use this wpkh address as a change script - // TODO: should this be confidential? + // TODO: this should be confidential fee_tx.add_output(PartialOutput::new( - self.get_wpkh_address()?.script_pubkey(), + self.get_address()?.script_pubkey(), PLACEHOLDER_FEE, self.network.policy_asset(), )); @@ -399,6 +399,10 @@ impl Signer { let (mut pst, secrets) = tx.extract_pst(); let inputs = tx.inputs(); + if tx.needs_blinding() { + pst.blind_last(&mut thread_rng(), &self.secp, &secrets)?; + } + for (index, input_i) in inputs.iter().enumerate() { // we need to prune the program if let Some(program_input) = &input_i.program_input { @@ -430,10 +434,6 @@ impl Signer { } } - if tx.needs_blinding() { - pst.blind_last(&mut thread_rng(), &self.secp, &secrets)?; - } - Ok(pst.extract_tx()?) } @@ -533,7 +533,7 @@ mod tests { fn keys_correspond_to_address() { let signer = create_signer(); - let address = signer.get_wpkh_address().unwrap(); + let address = signer.get_address().unwrap(); let pubkey = signer.get_ecdsa_public_key().unwrap(); let derived_addr = Address::p2wpkh(&pubkey, None, signer.get_provider().get_network().address_params()); @@ -545,7 +545,7 @@ mod tests { fn descriptors() { let signer = create_signer(); - println!("{}", signer.get_wpkh_address().unwrap()); - println!("{}", signer.get_wpkh_confidential_address().unwrap()); + println!("{}", signer.get_address().unwrap()); + println!("{}", signer.get_confidential_address().unwrap()); } } diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 223b2a8..4cbe8b4 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -29,13 +29,8 @@ pub struct FinalTransaction { outputs: Vec, } -impl Default for FinalTransaction { - fn default() -> Self { - Self::new() - } -} - impl FinalTransaction { + #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { inputs: Vec::new(), diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 9b4be0c..6b9771e 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -5,7 +5,7 @@ use electrsd::bitcoind::bitcoincore_rpc::Auth; use smplx_regtest::Regtest; use smplx_regtest::client::RegtestClient; -use smplx_sdk::provider::{EsploraProvider, ProviderTrait, SimplexProvider, SimplicityNetwork}; +use smplx_sdk::provider::{EsploraProvider, ProviderInfo, ProviderTrait, SimplexProvider, SimplicityNetwork}; use smplx_sdk::signer::Signer; use crate::config::TestConfig; @@ -14,6 +14,8 @@ use crate::error::TestError; #[allow(dead_code)] pub struct TestContext { _client: Option, + // since providers can't be cloned, we need this variable to create new signers + _provider_info: ProviderInfo, config: TestConfig, signer: Signer, } @@ -22,16 +24,41 @@ impl TestContext { pub fn new(config_path: PathBuf) -> Result { let config = TestConfig::from_file(&config_path)?; - let (signer, client) = Self::setup(&config)?; + let (signer, provider_info, client) = Self::setup(&config)?; Ok(Self { _client: client, + _provider_info: provider_info, config, signer, }) } - pub fn get_provider(&self) -> &dyn ProviderTrait { + pub fn create_signer(&self, mnemonic: &str) -> Result { + let provider: Box = if self._provider_info.elements_url.is_some() { + // local regtest or external regtest + Box::new(SimplexProvider::new( + self._provider_info.esplora_url.clone(), + self._provider_info.elements_url.clone().unwrap(), + self._provider_info.auth.clone().unwrap(), + *self.get_network(), + )?) + } else { + // external esplora + Box::new(EsploraProvider::new( + self._provider_info.esplora_url.clone(), + *self.get_network(), + )) + }; + + Ok(Signer::new(mnemonic, provider)?) + } + + pub fn get_default_signer(&self) -> &Signer { + &self.signer + } + + pub fn get_default_provider(&self) -> &dyn ProviderTrait { self.signer.get_provider() } @@ -43,12 +70,9 @@ impl TestContext { self.signer.get_provider().get_network() } - pub fn get_signer(&self) -> &Signer { - &self.signer - } - - fn setup(config: &TestConfig) -> Result<(Signer, Option), TestError> { + fn setup(config: &TestConfig) -> Result<(Signer, ProviderInfo, Option), TestError> { let client: Option; + let provider_info: ProviderInfo; let signer: Signer; match config.esplora.clone() { @@ -57,12 +81,17 @@ impl TestContext { // custom regtest case let auth = Auth::UserPass(rpc.username, rpc.password); let provider = Box::new(SimplexProvider::new( - esplora.url, - rpc.url, - auth, + esplora.url.clone(), + rpc.url.clone(), + auth.clone(), SimplicityNetwork::default_regtest(), )?); + provider_info = ProviderInfo { + esplora_url: esplora.url, + elements_url: Some(rpc.url), + auth: Some(auth), + }; signer = Signer::new(config.mnemonic.as_str(), provider)?; client = None; } @@ -74,8 +103,13 @@ impl TestContext { "ElementsRegtest" => SimplicityNetwork::default_regtest(), other => return Err(TestError::BadNetworkName(other.to_string())), }; - let provider = Box::new(EsploraProvider::new(esplora.url, network)); + let provider = Box::new(EsploraProvider::new(esplora.url.clone(), network)); + provider_info = ProviderInfo { + esplora_url: esplora.url, + elements_url: None, + auth: None, + }; signer = Signer::new(config.mnemonic.as_str(), provider)?; client = None; } @@ -84,12 +118,17 @@ impl TestContext { // simplex inner network let (regtest_client, regtest_signer) = Regtest::from_config(config.to_regtest_config())?; - client = Some(regtest_client); + provider_info = ProviderInfo { + esplora_url: regtest_client.esplora_url(), + elements_url: Some(regtest_client.rpc_url()), + auth: Some(regtest_client.auth()), + }; signer = regtest_signer; + client = Some(regtest_client); } } - Ok((signer, client)) + Ok((signer, provider_info, client)) } } diff --git a/examples/basic/README.md b/examples/basic/README.md index a3f8be4..17ed2bc 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -40,8 +40,6 @@ You will see the test passing. Under the hood, Simplex spins up a local Electrs + Elements regtest, establishes the connection, prefunds the signer specified in the `simplex.toml`, and runs the test marked via macros `#[simplex::test]`. -You are free to experiment with the other simplicity contracts provided in the example. - ### Regtest If you wish to keep the blockchain's state between the integration tests, you will need to spin un a local regest separately: diff --git a/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf b/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf deleted file mode 100644 index 0d11b5f..0000000 --- a/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Computes the "State Commitment" — the expected Script PubKey (address) - * for a specific state value. - * - * HOW IT WORKS: - * In Simplicity/Liquid, state is not stored in a dedicated database. Instead, - * it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO. - * - * This function reconstructs the Taproot structure to validate that the provided - * witness data (state_data) was indeed cryptographically embedded into the - * transaction output that is currently being spent. - * - * LOGIC FLOW: - * 1. Takes state_data (passed via witness at runtime). - * 2. Hashes it as a non-executable TapData leaf. - * 3. Combines it with the current program's CMR (tapleaf_hash). - * 4. Derives the tweaked_key (Internal Key + Merkle Root). - * 5. Returns the final SHA256 script hash (SegWit v1). - * - * USAGE: - * - In main, we verify: CalculatedHash(witness::STATE) == input_script_hash. - * - This assertion proves that the UTXO is "locked" not just by the code, - * but specifically by THIS instance of the state data. - */ - -fn script_hash_for_input_script(state_data: u256) -> u256 { - // This is the bulk of our "compute state commitment" logic from above. - let tap_leaf: u256 = jet::tapleaf_hash(); - let state_ctx1: Ctx8 = jet::tapdata_init(); - let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); - let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); - let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); - - // Compute a taptweak using this. - let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; - let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); - - // Turn the taptweak into a script hash - let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); - let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 - let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); - jet::sha_256_ctx_8_finalize(hash_ctx3) -} - -fn main() { - let state_data: u256 = witness::STATE; - let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(state_data); - - // Assert that the input is correct, i.e. "load". - assert!(jet::eq_256( - script_hash_for_input_script(state_data), - unwrap(jet::input_script_hash(jet::current_index())) - )); - - // Do a state update (and fail on 64-bit overflow even though we've got 192 other - // bits we could be using..) - let (carry, new_state4): (bool, u64) = jet::increment_64(state4); - assert!(jet::eq_1(::into(carry), 0)); - - let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); - // Assert that the output is correct, i.e. "store". - assert!(jet::eq_256( - script_hash_for_input_script(new_state), - unwrap(jet::output_script_hash(jet::current_index())) - )); -} \ No newline at end of file diff --git a/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf b/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf deleted file mode 100644 index e1a460a..0000000 --- a/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf +++ /dev/null @@ -1,592 +0,0 @@ -/* - * DCD: Dual Currency Deposit – price-attested settlement and funding windows - * - * Flows implemented: - * - Maker funding: deposit settlement asset and collateral, issue grantor tokens - * - Taker funding: deposit collateral in window and receive filler tokens - * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) - * selects LBTC vs ALT branch based on price <= STRIKE_PRICE - * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens - * - Merge: consolidate 2/3/4 token UTXOs - * - * All amounts and asset/script invariants are enforced on-chain; time guards use - * fallback locktime and height checks. - * - * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 - */ - -// Verify Schnorr signature against SHA256 of (u32 || u64) -fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { - let hasher: Ctx8 = jet::sha_256_ctx_8_init(); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); - let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); - jet::bip_0340_verify((pk, msg), sig); -} - -// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) -fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) -} - -fn signed_lt_u64(a: u64, b: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) -} - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS -fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { - let MAX_BASIS_POINTS: u64 = 10000; - - let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); - let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); - - assert!(jet::eq_256(arg1, arg2)); -} - -fn get_output_script_hash(index: u32) -> u256 { - unwrap(jet::output_script_hash(index)) -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn ensure_one_bit_or(bit1: bool, bit2: bool) { - assert!( - jet::eq_1( - ::into(jet::or_1(::into(bit1), ::into(bit2))), - 1 - ) - ); -} - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn ensure_input_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::input_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::output_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_input_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - assert!(jet::eq_32(jet::current_index(), index)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -fn merge_2_tokens() { - // 2 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 3)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 1)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); -} - -fn merge_3_tokens() { - // 3 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 4)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 2)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); -} - -fn merge_4_tokens() { - // 4 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 5)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 3)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); - assert!(jet::eq_256(script_hash, get_input_script_hash(3))); -} - -/* -* Maker funding path -* Params: -* 1. FILLER_PER_SETTLEMENT_COLLATERAL -* 2. FILLER_PER_SETTLEMENT_ASSET -* 3. FILLER_PER_PRINCIPAL_COLLATERAL -* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET -* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL -* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL -* 7. GRANTOR_PER_SETTLEMENT_ASSET -*/ -fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { - assert!(jet::eq_32(jet::num_inputs(), 5)); - assert!(jet::eq_32(jet::num_outputs(), 11)); - - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); - - ensure_input_and_output_script_hash_eq(0); - ensure_input_and_output_script_hash_eq(1); - ensure_input_and_output_script_hash_eq(2); - - assert!(jet::le_32(jet::current_index(), 2)); - - let script_hash: u256 = get_output_script_hash(0); - ensure_output_script_hash_eq(1, script_hash); - ensure_output_script_hash_eq(2, script_hash); - ensure_output_script_hash_eq(3, script_hash); - ensure_output_script_hash_eq(4, script_hash); - ensure_output_script_hash_eq(5, script_hash); - - let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); - let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); - let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); - let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); - let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); - assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); - assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); - - divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); - - assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); - constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); - - let MAX_BASIS_POINTS: u64 = 10000; - let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); - ensure_zero_bit(carry); - - constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); - - let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); - ensure_zero_bit(carry); - assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); - - let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); - ensure_zero_bit(carry); - - // Filler token constraints - divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); - divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); - divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); - - // Grantor token constraints - divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); - divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); - - divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); - // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); - // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); - assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); - - ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); - ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); - ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); - - ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); - ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); - - ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); - ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); - ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); -} - -fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); - assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); - - let filler_token_input_index: u32 = 0; - let collateral_input_index: u32 = 1; - - let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); - - // Check and ensure filler token change - ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); - - // Ensure collateral asset and script hash are correct - ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); - ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); - - ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); -} - -fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let filler_token_input_index: u32 = 1; - - let (return_filler_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); - - // Ensure filler token transferred to covenant - ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); - ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let grantor_collateral_token_input_index: u32 = 1; - - let (burn_grantor_collateral_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); - - // Burn grantor collateral token - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let settlement_asset_input_index: u32 = 0; - let grantor_settlement_token_input_index: u32 = 1; - - let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); - - // Burn grantor settlement token - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); -} - -fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { - match jet::eq_64(fee_basis_points, 0) { - true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), - false => { - let fee_output_index: u32 = increment_by(user_output_index, 1); - - let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); - assert!(jet::eq_256(user_asset_bits, asset_id)); - - let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); - assert!(jet::eq_256(fee_asset_bits, asset_id)); - - let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); - ensure_zero_bit(carry); - - constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); - - ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); - }, - }; -} - -fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Maker gets ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); - - // Burn grantor settlement and collateral tokens - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Maker gets the LBTC - let collateral_input_index: u32 = 0; - - let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); - - // Burn grantor collateral and settlement tokens - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Taker receives LBTC principal+interest - let collateral_input_index: u32 = 0; - - let (burn_filler_output_index, collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Taker receives ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure filler token transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn main() { - let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; - let merge_branch: Either, ()> = witness::MERGE_BRANCH; - - match witness::PATH { - Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { - // Funding branches - Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { - // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - Left(params: (u64, u64, u64, u64)) => { - let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; - maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - }, - // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - Right(params: (u64, u64, bool)) => { - let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; - taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - }, - }, - // Settlement branches (oracle price attested) - Right(params: (u64, Signature, u64, u64, bool)) => { - let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; - - match token_branch { - // Maker settlement: burn grantor token - Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - // Taker settlement: burn filler token - Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - // Termination flows (early termination or post-expiry) or Merge flows - Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { - Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { - // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) - Left(params: (bool, u64, u64)) => { - let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; - taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) - }, - // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch - Right(params: (bool, u64, u64)) => { - let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; - - match token_branch { - // Burn grantor collateral token -> receive collateral - Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - // Burn grantor settlement token -> receive settlement asset - Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - Right(u: ()) => { - // Merge tokens based on MERGE_BRANCH discriminator - match merge_branch { - Left(left_or_right: Either<(), ()>) => match left_or_right { - Left(u: ()) => merge_2_tokens(), - Right(u: ()) => merge_3_tokens(), - }, - Right(u: ()) => merge_4_tokens(), - } - }, - }, - } - -} diff --git a/examples/basic/simf/another_dir/array_tr_storage.simf b/examples/basic/simf/another_dir/array_tr_storage.simf deleted file mode 100644 index 4918cf3..0000000 --- a/examples/basic/simf/another_dir/array_tr_storage.simf +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Extends `bytes32_tr_storage` using `array_fold` for larger buffers. - * Optimized for small, fixed-size states where linear hashing is more efficient - * than Merkle Trees. By avoiding proof overhead like sibling hashes, we reduce - * witness size and simplify contract logic for small N. - * This approach is particularly advantageous when updating all slots within every transaction. - */ - -fn hash_array_tr_storage(elem: u256, ctx: Ctx8) -> Ctx8 { - jet::sha_256_ctx_8_add_32(ctx, elem) -} - -fn hash_array_tr_storage_with_update(elem: u256, triplet: (Ctx8, u16, u16)) -> (Ctx8, u16, u16) { - let (ctx, i, changed_index): (Ctx8, u16, u16) = triplet; - - match jet::eq_16(i, changed_index) { - true => { - let (_, val): (bool, u16) = jet::increment_16(i); - - // There may be arbitrary logic here - let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(elem); - let new_state4: u64 = 20; - - let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); - ( - jet::sha_256_ctx_8_add_32(ctx, new_state), - val, - changed_index, - ) - }, - false => { - let (_, val): (bool, u16) = jet::increment_16(i); - ( - jet::sha_256_ctx_8_add_32(ctx, elem), - val, - changed_index, - ) - } - } -} - -fn script_hash_for_input_script(state: [u256; 3], changed_index: Option) -> u256 { - let tap_leaf: u256 = jet::tapleaf_hash(); - let ctx: Ctx8 = jet::tapdata_init(); - - let (ctx, _, _): (Ctx8, u16, u16) = match changed_index { - Some(ind: u16) => { - array_fold::(state, (ctx, 0, ind)) - }, - None => { - (array_fold::(state, ctx), 0, 0) - } - }; - - let computed: u256 = jet::sha_256_ctx_8_finalize(ctx); - let tap_node: u256 = jet::build_tapbranch(tap_leaf, computed); - - let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; - let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); - - let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); - let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 - let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); - jet::sha_256_ctx_8_finalize(hash_ctx3) -} - -fn main() { - let state: [u256; 3] = witness::STATE; - - // Assert that the input is correct, i.e. "load". - assert!(jet::eq_256( - script_hash_for_input_script(state, None), - unwrap(jet::input_script_hash(jet::current_index())) - )); - - // Assert that the output is correct, i.e. "store". - assert!(jet::eq_256( - script_hash_for_input_script(state, Some(witness::CHANGED_INDEX)), - unwrap(jet::output_script_hash(jet::current_index())) - )); -} \ No newline at end of file diff --git a/examples/basic/simf/module/option_offer.simf b/examples/basic/simf/module/option_offer.simf deleted file mode 100644 index 5cb2108..0000000 --- a/examples/basic/simf/module/option_offer.simf +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Option Offer - * - * A covenant that allows a user to deposit collateral and premium assets, - * and have a counterparty swap settlement asset for both. - * The user can withdraw accumulated settlement asset at any time (with signature). - * After expiry, the user can reclaim any remaining collateral and premium (with signature). - * - * Paths: - * 1. Exercise: Counterparty swaps settlement asset for collateral + premium (no time restriction, optional change) - * 2. Withdraw: User withdraws settlement asset (no time restriction, signature required, full amount) - * 3. Expiry: User reclaims collateral + premium (after expiry, signature required, full amount) - * - * Constraints: - * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount - * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount - */ - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_zero_bit(bit: bool) { - assert!(jet::eq_1(::into(bit), 0)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn check_user_signature(sig: Signature) { - let msg: u256 = jet::sig_all_hash(); - jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, asset_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, asset_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -/* - * Exercise Path - * - * Counterparty swaps settlement asset for collateral + premium. - * No time restriction - works before and after expiry. - * - * Constraints: - * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount - * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount - * - * Layout: - * - * Both: - * Input[0]: Collateral from covenant - * Input[1]: Premium from covenant - * - * With change (partial swap): - * Output[0]: Collateral change → covenant - * Output[1]: Premium change → covenant - * Output[2]: Settlement asset → covenant - * Output[3]: Collateral → counterparty - * Output[4]: Premium → counterparty - * - * Without change (full swap): - * Output[0]: Settlement asset → covenant - * Output[1]: Collateral → counterparty - * Output[2]: Premium → counterparty - */ -fn exercise_path(collateral_amount: u64, is_change_needed: bool) { - assert!(jet::le_32(jet::current_index(), 1)); - - let expected_covenant_script_hash: u256 = get_input_script_hash(0); - - assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); - - let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); - let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); - assert!(jet::eq_64(left_part, 0)); - - // Check collateral changes - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); - ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); - - let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed { - true => (2, 3, 4), - false => (0, 1, 2), - }; - - ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); - - let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); - assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); - - divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); - - ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); -} - -/* - * Withdraw Path - * - * User withdraws accumulated settlement asset. - * No time restriction. - * Requires signature from USER_PUBKEY. - * No change - full withdrawal only. - * - * Layout: - * Input[0]: Settlement asset from covenant - * Output[0]: Settlement asset → user (any address) - */ -fn withdraw_path(sig: Signature) { - assert!(jet::eq_32(jet::current_index(), 0)); - - let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); - assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); - - check_user_signature(sig); - - ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); -} - -/* - * Expiry Path - * - * User reclaims remaining collateral and premium after expiry. - * Only allowed after EXPIRY_TIME. - * Requires signature from USER_PUBKEY. - * No change - full reclaim only. - * - * Layout: - * Input[0]: Collateral from covenant - * Input[1]: Premium from covenant - * Output[0]: Collateral → user (any address) - * Output[1]: Premium → user (any address) - */ -fn expiry_path(sig: Signature) { - jet::check_lock_time(param::EXPIRY_TIME); - - assert!(jet::le_32(jet::current_index(), 1)); - - let expected_covenant_script_hash: u256 = get_input_script_hash(0); - - assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); - - let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); - assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); - - let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); - assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); - - check_user_signature(sig); - - ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); -} - -fn main() { - match witness::PATH { - Left(params: (u64, bool)) => { - let (collateral_amount, is_change_needed): (u64, bool) = params; - exercise_path(collateral_amount, is_change_needed) - }, - Right(withdraw_or_expiry: Either) => match withdraw_or_expiry { - Left(sig: Signature) => withdraw_path(sig), - Right(sig: Signature) => expiry_path(sig), - }, - } -} diff --git a/examples/basic/simf/options.simf b/examples/basic/simf/options.simf deleted file mode 100644 index e7da014..0000000 --- a/examples/basic/simf/options.simf +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Options - * - * Important: Currently only the LBTC collateral is supported. - * - * Based on the https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf - * - * This contract implements cash-settled European-style options using covenant-locked collateral. - * - * Room for optimization: - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/2 (Use input asset to determine option covenent type) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/3 (Simplify match token_branch in funding_path.) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 (why batching is hard to implement) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/5 (Reduce Contract Parameters) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (explains why funding is limited) - */ - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -fn get_output_script_hash(index: u32) -> u256 { - unwrap(jet::output_script_hash(index)) -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn ensure_input_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::input_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::output_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_input_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - assert!(jet::eq_32(jet::current_index(), index)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -fn check_y(expected_y: Fe, actual_y: Fe) { - match jet::eq_256(expected_y, actual_y) { - true => {}, - false => { - assert!(jet::eq_256(expected_y, jet::fe_negate(actual_y))); - } - }; -} - -fn ensure_input_and_output_reissuance_token_eq(index: u32) { - let (input_asset, input_amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (output_asset, output_amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); - - match (input_asset) { - Left(in_conf: Point) => { - let (input_asset_parity, input_asset_x): (u1, u256) = in_conf; - let (output_asset_parity, output_asset_x): (u1, u256) = unwrap_left::(output_asset); - - assert!(jet::eq_1(input_asset_parity, output_asset_parity)); - assert!(jet::eq_256(input_asset_x, output_asset_x)); - }, - Right(in_expl: u256) => { - let out_expl: u256 = unwrap_right::(output_asset); - assert!(jet::eq_256(in_expl, out_expl)); - } - }; - - match (input_amount) { - Left(in_conf: Point) => { - let (input_amount_parity, input_amount_x): (u1, u256) = in_conf; - let (output_amount_parity, output_amount_x): (u1, u256) = unwrap_left::(output_amount); - - assert!(jet::eq_1(input_amount_parity, output_amount_parity)); - assert!(jet::eq_256(input_amount_x, output_amount_x)); - }, - Right(in_expl: u64) => { - let out_expl: u64 = unwrap_right::(output_amount); - assert!(jet::eq_64(in_expl, out_expl)); - } - }; -} - -// Verify that a reissuance token commitment matches the expected token ID using provided blinding factors. -// Reissuance tokens are confidential because, in Elements, -// the asset must be provided in blinded form in order to reissue tokens. -// https://github.com/BlockstreamResearch/simplicity-contracts/issues/21#issuecomment-3691599583 -fn verify_token_commitment(actual_asset: Asset1, actual_amount: Amount1, expected_token_id: u256, abf: u256, vbf: u256) { - match actual_asset { - Left(conf_token: Point) => { - let amount_scalar: u256 = 1; - let (actual_ax, actual_ay): Ge = unwrap(jet::decompress(conf_token)); - - let gej_point: Gej = (jet::hash_to_curve(expected_token_id), 1); - let asset_blind_point: Gej = jet::generate(abf); - - let asset_generator: Gej = jet::gej_add(gej_point, asset_blind_point); - let (ax, ay): Ge = unwrap(jet::gej_normalize(asset_generator)); - - assert!(jet::eq_256(actual_ax, ax)); - check_y(actual_ay, ay); - - // Check amount - let conf_val: Point = unwrap_left::(actual_amount); - let (actual_vx, actual_vy): Ge = unwrap(jet::decompress(conf_val)); - - let amount_part: Gej = jet::scale(amount_scalar, asset_generator); - let vbf_part: Gej = jet::generate(vbf); - - let value_generator: Gej = jet::gej_add(amount_part, vbf_part); - let (vx, vy): Ge = unwrap(jet::gej_normalize(value_generator)); - - assert!(jet::eq_256(actual_vx, vx)); - check_y(actual_vy, vy); - }, - Right(reissuance_token: u256) => { - let expected_amount: u64 = 1; - let actual_amount: u64 = unwrap_right::(actual_amount); - - assert!(jet::eq_64(expected_amount, actual_amount)); - assert!(jet::eq_256(reissuance_token, expected_token_id)); - } - }; -} - -fn verify_output_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { - let (asset, amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); - verify_token_commitment(asset, amount, expected_token_id, abf, vbf); -} - -fn verify_input_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { - let (asset, amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); - verify_token_commitment(asset, amount, expected_token_id, abf, vbf); -} - -/* - * Funding Path - */ -fn funding_path( - expected_asset_amount: u64, - input_option_abf: u256, - input_option_vbf: u256, - input_grantor_abf: u256, - input_grantor_vbf: u256, - output_option_abf: u256, - output_option_vbf: u256, - output_grantor_abf: u256, - output_grantor_vbf: u256 -) { - ensure_input_and_output_script_hash_eq(0); - ensure_input_and_output_script_hash_eq(1); - - verify_input_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, input_option_abf, input_option_vbf); - verify_input_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, input_grantor_abf, input_grantor_vbf); - - verify_output_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, output_option_abf, output_option_vbf); - verify_output_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, output_grantor_abf, output_grantor_vbf); - - assert!(dbg!(jet::eq_256(get_output_script_hash(0), get_output_script_hash(1)))); - - assert!(jet::le_32(jet::current_index(), 1)); - - ensure_output_script_hash_eq(2, get_output_script_hash(0)); - - let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(2); - let option_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); - let grantor_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); - assert!(jet::eq_64(option_token_amount, grantor_token_amount)); - - divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, option_token_amount); - divmod_eq(expected_asset_amount, param::SETTLEMENT_PER_CONTRACT, option_token_amount); - - ensure_output_asset_with_amount_eq(2, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(3, param::OPTION_TOKEN_ASSET, option_token_amount); - ensure_output_asset_with_amount_eq(4, param::GRANTOR_TOKEN_ASSET, grantor_token_amount); -} - -/* - * Cancellation Path - */ -fn cancellation_path(amount_to_burn: u64, collateral_amount_to_withdraw: u64, is_change_needed: bool) { - let collateral_input_index: u32 = 0; - let option_input_index: u32 = 1; - let grantor_input_index: u32 = 2; - - let (burn_option_output_index, burn_grantor_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_withdraw, expected_current_script_hash, is_change_needed); - - // Burn option and grantor tokens - ensure_output_is_op_return(burn_option_output_index); - ensure_output_is_op_return(burn_grantor_output_index); - - ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, amount_to_burn); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, amount_to_burn); - - // Ensure returned collateral amount is correct - divmod_eq(collateral_amount_to_withdraw, param::COLLATERAL_PER_CONTRACT, amount_to_burn); -} - -/* - * Exercise Path - */ -fn exercise_path(option_amount_to_burn: u64, collateral_amount_to_get: u64, asset_amount_to_pay: u64, is_change_needed: bool) { - jet::check_lock_time(param::START_TIME); - - let collateral_input_index: u32 = 0; - - let (burn_option_output_index, asset_to_covenant_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::COLLATERAL_PER_CONTRACT, option_amount_to_burn); - divmod_eq(asset_amount_to_pay, param::SETTLEMENT_PER_CONTRACT, option_amount_to_burn); - - // Burn option token - ensure_output_is_op_return(burn_option_output_index); - ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, option_amount_to_burn); - - // Ensure settlement asset and script hash are correct - ensure_output_asset_with_amount_eq(asset_to_covenant_output_index, param::SETTLEMENT_ASSET_ID, asset_amount_to_pay); - ensure_output_script_hash_eq(asset_to_covenant_output_index, expected_current_script_hash); -} - -/* - * Settlement Path - */ -fn settlement_path(grantor_token_amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) { - jet::check_lock_time(param::START_TIME); - - let target_asset_input_index: u32 = 0; - - let burn_grantor_output_index: u32 = match is_change_needed { - true => 1, - false => 0, - }; - - let expected_current_script_hash: u256 = get_input_script_hash(target_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, asset_amount, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset and grantor token amounts are correct - divmod_eq(asset_amount, param::SETTLEMENT_PER_CONTRACT, grantor_token_amount_to_burn); - - // Burn grantor token - ensure_output_is_op_return(burn_grantor_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); -} - -/* - * Expiry Path - */ -fn expiry_path(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) { - jet::check_lock_time(param::EXPIRY_TIME); - - let collateral_input_index: u32 = 0; - - let burn_grantor_output_index: u32 = match is_change_needed { - true => 1, - false => 0, - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_current_script_hash, is_change_needed); - - // Ensure collateral amount is correct - divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, grantor_token_amount_to_burn); - - // Burn grantor token - ensure_output_is_op_return(burn_grantor_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); -} - -fn main() { - match witness::PATH { - Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right { - Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => { - let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params; - funding_path( - expected_asset_amount, - input_option_abf, input_option_vbf, - input_grantor_abf, input_grantor_vbf, - output_option_abf, output_option_vbf, - output_grantor_abf, output_grantor_vbf - ); - }, - Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement { - Left(params: (bool, u64, u64, u64)) => { - let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params); - exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed) - }, - Right(params: (bool, u64, u64)) => { - let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params); - settlement_path(amount_to_burn, asset_amount, is_change_needed) - }, - }, - }, - Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right { - Left(params: (bool, u64, u64)) => { - let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params; - expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed) - }, - Right(params: (bool, u64, u64)) => { - let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params; - cancellation_path(amount_to_burn, collateral_amount, is_change_needed) - }, - }, - } -} diff --git a/examples/basic/tests/example_test.rs b/examples/basic/tests/basic_test.rs similarity index 86% rename from examples/basic/tests/example_test.rs rename to examples/basic/tests/basic_test.rs index 9224e63..54496c1 100644 --- a/examples/basic/tests/example_test.rs +++ b/examples/basic/tests/basic_test.rs @@ -8,7 +8,7 @@ use simplex_example::artifacts::p2pk::P2pkProgram; use simplex_example::artifacts::p2pk::derived_p2pk::{P2pkArguments, P2pkWitness}; fn get_p2pk(context: &simplex::TestContext) -> (P2pkProgram, Script) { - let signer = context.get_signer(); + let signer = context.get_default_signer(); let arguments = P2pkArguments { public_key: signer.get_schnorr_public_key().unwrap().serialize(), @@ -21,7 +21,7 @@ fn get_p2pk(context: &simplex::TestContext) -> (P2pkProgram, Script) { } fn spend_p2wpkh(context: &simplex::TestContext) -> Txid { - let signer = context.get_signer(); + let signer = context.get_default_signer(); let (_, p2pk_script) = get_p2pk(context); @@ -33,8 +33,8 @@ fn spend_p2wpkh(context: &simplex::TestContext) -> Txid { } fn spend_p2pk(context: &simplex::TestContext) -> Txid { - let signer = context.get_signer(); - let provider = context.get_provider(); + let signer = context.get_default_signer(); + let provider = context.get_default_provider(); let (p2pk, p2pk_script) = get_p2pk(context); @@ -63,8 +63,8 @@ fn spend_p2pk(context: &simplex::TestContext) -> Txid { } #[simplex::test] -fn dummy_test(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_provider(); +fn basic_test(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); let tx = spend_p2wpkh(&context); provider.wait(&tx)?; @@ -76,7 +76,5 @@ fn dummy_test(context: simplex::TestContext) -> anyhow::Result<()> { println!("Confirmed"); - println!("OK"); - Ok(()) } diff --git a/examples/basic/tests/confidential_test.rs b/examples/basic/tests/confidential_test.rs new file mode 100644 index 0000000..7ec8564 --- /dev/null +++ b/examples/basic/tests/confidential_test.rs @@ -0,0 +1,36 @@ +use simplex::transaction::{FinalTransaction, PartialOutput}; + +#[simplex::test] +fn confidential_test(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let alice = context.get_default_signer(); + let bob = context + .create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert") + .unwrap(); + + let mut ft = FinalTransaction::new(); + + ft.add_output( + PartialOutput::new( + bob.get_address().unwrap().script_pubkey(), + 100, + context.get_network().policy_asset(), + ) + .with_blinding_key(bob.get_blinding_public_key().unwrap()), + ); + + let tx = alice.broadcast(&ft).unwrap(); + println!("Broadcast: {}", tx); + + provider.wait(&tx)?; + println!("Confirmed"); + + let tx = bob.send(alice.get_address().unwrap().script_pubkey(), 50).unwrap(); + println!("Broadcast: {}", tx); + + provider.wait(&tx)?; + println!("Confirmed"); + + Ok(()) +} From 6cad4d4325d8f8644d1ec1726dc21dbac2d78cff Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 31 Mar 2026 20:59:33 +0300 Subject: [PATCH 08/10] fix fmt --- crates/sdk/src/provider/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs index 112aa05..1873ed7 100644 --- a/crates/sdk/src/provider/mod.rs +++ b/crates/sdk/src/provider/mod.rs @@ -5,7 +5,7 @@ pub mod network; pub mod rpc; pub mod simplex; -pub use core::{ProviderTrait, ProviderInfo}; +pub use core::{ProviderInfo, ProviderTrait}; pub use esplora::EsploraProvider; pub use rpc::elements::ElementsRpc; pub use simplex::SimplexProvider; From b78331be63aa16f5f5e93b98550131dd2dd36da7 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Wed, 1 Apr 2026 15:30:42 +0300 Subject: [PATCH 09/10] fix interfaces --- crates/cli/src/commands/regtest.rs | 4 +- crates/regtest/src/error.rs | 4 - crates/regtest/src/regtest.rs | 8 +- crates/sdk/src/program/core.rs | 18 +-- crates/sdk/src/provider/simplex.rs | 13 +- crates/sdk/src/signer/core.rs | 113 ++++++++++-------- crates/sdk/src/signer/error.rs | 4 - crates/sdk/src/transaction/error.rs | 5 - .../sdk/src/transaction/final_transaction.rs | 37 ++---- crates/sdk/src/transaction/mod.rs | 2 - crates/sdk/src/transaction/utxo.rs | 20 +++- crates/test/src/context.rs | 14 +-- crates/test/src/error.rs | 4 - examples/basic/tests/basic_test.rs | 33 +++-- examples/basic/tests/confidential_test.rs | 12 +- 15 files changed, 137 insertions(+), 154 deletions(-) delete mode 100644 crates/sdk/src/transaction/error.rs diff --git a/crates/cli/src/commands/regtest.rs b/crates/cli/src/commands/regtest.rs index 83a355e..896c457 100644 --- a/crates/cli/src/commands/regtest.rs +++ b/crates/cli/src/commands/regtest.rs @@ -10,7 +10,7 @@ pub struct Regtest {} impl Regtest { pub fn run(config: RegtestConfig) -> Result<(), CommandError> { - let (mut client, signer) = RegtestRunner::from_config(config)?; + let (mut client, signer) = RegtestRunner::new(config)?; let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); @@ -29,7 +29,7 @@ impl Regtest { println!("Esplora: {}", client.esplora_url()); println!("User: {:?}, Password: {:?}", auth.0.unwrap(), auth.1.unwrap()); println!(); - println!("Signer: {:?}", signer.get_address()?); + println!("Signer: {:?}", signer.get_address()); println!("======================================"); while running.load(Ordering::SeqCst) {} diff --git a/crates/regtest/src/error.rs b/crates/regtest/src/error.rs index c439a6d..77d953b 100644 --- a/crates/regtest/src/error.rs +++ b/crates/regtest/src/error.rs @@ -1,14 +1,10 @@ use std::io; -use smplx_sdk::provider::ProviderError; use smplx_sdk::provider::RpcError; use smplx_sdk::signer::SignerError; #[derive(thiserror::Error, Debug)] pub enum RegtestError { - #[error(transparent)] - Provider(#[from] ProviderError), - #[error(transparent)] Rpc(#[from] RpcError), diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index 1c8ccb0..92e103c 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -13,7 +13,7 @@ use super::error::RegtestError; pub struct Regtest {} impl Regtest { - pub fn from_config(config: RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { + pub fn new(config: RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { let client = RegtestClient::new(); let provider = Box::new(SimplexProvider::new( @@ -21,9 +21,9 @@ impl Regtest { client.rpc_url(), client.auth(), SimplicityNetwork::default_regtest(), - )?); + )); - let signer = Signer::new(config.mnemonic.as_str(), provider)?; + let signer = Signer::new(config.mnemonic.as_str(), provider); Self::prepare_signer(&client, &signer, config.bitcoins)?; @@ -38,7 +38,7 @@ impl Regtest { rpc_provider.sweep_initialfreecoins()?; rpc_provider.generate_blocks(100)?; - rpc_provider.send_to_address(&signer.get_address()?, btc2sat(bitcoins), None)?; + rpc_provider.send_to_address(&signer.get_address(), btc2sat(bitcoins), None)?; // wait for electrs to index let mut attempts = 0; diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index a45a622..45c1304 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -71,7 +71,7 @@ impl ProgramTrait for Program { } let target_utxo = &utxos[input_index]; - let script_pubkey = self.get_tr_address(network)?.script_pubkey(); + let script_pubkey = self.get_tr_address(network).script_pubkey(); if target_utxo.script_pubkey != script_pubkey { return Err(ProgramError::ScriptPubkeyMismatch { @@ -153,24 +153,24 @@ impl Program { } } - pub fn get_tr_address(&self, network: &SimplicityNetwork) -> Result { - let spend_info = self.taproot_spending_info()?; + pub fn get_tr_address(&self, network: &SimplicityNetwork) -> Address { + let spend_info = self.taproot_spending_info().unwrap(); - Ok(Address::p2tr( + Address::p2tr( secp256k1::SECP256K1, spend_info.internal_key(), spend_info.merkle_root(), None, network.address_params(), - )) + ) } - pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Result { - Ok(self.get_tr_address(network)?.script_pubkey()) + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.get_tr_address(network).script_pubkey() } - pub fn get_script_hash(&self, network: &SimplicityNetwork) -> Result<[u8; 32], ProgramError> { - Ok(hash_script(&self.get_script_pubkey(network)?)) + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + hash_script(&self.get_script_pubkey(network)) } fn load(&self) -> Result { diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs index 2189b40..c34f25b 100644 --- a/crates/sdk/src/provider/simplex.rs +++ b/crates/sdk/src/provider/simplex.rs @@ -17,16 +17,11 @@ pub struct SimplexProvider { } impl SimplexProvider { - pub fn new( - esplora_url: String, - elements_url: String, - auth: Auth, - network: SimplicityNetwork, - ) -> Result { - Ok(Self { + pub fn new(esplora_url: String, elements_url: String, auth: Auth, network: SimplicityNetwork) -> Self { + Self { esplora: EsploraProvider::new(esplora_url, network), - elements: ElementsRpc::new(elements_url, auth)?, - }) + elements: ElementsRpc::new(elements_url, auth).unwrap(), + } } } diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 26657c9..045fb15 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -72,7 +72,7 @@ impl SignerTrait for Signer { let env = program.get_env(pst, input_index, network)?; let msg = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); - let private_key = self.get_private_key()?; + let private_key = self.get_private_key(); let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); Ok(self.secp.sign_schnorr(&msg, &keypair)) @@ -92,7 +92,7 @@ impl SignerTrait for Signer { .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)? .to_secp_msg(); - let private_key = self.get_private_key()?; + let private_key = self.get_private_key(); let public_key = private_key.public_key(&self.secp); let signature = self.secp.sign_ecdsa_low_r(&message, &private_key.inner); @@ -107,23 +107,24 @@ enum Estimate { } impl Signer { - pub fn new(mnemonic: &str, provider: Box) -> Result { + pub fn new(mnemonic: &str, provider: Box) -> Self { let secp = Secp256k1::new(); let mnemonic: Mnemonic = mnemonic .parse() - .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?; + .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string())) + .unwrap(); let seed = mnemonic.to_seed(""); - let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?; + let xprv = Xpriv::new_master(NetworkKind::Test, &seed).unwrap(); let network = *provider.get_network(); - Ok(Self { + Self { mnemonic, xprv, provider, network, secp, - }) + } } // TODO: add an ability to send arbitrary assets @@ -160,11 +161,11 @@ impl Signer { signer_utxos.sort_by(|a, b| { let a_value = match a.secrets { Some(secrets) => secrets.value, - None => a.txout.value.explicit().unwrap(), + None => a.explicit_amount(), }; let b_value = match b.secrets { Some(secrets) => secrets.value, - None => b.txout.value.explicit().unwrap(), + None => b.explicit_amount(), }; b_value.cmp(&a_value) @@ -184,7 +185,7 @@ impl Signer { } } - fee_tx.add_input(PartialInput::new(utxo), RequiredSignature::NativeEcdsa)?; + fee_tx.add_input(PartialInput::new(utxo), RequiredSignature::NativeEcdsa); } // need to try one more time after the loop @@ -224,25 +225,32 @@ impl Signer { self.provider.as_ref() } - pub fn get_confidential_address(&self) -> Result { - let mut descriptor = ConfidentialDescriptor::::from_str(&self.get_slip77_descriptor()?) - .map_err(|e| SignerError::Slip77Descriptor(e.to_string()))?; + pub fn get_confidential_address(&self) -> Address { + let mut descriptor = + ConfidentialDescriptor::::from_str(&self.get_slip77_descriptor().unwrap()) + .map_err(|e| SignerError::Slip77Descriptor(e.to_string())) + .unwrap(); // confidential descriptor doesn't support multipath - descriptor.descriptor = descriptor.descriptor.into_single_descriptors()?[0].clone(); + descriptor.descriptor = descriptor.descriptor.into_single_descriptors().unwrap()[0].clone(); - Ok(descriptor - .at_derivation_index(1)? - .address(&self.secp, self.network.address_params())?) + descriptor + .at_derivation_index(1) + .unwrap() + .address(&self.secp, self.network.address_params()) + .unwrap() } - pub fn get_address(&self) -> Result { - let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor()?) - .map_err(|e| SignerError::WpkhDescriptor(e.to_string()))?; + pub fn get_address(&self) -> Address { + let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor().unwrap()) + .map_err(|e| SignerError::WpkhDescriptor(e.to_string())) + .unwrap(); - Ok(descriptor.into_single_descriptors()?[0] - .at_derivation_index(1)? - .address(self.network.address_params())?) + descriptor.into_single_descriptors().unwrap()[0] + .at_derivation_index(1) + .unwrap() + .address(self.network.address_params()) + .unwrap() } pub fn get_utxos(&self) -> Result, SignerError> { @@ -250,8 +258,8 @@ impl Signer { } pub fn get_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { - self.get_utxos_filter(&|utxo| utxo.txout.asset.explicit().unwrap() == asset, &|utxo| { - utxo.secrets.unwrap().asset == asset + self.get_utxos_filter(&|utxo| utxo.explicit_asset() == asset, &|utxo| { + utxo.unblinded_asset() == asset }) } @@ -266,7 +274,7 @@ impl Signer { confidential_filter: &dyn Fn(&UTXO) -> bool, ) -> Result, SignerError> { // fetch explicit and confidential utxos - let mut all_utxos = self.provider.fetch_address_utxos(&self.get_confidential_address()?)?; + let mut all_utxos = self.provider.fetch_address_utxos(&self.get_confidential_address())?; // filter out only confidential utxos and unblind them let mut confidential_utxos = self.unblind( @@ -288,46 +296,50 @@ impl Signer { Ok(all_utxos) } - pub fn get_schnorr_public_key(&self) -> Result { - let private_key = self.get_private_key()?; + pub fn get_schnorr_public_key(&self) -> XOnlyPublicKey { + let private_key = self.get_private_key(); let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); - Ok(keypair.x_only_public_key().0) + keypair.x_only_public_key().0 } - pub fn get_ecdsa_public_key(&self) -> Result { - Ok(self.get_private_key()?.public_key(&self.secp)) + pub fn get_ecdsa_public_key(&self) -> PublicKey { + self.get_private_key().public_key(&self.secp) } - pub fn get_blinding_public_key(&self) -> Result { - Ok(self.get_blinding_private_key()?.public_key(&self.secp)) + pub fn get_blinding_public_key(&self) -> PublicKey { + self.get_blinding_private_key().public_key(&self.secp) } - pub fn get_private_key(&self) -> Result { - let master_xprv = self.master_xpriv()?; - let full_path = self.get_derivation_path()?; + pub fn get_private_key(&self) -> PrivateKey { + let master_xprv = self.master_xpriv().unwrap(); + let full_path = self.get_derivation_path().unwrap(); - let derived = - full_path.extend(DerivationPath::from_str("0/1").map_err(|e| SignerError::DerivationPath(e.to_string()))?); + let derived = full_path.extend( + DerivationPath::from_str("0/1") + .map_err(|e| SignerError::DerivationPath(e.to_string())) + .unwrap(), + ); - let ext_derived = master_xprv.derive_priv(&self.secp, &derived)?; + let ext_derived = master_xprv.derive_priv(&self.secp, &derived).unwrap(); - Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test)) + PrivateKey::new(ext_derived.private_key, NetworkKind::Test) } - pub fn get_blinding_private_key(&self) -> Result { + pub fn get_blinding_private_key(&self) -> PrivateKey { let blinding_key = self - .master_slip77()? - .blinding_private_key(&self.get_address()?.script_pubkey()); + .master_slip77() + .unwrap() + .blinding_private_key(&self.get_address().script_pubkey()); - Ok(PrivateKey::new(blinding_key, NetworkKind::Test)) + PrivateKey::new(blinding_key, NetworkKind::Test) } fn unblind(&self, utxos: Vec) -> Result, SignerError> { let mut unblinded: Vec = Vec::new(); for mut utxo in utxos { - let blinding_key = self.get_blinding_private_key()?; + let blinding_key = self.get_blinding_private_key(); let secrets = utxo.txout.unblind(&self.secp, blinding_key.inner)?; utxo.secrets = Some(secrets); @@ -348,7 +360,7 @@ impl Signer { // use this wpkh address as a change script // TODO: this should be confidential fee_tx.add_output(PartialOutput::new( - self.get_address()?.script_pubkey(), + self.get_address().script_pubkey(), PLACEHOLDER_FEE, self.network.policy_asset(), )); @@ -526,15 +538,14 @@ mod tests { "exist carry drive collect lend cereal occur much tiger just involve mean", Box::new(EsploraProvider::new(url, network)), ) - .unwrap() } #[test] fn keys_correspond_to_address() { let signer = create_signer(); - let address = signer.get_address().unwrap(); - let pubkey = signer.get_ecdsa_public_key().unwrap(); + let address = signer.get_address(); + let pubkey = signer.get_ecdsa_public_key(); let derived_addr = Address::p2wpkh(&pubkey, None, signer.get_provider().get_network().address_params()); @@ -545,7 +556,7 @@ mod tests { fn descriptors() { let signer = create_signer(); - println!("{}", signer.get_address().unwrap()); - println!("{}", signer.get_confidential_address().unwrap()); + println!("{}", signer.get_address()); + println!("{}", signer.get_confidential_address()); } } diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index c0c6c48..7293936 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -1,6 +1,5 @@ use crate::program::ProgramError; use crate::provider::ProviderError; -use crate::transaction::TransactionError; #[derive(Debug, thiserror::Error)] pub enum SignerError { @@ -10,9 +9,6 @@ pub enum SignerError { #[error(transparent)] Provider(#[from] ProviderError), - #[error(transparent)] - Transaction(#[from] TransactionError), - #[error("Failed to parse a mnemonic: {0}")] Mnemonic(String), diff --git a/crates/sdk/src/transaction/error.rs b/crates/sdk/src/transaction/error.rs deleted file mode 100644 index 1591dfc..0000000 --- a/crates/sdk/src/transaction/error.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum TransactionError { - #[error("Invalid signature type requested: {0}")] - SignatureRequest(String), -} diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 4cbe8b4..83610ba 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -9,7 +9,6 @@ use simplicityhl::elements::{ use crate::provider::SimplicityNetwork; use crate::utils::asset_entropy; -use super::error::TransactionError; use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature}; use super::partial_output::PartialOutput; @@ -38,15 +37,9 @@ impl FinalTransaction { } } - pub fn add_input( - &mut self, - partial_input: PartialInput, - required_sig: RequiredSignature, - ) -> Result<(), TransactionError> { + pub fn add_input(&mut self, partial_input: PartialInput, required_sig: RequiredSignature) { if let RequiredSignature::Witness(_) = required_sig { - return Err(TransactionError::SignatureRequest( - "Requested signature is not NativeEcdsa or None".to_string(), - )); + panic!("Requested signature is not NativeEcdsa or None"); } self.inputs.push(FinalInput { @@ -55,8 +48,6 @@ impl FinalTransaction { issuance_input: None, required_sig, }); - - Ok(()) } pub fn add_program_input( @@ -64,11 +55,9 @@ impl FinalTransaction { partial_input: PartialInput, program_input: ProgramInput, required_sig: RequiredSignature, - ) -> Result<(), TransactionError> { + ) { if let RequiredSignature::NativeEcdsa = required_sig { - return Err(TransactionError::SignatureRequest( - "Requested signature is not Witness or None".to_string(), - )); + panic!("Requested signature is not Witness or None"); } self.inputs.push(FinalInput { @@ -77,8 +66,6 @@ impl FinalTransaction { issuance_input: None, required_sig, }); - - Ok(()) } pub fn add_issuance_input( @@ -86,11 +73,9 @@ impl FinalTransaction { partial_input: PartialInput, issuance_input: IssuanceInput, required_sig: RequiredSignature, - ) -> Result { + ) -> AssetId { if let RequiredSignature::Witness(_) = required_sig { - return Err(TransactionError::SignatureRequest( - "Requested signature is not NativeEcdsa or None".to_string(), - )); + panic!("Requested signature is not NativeEcdsa or None"); } let asset_id = AssetId::from_entropy(asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy)); @@ -102,7 +87,7 @@ impl FinalTransaction { required_sig, }); - Ok(asset_id) + asset_id } pub fn add_program_issuance_input( @@ -111,11 +96,9 @@ impl FinalTransaction { program_input: ProgramInput, issuance_input: IssuanceInput, required_sig: RequiredSignature, - ) -> Result { + ) -> AssetId { if let RequiredSignature::NativeEcdsa = required_sig { - return Err(TransactionError::SignatureRequest( - "Requested signature is not Witness or None".to_string(), - )); + panic!("Requested signature is not Witness or None"); } let asset_id = AssetId::from_entropy(asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy)); @@ -127,7 +110,7 @@ impl FinalTransaction { required_sig, }); - Ok(asset_id) + asset_id } pub fn remove_input(&mut self, index: usize) -> Option { diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs index 2816cd8..3bf0c5d 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -1,10 +1,8 @@ -pub mod error; pub mod final_transaction; pub mod partial_input; pub mod partial_output; pub mod utxo; -pub use error::TransactionError; pub use final_transaction::{FinalInput, FinalTransaction}; pub use partial_input::{PartialInput, ProgramInput, RequiredSignature}; pub use partial_output::PartialOutput; diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs index 387bdb3..3dad2bb 100644 --- a/crates/sdk/src/transaction/utxo.rs +++ b/crates/sdk/src/transaction/utxo.rs @@ -1,4 +1,4 @@ -use simplicityhl::elements::{OutPoint, TxOut, TxOutSecrets}; +use simplicityhl::elements::{AssetId, OutPoint, TxOut, TxOutSecrets}; #[derive(Debug, Clone)] pub struct UTXO { @@ -6,3 +6,21 @@ pub struct UTXO { pub txout: TxOut, pub secrets: Option, } + +impl UTXO { + pub fn explicit_asset(&self) -> AssetId { + self.txout.asset.explicit().expect("The UTXO's asset is not explicit") + } + + pub fn explicit_amount(&self) -> u64 { + self.txout.value.explicit().expect("The UTXO's amount is not explicit") + } + + pub fn unblinded_asset(&self) -> AssetId { + self.secrets.expect("The UTXO is not unblinded").asset + } + + pub fn unblinded_amount(&self) -> u64 { + self.secrets.expect("The UTXO is not unblinded").value + } +} diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 6b9771e..7127f25 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -34,7 +34,7 @@ impl TestContext { }) } - pub fn create_signer(&self, mnemonic: &str) -> Result { + pub fn create_signer(&self, mnemonic: &str) -> Signer { let provider: Box = if self._provider_info.elements_url.is_some() { // local regtest or external regtest Box::new(SimplexProvider::new( @@ -42,7 +42,7 @@ impl TestContext { self._provider_info.elements_url.clone().unwrap(), self._provider_info.auth.clone().unwrap(), *self.get_network(), - )?) + )) } else { // external esplora Box::new(EsploraProvider::new( @@ -51,7 +51,7 @@ impl TestContext { )) }; - Ok(Signer::new(mnemonic, provider)?) + Signer::new(mnemonic, provider) } pub fn get_default_signer(&self) -> &Signer { @@ -85,14 +85,14 @@ impl TestContext { rpc.url.clone(), auth.clone(), SimplicityNetwork::default_regtest(), - )?); + )); provider_info = ProviderInfo { esplora_url: esplora.url, elements_url: Some(rpc.url), auth: Some(auth), }; - signer = Signer::new(config.mnemonic.as_str(), provider)?; + signer = Signer::new(config.mnemonic.as_str(), provider); client = None; } None => { @@ -110,13 +110,13 @@ impl TestContext { elements_url: None, auth: None, }; - signer = Signer::new(config.mnemonic.as_str(), provider)?; + signer = Signer::new(config.mnemonic.as_str(), provider); client = None; } }, None => { // simplex inner network - let (regtest_client, regtest_signer) = Regtest::from_config(config.to_regtest_config())?; + let (regtest_client, regtest_signer) = Regtest::new(config.to_regtest_config())?; provider_info = ProviderInfo { esplora_url: regtest_client.esplora_url(), diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index 4b545e8..23ea154 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -1,7 +1,6 @@ use std::io; use smplx_sdk::provider::ProviderError; -use smplx_sdk::signer::SignerError; use smplx_regtest::error::RegtestError; @@ -13,9 +12,6 @@ pub enum TestError { #[error(transparent)] Provider(#[from] ProviderError), - #[error(transparent)] - Signer(#[from] SignerError), - #[error("Failed to deserialize config: '{0}'")] ConfigDeserialize(#[from] toml::de::Error), diff --git a/examples/basic/tests/basic_test.rs b/examples/basic/tests/basic_test.rs index 54496c1..c898292 100644 --- a/examples/basic/tests/basic_test.rs +++ b/examples/basic/tests/basic_test.rs @@ -11,36 +11,35 @@ fn get_p2pk(context: &simplex::TestContext) -> (P2pkProgram, Script) { let signer = context.get_default_signer(); let arguments = P2pkArguments { - public_key: signer.get_schnorr_public_key().unwrap().serialize(), + public_key: signer.get_schnorr_public_key().serialize(), }; let p2pk = P2pkProgram::new(tr_unspendable_key(), arguments); - let p2pk_script = p2pk.get_program().get_script_pubkey(context.get_network()).unwrap(); + let p2pk_script = p2pk.get_program().get_script_pubkey(context.get_network()); (p2pk, p2pk_script) } -fn spend_p2wpkh(context: &simplex::TestContext) -> Txid { +fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result { let signer = context.get_default_signer(); let (_, p2pk_script) = get_p2pk(context); - let res = signer.send(p2pk_script.clone(), 50).unwrap(); - + let res = signer.send(p2pk_script.clone(), 50)?; println!("Broadcast: {}", res); - res + Ok(res) } -fn spend_p2pk(context: &simplex::TestContext) -> Txid { +fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result { let signer = context.get_default_signer(); let provider = context.get_default_provider(); let (p2pk, p2pk_script) = get_p2pk(context); - let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script).unwrap(); + let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script)?; - p2pk_utxos.retain(|utxo| utxo.txout.asset.explicit().unwrap() == context.get_network().policy_asset()); + p2pk_utxos.retain(|utxo| utxo.explicit_asset() == context.get_network().policy_asset()); let mut ft = FinalTransaction::new(); @@ -52,28 +51,26 @@ fn spend_p2pk(context: &simplex::TestContext) -> Txid { PartialInput::new(p2pk_utxos[0].clone()), ProgramInput::new(Box::new(p2pk.get_program().clone()), Box::new(witness.clone())), RequiredSignature::Witness("SIGNATURE".to_string()), - ) - .unwrap(); - - let res = signer.broadcast(&ft).unwrap(); + ); + let res = signer.broadcast(&ft)?; println!("Broadcast: {}", res); - res + Ok(res) } #[simplex::test] fn basic_test(context: simplex::TestContext) -> anyhow::Result<()> { let provider = context.get_default_provider(); - let tx = spend_p2wpkh(&context); - provider.wait(&tx)?; + let tx = spend_p2wpkh(&context)?; + provider.wait(&tx)?; println!("Confirmed"); - let tx = spend_p2pk(&context); - provider.wait(&tx)?; + let tx = spend_p2pk(&context)?; + provider.wait(&tx)?; println!("Confirmed"); Ok(()) diff --git a/examples/basic/tests/confidential_test.rs b/examples/basic/tests/confidential_test.rs index 7ec8564..2c9a940 100644 --- a/examples/basic/tests/confidential_test.rs +++ b/examples/basic/tests/confidential_test.rs @@ -5,28 +5,26 @@ fn confidential_test(context: simplex::TestContext) -> anyhow::Result<()> { let provider = context.get_default_provider(); let alice = context.get_default_signer(); - let bob = context - .create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert") - .unwrap(); + let bob = context.create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert"); let mut ft = FinalTransaction::new(); ft.add_output( PartialOutput::new( - bob.get_address().unwrap().script_pubkey(), + bob.get_address().script_pubkey(), 100, context.get_network().policy_asset(), ) - .with_blinding_key(bob.get_blinding_public_key().unwrap()), + .with_blinding_key(bob.get_blinding_public_key()), ); - let tx = alice.broadcast(&ft).unwrap(); + let tx = alice.broadcast(&ft)?; println!("Broadcast: {}", tx); provider.wait(&tx)?; println!("Confirmed"); - let tx = bob.send(alice.get_address().unwrap().script_pubkey(), 50).unwrap(); + let tx = bob.send(alice.get_address().script_pubkey(), 50)?; println!("Broadcast: {}", tx); provider.wait(&tx)?; From f6232829520a62f41bab4b395e34db4d17ceb751 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Wed, 1 Apr 2026 15:35:47 +0300 Subject: [PATCH 10/10] fix clippy --- crates/cli/src/commands/regtest.rs | 2 +- crates/regtest/src/regtest.rs | 2 +- crates/test/src/context.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/commands/regtest.rs b/crates/cli/src/commands/regtest.rs index 896c457..5cb705c 100644 --- a/crates/cli/src/commands/regtest.rs +++ b/crates/cli/src/commands/regtest.rs @@ -10,7 +10,7 @@ pub struct Regtest {} impl Regtest { pub fn run(config: RegtestConfig) -> Result<(), CommandError> { - let (mut client, signer) = RegtestRunner::new(config)?; + let (mut client, signer) = RegtestRunner::from_config(config)?; let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index 92e103c..e132f71 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -13,7 +13,7 @@ use super::error::RegtestError; pub struct Regtest {} impl Regtest { - pub fn new(config: RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { + pub fn from_config(config: RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { let client = RegtestClient::new(); let provider = Box::new(SimplexProvider::new( diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 7127f25..787ad6a 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -116,7 +116,7 @@ impl TestContext { }, None => { // simplex inner network - let (regtest_client, regtest_signer) = Regtest::new(config.to_regtest_config())?; + let (regtest_client, regtest_signer) = Regtest::from_config(config.to_regtest_config())?; provider_info = ProviderInfo { esplora_url: regtest_client.esplora_url(),