From 280352f05379e294660a930e0fc2fc33a9cecb55 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:54:04 +0100 Subject: [PATCH 1/2] Fmt in lib-py --- lib-py/src/lib.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib-py/src/lib.rs b/lib-py/src/lib.rs index 5fb24e5..eabfb18 100644 --- a/lib-py/src/lib.rs +++ b/lib-py/src/lib.rs @@ -1,10 +1,9 @@ use dlccryptlib; -use pyo3::prelude::*; use pyo3::exceptions::PyException; +use pyo3::prelude::*; use pyo3::wrap_pyfunction; - // ##### Facade functions for easy Python invocations (pyo3/maturin) /// Initialize the library, load secret from encrypted file. Return the XPUB. @@ -51,12 +50,18 @@ pub fn get_address(index4: u32, index5: u32) -> PyResult { /// Verify a child public key. #[pyfunction] pub fn verify_public_key(index4: u32, index5: u32, pubkey: String) -> PyResult { - dlccryptlib::verify_public_key(index4, index5, &pubkey).map_err(|e| PyErr::new::(e)) + dlccryptlib::verify_public_key(index4, index5, &pubkey) + .map_err(|e| PyErr::new::(e)) } /// Sign a hash with a child private key (specified by index). #[pyfunction] -pub fn sign_hash_ecdsa(hash: String, signer_index4: u32, signer_index5: u32, signer_pubkey: String) -> PyResult { +pub fn sign_hash_ecdsa( + hash: String, + signer_index4: u32, + signer_index5: u32, + signer_pubkey: String, +) -> PyResult { dlccryptlib::sign_hash_ecdsa(&hash, signer_index4, signer_index5, &signer_pubkey) .map_err(|e| PyErr::new::(e)) } @@ -73,14 +78,24 @@ pub fn create_deterministic_nonce( /// Sign a message using Schnorr, with a nonce, using a child key #[pyfunction] -pub fn sign_schnorr_with_nonce(msg: String, nonce_sec_hex: String, index4: u32, index5: u32) -> PyResult { +pub fn sign_schnorr_with_nonce( + msg: String, + nonce_sec_hex: String, + index4: u32, + index5: u32, +) -> PyResult { dlccryptlib::sign_schnorr_with_nonce(&msg, &nonce_sec_hex, index4, index5) .map_err(|e| PyErr::new::(e)) } /// Verify a Schnorr signature over a message, using a child key #[pyfunction] -pub fn verify_schnorr(msg: String, signature_hex: String, index4: u32, index5: u32) -> PyResult { +pub fn verify_schnorr( + msg: String, + signature_hex: String, + index4: u32, + index5: u32, +) -> PyResult { dlccryptlib::verify_schnorr(&msg, &signature_hex, index4, index5) .map_err(|e| PyErr::new::(e)) } @@ -201,4 +216,3 @@ fn dlccryptlib_py(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(create_final_cet_sigs, m)?)?; Ok(()) } - From b778f97dff1a9303be10f6a75c475daf5c719df7 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:00:45 +0100 Subject: [PATCH 2/2] Add create_final_cet_sig for signature decrypting, used by the API --- Cargo.toml | 2 +- lib-py/src/lib.rs | 23 ++++++++ src/adaptor_signature.rs | 55 ++++++++++++++++++ src/lib.rs | 48 ++++++++++++++++ src/lib_struct.rs | 26 ++++++++- src/test_lib.rs | 120 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 270 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4e74753..6d3a7e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dlccryptlib" -version = "1.9.1" +version = "1.9.5" edition = "2021" description = "Library for working with DLC's with adaptor signatures (Discrete Log Contracts), by Cadena Bitcoin" license = "MIT" diff --git a/lib-py/src/lib.rs b/lib-py/src/lib.rs index eabfb18..2dff127 100644 --- a/lib-py/src/lib.rs +++ b/lib-py/src/lib.rs @@ -196,6 +196,28 @@ pub fn create_final_cet_sigs( .map_err(|e| PyErr::new::(e)) } +/// Perform final signing of a CET, decrypt a signature when outcome signatures are available. +/// Return the decrypted signature. +#[pyfunction] +pub fn create_final_cet_sig( + pubkey: String, + num_digits: u8, + oracle_signatures: String, + cet_value_wildcard: String, + cet_sighash: String, + adaptor_signature: String, +) -> PyResult { + dlccryptlib::create_final_cet_sig( + &pubkey, + num_digits, + &oracle_signatures, + &cet_value_wildcard, + &cet_sighash, + &adaptor_signature, + ) + .map_err(|e| PyErr::new::(e)) +} + #[pymodule] fn dlccryptlib_py(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(init, m)?)?; @@ -214,5 +236,6 @@ fn dlccryptlib_py(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(create_cet_adaptor_sigs, m)?)?; m.add_function(wrap_pyfunction!(verify_cet_adaptor_sigs, m)?)?; m.add_function(wrap_pyfunction!(create_final_cet_sigs, m)?)?; + m.add_function(wrap_pyfunction!(create_final_cet_sig, m)?)?; Ok(()) } diff --git a/src/adaptor_signature.rs b/src/adaptor_signature.rs index 7d63763..53b1e9a 100644 --- a/src/adaptor_signature.rs +++ b/src/adaptor_signature.rs @@ -498,3 +498,58 @@ pub fn create_final_cet_signatures( Ok((adapted_sig, my_sig)) } + +/// Decrypt a signature on a CET when outcome signatures are available. +/// Return the decrypted signature. +pub fn create_final_cet_signature( + pubkey: &PublicKey, + num_digits: u8, + oracle_signatures: &Vec, + cet_value_wildcard: &str, + cet_sighash: &[u8; 32], + adaptor_signature: &EcdsaAdaptorSignature, +) -> Result, String> { + // Decompose oracle signatures + if oracle_signatures.len() != num_digits as usize { + return Err(format!( + "Wrong number of oracle signatures {} {}", + oracle_signatures.len(), + num_digits + )); + } + debug_assert_eq!(oracle_signatures.len(), num_digits as usize); + let wildcard = cet_value_wildcard.as_bytes(); + let mut adaptor_secret_vec = Vec::new(); + for d in 0..num_digits { + let ch = wildcard[d as usize]; + if ch >= 48 && ch <= 57 { + let (_nonce, secret_value) = schnorrsig_decompose(&oracle_signatures[d as usize]) + .map_err(|e| format!("Error decomposing Schnorr signature {}", e.to_string()))?; + let adaptor_secret = SecretKey::from_slice(secret_value).map_err(|e| { + format!( + "Error retrieving adaptor secret from signature {}", + e.to_string() + ) + })?; + adaptor_secret_vec.push(adaptor_secret); + } + } + let adaptor_secret_aggregate = aggregate_secret_values(&adaptor_secret_vec)?; + + // Adaptor signature, from the other + let mut adapted_sig = adaptor_signature + .decrypt(&adaptor_secret_aggregate) + .map_err(|e| format!("Error in adaptor signature decryption {}", e.to_string()))? + .serialize_der() + .to_vec(); + adapted_sig.push(EcdsaSighashType::All as u8); + // verify signature + let _res = verify_ecdsa_signature(cet_sighash, &adapted_sig, &pubkey, true).map_err(|e| { + format!( + "Adaptor-derived signature verification failed {}", + e.to_string() + ) + })?; + + Ok(adapted_sig) +} diff --git a/src/lib.rs b/src/lib.rs index 45a19e7..35bdcdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -424,6 +424,54 @@ pub fn create_final_cet_sigs( Ok(sigs) } +pub fn create_final_cet_sig( + pubkey_str: &str, + num_digits: u8, + oracle_signatures_str: &str, + cet_value_wildcard: &str, + cet_sighash_str: &str, + adaptor_signature_str: &str, +) -> Result { + let pubkey = + pubkey_from_hex(pubkey_str).map_err(|e| format!("Failed to parse other pubkey {}", e))?; + + let sigs_split: Vec<_> = oracle_signatures_str.split(" ").collect(); + let mut sigs = Vec::::with_capacity(sigs_split.len()); + for i in 0..sigs_split.len() { + let sig_hex = sigs_split[i].trim(); + if sig_hex.len() > 0 { + let sig = schnorr_sig_from_hex(&sig_hex) + .map_err(|e| format!("Failed to parse element {} {}", i, e))?; + sigs.push(sig); + } + } + if sigs.len() != num_digits as usize { + return Err(format!( + "Wrong number of signatures {} {}", + sigs.len(), + num_digits + )); + } + + let cet_sighash = + hash_from_hex(cet_sighash_str).map_err(|e| format!("Failed to parse sighash {}", e))?; + + let adaptor_signature_bin = Vec::from_hex(adaptor_signature_str) + .map_err(|e| format!("Failed to parse adaptor sig {}", e))?; + let adaptor_signature = EcdsaAdaptorSignature::from_slice(&adaptor_signature_bin) + .map_err(|e| format!("Failed to parse adaptor sig {}", e))?; + let sig = global_lib().read().unwrap().create_final_cet_sig( + &pubkey, + num_digits, + &sigs, + cet_value_wildcard, + &cet_sighash, + &adaptor_signature, + )?; + + Ok(sig.to_lower_hex_string()) +} + // ##### Facade functions for C-style-interface invocations /// Initialize the library, provide the secret as parameter. Return the XPUB. diff --git a/src/lib_struct.rs b/src/lib_struct.rs index 5a0a353..96754a8 100644 --- a/src/lib_struct.rs +++ b/src/lib_struct.rs @@ -3,8 +3,9 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. use crate::adaptor_signature::{ - create_cet_adaptor_signatures, create_final_cet_signatures, sign_hash_ecdsa_with_key, - sign_schnorr_with_nonce_sec, verify_cet_adaptor_signatures, verify_schnorr, + create_cet_adaptor_signatures, create_final_cet_signature, create_final_cet_signatures, + sign_hash_ecdsa_with_key, sign_schnorr_with_nonce_sec, verify_cet_adaptor_signatures, + verify_schnorr, }; use crate::hd_wallet_storage::HDWalletStorage; @@ -282,6 +283,27 @@ impl Lib { other_adaptor_signature, ) } + + /// Decrypt a signature on a CET when outcome signatures are available. + /// Return the decrypted signature. + pub fn create_final_cet_sig( + &self, + pubkey: &PublicKey, + num_digits: u8, + oracle_signatures: &Vec, + cet_value_wildcard: &str, + cet_sighash: &[u8; 32], + adaptor_signature: &EcdsaAdaptorSignature, + ) -> Result, String> { + create_final_cet_signature( + pubkey, + num_digits, + oracle_signatures, + cet_value_wildcard, + cet_sighash, + adaptor_signature, + ) + } } pub(crate) fn global_lib() -> &'static RwLock { diff --git a/src/test_lib.rs b/src/test_lib.rs index 2409e69..620726b 100644 --- a/src/test_lib.rs +++ b/src/test_lib.rs @@ -8,7 +8,7 @@ use crate::{ init_with_entropy, keypair_from_sec_key_hex, sign_schnorr_with_nonce, verify_public_key, verify_schnorr, Lib, }; -use bitcoin::hex::FromHex; +use bitcoin::hex::{DisplayHex, FromHex}; use bitcoin::secp256k1::PublicKey; use secp256k1_zkp::schnorr::Signature as SchnorrSignature; use secp256k1_zkp::EcdsaAdaptorSignature; @@ -533,3 +533,121 @@ fn test_create_final_cet_sigs() { let verif_res2 = verify_ecdsa_signature(&sighash, &my_sig, &my_pubkey, true).unwrap(); assert!(verif_res2); } + +#[test] +fn test_create_final_cet_sig_full() { + let event_id = "btcusd1741474920"; + let digits_template_string = "Outcome:btcusd1741474920:{digit_index}:{digit_outcome}"; + let digits = 4 as u8; + let final_cet_wildcard = "9534"; + let sighash = dummy_bytes32(7); + + // First preparation: create oracle signatures + let mut lib_ora = Lib::new_empty(); + let _xpub = lib_ora.init_with_entropy(&dummy_bytes32(3).to_vec(), DEFAULT_NETWORK); + let oracle_pubkey = lib_ora.get_child_public_key(0, 0).unwrap(); + assert_eq!( + oracle_pubkey.to_string(), + "020a5e571a47cc259d3cc0454a8b7e58bba16e01156bb72d0ce490823f51117cce" + ); + + // Prepare nonces + let mut nonces_sec_vec = Vec::new(); + let mut nonces_pub_vec = Vec::new(); + let mut nonces = String::new(); + for i in 0..(digits as usize) { + let (nsec, npub) = lib_ora + .create_deterministic_nonce(event_id, i as u32) + .unwrap(); + nonces_sec_vec.push(<[u8; 32]>::from_hex(&nsec).unwrap()); + nonces_pub_vec.push(npub.clone()); + nonces += &format!("{} ", npub); + } + assert_eq!(nonces, "03829589a7db8530b521577ce5b9560e31cb29b943927b417c580ec3b6e57317a9 0278d6b6808d5370da62c9304f66415c1f9f408a2ee9d95a9dc836512218a7b04f 027c14675c2bd2e728e5760d5017d4ae2b22a4a33193689654e5eb13111ab7f491 02e7657c7d006d27b248642974875348b41299690a6415bc019ac71e6988434daa "); + + let mut oracle_signatures = Vec::new(); + for i in 0..(digits as usize) { + let digit_value = final_cet_wildcard.chars().collect::>()[i as usize]; + let digit_string = digits_template_string + .replace("{digit_index}", &format!("{}", i)) + .replace("{digit_outcome}", &format!("{}", digit_value)); + let sig = lib_ora + .sign_schnorr_with_nonce(&digit_string, &nonces_sec_vec[i], 0, 0) + .unwrap(); + oracle_signatures.push(sig); + } + + let mut lib1 = Lib::new_empty(); + let _xpub = lib1 + .init_with_entropy(&dummy_bytes32(1).to_vec(), DEFAULT_NETWORK) + .unwrap(); + let my_pubkey = lib1.get_child_public_key(0, 0).unwrap(); + assert_eq!( + my_pubkey.to_string(), + "035bcac7323e9971268213a188d8268277abcd962cdf096e68e2b58c228216f104" + ); + + // Create adaptor sig + // only use one CET + let adaptor_sigs_vec = lib1 + .create_cet_adaptor_sigs( + digits, + 1, // num_cets + digits_template_string, + &oracle_pubkey, + 0, + 0, + &my_pubkey, + &nonces_pub_vec, + &vec![final_cet_wildcard.to_string()], // interval_wildcards + &vec![sighash], + ) + .unwrap(); + let adaptor_sig = adaptor_sigs_vec[0]; + // adator sig is variable, cannot assert + assert_eq!(adaptor_sig.to_string().len(), 324); + + // And now decode the CET signature + let my_final_sig = lib1 + .create_final_cet_sig( + &my_pubkey, + digits, + &oracle_signatures, + final_cet_wildcard, + &sighash, + &adaptor_sig, + ) + .unwrap(); + // Result is variable, cannot assert + assert!(my_final_sig.len() >= 70 && my_final_sig.len() <= 72); +} + +#[test] +fn test_create_final_cet_sig() { + let digits = 4 as u8; + let final_cet_wildcard = "9534"; + let sighash = dummy_bytes32(7); + let oracle_signatures = vec![ + SchnorrSignature::from_str("829589a7db8530b521577ce5b9560e31cb29b943927b417c580ec3b6e57317a91e36a673186742bd75a85f6751473fd10155f44fec71a62e95ca6e2f6a436de6").unwrap(), + SchnorrSignature::from_str("78d6b6808d5370da62c9304f66415c1f9f408a2ee9d95a9dc836512218a7b04fd67ccbe7eefd9cd96c396a64b5ddc8e6a42d82e8e753ced18e162d238787da50").unwrap(), + SchnorrSignature::from_str("7c14675c2bd2e728e5760d5017d4ae2b22a4a33193689654e5eb13111ab7f491f5889b410410014ddc5bef26d55b3cfd6f3c0bec338fc8c29edc323da3e301e1").unwrap(), + SchnorrSignature::from_str("e7657c7d006d27b248642974875348b41299690a6415bc019ac71e6988434daaf722c5e3e015c26180d7a5e5543f6668297d188536e98b9de6313df4c5ca5f60").unwrap(), + ]; + let my_pubkey = + PublicKey::from_str("035bcac7323e9971268213a188d8268277abcd962cdf096e68e2b58c228216f104") + .unwrap(); + let adaptor_sig = EcdsaAdaptorSignature::from_str("02b916af8ad219724712ec349ed2a0cb200efe228b8ad4bcbe048aa41d687b07c5039325102a25ead4dd1ce4cccd0fe49d444c041a2f230ee0469eb3f3cf34efe41deb736f677a9fbd248e8fcd6afae74c418fc8f791075f62d9ec66f03a761be15b7e803a66e5b57db2920cfaf5343bd68027ca01689967aa316c2af9ca75c35e68338436fb24963572af2cc762b8666653645282569b16997324cf884375ca6684").unwrap(); + + let lib = Lib::new_empty(); + let final_sig = lib + .create_final_cet_sig( + &my_pubkey, + digits, + &oracle_signatures, + final_cet_wildcard, + &sighash, + &adaptor_sig, + ) + .unwrap(); + assert_eq!(final_sig.to_lower_hex_string(), "3045022100b916af8ad219724712ec349ed2a0cb200efe228b8ad4bcbe048aa41d687b07c502202fe73c65c04abd4394375f4225ae11e0dfad77362898497e4d79b0df0b2b8fdf01"); +}