From fbeeb6e0abc915c1667d596b05882ad0a6a0303f Mon Sep 17 00:00:00 2001 From: yomanthunder Date: Mon, 22 Dec 2025 15:53:06 +0530 Subject: [PATCH 1/3] feat(maker-config): validate fidelity timelock check at config Introduce maximum and minimum allowed fidelity timelocks in blocks Ensuring MakerConfig::new() returns a valid configuration --- src/maker/api2.rs | 6 ++++- src/maker/config.rs | 57 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/maker/api2.rs b/src/maker/api2.rs index 2b9b41cb5..ac149c0b3 100644 --- a/src/maker/api2.rs +++ b/src/maker/api2.rs @@ -1529,7 +1529,11 @@ fn recover_via_hashlock(maker: Arc, incoming: IncomingSwapCoinV2) -> Resu ); None } else { - let preimage = incoming.hash_preimage.unwrap(); + let preimage = if let Some(preimage) = incoming.hash_preimage { + preimage + } else { + return Err(MakerError::General("Preimage missing for hashlock recovery")); + }; let mut wallet = maker.wallet.write()?; // Attempt to spend via hashlock diff --git a/src/maker/config.rs b/src/maker/config.rs index 193f0fcde..47423921c 100644 --- a/src/maker/config.rs +++ b/src/maker/config.rs @@ -12,6 +12,11 @@ use crate::utill::{get_maker_dir, parse_field}; use super::api::MIN_SWAP_AMOUNT; +/// Maximum and Minimum allowed timelocks for Fidelity Bonds +/// 1 block = ~10 minutes +const MIN_FIDELITY_TIMELOCK: u32 = 12_960; +const MAX_FIDELITY_TIMELOCK: u32 = 25_920; + /// Maker Configuration /// /// This struct defines all configurable parameters for the Maker module, including: @@ -100,8 +105,7 @@ impl MakerConfig { "Successfully loaded config file from : {}", config_path.display() ); - - Ok(MakerConfig { + let config = MakerConfig { rpc_port: parse_field(config_map.get("rpc_port"), default_config.rpc_port), min_swap_amount: parse_field( config_map.get("min_swap_amount"), @@ -127,7 +131,11 @@ impl MakerConfig { config_map.get("amount_relative_fee_pct"), default_config.amount_relative_fee_pct, ), - }) + }; + config + .validate() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + Ok(config) } /// This function serializes the MakerConfig into a TOML format and writes it to disk. @@ -174,6 +182,23 @@ amount_relative_fee_pct = {} file.flush()?; Ok(()) } + + /// Validates the MakerConfig parameters with bound checks. + pub(crate) fn validate(&self) -> Result<(), String> { + if self.fidelity_timelock < MIN_FIDELITY_TIMELOCK { + return Err(format!( + "Fidelity timelock too low: {}. Minimum is {} blocks.", + self.fidelity_timelock, MIN_FIDELITY_TIMELOCK + )); + } + if self.fidelity_timelock > MAX_FIDELITY_TIMELOCK { + return Err(format!( + "Fidelity timelock too high: {}. Maximum is {} blocks.", + self.fidelity_timelock, MAX_FIDELITY_TIMELOCK + )); + } + Ok(()) + } } #[cfg(test)] @@ -252,4 +277,30 @@ mod tests { remove_temp_config(&config_path); assert_eq!(config, MakerConfig::default()); } + #[test] + fn test_fidelity_timelock_validation_cases() { + let cases = vec![ + (12_960, true), + (15_000, true), + (25_920, true), + (12_000, false), + (30_000, false), + ]; + + for (timelock, should_pass) in cases { + let contents = format!("fidelity_timelock = {}", timelock); + let path = create_temp_config(&contents, "timelock_test.toml"); + + let result = MakerConfig::new(Some(&path)); + + assert_eq!( + result.is_ok(), + should_pass, + "timelock {} validation mismatch", + timelock + ); + + remove_temp_config(&path); + } + } } From 1d8c36864c2d315e781f19978190b1d61ea02d84 Mon Sep 17 00:00:00 2001 From: yomanthunder Date: Mon, 22 Dec 2025 19:53:04 +0530 Subject: [PATCH 2/3] refactor(fidelity): make fidelity_redeemscript private Moves fidelity verification logic out of utils and into wallet/fidelity, allowing fidelity_redeemscript to remain private and enforcing module boundaries. --- src/maker/api2.rs | 4 ++- src/maker/config.rs | 2 +- src/utill.rs | 71 +++--------------------------------------- src/wallet/fidelity.rs | 66 +++++++++++++++++++++++++++++++++++++-- src/wallet/mod.rs | 2 +- 5 files changed, 72 insertions(+), 73 deletions(-) diff --git a/src/maker/api2.rs b/src/maker/api2.rs index ac149c0b3..bd01a33a1 100644 --- a/src/maker/api2.rs +++ b/src/maker/api2.rs @@ -1532,7 +1532,9 @@ fn recover_via_hashlock(maker: Arc, incoming: IncomingSwapCoinV2) -> Resu let preimage = if let Some(preimage) = incoming.hash_preimage { preimage } else { - return Err(MakerError::General("Preimage missing for hashlock recovery")); + return Err(MakerError::General( + "Preimage missing for hashlock recovery", + )); }; let mut wallet = maker.wallet.write()?; diff --git a/src/maker/config.rs b/src/maker/config.rs index 47423921c..066c8dd17 100644 --- a/src/maker/config.rs +++ b/src/maker/config.rs @@ -182,7 +182,7 @@ amount_relative_fee_pct = {} file.flush()?; Ok(()) } - + /// Validates the MakerConfig parameters with bound checks. pub(crate) fn validate(&self) -> Result<(), String> { if self.fidelity_timelock < MIN_FIDELITY_TIMELOCK { diff --git a/src/utill.rs b/src/utill.rs index ad6636194..8559d356c 100644 --- a/src/utill.rs +++ b/src/utill.rs @@ -1,11 +1,10 @@ //! Various utility and helper functions for both Taker and Maker. use bitcoin::{ - absolute::LockTime, hashes::Hash, key::{rand::thread_rng, Keypair}, - secp256k1::{Message, Secp256k1, SecretKey}, - Address, Amount, FeeRate, PublicKey, ScriptBuf, Transaction, WitnessProgram, WitnessVersion, + secp256k1::{Secp256k1, SecretKey}, + Amount, FeeRate, PublicKey, ScriptBuf, Transaction, WitnessProgram, WitnessVersion, }; use bitcoind::bitcoincore_rpc::json::ListUnspentResultEntry; use log::LevelFilter; @@ -44,11 +43,9 @@ static LOGGER: OnceLock<()> = OnceLock::new(); use crate::{ error::NetError, protocol::{ - contract::derive_maker_pubkey_and_nonce, - error::ProtocolError, - messages::{FidelityProof, MultisigPrivkey}, + contract::derive_maker_pubkey_and_nonce, error::ProtocolError, messages::MultisigPrivkey, }, - wallet::{fidelity_redeemscript, FidelityError, SwapCoin, UTXOSpendInfo, WalletError}, + wallet::{SwapCoin, UTXOSpendInfo, WalletError}, }; const INPUT_CHARSET: &str = @@ -479,66 +476,6 @@ pub fn parse_proxy_auth(s: &str) -> Result<(String, String), NetError> { Ok((user, passwd)) } -pub(crate) fn verify_fidelity_checks( - proof: &FidelityProof, - addr: &str, - tx: Transaction, - current_height: u64, -) -> Result<(), WalletError> { - // Check if bond lock time has expired - let lock_time = LockTime::from_height(current_height as u32)?; - if lock_time > proof.bond.lock_time { - return Err(FidelityError::BondLocktimeExpired.into()); - } - - // Verify certificate hash - let expected_cert_hash = proof - .bond - .generate_cert_hash(addr) - .expect("Bond is not yet confirmed"); - if proof.cert_hash != expected_cert_hash { - return Err(FidelityError::InvalidCertHash.into()); - } - - let networks = vec![ - bitcoin::network::Network::Regtest, - bitcoin::network::Network::Testnet, - bitcoin::network::Network::Bitcoin, - bitcoin::network::Network::Signet, - ]; - - let mut all_failed = true; - - for network in networks { - // Validate redeem script and corresponding address - let fidelity_redeem_script = - fidelity_redeemscript(&proof.bond.lock_time, &proof.bond.pubkey); - let expected_address = Address::p2wsh(fidelity_redeem_script.as_script(), network); - - let derived_script_pubkey = expected_address.script_pubkey(); - let tx_out = tx - .tx_out(proof.bond.outpoint.vout as usize) - .map_err(|_| WalletError::General("Outputs index error".to_string()))?; - - if tx_out.script_pubkey == derived_script_pubkey { - all_failed = false; - break; // No need to continue checking once we find a successful match - } - } - - // Only throw error if all checks fail - if all_failed { - return Err(FidelityError::BondDoesNotExist.into()); - } - - // Verify ECDSA signature - let secp = Secp256k1::new(); - let cert_message = Message::from_digest_slice(proof.cert_hash.as_byte_array())?; - secp.verify_ecdsa(&cert_message, &proof.cert_sig, &proof.bond.pubkey.inner)?; - - Ok(()) -} - /// Tor Error grades #[derive(Debug)] pub enum TorError { diff --git a/src/wallet/fidelity.rs b/src/wallet/fidelity.rs index 3366a9c64..a0e83348e 100644 --- a/src/wallet/fidelity.rs +++ b/src/wallet/fidelity.rs @@ -1,6 +1,6 @@ use crate::{ protocol::messages::FidelityProof, - utill::{redeemscript_to_scriptpubkey, verify_fidelity_checks, MIN_FEE_RATE}, + utill::{redeemscript_to_scriptpubkey, MIN_FEE_RATE}, wallet::Wallet, }; use bitcoin::{ @@ -10,7 +10,7 @@ use bitcoin::{ opcodes::all::{OP_CHECKSIGVERIFY, OP_CLTV}, script::{Builder, Instruction}, secp256k1::{Keypair, Message, Secp256k1}, - Address, Amount, OutPoint, PublicKey, ScriptBuf, + Address, Amount, OutPoint, PublicKey, ScriptBuf, Transaction, }; use bitcoind::bitcoincore_rpc::RpcApi; use serde::{Deserialize, Serialize}; @@ -56,7 +56,7 @@ pub enum FidelityError { /// Old script: /// The new script drops the extra byte /// New script: -pub(crate) fn fidelity_redeemscript(lock_time: &LockTime, pubkey: &PublicKey) -> ScriptBuf { +fn fidelity_redeemscript(lock_time: &LockTime, pubkey: &PublicKey) -> ScriptBuf { Builder::new() .push_key(pubkey) .push_opcode(OP_CHECKSIGVERIFY) @@ -65,6 +65,66 @@ pub(crate) fn fidelity_redeemscript(lock_time: &LockTime, pubkey: &PublicKey) -> .into_script() } +pub(crate) fn verify_fidelity_checks( + proof: &FidelityProof, + addr: &str, + tx: Transaction, + current_height: u64, +) -> Result<(), WalletError> { + // Check if bond lock time has expired + let lock_time = LockTime::from_height(current_height as u32)?; + if lock_time > proof.bond.lock_time { + return Err(FidelityError::BondLocktimeExpired.into()); + } + + // Verify certificate hash + let expected_cert_hash = proof + .bond + .generate_cert_hash(addr) + .expect("Bond is not yet confirmed"); + if proof.cert_hash != expected_cert_hash { + return Err(FidelityError::InvalidCertHash.into()); + } + + let networks = vec![ + bitcoin::network::Network::Regtest, + bitcoin::network::Network::Testnet, + bitcoin::network::Network::Bitcoin, + bitcoin::network::Network::Signet, + ]; + + let mut all_failed = true; + + for network in networks { + // Validate redeem script and corresponding address + let fidelity_redeem_script = + fidelity_redeemscript(&proof.bond.lock_time, &proof.bond.pubkey); + let expected_address = Address::p2wsh(fidelity_redeem_script.as_script(), network); + + let derived_script_pubkey = expected_address.script_pubkey(); + let tx_out = tx + .tx_out(proof.bond.outpoint.vout as usize) + .map_err(|_| WalletError::General("Outputs index error".to_string()))?; + + if tx_out.script_pubkey == derived_script_pubkey { + all_failed = false; + break; // No need to continue checking once we find a successful match + } + } + + // Only throw error if all checks fail + if all_failed { + return Err(FidelityError::BondDoesNotExist.into()); + } + + // Verify ECDSA signature + let secp = Secp256k1::new(); + let cert_message = Message::from_digest_slice(proof.cert_hash.as_byte_array())?; + secp.verify_ecdsa(&cert_message, &proof.cert_sig, &proof.bond.pubkey.inner)?; + + Ok(()) +} + #[allow(unused)] /// Reads the locktime from a fidelity redeemscript. fn read_locktime_from_fidelity_script(redeemscript: &ScriptBuf) -> Result { diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f7e9538d3..067e1600f 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -17,7 +17,7 @@ pub use api::{Balances, UTXOSpendInfo, Wallet}; pub use backup::WalletBackup; pub use error::WalletError; pub use fidelity::FidelityBond; -pub(crate) use fidelity::{fidelity_redeemscript, FidelityError}; +pub(crate) use fidelity::FidelityError; pub use rpc::RPCConfig; pub use spend::Destination; pub(crate) use swapcoin::{ From ba667bd44b819db85b3da2eab121d3d94af86f6b Mon Sep 17 00:00:00 2001 From: yomanthunder Date: Wed, 24 Dec 2025 13:02:20 +0530 Subject: [PATCH 3/3] fix: align default fidelity timelock with allowed bounds --- src/maker/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/maker/config.rs b/src/maker/config.rs index 066c8dd17..6bbaeb371 100644 --- a/src/maker/config.rs +++ b/src/maker/config.rs @@ -52,7 +52,7 @@ impl Default for MakerConfig { fn default() -> Self { let (fidelity_amount, fidelity_timelock, base_fee, amount_relative_fee_pct) = if cfg!(feature = "integration-test") { - (5_000_000, 26_000, 1000, 2.50) // Test values + (5_000_000, 25_000, 1000, 2.50) // Test values } else { (50_000, 13104, 100, 0.1) // Production values };