From de219be530d9eb41e9c6c4d40129b49b5da89c71 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 1 Jul 2026 14:11:57 +0300 Subject: [PATCH 1/6] sdk: add KeyProvider trait to move logic for determining keys into separate entity * add HDKeyOrigin and SingleKeyOrigin * impement KeyProvider trait for them and adapt tests --- crates/regtest/src/regtest.rs | 14 +- crates/sdk/src/provider/network.rs | 18 + crates/sdk/src/signer/core.rs | 588 ++++++++++++++-------- crates/sdk/src/signer/mod.rs | 2 +- crates/test/src/context.rs | 23 +- examples/basic/tests/confidential_test.rs | 13 +- fixtures/tests/confidential_test.rs | 13 +- fixtures/tests/reissuance_test.rs | 18 +- 8 files changed, 451 insertions(+), 238 deletions(-) diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index b4c27576..8e042ddc 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -3,7 +3,8 @@ use std::time::Duration; use smplx_sdk::provider::ElementsRpc; use smplx_sdk::provider::SimplexProvider; use smplx_sdk::provider::SimplicityNetwork; -use smplx_sdk::signer::Signer; +use smplx_sdk::signer::core::HDKeyOrigin; +use smplx_sdk::signer::{KeyProvider, Signer}; use smplx_sdk::utils::btc2sat; use super::RegtestConfig; @@ -23,7 +24,7 @@ impl Regtest { /// /// # Panics /// Panics if the background indexer (`electrs`) fails to index the unspent outputs within the timeout window (10 seconds). - pub fn from_config(config: &RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { + pub fn from_config(config: &RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { let client = RegtestClient::new(config); let provider = Box::new(SimplexProvider::new( @@ -33,14 +34,19 @@ impl Regtest { SimplicityNetwork::default_regtest(), )); - let signer = Signer::new(config.mnemonic.as_str(), provider); + let hd_key_origin = HDKeyOrigin::new(config.mnemonic.as_str())?; + let signer = Signer::new(hd_key_origin, provider); Self::prepare_signer(&client, &signer, config.bitcoins)?; Ok((client, signer)) } - fn prepare_signer(client: &RegtestClient, signer: &Signer, bitcoins: u64) -> Result<(), RegtestError> { + fn prepare_signer( + client: &RegtestClient, + signer: &Signer, + bitcoins: u64, + ) -> Result<(), RegtestError> { let rpc_provider = ElementsRpc::new(client.rpc_url(), client.auth())?; rpc_provider.generate_blocks(1)?; diff --git a/crates/sdk/src/provider/network.rs b/crates/sdk/src/provider/network.rs index 1b9ffeb0..88d7354d 100644 --- a/crates/sdk/src/provider/network.rs +++ b/crates/sdk/src/provider/network.rs @@ -3,6 +3,8 @@ use std::str::FromStr; use simplicityhl::simplicity::elements; use simplicityhl::simplicity::hashes::{Hash, sha256}; +use elements_miniscript::bitcoin::NetworkKind; + use crate::constants::{LIQUID_DEFAULT_REGTEST_ASSET_STR, LIQUID_POLICY_ASSET_STR, LIQUID_TESTNET_POLICY_ASSET_STR}; /// The default Bitcoin `AssetId` used on Liquid testnet. @@ -101,3 +103,19 @@ impl SimplicityNetwork { } } } + +impl From for NetworkKind { + fn from(value: SimplicityNetwork) -> Self { + (&value).into() + } +} + +impl From<&SimplicityNetwork> for NetworkKind { + fn from(value: &SimplicityNetwork) -> Self { + if value.is_mainnet() { + NetworkKind::Main + } else { + NetworkKind::Test + } + } +} diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index dc53b4d2..e70562b2 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -5,7 +5,7 @@ use std::sync::Arc; 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::secp256k1_zkp::{All, Keypair, Message, Secp256k1, SecretKey, ecdsa, schnorr}; use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Transaction, Txid}; use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; use simplicityhl::simplicity::hashes::Hash; @@ -66,53 +66,298 @@ pub trait SignerTrait { ) -> Result<(PublicKey, ecdsa::Signature), SignerError>; } +/// A generalized interface for providing cryptographic keys and addresses. +/// +/// This trait abstracts the origin of the wallet's keys, allowing the `Signer` to remain +/// agnostic to whether the keys are derived from a BIP39 mnemonic, a hardware wallet, +/// or a single injected secret key. +pub trait KeyProvider { + /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. + #[must_use] + fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey; + + /// Resolves the standard format ECDSA public key. + #[must_use] + fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey; + + /// Resolves the corresponding blinding public key. + #[must_use] + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey; + + /// Internally derives and exposes the wallet's signing active private key. + /// + /// # Panics + /// Panics if the master private key or derivation path cannot be derived. + #[must_use] + fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey; + + /// Generates the private key linked to confidential payload blinding. + /// + /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind. + /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address. + /// + /// # Panics + /// Panics if the master SLIP77 key cannot be derived. + #[must_use] + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey; + + /// Returns the confidential elements address matching the local wallet logic. + /// + /// # Panics + /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. + #[must_use] + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; + + /// Returns the standard unblinded address matching the local wallet logic. + /// + /// # Panics + /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. + #[must_use] + fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; +} + /// Core interface responsible for managing keys, interfacing with the blockchain provider, /// assembling descriptors, estimating fees, and finalizing/signing transactions. -pub struct Signer { - mnemonic: Mnemonic, - xprv: Xpriv, +pub struct Signer { + key_origin: K, provider: Box, network: SimplicityNetwork, secp: Secp256k1, } -impl SignerTrait for Signer { - fn sign_program( - &self, - pst: &PartiallySignedTransaction, - program: &dyn ProgramTrait, - input_index: usize, - network: &SimplicityNetwork, - ) -> Result { - let env = program.get_env(pst, input_index, network)?; - let msg = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); +/// A Hierarchical Deterministic (HD) key provider based on BIP39 and SLIP77. +/// +/// `HDKeyOrigin` derives its key material from a standard mnemonic seed phrase. +/// It handles BIP32 derivation paths for standard transaction signing and uses +/// SLIP77 to generate deterministic blinding keys for confidential transactions +/// on the Elements/Liquid network. +pub struct HDKeyOrigin { + xprv: Xpriv, + master_blinding: MasterBlindingKey, +} - let private_key = self.get_private_key(); - let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); +impl HDKeyOrigin { + /// Constructs a new `HDKeyOrigin` from a BIP39 mnemonic phrase. + /// + /// # Errors + /// Returns a `SignerError::Mnemonic` if the provided phrase is invalid. + pub fn new(mnemonic: &str) -> Result { + let mnemonic: Mnemonic = mnemonic + .parse() + .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?; + let seed = mnemonic.to_seed(""); + let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?; - Ok(self.secp.sign_schnorr(&msg, &keypair)) + let master_blinding_key = MasterBlindingKey::from_seed(&seed[..]); + + Ok(Self { + master_blinding: master_blinding_key, + xprv, + }) } - fn sign_input( - &self, - pst: &PartiallySignedTransaction, - input_index: usize, - ) -> Result<(PublicKey, ecdsa::Signature), SignerError> { - let tx = pst.extract_tx()?; + fn derive_xpriv(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { + Ok(self.xprv.derive_priv(secp, path)?) + } - let mut sighash_cache = SighashCache::new(&tx); - let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros(); + fn master_xpriv(&self, secp: &Secp256k1) -> Result { + self.derive_xpriv(&DerivationPath::master(), secp) + } - let message = pst - .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)? - .to_secp_msg(); + fn derive_xpub(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { + let derived = self.derive_xpriv(path, secp)?; - let private_key = self.get_private_key(); - let public_key = private_key.public_key(&self.secp); + Ok(Xpub::from_priv(secp, &derived)) + } - let signature = self.secp.sign_ecdsa_low_r(&message, &private_key.inner); + fn master_xpub(&self, secp: &Secp256k1) -> Result { + self.derive_xpub(&DerivationPath::master(), secp) + } - Ok((public_key, signature)) + fn fingerprint(&self, secp: &Secp256k1) -> Result { + Ok(self.master_xpub(secp)?.fingerprint()) + } + + fn get_slip77_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { + let wpkh_descriptor = self.get_wpkh_descriptor(secp, network)?; + let blinding_key = self.master_blinding; + + Ok(format!("ct(slip77({blinding_key}),{wpkh_descriptor})")) + } + + fn get_wpkh_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { + let fingerprint = self.fingerprint(secp)?; + let path = self.get_derivation_path(network)?; + let xpub = self.derive_xpub(&path, secp)?; + + Ok(format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)")) + } + + #[allow(clippy::unused_self)] + fn get_derivation_path(&self, network: &SimplicityNetwork) -> Result { + let coin_type = if network.is_mainnet() { 1776 } else { 1 }; + let path = format!("84h/{coin_type}h/0h"); + + DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string())) + } +} + +impl KeyProvider for HDKeyOrigin { + /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. + fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey { + let private_key = self.get_private_key(secp, network); + let keypair = Keypair::from_secret_key(secp, &private_key.inner); + + keypair.x_only_public_key().0 + } + + /// Resolves the standard format ECDSA public key. + fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_private_key(secp, network).public_key(secp) + } + + /// Resolves the corresponding blinding public key. + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_blinding_private_key(secp, network).public_key(secp) + } + + /// Internally derives and exposes the wallet's signing active private key. + /// + /// # Panics + /// Panics if the master private key or derivation path cannot be derived. + fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + let master_xprv = self.master_xpriv(secp).unwrap(); + let full_path = self.get_derivation_path(network).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(secp, &derived).unwrap(); + + PrivateKey::new(ext_derived.private_key, NetworkKind::Test) + } + + /// Generates the private key linked to confidential payload blinding. + /// + /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind. + /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address. + /// + /// # Panics + /// Panics if the master SLIP77 key cannot be derived. + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + let blinding_key = self + .master_blinding + .blinding_private_key(&self.get_address(secp, network).script_pubkey()); + + PrivateKey::new(blinding_key, NetworkKind::Test) + } + + /// Returns the confidential elements address matching the local wallet logic. + /// + /// # Panics + /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let mut descriptor = ConfidentialDescriptor::::from_str( + &self.get_slip77_descriptor(secp, network).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(); + + descriptor + .at_derivation_index(1) + .unwrap() + .address(secp, network.address_params()) + .unwrap() + } + + /// Returns the standard unblinded address matching the local wallet logic. + /// + /// # Panics + /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. + fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor(secp, network).unwrap()) + .map_err(|e| SignerError::WpkhDescriptor(e.to_string())) + .unwrap(); + + descriptor.into_single_descriptors().unwrap()[0] + .at_derivation_index(1) + .unwrap() + .address(network.address_params()) + .unwrap() + } +} + +/// A simplified key provider powered by a single static secret key. +/// +/// Unlike `HDKeyOrigin` which derives paths hierarchically, `SingleKeyOrigin` uses +/// exactly one `SecretKey` for all signing operations. It can optionally accept a +/// `MasterBlindingKey` to support confidential transactions and blinded addresses. +pub struct SingleKeyOrigin { + secret_key: SecretKey, + blinding_key: Option, +} + +impl SingleKeyOrigin { + /// Creates a new `SingleKeyOrigin`. + /// + /// # Arguments + /// * `secret_key` - The base static secret key used for ECDSA and Schnorr signatures. + /// * `blinding_key` - An optional SLIP77 master blinding key. + #[must_use] + pub fn new(secret_key: SecretKey, blinding_key: Option) -> Self { + Self { + secret_key, + blinding_key, + } + } +} + +impl KeyProvider for SingleKeyOrigin { + fn get_schnorr_public_key(&self, secp: &Secp256k1, _network: &SimplicityNetwork) -> XOnlyPublicKey { + let keypair = Keypair::from_secret_key(secp, &self.secret_key); + keypair.x_only_public_key().0 + } + + fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_private_key(secp, network).public_key(secp) + } + + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_blinding_private_key(secp, network).public_key(secp) + } + + fn get_private_key(&self, _secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + PrivateKey::new(self.secret_key, network) + } + + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + let master_blinding = self + .blinding_key + .expect("Blinding key is required for confidential operations"); + + let script_pubkey = self.get_address(secp, network).script_pubkey(); + let blinding_key = master_blinding.blinding_private_key(&script_pubkey); + + PrivateKey::new(blinding_key, network) + } + + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let ecdsa_pubkey = self.get_ecdsa_public_key(secp, network); + let blinding_pubkey = self.get_blinding_public_key(secp, network); + + Address::p2wpkh(&ecdsa_pubkey, Some(blinding_pubkey.inner), network.address_params()) + } + + fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let ecdsa_pubkey = self.get_ecdsa_public_key(secp, network); + + Address::p2wpkh(&ecdsa_pubkey, None, network.address_params()) } } @@ -121,32 +366,39 @@ enum Estimate { Failure(u64), } -impl Signer { - /// Creates a new `Signer` instance seeded from the provided mnemonic and paired with the specified provider. - /// - /// # Panics - /// Panics if the mnemonic fails to parse, or if deriving the master private key fails. +impl Signer { + /// Returns a reference to the active configured network provider. #[must_use] - 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())) - .unwrap(); - let seed = mnemonic.to_seed(""); - let xprv = Xpriv::new_master(NetworkKind::Test, &seed).unwrap(); + pub fn get_provider(&self) -> &dyn ProviderTrait { + self.provider.as_ref() + } +} +impl Signer { + /// Creates a new `Signer` instance seeded from the provided key origin and paired with the specified provider. + #[must_use] + pub fn new(key_origin: K, provider: Box) -> Self { + let secp = Secp256k1::new(); let network = *provider.get_network(); Self { - mnemonic, - xprv, + key_origin, provider, network, secp, } } + /// Evaluates, funds, and broadcasts an already assembled `FinalTransaction`. + /// + /// # Errors + /// Returns a `SignerError` if finalizing the payload fails or if the network rejects the broadcast. + pub fn broadcast(&self, tx: &FinalTransaction) -> Result, SignerError> { + let (tx, _fee) = self.finalize(tx)?; + + Ok(self.provider.broadcast_transaction(&tx)?) + } + /// Composes, funds, and broadcasts a standard network transaction sending the specified value of the primary policy asset. /// /// # Errors @@ -162,14 +414,41 @@ impl Signer { Ok(self.provider.broadcast_transaction(&tx)?) } - /// Evaluates, funds, and broadcasts an already assembled `FinalTransaction`. - /// - /// # Errors - /// Returns a `SignerError` if finalizing the payload fails or if the network rejects the broadcast. - pub fn broadcast(&self, tx: &FinalTransaction) -> Result, SignerError> { - let (tx, _fee) = self.finalize(tx)?; + /// Proxies to the underlying key provider to get the Schnorr public key. + pub fn get_schnorr_public_key(&self) -> XOnlyPublicKey { + self.key_origin.get_schnorr_public_key(&self.secp, &self.network) + } - Ok(self.provider.broadcast_transaction(&tx)?) + /// Proxies to the underlying key provider to get the ECDSA public key. + pub fn get_ecdsa_public_key(&self) -> PublicKey { + self.key_origin.get_ecdsa_public_key(&self.secp, &self.network) + } + + /// Proxies to the underlying key provider to get the blinding public key. + pub fn get_blinding_public_key(&self) -> PublicKey { + self.key_origin.get_blinding_public_key(&self.secp, &self.network) + } + + /// Proxies to the underlying key provider to get the active private key. + pub fn get_private_key(&self) -> PrivateKey { + self.key_origin.get_private_key(&self.secp, &self.network) + } + + /// Proxies to the underlying key provider to get the blinding private key. + pub fn get_blinding_private_key(&self) -> PrivateKey { + self.key_origin.get_blinding_private_key(&self.secp, &self.network) + } + + /// Proxies to the underlying key provider to get the confidential address. + #[must_use] + pub fn get_confidential_address(&self) -> Address { + self.key_origin.get_confidential_address(&self.secp, &self.network) + } + + /// Proxies to the underlying key provider to get the standard unblinded address. + #[must_use] + pub fn get_address(&self) -> Address { + self.key_origin.get_address(&self.secp, &self.network) } /// Evaluates the input components of a `FinalTransaction`, iteratively selecting available wallet UTXOs to cover outputs and estimated fees. @@ -257,50 +536,6 @@ impl Signer { } } - /// Returns a reference to the active configured network provider. - #[must_use] - pub fn get_provider(&self) -> &dyn ProviderTrait { - self.provider.as_ref() - } - - /// Returns the confidential elements address matching the local wallet logic. - /// - /// # Panics - /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. - #[must_use] - 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(); - - descriptor - .at_derivation_index(1) - .unwrap() - .address(&self.secp, self.network.address_params()) - .unwrap() - } - - /// Returns the standard unblinded address matching the local wallet logic. - /// - /// # Panics - /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. - #[must_use] - 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(); - - descriptor.into_single_descriptors().unwrap()[0] - .at_derivation_index(1) - .unwrap() - .address(self.network.address_params()) - .unwrap() - } - /// Iterates against the network provider to select and unblind all known UTXOs. /// /// # Errors @@ -337,7 +572,9 @@ impl Signer { confidential_filter: &dyn Fn(&UTXO) -> bool, ) -> Result, SignerError> { // fetch explicit and confidential utxos - let mut all_utxos = self.provider.fetch_address_utxos(&self.get_confidential_address())?; + let mut all_utxos = self + .provider + .fetch_address_utxos(&self.key_origin.get_confidential_address(&self.secp, &self.network))?; // filter out only confidential utxos and unblind them let mut confidential_utxos = self.unblind( @@ -359,64 +596,6 @@ impl Signer { Ok(all_utxos) } - /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. - #[must_use] - 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); - - keypair.x_only_public_key().0 - } - - /// Resolves the standard format ECDSA public key. - #[must_use] - pub fn get_ecdsa_public_key(&self) -> PublicKey { - self.get_private_key().public_key(&self.secp) - } - - /// Resolves the corresponding blinding public key. - #[must_use] - pub fn get_blinding_public_key(&self) -> PublicKey { - self.get_blinding_private_key().public_key(&self.secp) - } - - /// Internally derives and exposes the wallet's signing active private key. - /// - /// # Panics - /// Panics if the master private key or derivation path cannot be derived. - #[must_use] - 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) - } - - /// Generates the private key linked to confidential payload blinding. - /// - /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind. - /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address. - /// - /// # Panics - /// Panics if the master SLIP77 key cannot be derived. - #[must_use] - 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) - } - fn unblind(&self, utxos: Vec) -> Result, SignerError> { let mut unblinded: Vec = Vec::new(); @@ -539,6 +718,44 @@ impl Signer { Ok(pst.extract_tx()?) } + fn sign_program( + &self, + pst: &PartiallySignedTransaction, + program: &dyn ProgramTrait, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result { + 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 keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); + + Ok(self.secp.sign_schnorr(&msg, &keypair)) + } + + fn sign_input( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + ) -> Result<(PublicKey, ecdsa::Signature), SignerError> { + let tx = pst.extract_tx()?; + + let mut sighash_cache = SighashCache::new(&tx); + let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros(); + + let message = pst + .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)? + .to_secp_msg(); + + 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); + + Ok((public_key, signature)) + } + fn get_signed_program_witness( &self, pst: &PartiallySignedTransaction, @@ -584,57 +801,6 @@ impl Signer { Ok(WitnessValues::from(hm)) } - - #[allow(clippy::unnecessary_wraps)] - 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)?) - } - - fn master_xpriv(&self) -> Result { - self.derive_xpriv(&DerivationPath::master()) - } - - fn derive_xpub(&self, path: &DerivationPath) -> Result { - let derived = self.derive_xpriv(path)?; - - Ok(Xpub::from_priv(&self.secp, &derived)) - } - - fn master_xpub(&self) -> Result { - self.derive_xpub(&DerivationPath::master()) - } - - fn fingerprint(&self) -> Result { - 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"); - - DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string())) - } } #[cfg(test)] @@ -644,11 +810,13 @@ mod tests { use super::*; - fn create_signer() -> Signer { + fn create_signer() -> Signer { let url = "https://blockstream.info/liquidtestnet/api".to_string(); let network = SimplicityNetwork::LiquidTestnet; - Signer::new(random_mnemonic().as_str(), Box::new(EsploraProvider::new(url, network))) + let hd_origin = HDKeyOrigin::new(random_mnemonic().as_str()).unwrap(); + + Signer::new(hd_origin, Box::new(EsploraProvider::new(url, network))) } #[test] diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs index dc8007e9..da24a8aa 100644 --- a/crates/sdk/src/signer/mod.rs +++ b/crates/sdk/src/signer/mod.rs @@ -5,5 +5,5 @@ pub mod error; /// Utilities for injecting witness data bindings into Simplicity environments. mod wtns_injector; -pub use core::{Signer, SignerTrait}; +pub use core::{HDKeyOrigin, KeyProvider, Signer, SignerTrait, SingleKeyOrigin}; pub use error::SignerError; diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 92ce061b..43418612 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -9,7 +9,7 @@ use smplx_sdk::global::GlobalConfig; use smplx_sdk::provider::{ ElementsRpc, EsploraProvider, ProviderInfo, ProviderTrait, SimplexProvider, SimplicityNetwork, }; -use smplx_sdk::signer::Signer; +use smplx_sdk::signer::{HDKeyOrigin, Signer}; use smplx_sdk::utils::random_mnemonic; use crate::config::TestConfig; @@ -22,7 +22,7 @@ pub struct TestContext { // since providers can't be cloned, we need this variable to create new signers _provider_info: ProviderInfo, config: TestConfig, - signer: Signer, + signer: Signer, } impl TestContext { @@ -42,7 +42,7 @@ impl TestContext { }) } - pub fn create_signer(&self, mnemonic: &str) -> Signer { + 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( @@ -59,14 +59,15 @@ impl TestContext { )) }; - Signer::new(mnemonic, provider) + let hd_key_origin = HDKeyOrigin::new(mnemonic).unwrap(); + Signer::new(hd_key_origin, provider) } - pub fn random_signer(&self) -> Signer { + pub fn random_signer(&self) -> Signer { self.create_signer(random_mnemonic().as_str()) } - pub fn get_default_signer(&self) -> &Signer { + pub fn get_default_signer(&self) -> &Signer { &self.signer } @@ -100,10 +101,10 @@ impl TestContext { NetworkUtils::new(regtest_rpc, esplora) } - fn setup(config: &TestConfig) -> Result<(Signer, ProviderInfo, Option), TestError> { + fn setup(config: &TestConfig) -> Result<(Signer, ProviderInfo, Option), TestError> { let client: Option; let provider_info: ProviderInfo; - let signer: Signer; + let signer: Signer; match config.esplora.clone() { Some(esplora) => match config.rpc.clone() { @@ -122,7 +123,8 @@ impl TestContext { elements_url: Some(rpc.url), auth: Some(auth), }; - signer = Signer::new(config.mnemonic.as_str(), provider); + let hd_key_origin = HDKeyOrigin::new(config.mnemonic.as_str()).unwrap(); + signer = Signer::new(hd_key_origin, provider); client = None; } None => { @@ -140,7 +142,8 @@ impl TestContext { elements_url: None, auth: None, }; - signer = Signer::new(config.mnemonic.as_str(), provider); + let hd_key_origin = HDKeyOrigin::new(config.mnemonic.as_str()).unwrap(); + signer = Signer::new(hd_key_origin, provider); client = None; } }, diff --git a/examples/basic/tests/confidential_test.rs b/examples/basic/tests/confidential_test.rs index f5bbab22..194d3e33 100644 --- a/examples/basic/tests/confidential_test.rs +++ b/examples/basic/tests/confidential_test.rs @@ -1,10 +1,14 @@ use simplex::simplicityhl::elements::AssetId; -use simplex::signer::Signer; +use simplex::signer::{KeyProvider, Signer}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt}; -fn make_confidential_to_bob<'a>(alice: &'a Signer, bob: &Signer, asset: AssetId) -> anyhow::Result> { +fn make_confidential_to_bob<'a, K1: KeyProvider, K2: KeyProvider>( + alice: &'a Signer, + bob: &Signer, + asset: AssetId, +) -> anyhow::Result> { let mut ft = FinalTransaction::new(); ft.add_output( @@ -18,7 +22,10 @@ fn make_confidential_to_bob<'a>(alice: &'a Signer, bob: &Signer, asset: AssetId) Ok(tx_receipt) } -fn issue_confidential_to_alice<'a>(alice: &Signer, bob: &'a Signer) -> anyhow::Result> { +fn issue_confidential_to_alice<'a, K1: KeyProvider, K2: KeyProvider>( + alice: &Signer, + bob: &'a Signer, +) -> anyhow::Result> { let utxos = bob.get_utxos()?; let mut ft = FinalTransaction::new(); diff --git a/fixtures/tests/confidential_test.rs b/fixtures/tests/confidential_test.rs index 5e1784d8..e881c931 100644 --- a/fixtures/tests/confidential_test.rs +++ b/fixtures/tests/confidential_test.rs @@ -1,10 +1,14 @@ use simplex::simplicityhl::elements::AssetId; -use simplex::signer::Signer; +use simplex::signer::{KeyProvider, Signer}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; -fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> anyhow::Result<()> { +fn make_confidential_to_bob( + alice: &Signer, + bob: &Signer, + asset: AssetId, +) -> anyhow::Result<()> { let mut ft = FinalTransaction::new(); ft.add_output( @@ -18,7 +22,10 @@ fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> any Ok(()) } -fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result<()> { +fn issue_confidential_to_alice( + alice: &Signer, + bob: &Signer, +) -> anyhow::Result<()> { let utxos = bob.get_utxos()?; let mut ft = FinalTransaction::new(); diff --git a/fixtures/tests/reissuance_test.rs b/fixtures/tests/reissuance_test.rs index a4e4f2ca..f7167c4e 100644 --- a/fixtures/tests/reissuance_test.rs +++ b/fixtures/tests/reissuance_test.rs @@ -1,12 +1,16 @@ use simplex::simplicityhl::elements::AssetId; -use simplex::signer::Signer; +use simplex::signer::{KeyProvider, Signer}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{ FinalTransaction, IssuanceDetails, PartialInput, PartialOutput, RequiredSignature, TxReceipt, }; -fn make_confidential_to_bob<'a>(alice: &'a Signer, bob: &Signer, asset: AssetId) -> anyhow::Result> { +fn make_confidential_to_bob<'a, K1: KeyProvider, K2: KeyProvider>( + alice: &'a Signer, + bob: &Signer, + asset: AssetId, +) -> anyhow::Result> { let mut ft = FinalTransaction::new(); ft.add_output( @@ -20,9 +24,9 @@ fn make_confidential_to_bob<'a>(alice: &'a Signer, bob: &Signer, asset: AssetId) Ok(tx_receipt) } -fn issue_explicit_to_alice_with_reissuance<'a>( - alice: &Signer, - bob: &'a Signer, +fn issue_explicit_to_alice_with_reissuance<'a, K1: KeyProvider, K2: KeyProvider>( + alice: &Signer, + bob: &'a Signer, ) -> anyhow::Result<(TxReceipt<'a>, IssuanceDetails)> { let utxos = bob.get_utxos()?; @@ -54,8 +58,8 @@ fn issue_explicit_to_alice_with_reissuance<'a>( Ok((tx_receipt, issuance_details)) } -fn reissue_tokens_to_bob<'a>( - bob: &'a Signer, +fn reissue_tokens_to_bob<'a, K: KeyProvider>( + bob: &'a Signer, issuance_details: &IssuanceDetails, reissuance_amount: u64, ) -> anyhow::Result> { From d2a72a01e420e5a14f36417ddb921f3bc5c4fe40 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 1 Jul 2026 18:01:18 +0300 Subject: [PATCH 2/6] sdk: rename structs, fix lints rename: * KeyProvider - KeyOrigin * HDKeyOrigin - HDKey * SingleKeyOrigin - SingleKey remove network conversion to bitcoin type --- crates/regtest/src/regtest.rs | 10 ++-- crates/sdk/src/provider/network.rs | 18 -------- crates/sdk/src/signer/core.rs | 56 +++++++---------------- crates/sdk/src/signer/mod.rs | 2 +- crates/test/src/context.rs | 20 ++++---- examples/basic/tests/confidential_test.rs | 6 +-- fixtures/tests/confidential_test.rs | 6 +-- fixtures/tests/reissuance_test.rs | 8 ++-- 8 files changed, 43 insertions(+), 83 deletions(-) diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index 8e042ddc..920b4c5e 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -3,8 +3,8 @@ use std::time::Duration; use smplx_sdk::provider::ElementsRpc; use smplx_sdk::provider::SimplexProvider; use smplx_sdk::provider::SimplicityNetwork; -use smplx_sdk::signer::core::HDKeyOrigin; -use smplx_sdk::signer::{KeyProvider, Signer}; +use smplx_sdk::signer::core::HDKey; +use smplx_sdk::signer::{KeyOrigin, Signer}; use smplx_sdk::utils::btc2sat; use super::RegtestConfig; @@ -24,7 +24,7 @@ impl Regtest { /// /// # Panics /// Panics if the background indexer (`electrs`) fails to index the unspent outputs within the timeout window (10 seconds). - pub fn from_config(config: &RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { + pub fn from_config(config: &RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { let client = RegtestClient::new(config); let provider = Box::new(SimplexProvider::new( @@ -34,7 +34,7 @@ impl Regtest { SimplicityNetwork::default_regtest(), )); - let hd_key_origin = HDKeyOrigin::new(config.mnemonic.as_str())?; + let hd_key_origin = HDKey::new(config.mnemonic.as_str())?; let signer = Signer::new(hd_key_origin, provider); Self::prepare_signer(&client, &signer, config.bitcoins)?; @@ -42,7 +42,7 @@ impl Regtest { Ok((client, signer)) } - fn prepare_signer( + fn prepare_signer( client: &RegtestClient, signer: &Signer, bitcoins: u64, diff --git a/crates/sdk/src/provider/network.rs b/crates/sdk/src/provider/network.rs index 88d7354d..1b9ffeb0 100644 --- a/crates/sdk/src/provider/network.rs +++ b/crates/sdk/src/provider/network.rs @@ -3,8 +3,6 @@ use std::str::FromStr; use simplicityhl::simplicity::elements; use simplicityhl::simplicity::hashes::{Hash, sha256}; -use elements_miniscript::bitcoin::NetworkKind; - use crate::constants::{LIQUID_DEFAULT_REGTEST_ASSET_STR, LIQUID_POLICY_ASSET_STR, LIQUID_TESTNET_POLICY_ASSET_STR}; /// The default Bitcoin `AssetId` used on Liquid testnet. @@ -103,19 +101,3 @@ impl SimplicityNetwork { } } } - -impl From for NetworkKind { - fn from(value: SimplicityNetwork) -> Self { - (&value).into() - } -} - -impl From<&SimplicityNetwork> for NetworkKind { - fn from(value: &SimplicityNetwork) -> Self { - if value.is_mainnet() { - NetworkKind::Main - } else { - NetworkKind::Test - } - } -} diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index e70562b2..27b291fa 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -71,7 +71,7 @@ pub trait SignerTrait { /// This trait abstracts the origin of the wallet's keys, allowing the `Signer` to remain /// agnostic to whether the keys are derived from a BIP39 mnemonic, a hardware wallet, /// or a single injected secret key. -pub trait KeyProvider { +pub trait KeyOrigin { /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. #[must_use] fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey; @@ -127,17 +127,17 @@ pub struct Signer { /// A Hierarchical Deterministic (HD) key provider based on BIP39 and SLIP77. /// -/// `HDKeyOrigin` derives its key material from a standard mnemonic seed phrase. +/// `HDKey` derives its key material from a standard mnemonic seed phrase. /// It handles BIP32 derivation paths for standard transaction signing and uses /// SLIP77 to generate deterministic blinding keys for confidential transactions /// on the Elements/Liquid network. -pub struct HDKeyOrigin { +pub struct HDKey { xprv: Xpriv, master_blinding: MasterBlindingKey, } -impl HDKeyOrigin { - /// Constructs a new `HDKeyOrigin` from a BIP39 mnemonic phrase. +impl HDKey { + /// Constructs a new `HDKey` from a BIP39 mnemonic phrase. /// /// # Errors /// Returns a `SignerError::Mnemonic` if the provided phrase is invalid. @@ -202,8 +202,7 @@ impl HDKeyOrigin { } } -impl KeyProvider for HDKeyOrigin { - /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. +impl KeyOrigin for HDKey { fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey { let private_key = self.get_private_key(secp, network); let keypair = Keypair::from_secret_key(secp, &private_key.inner); @@ -211,20 +210,14 @@ impl KeyProvider for HDKeyOrigin { keypair.x_only_public_key().0 } - /// Resolves the standard format ECDSA public key. fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { self.get_private_key(secp, network).public_key(secp) } - /// Resolves the corresponding blinding public key. fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { self.get_blinding_private_key(secp, network).public_key(secp) } - /// Internally derives and exposes the wallet's signing active private key. - /// - /// # Panics - /// Panics if the master private key or derivation path cannot be derived. fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { let master_xprv = self.master_xpriv(secp).unwrap(); let full_path = self.get_derivation_path(network).unwrap(); @@ -240,13 +233,6 @@ impl KeyProvider for HDKeyOrigin { PrivateKey::new(ext_derived.private_key, NetworkKind::Test) } - /// Generates the private key linked to confidential payload blinding. - /// - /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind. - /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address. - /// - /// # Panics - /// Panics if the master SLIP77 key cannot be derived. fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { let blinding_key = self .master_blinding @@ -255,10 +241,6 @@ impl KeyProvider for HDKeyOrigin { PrivateKey::new(blinding_key, NetworkKind::Test) } - /// Returns the confidential elements address matching the local wallet logic. - /// - /// # Panics - /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { let mut descriptor = ConfidentialDescriptor::::from_str( &self.get_slip77_descriptor(secp, network).unwrap(), @@ -276,10 +258,6 @@ impl KeyProvider for HDKeyOrigin { .unwrap() } - /// Returns the standard unblinded address matching the local wallet logic. - /// - /// # Panics - /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor(secp, network).unwrap()) .map_err(|e| SignerError::WpkhDescriptor(e.to_string())) @@ -295,16 +273,16 @@ impl KeyProvider for HDKeyOrigin { /// A simplified key provider powered by a single static secret key. /// -/// Unlike `HDKeyOrigin` which derives paths hierarchically, `SingleKeyOrigin` uses +/// Unlike `HDKey` which derives paths hierarchically, `SingleKey` uses /// exactly one `SecretKey` for all signing operations. It can optionally accept a /// `MasterBlindingKey` to support confidential transactions and blinded addresses. -pub struct SingleKeyOrigin { +pub struct SingleKey { secret_key: SecretKey, blinding_key: Option, } -impl SingleKeyOrigin { - /// Creates a new `SingleKeyOrigin`. +impl SingleKey { + /// Creates a new `SingleKey`. /// /// # Arguments /// * `secret_key` - The base static secret key used for ECDSA and Schnorr signatures. @@ -318,7 +296,7 @@ impl SingleKeyOrigin { } } -impl KeyProvider for SingleKeyOrigin { +impl KeyOrigin for SingleKey { fn get_schnorr_public_key(&self, secp: &Secp256k1, _network: &SimplicityNetwork) -> XOnlyPublicKey { let keypair = Keypair::from_secret_key(secp, &self.secret_key); keypair.x_only_public_key().0 @@ -332,8 +310,8 @@ impl KeyProvider for SingleKeyOrigin { self.get_blinding_private_key(secp, network).public_key(secp) } - fn get_private_key(&self, _secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { - PrivateKey::new(self.secret_key, network) + fn get_private_key(&self, _secp: &Secp256k1, _network: &SimplicityNetwork) -> PrivateKey { + PrivateKey::new(self.secret_key, NetworkKind::Test) } fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { @@ -344,7 +322,7 @@ impl KeyProvider for SingleKeyOrigin { let script_pubkey = self.get_address(secp, network).script_pubkey(); let blinding_key = master_blinding.blinding_private_key(&script_pubkey); - PrivateKey::new(blinding_key, network) + PrivateKey::new(blinding_key, NetworkKind::Test) } fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { @@ -374,7 +352,7 @@ impl Signer { } } -impl Signer { +impl Signer { /// Creates a new `Signer` instance seeded from the provided key origin and paired with the specified provider. #[must_use] pub fn new(key_origin: K, provider: Box) -> Self { @@ -810,11 +788,11 @@ mod tests { use super::*; - fn create_signer() -> Signer { + fn create_signer() -> Signer { let url = "https://blockstream.info/liquidtestnet/api".to_string(); let network = SimplicityNetwork::LiquidTestnet; - let hd_origin = HDKeyOrigin::new(random_mnemonic().as_str()).unwrap(); + let hd_origin = HDKey::new(random_mnemonic().as_str()).unwrap(); Signer::new(hd_origin, Box::new(EsploraProvider::new(url, network))) } diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs index da24a8aa..30e48c6e 100644 --- a/crates/sdk/src/signer/mod.rs +++ b/crates/sdk/src/signer/mod.rs @@ -5,5 +5,5 @@ pub mod error; /// Utilities for injecting witness data bindings into Simplicity environments. mod wtns_injector; -pub use core::{HDKeyOrigin, KeyProvider, Signer, SignerTrait, SingleKeyOrigin}; +pub use core::{HDKey, KeyOrigin, Signer, SignerTrait, SingleKey}; pub use error::SignerError; diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 43418612..873a3c6f 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -9,7 +9,7 @@ use smplx_sdk::global::GlobalConfig; use smplx_sdk::provider::{ ElementsRpc, EsploraProvider, ProviderInfo, ProviderTrait, SimplexProvider, SimplicityNetwork, }; -use smplx_sdk::signer::{HDKeyOrigin, Signer}; +use smplx_sdk::signer::{HDKey, Signer}; use smplx_sdk::utils::random_mnemonic; use crate::config::TestConfig; @@ -22,7 +22,7 @@ pub struct TestContext { // since providers can't be cloned, we need this variable to create new signers _provider_info: ProviderInfo, config: TestConfig, - signer: Signer, + signer: Signer, } impl TestContext { @@ -42,7 +42,7 @@ impl TestContext { }) } - pub fn create_signer(&self, mnemonic: &str) -> Signer { + 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( @@ -59,15 +59,15 @@ impl TestContext { )) }; - let hd_key_origin = HDKeyOrigin::new(mnemonic).unwrap(); + let hd_key_origin = HDKey::new(mnemonic).unwrap(); Signer::new(hd_key_origin, provider) } - pub fn random_signer(&self) -> Signer { + pub fn random_signer(&self) -> Signer { self.create_signer(random_mnemonic().as_str()) } - pub fn get_default_signer(&self) -> &Signer { + pub fn get_default_signer(&self) -> &Signer { &self.signer } @@ -101,10 +101,10 @@ impl TestContext { NetworkUtils::new(regtest_rpc, esplora) } - fn setup(config: &TestConfig) -> Result<(Signer, ProviderInfo, Option), TestError> { + fn setup(config: &TestConfig) -> Result<(Signer, ProviderInfo, Option), TestError> { let client: Option; let provider_info: ProviderInfo; - let signer: Signer; + let signer: Signer; match config.esplora.clone() { Some(esplora) => match config.rpc.clone() { @@ -123,7 +123,7 @@ impl TestContext { elements_url: Some(rpc.url), auth: Some(auth), }; - let hd_key_origin = HDKeyOrigin::new(config.mnemonic.as_str()).unwrap(); + let hd_key_origin = HDKey::new(config.mnemonic.as_str()).unwrap(); signer = Signer::new(hd_key_origin, provider); client = None; } @@ -142,7 +142,7 @@ impl TestContext { elements_url: None, auth: None, }; - let hd_key_origin = HDKeyOrigin::new(config.mnemonic.as_str()).unwrap(); + let hd_key_origin = HDKey::new(config.mnemonic.as_str()).unwrap(); signer = Signer::new(hd_key_origin, provider); client = None; } diff --git a/examples/basic/tests/confidential_test.rs b/examples/basic/tests/confidential_test.rs index 194d3e33..68356eac 100644 --- a/examples/basic/tests/confidential_test.rs +++ b/examples/basic/tests/confidential_test.rs @@ -1,10 +1,10 @@ use simplex::simplicityhl::elements::AssetId; -use simplex::signer::{KeyProvider, Signer}; +use simplex::signer::{KeyOrigin, Signer}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt}; -fn make_confidential_to_bob<'a, K1: KeyProvider, K2: KeyProvider>( +fn make_confidential_to_bob<'a, K1: KeyOrigin, K2: KeyOrigin>( alice: &'a Signer, bob: &Signer, asset: AssetId, @@ -22,7 +22,7 @@ fn make_confidential_to_bob<'a, K1: KeyProvider, K2: KeyProvider>( Ok(tx_receipt) } -fn issue_confidential_to_alice<'a, K1: KeyProvider, K2: KeyProvider>( +fn issue_confidential_to_alice<'a, K1: KeyOrigin, K2: KeyOrigin>( alice: &Signer, bob: &'a Signer, ) -> anyhow::Result> { diff --git a/fixtures/tests/confidential_test.rs b/fixtures/tests/confidential_test.rs index e881c931..f0099d2c 100644 --- a/fixtures/tests/confidential_test.rs +++ b/fixtures/tests/confidential_test.rs @@ -1,10 +1,10 @@ use simplex::simplicityhl::elements::AssetId; -use simplex::signer::{KeyProvider, Signer}; +use simplex::signer::{KeyOrigin, Signer}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; -fn make_confidential_to_bob( +fn make_confidential_to_bob( alice: &Signer, bob: &Signer, asset: AssetId, @@ -22,7 +22,7 @@ fn make_confidential_to_bob( Ok(()) } -fn issue_confidential_to_alice( +fn issue_confidential_to_alice( alice: &Signer, bob: &Signer, ) -> anyhow::Result<()> { diff --git a/fixtures/tests/reissuance_test.rs b/fixtures/tests/reissuance_test.rs index f7167c4e..2f41a7cf 100644 --- a/fixtures/tests/reissuance_test.rs +++ b/fixtures/tests/reissuance_test.rs @@ -1,12 +1,12 @@ use simplex::simplicityhl::elements::AssetId; -use simplex::signer::{KeyProvider, Signer}; +use simplex::signer::{KeyOrigin, Signer}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{ FinalTransaction, IssuanceDetails, PartialInput, PartialOutput, RequiredSignature, TxReceipt, }; -fn make_confidential_to_bob<'a, K1: KeyProvider, K2: KeyProvider>( +fn make_confidential_to_bob<'a, K1: KeyOrigin, K2: KeyOrigin>( alice: &'a Signer, bob: &Signer, asset: AssetId, @@ -24,7 +24,7 @@ fn make_confidential_to_bob<'a, K1: KeyProvider, K2: KeyProvider>( Ok(tx_receipt) } -fn issue_explicit_to_alice_with_reissuance<'a, K1: KeyProvider, K2: KeyProvider>( +fn issue_explicit_to_alice_with_reissuance<'a, K1: KeyOrigin, K2: KeyOrigin>( alice: &Signer, bob: &'a Signer, ) -> anyhow::Result<(TxReceipt<'a>, IssuanceDetails)> { @@ -58,7 +58,7 @@ fn issue_explicit_to_alice_with_reissuance<'a, K1: KeyProvider, K2: KeyProvider> Ok((tx_receipt, issuance_details)) } -fn reissue_tokens_to_bob<'a, K: KeyProvider>( +fn reissue_tokens_to_bob<'a, K: KeyOrigin>( bob: &'a Signer, issuance_details: &IssuanceDetails, reissuance_amount: u64, From 6ef400eb3b3a8e94b5584ed47828445f86534ea6 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 2 Jul 2026 10:52:41 +0300 Subject: [PATCH 3/6] sdk: move key origin implementation to file * inline HDKey initialization in tests --- crates/regtest/src/regtest.rs | 6 +- crates/sdk/src/signer/core.rs | 287 +--------------------------- crates/sdk/src/signer/key_origin.rs | 277 +++++++++++++++++++++++++++ crates/sdk/src/signer/mod.rs | 5 +- crates/test/src/context.rs | 9 +- 5 files changed, 295 insertions(+), 289 deletions(-) create mode 100644 crates/sdk/src/signer/key_origin.rs diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index 920b4c5e..1565a578 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -3,8 +3,7 @@ use std::time::Duration; use smplx_sdk::provider::ElementsRpc; use smplx_sdk::provider::SimplexProvider; use smplx_sdk::provider::SimplicityNetwork; -use smplx_sdk::signer::core::HDKey; -use smplx_sdk::signer::{KeyOrigin, Signer}; +use smplx_sdk::signer::{HDKey, KeyOrigin, Signer}; use smplx_sdk::utils::btc2sat; use super::RegtestConfig; @@ -34,8 +33,7 @@ impl Regtest { SimplicityNetwork::default_regtest(), )); - let hd_key_origin = HDKey::new(config.mnemonic.as_str())?; - let signer = Signer::new(hd_key_origin, provider); + let signer = Signer::new(HDKey::new(config.mnemonic.as_str())?, provider); Self::prepare_signer(&client, &signer, config.bitcoins)?; diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 27b291fa..026dd2d0 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -1,31 +1,23 @@ use std::collections::{HashMap, HashSet}; -use std::str::FromStr; use std::sync::Arc; use simplicityhl::Value; use simplicityhl::WitnessValues; use simplicityhl::elements::pset::PartiallySignedTransaction; -use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, SecretKey, ecdsa, schnorr}; +use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr}; 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::{ - ConfidentialDescriptor, Descriptor, DescriptorPublicKey, - bitcoin::{NetworkKind, PrivateKey, PublicKey, bip32::DerivationPath}, - elements::{ - EcdsaSighashType, - bitcoin::bip32::{Fingerprint, Xpriv, Xpub}, - sighash::SighashCache, - }, + bitcoin::{PrivateKey, PublicKey}, + elements::{EcdsaSighashType, sighash::SighashCache}, elementssig_to_rawsig, psbt::PsbtExt, - slip77::MasterBlindingKey, }; use crate::constants::MIN_FEE; @@ -33,6 +25,7 @@ use crate::program::ProgramTrait; use crate::program::logger::ProgramLogger; use crate::provider::ProviderTrait; use crate::provider::SimplicityNetwork; +use crate::signer::KeyOrigin; use crate::signer::wtns_injector::WtnsInjector; use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt, UTXO}; @@ -66,56 +59,6 @@ pub trait SignerTrait { ) -> Result<(PublicKey, ecdsa::Signature), SignerError>; } -/// A generalized interface for providing cryptographic keys and addresses. -/// -/// This trait abstracts the origin of the wallet's keys, allowing the `Signer` to remain -/// agnostic to whether the keys are derived from a BIP39 mnemonic, a hardware wallet, -/// or a single injected secret key. -pub trait KeyOrigin { - /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. - #[must_use] - fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey; - - /// Resolves the standard format ECDSA public key. - #[must_use] - fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey; - - /// Resolves the corresponding blinding public key. - #[must_use] - fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey; - - /// Internally derives and exposes the wallet's signing active private key. - /// - /// # Panics - /// Panics if the master private key or derivation path cannot be derived. - #[must_use] - fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey; - - /// Generates the private key linked to confidential payload blinding. - /// - /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind. - /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address. - /// - /// # Panics - /// Panics if the master SLIP77 key cannot be derived. - #[must_use] - fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey; - - /// Returns the confidential elements address matching the local wallet logic. - /// - /// # Panics - /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. - #[must_use] - fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; - - /// Returns the standard unblinded address matching the local wallet logic. - /// - /// # Panics - /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. - #[must_use] - fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; -} - /// Core interface responsible for managing keys, interfacing with the blockchain provider, /// assembling descriptors, estimating fees, and finalizing/signing transactions. pub struct Signer { @@ -125,220 +68,6 @@ pub struct Signer { secp: Secp256k1, } -/// A Hierarchical Deterministic (HD) key provider based on BIP39 and SLIP77. -/// -/// `HDKey` derives its key material from a standard mnemonic seed phrase. -/// It handles BIP32 derivation paths for standard transaction signing and uses -/// SLIP77 to generate deterministic blinding keys for confidential transactions -/// on the Elements/Liquid network. -pub struct HDKey { - xprv: Xpriv, - master_blinding: MasterBlindingKey, -} - -impl HDKey { - /// Constructs a new `HDKey` from a BIP39 mnemonic phrase. - /// - /// # Errors - /// Returns a `SignerError::Mnemonic` if the provided phrase is invalid. - pub fn new(mnemonic: &str) -> Result { - let mnemonic: Mnemonic = mnemonic - .parse() - .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?; - let seed = mnemonic.to_seed(""); - let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?; - - let master_blinding_key = MasterBlindingKey::from_seed(&seed[..]); - - Ok(Self { - master_blinding: master_blinding_key, - xprv, - }) - } - - fn derive_xpriv(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { - Ok(self.xprv.derive_priv(secp, path)?) - } - - fn master_xpriv(&self, secp: &Secp256k1) -> Result { - self.derive_xpriv(&DerivationPath::master(), secp) - } - - fn derive_xpub(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { - let derived = self.derive_xpriv(path, secp)?; - - Ok(Xpub::from_priv(secp, &derived)) - } - - fn master_xpub(&self, secp: &Secp256k1) -> Result { - self.derive_xpub(&DerivationPath::master(), secp) - } - - fn fingerprint(&self, secp: &Secp256k1) -> Result { - Ok(self.master_xpub(secp)?.fingerprint()) - } - - fn get_slip77_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { - let wpkh_descriptor = self.get_wpkh_descriptor(secp, network)?; - let blinding_key = self.master_blinding; - - Ok(format!("ct(slip77({blinding_key}),{wpkh_descriptor})")) - } - - fn get_wpkh_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { - let fingerprint = self.fingerprint(secp)?; - let path = self.get_derivation_path(network)?; - let xpub = self.derive_xpub(&path, secp)?; - - Ok(format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)")) - } - - #[allow(clippy::unused_self)] - fn get_derivation_path(&self, network: &SimplicityNetwork) -> Result { - let coin_type = if network.is_mainnet() { 1776 } else { 1 }; - let path = format!("84h/{coin_type}h/0h"); - - DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string())) - } -} - -impl KeyOrigin for HDKey { - fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey { - let private_key = self.get_private_key(secp, network); - let keypair = Keypair::from_secret_key(secp, &private_key.inner); - - keypair.x_only_public_key().0 - } - - fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { - self.get_private_key(secp, network).public_key(secp) - } - - fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { - self.get_blinding_private_key(secp, network).public_key(secp) - } - - fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { - let master_xprv = self.master_xpriv(secp).unwrap(); - let full_path = self.get_derivation_path(network).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(secp, &derived).unwrap(); - - PrivateKey::new(ext_derived.private_key, NetworkKind::Test) - } - - fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { - let blinding_key = self - .master_blinding - .blinding_private_key(&self.get_address(secp, network).script_pubkey()); - - PrivateKey::new(blinding_key, NetworkKind::Test) - } - - fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { - let mut descriptor = ConfidentialDescriptor::::from_str( - &self.get_slip77_descriptor(secp, network).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(); - - descriptor - .at_derivation_index(1) - .unwrap() - .address(secp, network.address_params()) - .unwrap() - } - - fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { - let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor(secp, network).unwrap()) - .map_err(|e| SignerError::WpkhDescriptor(e.to_string())) - .unwrap(); - - descriptor.into_single_descriptors().unwrap()[0] - .at_derivation_index(1) - .unwrap() - .address(network.address_params()) - .unwrap() - } -} - -/// A simplified key provider powered by a single static secret key. -/// -/// Unlike `HDKey` which derives paths hierarchically, `SingleKey` uses -/// exactly one `SecretKey` for all signing operations. It can optionally accept a -/// `MasterBlindingKey` to support confidential transactions and blinded addresses. -pub struct SingleKey { - secret_key: SecretKey, - blinding_key: Option, -} - -impl SingleKey { - /// Creates a new `SingleKey`. - /// - /// # Arguments - /// * `secret_key` - The base static secret key used for ECDSA and Schnorr signatures. - /// * `blinding_key` - An optional SLIP77 master blinding key. - #[must_use] - pub fn new(secret_key: SecretKey, blinding_key: Option) -> Self { - Self { - secret_key, - blinding_key, - } - } -} - -impl KeyOrigin for SingleKey { - fn get_schnorr_public_key(&self, secp: &Secp256k1, _network: &SimplicityNetwork) -> XOnlyPublicKey { - let keypair = Keypair::from_secret_key(secp, &self.secret_key); - keypair.x_only_public_key().0 - } - - fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { - self.get_private_key(secp, network).public_key(secp) - } - - fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { - self.get_blinding_private_key(secp, network).public_key(secp) - } - - fn get_private_key(&self, _secp: &Secp256k1, _network: &SimplicityNetwork) -> PrivateKey { - PrivateKey::new(self.secret_key, NetworkKind::Test) - } - - fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { - let master_blinding = self - .blinding_key - .expect("Blinding key is required for confidential operations"); - - let script_pubkey = self.get_address(secp, network).script_pubkey(); - let blinding_key = master_blinding.blinding_private_key(&script_pubkey); - - PrivateKey::new(blinding_key, NetworkKind::Test) - } - - fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { - let ecdsa_pubkey = self.get_ecdsa_public_key(secp, network); - let blinding_pubkey = self.get_blinding_public_key(secp, network); - - Address::p2wpkh(&ecdsa_pubkey, Some(blinding_pubkey.inner), network.address_params()) - } - - fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { - let ecdsa_pubkey = self.get_ecdsa_public_key(secp, network); - - Address::p2wpkh(&ecdsa_pubkey, None, network.address_params()) - } -} - enum Estimate { Success(Transaction, u64), Failure(u64), @@ -784,6 +513,7 @@ impl Signer { #[cfg(test)] mod tests { use crate::provider::EsploraProvider; + use crate::signer::HDKey; use crate::utils::random_mnemonic; use super::*; @@ -792,9 +522,10 @@ mod tests { let url = "https://blockstream.info/liquidtestnet/api".to_string(); let network = SimplicityNetwork::LiquidTestnet; - let hd_origin = HDKey::new(random_mnemonic().as_str()).unwrap(); - - Signer::new(hd_origin, Box::new(EsploraProvider::new(url, network))) + Signer::new( + HDKey::new(random_mnemonic().as_str()).unwrap(), + Box::new(EsploraProvider::new(url, network)), + ) } #[test] diff --git a/crates/sdk/src/signer/key_origin.rs b/crates/sdk/src/signer/key_origin.rs new file mode 100644 index 00000000..7b7381c2 --- /dev/null +++ b/crates/sdk/src/signer/key_origin.rs @@ -0,0 +1,277 @@ +use std::str::FromStr; + +use bip39::Mnemonic; +use bitcoincore_rpc::bitcoin::bip32::{DerivationPath, Fingerprint, Xpriv, Xpub}; +use bitcoincore_rpc::bitcoin::key::{Keypair, Secp256k1}; +use bitcoincore_rpc::bitcoin::secp256k1::{All, SecretKey}; +use bitcoincore_rpc::bitcoin::{NetworkKind, PrivateKey, PublicKey, XOnlyPublicKey}; +use elements_miniscript::elements::Address; +use elements_miniscript::slip77::MasterBlindingKey; +use elements_miniscript::{ConfidentialDescriptor, Descriptor, DescriptorPublicKey}; + +use crate::provider::SimplicityNetwork; +use crate::signer::SignerError; + +/// A generalized interface for providing cryptographic keys and addresses. +/// +/// This trait abstracts the origin of the wallet's keys, allowing the `Signer` to remain +/// agnostic to whether the keys are derived from a BIP39 mnemonic, a hardware wallet, +/// or a single injected secret key. +pub trait KeyOrigin { + /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. + #[must_use] + fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey; + + /// Resolves the standard format ECDSA public key. + #[must_use] + fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey; + + /// Resolves the corresponding blinding public key. + #[must_use] + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey; + + /// Internally derives and exposes the wallet's signing active private key. + /// + /// # Panics + /// Panics if the master private key or derivation path cannot be derived. + #[must_use] + fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey; + + /// Generates the private key linked to confidential payload blinding. + /// + /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind. + /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address. + /// + /// # Panics + /// Panics if the master SLIP77 key cannot be derived. + #[must_use] + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey; + + /// Returns the confidential elements address matching the local wallet logic. + /// + /// # Panics + /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. + #[must_use] + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; + + /// Returns the standard unblinded address matching the local wallet logic. + /// + /// # Panics + /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. + #[must_use] + fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; +} + +/// A Hierarchical Deterministic (HD) key provider based on BIP39 and SLIP77. +/// +/// `HDKey` derives its key material from a standard mnemonic seed phrase. +/// It handles BIP32 derivation paths for standard transaction signing and uses +/// SLIP77 to generate deterministic blinding keys for confidential transactions +/// on the Elements/Liquid network. +pub struct HDKey { + xprv: Xpriv, + master_blinding: MasterBlindingKey, +} + +impl HDKey { + /// Constructs a new `HDKey` from a BIP39 mnemonic phrase. + /// + /// # Errors + /// Returns a `SignerError::Mnemonic` if the provided phrase is invalid. + pub fn new(mnemonic: &str) -> Result { + let mnemonic: Mnemonic = mnemonic + .parse() + .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?; + let seed = mnemonic.to_seed(""); + let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?; + + let master_blinding_key = MasterBlindingKey::from_seed(&seed[..]); + + Ok(Self { + master_blinding: master_blinding_key, + xprv, + }) + } + + fn derive_xpriv(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { + Ok(self.xprv.derive_priv(secp, path)?) + } + + fn master_xpriv(&self, secp: &Secp256k1) -> Result { + self.derive_xpriv(&DerivationPath::master(), secp) + } + + fn derive_xpub(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { + let derived = self.derive_xpriv(path, secp)?; + + Ok(Xpub::from_priv(secp, &derived)) + } + + fn master_xpub(&self, secp: &Secp256k1) -> Result { + self.derive_xpub(&DerivationPath::master(), secp) + } + + fn fingerprint(&self, secp: &Secp256k1) -> Result { + Ok(self.master_xpub(secp)?.fingerprint()) + } + + fn get_slip77_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { + let wpkh_descriptor = self.get_wpkh_descriptor(secp, network)?; + let blinding_key = self.master_blinding; + + Ok(format!("ct(slip77({blinding_key}),{wpkh_descriptor})")) + } + + fn get_wpkh_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { + let fingerprint = self.fingerprint(secp)?; + let path = self.get_derivation_path(network)?; + let xpub = self.derive_xpub(&path, secp)?; + + Ok(format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)")) + } + + #[allow(clippy::unused_self)] + fn get_derivation_path(&self, network: &SimplicityNetwork) -> Result { + let coin_type = if network.is_mainnet() { 1776 } else { 1 }; + let path = format!("84h/{coin_type}h/0h"); + + DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string())) + } +} + +impl KeyOrigin for HDKey { + fn get_schnorr_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> XOnlyPublicKey { + let private_key = self.get_private_key(secp, network); + let keypair = Keypair::from_secret_key(secp, &private_key.inner); + + keypair.x_only_public_key().0 + } + + fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_private_key(secp, network).public_key(secp) + } + + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_blinding_private_key(secp, network).public_key(secp) + } + + fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + let master_xprv = self.master_xpriv(secp).unwrap(); + let full_path = self.get_derivation_path(network).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(secp, &derived).unwrap(); + + PrivateKey::new(ext_derived.private_key, NetworkKind::Test) + } + + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + let blinding_key = self + .master_blinding + .blinding_private_key(&self.get_address(secp, network).script_pubkey()); + + PrivateKey::new(blinding_key, NetworkKind::Test) + } + + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let mut descriptor = ConfidentialDescriptor::::from_str( + &self.get_slip77_descriptor(secp, network).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(); + + descriptor + .at_derivation_index(1) + .unwrap() + .address(secp, network.address_params()) + .unwrap() + } + + fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor(secp, network).unwrap()) + .map_err(|e| SignerError::WpkhDescriptor(e.to_string())) + .unwrap(); + + descriptor.into_single_descriptors().unwrap()[0] + .at_derivation_index(1) + .unwrap() + .address(network.address_params()) + .unwrap() + } +} + +/// A simplified key provider powered by a single static secret key. +/// +/// Unlike `HDKey` which derives paths hierarchically, `SingleKey` uses +/// exactly one `SecretKey` for all signing operations. It can optionally accept a +/// `MasterBlindingKey` to support confidential transactions and blinded addresses. +pub struct SingleKey { + secret_key: SecretKey, + blinding_key: Option, +} + +impl SingleKey { + /// Creates a new `SingleKey`. + /// + /// # Arguments + /// * `secret_key` - The base static secret key used for ECDSA and Schnorr signatures. + /// * `blinding_key` - An optional SLIP77 master blinding key. + #[must_use] + pub fn new(secret_key: SecretKey, blinding_key: Option) -> Self { + Self { + secret_key, + blinding_key, + } + } +} + +impl KeyOrigin for SingleKey { + fn get_schnorr_public_key(&self, secp: &Secp256k1, _network: &SimplicityNetwork) -> XOnlyPublicKey { + let keypair = Keypair::from_secret_key(secp, &self.secret_key); + keypair.x_only_public_key().0 + } + + fn get_ecdsa_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_private_key(secp, network).public_key(secp) + } + + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { + self.get_blinding_private_key(secp, network).public_key(secp) + } + + fn get_private_key(&self, _secp: &Secp256k1, _network: &SimplicityNetwork) -> PrivateKey { + PrivateKey::new(self.secret_key, NetworkKind::Test) + } + + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + let master_blinding = self + .blinding_key + .expect("Blinding key is required for confidential operations"); + + let script_pubkey = self.get_address(secp, network).script_pubkey(); + let blinding_key = master_blinding.blinding_private_key(&script_pubkey); + + PrivateKey::new(blinding_key, NetworkKind::Test) + } + + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let ecdsa_pubkey = self.get_ecdsa_public_key(secp, network); + let blinding_pubkey = self.get_blinding_public_key(secp, network); + + Address::p2wpkh(&ecdsa_pubkey, Some(blinding_pubkey.inner), network.address_params()) + } + + fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + let ecdsa_pubkey = self.get_ecdsa_public_key(secp, network); + + Address::p2wpkh(&ecdsa_pubkey, None, network.address_params()) + } +} diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs index 30e48c6e..f26e0aca 100644 --- a/crates/sdk/src/signer/mod.rs +++ b/crates/sdk/src/signer/mod.rs @@ -2,8 +2,11 @@ pub mod core; /// Signer-specific error enumerations capturing execution constraints and mapping internal failure types. pub mod error; +/// Contains abstractions that allow the `Signer` to remain agnostic to the origin of its keys. +mod key_origin; /// Utilities for injecting witness data bindings into Simplicity environments. mod wtns_injector; -pub use core::{HDKey, KeyOrigin, Signer, SignerTrait, SingleKey}; +pub use core::{Signer, SignerTrait}; pub use error::SignerError; +pub use key_origin::{HDKey, KeyOrigin, SingleKey}; diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 873a3c6f..243f1942 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -59,8 +59,7 @@ impl TestContext { )) }; - let hd_key_origin = HDKey::new(mnemonic).unwrap(); - Signer::new(hd_key_origin, provider) + Signer::new(HDKey::new(mnemonic).unwrap(), provider) } pub fn random_signer(&self) -> Signer { @@ -123,8 +122,7 @@ impl TestContext { elements_url: Some(rpc.url), auth: Some(auth), }; - let hd_key_origin = HDKey::new(config.mnemonic.as_str()).unwrap(); - signer = Signer::new(hd_key_origin, provider); + signer = Signer::new(HDKey::new(config.mnemonic.as_str()).unwrap(), provider); client = None; } None => { @@ -142,8 +140,7 @@ impl TestContext { elements_url: None, auth: None, }; - let hd_key_origin = HDKey::new(config.mnemonic.as_str()).unwrap(); - signer = Signer::new(hd_key_origin, provider); + signer = Signer::new(HDKey::new(config.mnemonic.as_str()).unwrap(), provider); client = None; } }, From d030d56dd29691ca87446eab9a5345a2a9cd6bf1 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 2 Jul 2026 14:46:23 +0300 Subject: [PATCH 4/6] sdk: add space to separate imports --- crates/sdk/src/signer/key_origin.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/sdk/src/signer/key_origin.rs b/crates/sdk/src/signer/key_origin.rs index 7b7381c2..411b8f23 100644 --- a/crates/sdk/src/signer/key_origin.rs +++ b/crates/sdk/src/signer/key_origin.rs @@ -5,6 +5,7 @@ use bitcoincore_rpc::bitcoin::bip32::{DerivationPath, Fingerprint, Xpriv, Xpub}; use bitcoincore_rpc::bitcoin::key::{Keypair, Secp256k1}; use bitcoincore_rpc::bitcoin::secp256k1::{All, SecretKey}; use bitcoincore_rpc::bitcoin::{NetworkKind, PrivateKey, PublicKey, XOnlyPublicKey}; + use elements_miniscript::elements::Address; use elements_miniscript::slip77::MasterBlindingKey; use elements_miniscript::{ConfidentialDescriptor, Descriptor, DescriptorPublicKey}; From 74f0420bfde991aabdde0d1a53edf6660d793d89 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 2 Jul 2026 19:26:49 +0300 Subject: [PATCH 5/6] sdk: adapt interface due to possible absence of blinding key * improve allocations in get_utxos_filter * remove &dyn in Fn closure passing * return SignerTrait implementation --- crates/regtest/src/error.rs | 4 + crates/sdk/src/signer/core.rs | 174 +++++++++++----------- crates/sdk/src/signer/error.rs | 60 +++++--- crates/sdk/src/signer/key_origin.rs | 144 +++++++++++++----- examples/basic/tests/confidential_test.rs | 6 +- fixtures/tests/confidential_test.rs | 6 +- fixtures/tests/reissuance_test.rs | 6 +- 7 files changed, 244 insertions(+), 156 deletions(-) diff --git a/crates/regtest/src/error.rs b/crates/regtest/src/error.rs index 77d953b0..8957871a 100644 --- a/crates/regtest/src/error.rs +++ b/crates/regtest/src/error.rs @@ -2,6 +2,7 @@ use std::io; use smplx_sdk::provider::RpcError; use smplx_sdk::signer::SignerError; +use smplx_sdk::signer::error::KeyOriginError; #[derive(thiserror::Error, Debug)] pub enum RegtestError { @@ -11,6 +12,9 @@ pub enum RegtestError { #[error(transparent)] Signer(#[from] SignerError), + #[error(transparent)] + KeyOrigin(#[from] KeyOriginError), + #[error("Failed to terminate elements")] ElementsTermination(), diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 026dd2d0..3355db43 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -29,7 +29,7 @@ use crate::signer::KeyOrigin; use crate::signer::wtns_injector::WtnsInjector; use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt, UTXO}; -use super::error::SignerError; +use super::error::{KeyOriginError, SignerError}; /// A placeholder dummy fee amount used during transaction estimation. pub const PLACEHOLDER_FEE: u64 = 1; @@ -68,6 +68,46 @@ pub struct Signer { secp: Secp256k1, } +impl SignerTrait for Signer { + fn sign_program( + &self, + pst: &PartiallySignedTransaction, + program: &dyn ProgramTrait, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result { + 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 keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); + + Ok(self.secp.sign_schnorr(&msg, &keypair)) + } + + fn sign_input( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + ) -> Result<(PublicKey, ecdsa::Signature), SignerError> { + let tx = pst.extract_tx()?; + + let mut sighash_cache = SighashCache::new(&tx); + let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros(); + + let message = pst + .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)? + .to_secp_msg(); + + 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); + + Ok((public_key, signature)) + } +} + enum Estimate { Success(Transaction, u64), Failure(u64), @@ -132,8 +172,14 @@ impl Signer { } /// Proxies to the underlying key provider to get the blinding public key. - pub fn get_blinding_public_key(&self) -> PublicKey { - self.key_origin.get_blinding_public_key(&self.secp, &self.network) + /// + /// # Errors + /// Returns a `SignerError` if the unblinding key is missing or fails to derive. + pub fn get_blinding_public_key(&self) -> Result { + Ok(self + .key_origin + .get_blinding_public_key(&self.secp, &self.network) + .ok_or(KeyOriginError::RequiredUnblindingKey)?) } /// Proxies to the underlying key provider to get the active private key. @@ -142,14 +188,25 @@ impl Signer { } /// Proxies to the underlying key provider to get the blinding private key. - pub fn get_blinding_private_key(&self) -> PrivateKey { - self.key_origin.get_blinding_private_key(&self.secp, &self.network) + /// + /// # Errors + /// Returns a `SignerError` if the unblinding key is missing or fails to derive. + pub fn get_blinding_private_key(&self) -> Result { + Ok(self + .key_origin + .get_blinding_private_key(&self.secp, &self.network) + .ok_or(KeyOriginError::RequiredUnblindingKey)?) } /// Proxies to the underlying key provider to get the confidential address. - #[must_use] - pub fn get_confidential_address(&self) -> Address { - self.key_origin.get_confidential_address(&self.secp, &self.network) + /// + /// # Errors + /// Returns a `SignerError` if the unblinding key or confidential address fails to derive. + pub fn get_confidential_address(&self) -> Result { + Ok(self + .key_origin + .get_confidential_address(&self.secp, &self.network) + .ok_or(KeyOriginError::RequiredUnblindingKey)?) } /// Proxies to the underlying key provider to get the standard unblinded address. @@ -248,7 +305,7 @@ impl Signer { /// # Errors /// Returns a `SignerError` if querying the network or unblinding operations fail. pub fn get_utxos(&self) -> Result, SignerError> { - self.get_utxos_filter(&|_| true, &|_| true) + self.get_utxos_filter(|_| true, |_| true) } /// Finds all known UTXOs belonging to the specific `AssetId`. @@ -256,7 +313,7 @@ impl Signer { /// # Errors /// Returns a `SignerError` if network interaction or confidential output decryption fails. pub fn get_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { - self.get_utxos_filter(&|utxo| utxo.asset() == asset, &|utxo| utxo.asset() == asset) + self.get_utxos_filter(move |utxo| utxo.asset() == asset, move |utxo| utxo.asset() == asset) } /// Finds all known UTXOs deriving from a targeted `Txid`. @@ -265,7 +322,10 @@ impl Signer { /// Returns a `SignerError` if querying the network fails. // TODO: can this be optimized to not populate TxOuts that are filtered out? pub fn get_utxos_txid(&self, txid: Txid) -> Result, SignerError> { - self.get_utxos_filter(&|utxo| utxo.outpoint.txid == txid, &|utxo| utxo.outpoint.txid == txid) + self.get_utxos_filter( + move |utxo| utxo.outpoint.txid == txid, + move |utxo| utxo.outpoint.txid == txid, + ) } /// Maps UTXOs retrieved from the provider through arbitrary functional filters. @@ -275,47 +335,31 @@ impl Signer { /// Returns a `SignerError` if retrieving remote outputs or executing confidential node unblinding throws an error. pub fn get_utxos_filter( &self, - explicit_filter: &dyn Fn(&UTXO) -> bool, - confidential_filter: &dyn Fn(&UTXO) -> bool, + explicit_filter: impl Fn(&UTXO) -> bool, + confidential_filter: impl Fn(&UTXO) -> bool, ) -> Result, SignerError> { - // fetch explicit and confidential utxos - let mut all_utxos = self - .provider - .fetch_address_utxos(&self.key_origin.get_confidential_address(&self.secp, &self.network))?; - - // 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); + let confidential_addr = self.get_confidential_address()?; + let all_utxos = self.provider.fetch_address_utxos(&confidential_addr)?; - // push unblinded utxos to explicit ones - all_utxos.extend(confidential_utxos); - - Ok(all_utxos) - } + // partition confidential and explicit utxos + let (confidential_utxos, mut explicit_utxos): (Vec, Vec) = all_utxos + .into_iter() + .partition(|utxo| utxo.txout.value.is_confidential()); - fn unblind(&self, utxos: Vec) -> Result, SignerError> { - let mut unblinded: Vec = Vec::new(); + let mut unblinded_utxos = self.unblind(confidential_utxos)?; - for mut utxo in utxos { - let blinding_key = self.get_blinding_private_key(); - let secrets = utxo.txout.unblind(&self.secp, blinding_key.inner)?; + unblinded_utxos.retain(confidential_filter); + explicit_utxos.retain(explicit_filter); - utxo.secrets = Some(secrets); + // push unblinded utxos to explicit ones + explicit_utxos.extend(unblinded_utxos); - unblinded.push(utxo); - } + Ok(explicit_utxos) + } - Ok(unblinded) + /// Proxies to the underlying key provider to get unblinded UTXOs. + fn unblind(&self, utxos: Vec) -> Result, SignerError> { + Ok(self.key_origin.unblind(&self.secp, &self.network, utxos)?) } fn estimate_tx( @@ -425,44 +469,6 @@ impl Signer { Ok(pst.extract_tx()?) } - fn sign_program( - &self, - pst: &PartiallySignedTransaction, - program: &dyn ProgramTrait, - input_index: usize, - network: &SimplicityNetwork, - ) -> Result { - 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 keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); - - Ok(self.secp.sign_schnorr(&msg, &keypair)) - } - - fn sign_input( - &self, - pst: &PartiallySignedTransaction, - input_index: usize, - ) -> Result<(PublicKey, ecdsa::Signature), SignerError> { - let tx = pst.extract_tx()?; - - let mut sighash_cache = SighashCache::new(&tx); - let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros(); - - let message = pst - .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)? - .to_secp_msg(); - - 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); - - Ok((public_key, signature)) - } - fn get_signed_program_witness( &self, pst: &PartiallySignedTransaction, @@ -545,6 +551,6 @@ mod tests { let signer = create_signer(); println!("{}", signer.get_address()); - println!("{}", signer.get_confidential_address()); + println!("{}", signer.get_confidential_address().unwrap()); } } diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index d41c7076..f8db9028 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -16,18 +16,10 @@ pub enum SignerError { #[error(transparent)] WtnsInjectError(#[from] WtnsWrappingError), - /// Error indicating an incorrectly formatted mnemonic phrase. - #[error("Failed to parse a mnemonic: {0}")] - Mnemonic(String), - /// Error thrown when PSET transaction extraction fails. #[error("Failed to extract tx from pst: {0}")] TxExtraction(#[from] simplicityhl::elements::pset::Error), - /// Error indicating failure to unblind a confidential transaction output. - #[error("Failed to unblind txout: {0}")] - Unblind(#[from] simplicityhl::elements::UnblindError), - /// Error thrown when PSET blinding fails. #[error("Failed to blind a PST: {0}")] PsetBlind(#[from] simplicityhl::elements::pset::PsetBlindError), @@ -52,22 +44,6 @@ pub enum SignerError { #[error("Invalid secret key")] InvalidSecretKey(#[from] simplicityhl::elements::secp256k1_zkp::UpstreamError), - /// Error thrown when HD wallet private key derivation fails. - #[error("Failed to derive a private key: {0}")] - PrivateKeyDerivation(#[from] elements_miniscript::bitcoin::bip32::Error), - - /// Error thrown when constructing a derivation path string fails. - #[error("Failed to construct a derivation path: {0}")] - DerivationPath(String), - - /// Error indicating failure to construct a valid WPKH (Witness Public Key Hash) descriptor. - #[error("Failed to construct a wpkh descriptor: {0}")] - WpkhDescriptor(String), - - /// Error indicating failure to construct a valid SLIP77 blinding key descriptor. - #[error("Failed to construct a slip77 descriptor: {0}")] - Slip77Descriptor(String), - /// Error thrown if there's a problem during descriptor conversion. #[error("Failed to convert a descriptor: {0}")] DescriptorConversion(#[from] elements_miniscript::descriptor::ConversionError), @@ -79,6 +55,10 @@ pub enum SignerError { /// Error indicating an expected witness field could not be found. #[error("Missing such witness field: {0}")] WtnsFieldNotFound(String), + + /// Error forwarded from `KeyOrigin`. + #[error(transparent)] + KeyOrigin(#[from] KeyOriginError), } /// Errors originating from manipulating witness paths and injecting values. @@ -104,3 +84,35 @@ pub enum WtnsWrappingError { #[error("Path reached undefined branch of Either")] EitherBranchMismatch, } + +/// Errors originating from manipulating witness paths and injecting values. +#[derive(Debug, thiserror::Error)] +pub enum KeyOriginError { + /// Error indicating an incorrectly formatted mnemonic phrase. + #[error("Failed to parse a mnemonic: {0}")] + Mnemonic(String), + + /// Error thrown when constructing a derivation path string fails. + #[error("Failed to construct a derivation path: {0}")] + DerivationPath(String), + + /// Error indicating failure to construct a valid SLIP77 blinding key descriptor. + #[error("Failed to construct a slip77 descriptor: {0}")] + Slip77Descriptor(String), + + /// Error indicating failure to construct a valid WPKH (Witness Public Key Hash) descriptor. + #[error("Failed to construct a wpkh descriptor: {0}")] + WpkhDescriptor(String), + + /// Error thrown when HD wallet private key derivation fails. + #[error("Failed to derive a private key: {0}")] + PrivateKeyDerivation(#[from] elements_miniscript::bitcoin::bip32::Error), + + /// Error thrown when there is no pissibility to obtain a . + #[error("Required unblinding key, but it's empty")] + RequiredUnblindingKey, + + /// Error indicating failure to unblind a confidential transaction output. + #[error("Failed to unblind txout: {0}")] + Unblind(#[from] simplicityhl::elements::UnblindError), +} diff --git a/crates/sdk/src/signer/key_origin.rs b/crates/sdk/src/signer/key_origin.rs index 411b8f23..f81aab1f 100644 --- a/crates/sdk/src/signer/key_origin.rs +++ b/crates/sdk/src/signer/key_origin.rs @@ -11,7 +11,8 @@ use elements_miniscript::slip77::MasterBlindingKey; use elements_miniscript::{ConfidentialDescriptor, Descriptor, DescriptorPublicKey}; use crate::provider::SimplicityNetwork; -use crate::signer::SignerError; +use crate::signer::error::KeyOriginError; +use crate::transaction::UTXO; /// A generalized interface for providing cryptographic keys and addresses. /// @@ -29,7 +30,7 @@ pub trait KeyOrigin { /// Resolves the corresponding blinding public key. #[must_use] - fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey; + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option; /// Internally derives and exposes the wallet's signing active private key. /// @@ -46,14 +47,14 @@ pub trait KeyOrigin { /// # Panics /// Panics if the master SLIP77 key cannot be derived. #[must_use] - fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey; + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option; /// Returns the confidential elements address matching the local wallet logic. /// /// # Panics /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. #[must_use] - fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option
; /// Returns the standard unblinded address matching the local wallet logic. /// @@ -61,6 +62,17 @@ pub trait KeyOrigin { /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. #[must_use] fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address; + + /// Unblinds a list of transaction outputs (UTXOs) using the blinding keys. + /// + /// # Errors + /// Returns a `KeyOriginError` if the blinding keys cannot be resolved or if unblinding fails. + fn unblind( + &self, + secp: &Secp256k1, + network: &SimplicityNetwork, + utxos: Vec, + ) -> Result, KeyOriginError>; } /// A Hierarchical Deterministic (HD) key provider based on BIP39 and SLIP77. @@ -78,11 +90,11 @@ impl HDKey { /// Constructs a new `HDKey` from a BIP39 mnemonic phrase. /// /// # Errors - /// Returns a `SignerError::Mnemonic` if the provided phrase is invalid. - pub fn new(mnemonic: &str) -> Result { + /// Returns a `KeyOriginError::Mnemonic` if the provided phrase is invalid. + pub fn new(mnemonic: &str) -> Result { let mnemonic: Mnemonic = mnemonic .parse() - .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?; + .map_err(|e: bip39::Error| KeyOriginError::Mnemonic(e.to_string()))?; let seed = mnemonic.to_seed(""); let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?; @@ -94,36 +106,44 @@ impl HDKey { }) } - fn derive_xpriv(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { + fn derive_xpriv(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { Ok(self.xprv.derive_priv(secp, path)?) } - fn master_xpriv(&self, secp: &Secp256k1) -> Result { + fn master_xpriv(&self, secp: &Secp256k1) -> Result { self.derive_xpriv(&DerivationPath::master(), secp) } - fn derive_xpub(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { + fn derive_xpub(&self, path: &DerivationPath, secp: &Secp256k1) -> Result { let derived = self.derive_xpriv(path, secp)?; Ok(Xpub::from_priv(secp, &derived)) } - fn master_xpub(&self, secp: &Secp256k1) -> Result { + fn master_xpub(&self, secp: &Secp256k1) -> Result { self.derive_xpub(&DerivationPath::master(), secp) } - fn fingerprint(&self, secp: &Secp256k1) -> Result { + fn fingerprint(&self, secp: &Secp256k1) -> Result { Ok(self.master_xpub(secp)?.fingerprint()) } - fn get_slip77_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { + fn get_slip77_descriptor( + &self, + secp: &Secp256k1, + network: &SimplicityNetwork, + ) -> Result { let wpkh_descriptor = self.get_wpkh_descriptor(secp, network)?; let blinding_key = self.master_blinding; Ok(format!("ct(slip77({blinding_key}),{wpkh_descriptor})")) } - fn get_wpkh_descriptor(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Result { + fn get_wpkh_descriptor( + &self, + secp: &Secp256k1, + network: &SimplicityNetwork, + ) -> Result { let fingerprint = self.fingerprint(secp)?; let path = self.get_derivation_path(network)?; let xpub = self.derive_xpub(&path, secp)?; @@ -132,11 +152,11 @@ impl HDKey { } #[allow(clippy::unused_self)] - fn get_derivation_path(&self, network: &SimplicityNetwork) -> Result { + fn get_derivation_path(&self, network: &SimplicityNetwork) -> Result { let coin_type = if network.is_mainnet() { 1776 } else { 1 }; let path = format!("84h/{coin_type}h/0h"); - DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string())) + DerivationPath::from_str(&format!("m/{path}")).map_err(|e| KeyOriginError::DerivationPath(e.to_string())) } } @@ -152,8 +172,8 @@ impl KeyOrigin for HDKey { self.get_private_key(secp, network).public_key(secp) } - fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { - self.get_blinding_private_key(secp, network).public_key(secp) + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option { + Some(self.get_blinding_private_key(secp, network).unwrap().public_key(secp)) } fn get_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { @@ -162,7 +182,7 @@ impl KeyOrigin for HDKey { let derived = full_path.extend( DerivationPath::from_str("0/1") - .map_err(|e| SignerError::DerivationPath(e.to_string())) + .map_err(|e| KeyOriginError::DerivationPath(e.to_string())) .unwrap(), ); @@ -171,34 +191,36 @@ impl KeyOrigin for HDKey { PrivateKey::new(ext_derived.private_key, NetworkKind::Test) } - fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option { let blinding_key = self .master_blinding .blinding_private_key(&self.get_address(secp, network).script_pubkey()); - PrivateKey::new(blinding_key, NetworkKind::Test) + Some(PrivateKey::new(blinding_key, NetworkKind::Test)) } - fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option
{ let mut descriptor = ConfidentialDescriptor::::from_str( &self.get_slip77_descriptor(secp, network).unwrap(), ) - .map_err(|e| SignerError::Slip77Descriptor(e.to_string())) + .map_err(|e| KeyOriginError::Slip77Descriptor(e.to_string())) .unwrap(); // confidential descriptor doesn't support multipath descriptor.descriptor = descriptor.descriptor.into_single_descriptors().unwrap()[0].clone(); - descriptor - .at_derivation_index(1) - .unwrap() - .address(secp, network.address_params()) - .unwrap() + Some( + descriptor + .at_derivation_index(1) + .unwrap() + .address(secp, network.address_params()) + .unwrap(), + ) } fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor(secp, network).unwrap()) - .map_err(|e| SignerError::WpkhDescriptor(e.to_string())) + .map_err(|e| KeyOriginError::WpkhDescriptor(e.to_string())) .unwrap(); descriptor.into_single_descriptors().unwrap()[0] @@ -207,6 +229,26 @@ impl KeyOrigin for HDKey { .address(network.address_params()) .unwrap() } + + fn unblind( + &self, + secp: &Secp256k1, + network: &SimplicityNetwork, + utxos: Vec, + ) -> Result, KeyOriginError> { + let blinding_key = self.get_blinding_private_key(secp, network).unwrap(); + let mut unblinded: Vec = Vec::with_capacity(utxos.len()); + + for mut utxo in utxos { + let secrets = utxo.txout.unblind(secp, blinding_key.inner)?; + + utxo.secrets = Some(secrets); + + unblinded.push(utxo); + } + + Ok(unblinded) + } } /// A simplified key provider powered by a single static secret key. @@ -244,30 +286,32 @@ impl KeyOrigin for SingleKey { self.get_private_key(secp, network).public_key(secp) } - fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PublicKey { - self.get_blinding_private_key(secp, network).public_key(secp) + fn get_blinding_public_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option { + self.get_blinding_private_key(secp, network).map(|x| x.public_key(secp)) } fn get_private_key(&self, _secp: &Secp256k1, _network: &SimplicityNetwork) -> PrivateKey { PrivateKey::new(self.secret_key, NetworkKind::Test) } - fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> PrivateKey { - let master_blinding = self - .blinding_key - .expect("Blinding key is required for confidential operations"); + fn get_blinding_private_key(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option { + let master_blinding = self.blinding_key?; let script_pubkey = self.get_address(secp, network).script_pubkey(); let blinding_key = master_blinding.blinding_private_key(&script_pubkey); - PrivateKey::new(blinding_key, NetworkKind::Test) + Some(PrivateKey::new(blinding_key, NetworkKind::Test)) } - fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { + fn get_confidential_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Option
{ + let blinding_pubkey = self.get_blinding_public_key(secp, network)?; let ecdsa_pubkey = self.get_ecdsa_public_key(secp, network); - let blinding_pubkey = self.get_blinding_public_key(secp, network); - Address::p2wpkh(&ecdsa_pubkey, Some(blinding_pubkey.inner), network.address_params()) + Some(Address::p2wpkh( + &ecdsa_pubkey, + Some(blinding_pubkey.inner), + network.address_params(), + )) } fn get_address(&self, secp: &Secp256k1, network: &SimplicityNetwork) -> Address { @@ -275,4 +319,26 @@ impl KeyOrigin for SingleKey { Address::p2wpkh(&ecdsa_pubkey, None, network.address_params()) } + + fn unblind( + &self, + secp: &Secp256k1, + network: &SimplicityNetwork, + utxos: Vec, + ) -> Result, KeyOriginError> { + let blinding_key = self + .get_blinding_private_key(secp, network) + .ok_or(KeyOriginError::RequiredUnblindingKey)?; + let mut unblinded: Vec = Vec::with_capacity(utxos.len()); + + for mut utxo in utxos { + let secrets = utxo.txout.unblind(secp, blinding_key.inner)?; + + utxo.secrets = Some(secrets); + + unblinded.push(utxo); + } + + Ok(unblinded) + } } diff --git a/examples/basic/tests/confidential_test.rs b/examples/basic/tests/confidential_test.rs index 68356eac..cedc8d09 100644 --- a/examples/basic/tests/confidential_test.rs +++ b/examples/basic/tests/confidential_test.rs @@ -13,7 +13,7 @@ fn make_confidential_to_bob<'a, K1: KeyOrigin, K2: KeyOrigin>( ft.add_output( PartialOutput::new(bob.get_address().script_pubkey(), 1000, asset) - .with_blinding_key(bob.get_blinding_public_key()), + .with_blinding_key(bob.get_blinding_public_key()?), ); let tx_receipt = alice.broadcast(&ft)?; @@ -38,7 +38,7 @@ fn issue_confidential_to_alice<'a, K1: KeyOrigin, K2: KeyOrigin>( ft.add_output( PartialOutput::new(alice.get_address().script_pubkey(), 1000, issuance_details.asset_id) - .with_blinding_key(alice.get_blinding_public_key()), + .with_blinding_key(alice.get_blinding_public_key()?), ); ft.add_output( PartialOutput::new( @@ -46,7 +46,7 @@ fn issue_confidential_to_alice<'a, K1: KeyOrigin, K2: KeyOrigin>( 100, issuance_details.inflation_asset_id, ) - .with_blinding_key(alice.get_blinding_public_key()), + .with_blinding_key(alice.get_blinding_public_key()?), ); let tx_receipt = bob.broadcast(&ft)?; diff --git a/fixtures/tests/confidential_test.rs b/fixtures/tests/confidential_test.rs index f0099d2c..af11de28 100644 --- a/fixtures/tests/confidential_test.rs +++ b/fixtures/tests/confidential_test.rs @@ -13,7 +13,7 @@ fn make_confidential_to_bob( ft.add_output( PartialOutput::new(bob.get_address().script_pubkey(), 1000, asset) - .with_blinding_key(bob.get_blinding_public_key()), + .with_blinding_key(bob.get_blinding_public_key()?), ); let tx_receipt = alice.broadcast(&ft)?; @@ -38,7 +38,7 @@ fn issue_confidential_to_alice( ft.add_output( PartialOutput::new(alice.get_address().script_pubkey(), 1000, issuance_details.asset_id) - .with_blinding_key(alice.get_blinding_public_key()), + .with_blinding_key(alice.get_blinding_public_key()?), ); ft.add_output( PartialOutput::new( @@ -46,7 +46,7 @@ fn issue_confidential_to_alice( 100, issuance_details.inflation_asset_id, ) - .with_blinding_key(alice.get_blinding_public_key()), + .with_blinding_key(alice.get_blinding_public_key()?), ); let tx_receipt = bob.broadcast(&ft)?; diff --git a/fixtures/tests/reissuance_test.rs b/fixtures/tests/reissuance_test.rs index 2f41a7cf..33aa8154 100644 --- a/fixtures/tests/reissuance_test.rs +++ b/fixtures/tests/reissuance_test.rs @@ -15,7 +15,7 @@ fn make_confidential_to_bob<'a, K1: KeyOrigin, K2: KeyOrigin>( ft.add_output( PartialOutput::new(bob.get_address().script_pubkey(), 1000, asset) - .with_blinding_key(bob.get_blinding_public_key()), + .with_blinding_key(bob.get_blinding_public_key()?), ); let tx_receipt = alice.broadcast(&ft)?; @@ -49,7 +49,7 @@ fn issue_explicit_to_alice_with_reissuance<'a, K1: KeyOrigin, K2: KeyOrigin>( 100, issuance_details.inflation_asset_id, ) - .with_blinding_key(bob.get_blinding_public_key()), + .with_blinding_key(bob.get_blinding_public_key()?), ); let tx_receipt = bob.broadcast(&ft)?; @@ -73,7 +73,7 @@ fn reissue_tokens_to_bob<'a, K: KeyOrigin>( reissuance_token_utxo.unblinded_amount(), reissuance_token_utxo.unblinded_asset(), ) - .with_blinding_key(bob.get_blinding_public_key()), + .with_blinding_key(bob.get_blinding_public_key()?), ); ft.add_issuance_input( From d286323b7acca7c768c664c60c40f71e959210eb Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Mon, 6 Jul 2026 09:46:09 +0300 Subject: [PATCH 6/6] sdk: change panic in `SingleKey::unblind` to return an empty vec --- crates/sdk/src/signer/key_origin.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/sdk/src/signer/key_origin.rs b/crates/sdk/src/signer/key_origin.rs index f81aab1f..9afd2fcd 100644 --- a/crates/sdk/src/signer/key_origin.rs +++ b/crates/sdk/src/signer/key_origin.rs @@ -91,8 +91,9 @@ impl HDKey { /// /// # Errors /// Returns a `KeyOriginError::Mnemonic` if the provided phrase is invalid. - pub fn new(mnemonic: &str) -> Result { + pub fn new(mnemonic: impl AsRef) -> Result { let mnemonic: Mnemonic = mnemonic + .as_ref() .parse() .map_err(|e: bip39::Error| KeyOriginError::Mnemonic(e.to_string()))?; let seed = mnemonic.to_seed(""); @@ -324,11 +325,13 @@ impl KeyOrigin for SingleKey { &self, secp: &Secp256k1, network: &SimplicityNetwork, - utxos: Vec, + mut utxos: Vec, ) -> Result, KeyOriginError> { - let blinding_key = self - .get_blinding_private_key(secp, network) - .ok_or(KeyOriginError::RequiredUnblindingKey)?; + let Some(blinding_key) = self.get_blinding_private_key(secp, network) else { + utxos.clear(); + return Ok(utxos); + }; + let mut unblinded: Vec = Vec::with_capacity(utxos.len()); for mut utxo in utxos {