Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/maker/api2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1552,7 +1552,13 @@ fn recover_via_hashlock(maker: Arc<Maker>, 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
Expand Down
59 changes: 55 additions & 4 deletions src/maker/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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"),
Expand All @@ -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.
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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);
}
}
}
71 changes: 4 additions & 67 deletions src/utill.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 {
Expand Down
64 changes: 62 additions & 2 deletions src/wallet/fidelity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -56,7 +56,7 @@ pub enum FidelityError {
/// Old script: <locktime> <OP_CLTV> <OP_DROP> <pubkey> <OP_CHECKSIG>
/// The new script drops the extra byte <OP_DROP>
/// New script: <pubkey> <OP_CHECKSIGVERIFY> <locktime> <OP_CLTV>
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)
Expand All @@ -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<LockTime, FidelityError> {
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down