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
52 changes: 49 additions & 3 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;

// Fidelity Bond relative timelock in number of blocks ( 1 block ~= 10mins)
// Must be between 12,960 (≈3 months) and 25,920 (≈6 months)
const MIN_FIDELITY_TIMELOCK: u32 = 12_960; // No. of blocks produce in 3 months(144*90)
const MAX_FIDELITY_TIMELOCK: u32 = 25_920; // No. of blocks produce in 6 months(144*180)

/// 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, MAX_FIDELITY_TIMELOCK, 1000, 2.50) // Test values
} else {
(50_000, 13104, 100, 0.1) // Production values
};
Expand All @@ -66,6 +71,26 @@ impl Default for MakerConfig {
}
}
}
/// Ensure fidelity timelock lies in the allowed range (3months-6months)
fn validate_fidelity_timelock(timelock: u32) -> u32 {
if timelock < MIN_FIDELITY_TIMELOCK {
log::warn!(
"fidelity_timelock too low ({} blocks). Clamping to minimum allowed: {} blocks",
timelock,
MIN_FIDELITY_TIMELOCK
);
MIN_FIDELITY_TIMELOCK
} else if timelock > MAX_FIDELITY_TIMELOCK {
log::warn!(
"fidelity_timelock too high ({} blocks). Clamping to maximum allowed: {} blocks",
timelock,
MAX_FIDELITY_TIMELOCK
);
MAX_FIDELITY_TIMELOCK
} else {
timelock
}
}

impl MakerConfig {
/// Constructs a [`MakerConfig`] from a specified data directory. Or creates default configs and load them.
Expand Down Expand Up @@ -118,10 +143,10 @@ impl MakerConfig {
config_map.get("fidelity_amount"),
default_config.fidelity_amount,
),
fidelity_timelock: parse_field(
fidelity_timelock: validate_fidelity_timelock(parse_field(
config_map.get("fidelity_timelock"),
default_config.fidelity_timelock,
),
)),
base_fee: parse_field(config_map.get("base_fee"), default_config.base_fee),
amount_relative_fee_pct: parse_field(
config_map.get("amount_relative_fee_pct"),
Expand Down Expand Up @@ -150,6 +175,7 @@ min_swap_amount = {}
# Fidelity Bond amount in satoshis
fidelity_amount = {}
# Fidelity Bond relative timelock in number of blocks
# Must be between {} (~3 months) and {} (~6 months)
fidelity_timelock = {}
# A fixed base fee charged by the Maker for providing its services (in satoshis)
base_fee = {}
Expand All @@ -163,6 +189,8 @@ amount_relative_fee_pct = {}
self.tor_auth_password,
self.min_swap_amount,
self.fidelity_amount,
MIN_FIDELITY_TIMELOCK,
MAX_FIDELITY_TIMELOCK,
self.fidelity_timelock,
self.base_fee,
self.amount_relative_fee_pct,
Expand Down Expand Up @@ -252,4 +280,22 @@ mod tests {
remove_temp_config(&config_path);
assert_eq!(config, MakerConfig::default());
}

#[test]
fn fidelity_timelock_clamped_low() {
let contents = r#"fidelity_timelock = 1000"#;
let path = create_temp_config(contents, "low.toml");
let cfg = MakerConfig::new(Some(&path)).unwrap();
remove_temp_config(&path);
assert_eq!(cfg.fidelity_timelock, MIN_FIDELITY_TIMELOCK);
}

#[test]
fn fidelity_timelock_clamped_high() {
let contents = r#"fidelity_timelock = 50000"#;
let path = create_temp_config(contents, "high.toml");
let cfg = MakerConfig::new(Some(&path)).unwrap();
remove_temp_config(&path);
assert_eq!(cfg.fidelity_timelock, MAX_FIDELITY_TIMELOCK);
}
}
3 changes: 2 additions & 1 deletion src/taker/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ use crate::{
api2::connect_to_maker,
routines::handshake_maker,
},
utill::{read_message, send_message, verify_fidelity_checks},
utill::{read_message, send_message},
wallet::verify_fidelity_checks,
watch_tower::{rpc_backend::BitcoinRpc, service::WatchService, watcher::WatcherEvent},
};

Expand Down
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
66 changes: 64 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,68 @@ pub(crate) fn fidelity_redeemscript(lock_time: &LockTime, pubkey: &PublicKey) ->
.into_script()
}

/// Verifies a fidelity bond by checking timelock validity,
/// certificate integrity, redeem script existence, and ECDSA signature correctness.
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::{verify_fidelity_checks, FidelityError};
pub use rpc::RPCConfig;
pub use spend::Destination;
pub use storage::AddressType;
Expand Down
Loading