diff --git a/src/maker/api2.rs b/src/maker/api2.rs index fe0a3f26..ae3880df 100644 --- a/src/maker/api2.rs +++ b/src/maker/api2.rs @@ -1552,7 +1552,13 @@ 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 193f0fcd..6bbaeb37 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: @@ -47,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 }; @@ -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); + } + } } diff --git a/src/utill.rs b/src/utill.rs index 48537565..f5cc658e 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; @@ -53,11 +52,9 @@ pub const COINSWAP_KIND: u16 = 37777; 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 = @@ -488,66 +485,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 e8aad80f..35e68a41 100644 --- a/src/wallet/fidelity.rs +++ b/src/wallet/fidelity.rs @@ -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 b0ea62a8..0573c1d4 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 use storage::AddressType;