diff --git a/crates/cli/src/commands/regtest.rs b/crates/cli/src/commands/regtest.rs index 4c44eea..5cb705c 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/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 c99386a..e132f71 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -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,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..45c1304 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; @@ -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 { @@ -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)?; @@ -152,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 { @@ -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 fbea454..a5c76da 100644 --- a/crates/sdk/src/provider/core.rs +++ b/crates/sdk/src/provider/core.rs @@ -1,14 +1,24 @@ use std::collections::HashMap; -use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; +use electrsd::bitcoind::bitcoincore_rpc::Auth; + +use simplicityhl::elements::{Address, Script, Transaction, Txid}; use crate::provider::SimplicityNetwork; +use crate::transaction::UTXO; 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; @@ -22,9 +32,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..886c51e 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -6,11 +6,12 @@ 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; 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,10 @@ 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(), + secrets: None, }) .collect()) } @@ -250,7 +250,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 +275,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/mod.rs b/crates/sdk/src/provider/mod.rs index 9cae962..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; +pub use core::{ProviderInfo, ProviderTrait}; pub use esplora::EsploraProvider; pub use rpc::elements::ElementsRpc; pub use simplex::SimplexProvider; diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs index d94bf8b..c34f25b 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 { @@ -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(), + } } } @@ -59,11 +54,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 9439b85..045fb15 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -1,25 +1,22 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; -use elements_miniscript::Descriptor; -use elements_miniscript::bitcoin::PublicKey; -use elements_miniscript::descriptor::Wpkh; - 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; 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}, @@ -27,17 +24,16 @@ use elements_miniscript::{ }, elementssig_to_rawsig, psbt::PsbtExt, + 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; @@ -58,6 +54,7 @@ pub trait SignerTrait { } pub struct Signer { + mnemonic: Mnemonic, xprv: Xpriv, provider: Box, network: SimplicityNetwork, @@ -75,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)) @@ -95,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); @@ -110,27 +107,29 @@ 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 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())); @@ -146,7 +145,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() { @@ -156,15 +155,28 @@ 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)); + + // 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.explicit_amount(), + }; + let b_value = match b.secrets { + Some(secrets) => secrets.value, + None => b.explicit_amount(), + }; + + 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)? { @@ -173,11 +185,11 @@ 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 - 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)? { @@ -194,7 +206,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)); @@ -213,66 +225,129 @@ 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_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().unwrap()[0].clone(); - let desc = format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)"); + descriptor + .at_derivation_index(1) + .unwrap() + .address(&self.secp, self.network.address_params()) + .unwrap() + } - let descriptor: Descriptor = - Descriptor::Wpkh(Wpkh::from_str(&desc).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.clone().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_wpkh_utxos(&self) -> Result, SignerError> { - self.get_wpkh_utxos_filter(|_| 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(|(_, txout)| txout.asset.explicit().unwrap() == asset) + pub fn get_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { + self.get_utxos_filter(&|utxo| utxo.explicit_asset() == asset, &|utxo| { + utxo.unblinded_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(|(outpoint, _)| 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(&self, filter: F) -> Result, SignerError> - where - F: FnMut(&(OutPoint, TxOut)) -> bool, - { - let mut utxos = self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?; + 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_confidential_address())?; + + // 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 + all_utxos.extend(confidential_utxos); + + Ok(all_utxos) + } - utxos.retain(filter); + 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(utxos) + keypair.x_only_public_key().0 } - pub fn get_schnorr_public_key(&self) -> Result { - let private_key = self.get_private_key()?; - let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); + pub fn get_ecdsa_public_key(&self) -> PublicKey { + self.get_private_key().public_key(&self.secp) + } - Ok(keypair.x_only_public_key().0) + pub fn get_blinding_public_key(&self) -> PublicKey { + self.get_blinding_private_key().public_key(&self.secp) } - pub fn get_ecdsa_public_key(&self) -> Result { - Ok(self.get_private_key()?.public_key(&self.secp)) + 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())) + .unwrap(), + ); + + let ext_derived = master_xprv.derive_priv(&self.secp, &derived).unwrap(); + + PrivateKey::new(ext_derived.private_key, NetworkKind::Test) + } + + pub fn get_blinding_private_key(&self) -> PrivateKey { + let blinding_key = self + .master_slip77() + .unwrap() + .blinding_private_key(&self.get_address().script_pubkey()); + + PrivateKey::new(blinding_key, NetworkKind::Test) } - pub fn get_private_key(&self) -> Result { - let master_xprv = self.master_xpriv()?; - let full_path = self.get_derivation_path()?; + fn unblind(&self, utxos: Vec) -> Result, SignerError> { + let mut unblinded: Vec = Vec::new(); - let derived = - full_path.extend(DerivationPath::from_str("0/1").map_err(|e| SignerError::DerivationPath(e.to_string()))?); + for mut utxo in utxos { + let blinding_key = self.get_blinding_private_key(); + let secrets = utxo.txout.unblind(&self.secp, blinding_key.inner)?; - let ext_derived = master_xprv.derive_priv(&self.secp, &derived)?; + utxo.secrets = Some(secrets); - Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test)) + unblinded.push(utxo); + } + + Ok(unblinded) } fn estimate_tx( @@ -283,8 +358,9 @@ impl Signer { ) -> Result { // estimate the tx fee with the change // use this wpkh address as a change script + // 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(), )); @@ -296,7 +372,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 @@ -314,7 +390,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)); @@ -332,9 +408,13 @@ 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(); + 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 { @@ -393,6 +473,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 +501,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 +530,33 @@ 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(); + } - let address = signer.get_wpkh_address().unwrap(); - let pubkey = signer.get_ecdsa_public_key().unwrap(); + #[test] + fn keys_correspond_to_address() { + let signer = create_signer(); + + let address = signer.get_address(); + let pubkey = signer.get_ecdsa_public_key(); - 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_address()); + println!("{}", signer.get_confidential_address()); + } } diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index 6f91f07..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,15 +9,18 @@ pub enum SignerError { #[error(transparent)] Provider(#[from] ProviderError), - #[error(transparent)] - Transaction(#[from] TransactionError), - #[error("Failed to parse a mnemonic: {0}")] Mnemonic(String), #[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 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), @@ -43,6 +45,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), 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 5150ccd..83610ba 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -1,10 +1,14 @@ -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; -use super::error::TransactionError; use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature}; use super::partial_output::PartialOutput; @@ -20,29 +24,22 @@ pub struct FinalInput { #[derive(Clone)] pub struct FinalTransaction { - pub network: SimplicityNetwork, inputs: Vec, outputs: Vec, } impl FinalTransaction { - pub fn new(network: SimplicityNetwork) -> Self { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { Self { - network, inputs: Vec::new(), outputs: Vec::new(), } } - 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 { @@ -51,8 +48,6 @@ impl FinalTransaction { issuance_input: None, required_sig, }); - - Ok(()) } pub fn add_program_input( @@ -60,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 { @@ -73,8 +66,6 @@ impl FinalTransaction { issuance_input: None, required_sig, }); - - Ok(()) } pub fn add_issuance_input( @@ -82,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)); @@ -98,7 +87,7 @@ impl FinalTransaction { required_sig, }); - Ok(asset_id) + asset_id } pub fn add_program_issuance_input( @@ -107,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)); @@ -123,7 +110,7 @@ impl FinalTransaction { required_sig, }); - Ok(asset_id) + asset_id } pub fn remove_input(&mut self, index: usize) -> Option { @@ -170,17 +157,34 @@ impl FinalTransaction { self.outputs.len() } - 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 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; + + 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 @@ -192,29 +196,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 { + // 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/mod.rs b/crates/sdk/src/transaction/mod.rs index 41c0ba3..3bf0c5d 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -1,9 +1,9 @@ -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 ce597c5..e3122e3 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -1,10 +1,12 @@ 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, TxOutSecrets, Txid}; use crate::program::ProgramTrait; use crate::program::WitnessTrait; +use super::UTXO; + #[derive(Debug, Clone)] pub enum RequiredSignature { None, @@ -18,8 +20,12 @@ pub struct PartialInput { pub witness_output_index: u32, 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, } #[derive(Clone)] @@ -35,30 +41,40 @@ 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 { + 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, - sequence, + witness_txid: utxo.outpoint.txid, + witness_output_index: utxo.outpoint.vout, + witness_utxo: utxo.txout, + sequence: Sequence::default(), + locktime: LockTime::ZERO, amount, asset, + secrets: utxo.secrets, } } + 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, @@ -66,12 +82,24 @@ 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, + }; + // 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() @@ -93,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 } } diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs new file mode 100644 index 0000000..3dad2bb --- /dev/null +++ b/crates/sdk/src/transaction/utxo.rs @@ -0,0 +1,26 @@ +use simplicityhl::elements::{AssetId, OutPoint, TxOut, TxOutSecrets}; + +#[derive(Debug, Clone)] +pub struct UTXO { + pub outpoint: OutPoint, + 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/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)?) } diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 9b4be0c..787ad6a 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) -> Signer { + 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(), + )) + }; + + 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,13 +81,18 @@ 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(), - )?); + )); - signer = Signer::new(config.mnemonic.as_str(), provider)?; + 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; } None => { @@ -74,9 +103,14 @@ 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)); - signer = Signer::new(config.mnemonic.as_str(), provider)?; + 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/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/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 59% rename from examples/basic/tests/example_test.rs rename to examples/basic/tests/basic_test.rs index 9a7ab3c..c898292 100644 --- a/examples/basic/tests/example_test.rs +++ b/examples/basic/tests/basic_test.rs @@ -8,41 +8,40 @@ 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(), + 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 { - let signer = context.get_signer(); +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 { - let signer = context.get_signer(); - let provider = context.get_provider(); +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(|el| el.1.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(*context.get_network()); + let mut ft = FinalTransaction::new(); let witness = P2pkWitness { signature: DUMMY_SIGNATURE, @@ -52,31 +51,27 @@ 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 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)?; + 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"); - 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..2c9a940 --- /dev/null +++ b/examples/basic/tests/confidential_test.rs @@ -0,0 +1,34 @@ +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"); + + let mut ft = FinalTransaction::new(); + + ft.add_output( + PartialOutput::new( + bob.get_address().script_pubkey(), + 100, + context.get_network().policy_asset(), + ) + .with_blinding_key(bob.get_blinding_public_key()), + ); + + let tx = alice.broadcast(&ft)?; + println!("Broadcast: {}", tx); + + provider.wait(&tx)?; + println!("Confirmed"); + + let tx = bob.send(alice.get_address().script_pubkey(), 50)?; + println!("Broadcast: {}", tx); + + provider.wait(&tx)?; + println!("Confirmed"); + + Ok(()) +}