Skip to content

Commit df3cf62

Browse files
committed
fix: Limit the fidelity timelock b/w 3-6months in maker config
1 parent 128c0ac commit df3cf62

File tree

5 files changed

+120
-74
lines changed

5 files changed

+120
-74
lines changed

src/maker/config.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ use crate::utill::{get_maker_dir, parse_field};
1212

1313
use super::api::MIN_SWAP_AMOUNT;
1414

15+
// Fidelity Bond relative timelock in number of blocks ( 1 block ~= 10mins)
16+
// Must be between 12,960 (≈3 months) and 25,920 (≈6 months)
17+
const MIN_FIDELITY_TIMELOCK: u32 = 12_960; // No. of blocks produce in 3 months(144*90)
18+
const MAX_FIDELITY_TIMELOCK: u32 = 25_920; // No. of blocks produce in 6 months(144*180)
19+
1520
/// Maker Configuration
1621
///
1722
/// This struct defines all configurable parameters for the Maker module, including:
@@ -47,7 +52,7 @@ impl Default for MakerConfig {
4752
fn default() -> Self {
4853
let (fidelity_amount, fidelity_timelock, base_fee, amount_relative_fee_pct) =
4954
if cfg!(feature = "integration-test") {
50-
(5_000_000, 26_000, 1000, 2.50) // Test values
55+
(5_000_000, MAX_FIDELITY_TIMELOCK, 1000, 2.50) // Test values
5156
} else {
5257
(50_000, 13104, 100, 0.1) // Production values
5358
};
@@ -66,6 +71,26 @@ impl Default for MakerConfig {
6671
}
6772
}
6873
}
74+
/// Ensure fidelity timelock lies in the allowed range (3months-6months)
75+
fn validate_fidelity_timelock(timelock: u32) -> u32 {
76+
if timelock < MIN_FIDELITY_TIMELOCK {
77+
log::warn!(
78+
"fidelity_timelock too low ({} blocks). Clamping to minimum allowed: {} blocks",
79+
timelock,
80+
MIN_FIDELITY_TIMELOCK
81+
);
82+
MIN_FIDELITY_TIMELOCK
83+
} else if timelock > MAX_FIDELITY_TIMELOCK {
84+
log::warn!(
85+
"fidelity_timelock too high ({} blocks). Clamping to maximum allowed: {} blocks",
86+
timelock,
87+
MAX_FIDELITY_TIMELOCK
88+
);
89+
MAX_FIDELITY_TIMELOCK
90+
} else {
91+
timelock
92+
}
93+
}
6994

7095
impl MakerConfig {
7196
/// Constructs a [`MakerConfig`] from a specified data directory. Or creates default configs and load them.
@@ -118,10 +143,10 @@ impl MakerConfig {
118143
config_map.get("fidelity_amount"),
119144
default_config.fidelity_amount,
120145
),
121-
fidelity_timelock: parse_field(
146+
fidelity_timelock: validate_fidelity_timelock(parse_field(
122147
config_map.get("fidelity_timelock"),
123148
default_config.fidelity_timelock,
124-
),
149+
)),
125150
base_fee: parse_field(config_map.get("base_fee"), default_config.base_fee),
126151
amount_relative_fee_pct: parse_field(
127152
config_map.get("amount_relative_fee_pct"),
@@ -150,6 +175,7 @@ min_swap_amount = {}
150175
# Fidelity Bond amount in satoshis
151176
fidelity_amount = {}
152177
# Fidelity Bond relative timelock in number of blocks
178+
# Must be between {} (~3 months) and {} (~6 months)
153179
fidelity_timelock = {}
154180
# A fixed base fee charged by the Maker for providing its services (in satoshis)
155181
base_fee = {}
@@ -163,6 +189,8 @@ amount_relative_fee_pct = {}
163189
self.tor_auth_password,
164190
self.min_swap_amount,
165191
self.fidelity_amount,
192+
MIN_FIDELITY_TIMELOCK,
193+
MAX_FIDELITY_TIMELOCK,
166194
self.fidelity_timelock,
167195
self.base_fee,
168196
self.amount_relative_fee_pct,
@@ -252,4 +280,22 @@ mod tests {
252280
remove_temp_config(&config_path);
253281
assert_eq!(config, MakerConfig::default());
254282
}
283+
284+
#[test]
285+
fn fidelity_timelock_clamped_low() {
286+
let contents = r#"fidelity_timelock = 1000"#;
287+
let path = create_temp_config(contents, "low.toml");
288+
let cfg = MakerConfig::new(Some(&path)).unwrap();
289+
remove_temp_config(&path);
290+
assert_eq!(cfg.fidelity_timelock, MIN_FIDELITY_TIMELOCK);
291+
}
292+
293+
#[test]
294+
fn fidelity_timelock_clamped_high() {
295+
let contents = r#"fidelity_timelock = 50000"#;
296+
let path = create_temp_config(contents, "high.toml");
297+
let cfg = MakerConfig::new(Some(&path)).unwrap();
298+
remove_temp_config(&path);
299+
assert_eq!(cfg.fidelity_timelock, MAX_FIDELITY_TIMELOCK);
300+
}
255301
}

src/taker/offers.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ use crate::{
3737
api2::connect_to_maker,
3838
routines::handshake_maker,
3939
},
40-
utill::{read_message, send_message, verify_fidelity_checks},
40+
utill::{read_message, send_message},
41+
wallet::verify_fidelity_checks,
4142
watch_tower::{rpc_backend::BitcoinRpc, service::WatchService, watcher::WatcherEvent},
4243
};
4344

src/utill.rs

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
//! Various utility and helper functions for both Taker and Maker.
22
33
use bitcoin::{
4-
absolute::LockTime,
54
hashes::Hash,
65
key::{rand::thread_rng, Keypair},
7-
secp256k1::{Message, Secp256k1, SecretKey},
8-
Address, Amount, FeeRate, PublicKey, ScriptBuf, Transaction, WitnessProgram, WitnessVersion,
6+
secp256k1::{Secp256k1, SecretKey},
7+
Amount, FeeRate, PublicKey, ScriptBuf, Transaction, WitnessProgram, WitnessVersion,
98
};
109
use bitcoind::bitcoincore_rpc::json::ListUnspentResultEntry;
1110
use log::LevelFilter;
@@ -53,11 +52,9 @@ pub const COINSWAP_KIND: u16 = 37777;
5352
use crate::{
5453
error::NetError,
5554
protocol::{
56-
contract::derive_maker_pubkey_and_nonce,
57-
error::ProtocolError,
58-
messages::{FidelityProof, MultisigPrivkey},
55+
contract::derive_maker_pubkey_and_nonce, error::ProtocolError, messages::MultisigPrivkey,
5956
},
60-
wallet::{fidelity_redeemscript, FidelityError, SwapCoin, UTXOSpendInfo, WalletError},
57+
wallet::{SwapCoin, UTXOSpendInfo, WalletError},
6158
};
6259

6360
const INPUT_CHARSET: &str =
@@ -488,66 +485,6 @@ pub fn parse_proxy_auth(s: &str) -> Result<(String, String), NetError> {
488485
Ok((user, passwd))
489486
}
490487

491-
pub(crate) fn verify_fidelity_checks(
492-
proof: &FidelityProof,
493-
addr: &str,
494-
tx: Transaction,
495-
current_height: u64,
496-
) -> Result<(), WalletError> {
497-
// Check if bond lock time has expired
498-
let lock_time = LockTime::from_height(current_height as u32)?;
499-
if lock_time > proof.bond.lock_time {
500-
return Err(FidelityError::BondLocktimeExpired.into());
501-
}
502-
503-
// Verify certificate hash
504-
let expected_cert_hash = proof
505-
.bond
506-
.generate_cert_hash(addr)
507-
.expect("Bond is not yet confirmed");
508-
if proof.cert_hash != expected_cert_hash {
509-
return Err(FidelityError::InvalidCertHash.into());
510-
}
511-
512-
let networks = vec![
513-
bitcoin::network::Network::Regtest,
514-
bitcoin::network::Network::Testnet,
515-
bitcoin::network::Network::Bitcoin,
516-
bitcoin::network::Network::Signet,
517-
];
518-
519-
let mut all_failed = true;
520-
521-
for network in networks {
522-
// Validate redeem script and corresponding address
523-
let fidelity_redeem_script =
524-
fidelity_redeemscript(&proof.bond.lock_time, &proof.bond.pubkey);
525-
let expected_address = Address::p2wsh(fidelity_redeem_script.as_script(), network);
526-
527-
let derived_script_pubkey = expected_address.script_pubkey();
528-
let tx_out = tx
529-
.tx_out(proof.bond.outpoint.vout as usize)
530-
.map_err(|_| WalletError::General("Outputs index error".to_string()))?;
531-
532-
if tx_out.script_pubkey == derived_script_pubkey {
533-
all_failed = false;
534-
break; // No need to continue checking once we find a successful match
535-
}
536-
}
537-
538-
// Only throw error if all checks fail
539-
if all_failed {
540-
return Err(FidelityError::BondDoesNotExist.into());
541-
}
542-
543-
// Verify ECDSA signature
544-
let secp = Secp256k1::new();
545-
let cert_message = Message::from_digest_slice(proof.cert_hash.as_byte_array())?;
546-
secp.verify_ecdsa(&cert_message, &proof.cert_sig, &proof.bond.pubkey.inner)?;
547-
548-
Ok(())
549-
}
550-
551488
/// Tor Error grades
552489
#[derive(Debug)]
553490
pub enum TorError {

src/wallet/fidelity.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use bitcoin::{
1010
opcodes::all::{OP_CHECKSIGVERIFY, OP_CLTV},
1111
script::{Builder, Instruction},
1212
secp256k1::{Keypair, Message, Secp256k1},
13-
Address, Amount, OutPoint, PublicKey, ScriptBuf,
13+
Address, Amount, OutPoint, PublicKey, ScriptBuf, Transaction,
1414
};
1515
use bitcoind::bitcoincore_rpc::RpcApi;
1616
use serde::{Deserialize, Serialize};
@@ -56,7 +56,7 @@ pub enum FidelityError {
5656
/// Old script: <locktime> <OP_CLTV> <OP_DROP> <pubkey> <OP_CHECKSIG>
5757
/// The new script drops the extra byte <OP_DROP>
5858
/// New script: <pubkey> <OP_CHECKSIGVERIFY> <locktime> <OP_CLTV>
59-
pub(crate) fn fidelity_redeemscript(lock_time: &LockTime, pubkey: &PublicKey) -> ScriptBuf {
59+
fn fidelity_redeemscript(lock_time: &LockTime, pubkey: &PublicKey) -> ScriptBuf {
6060
Builder::new()
6161
.push_key(pubkey)
6262
.push_opcode(OP_CHECKSIGVERIFY)
@@ -65,6 +65,68 @@ pub(crate) fn fidelity_redeemscript(lock_time: &LockTime, pubkey: &PublicKey) ->
6565
.into_script()
6666
}
6767

68+
/// Verifies a fidelity bond by checking timelock validity,
69+
/// certificate integrity, redeem script existence, and ECDSA signature correctness.
70+
pub(crate) fn verify_fidelity_checks(
71+
proof: &FidelityProof,
72+
addr: &str,
73+
tx: Transaction,
74+
current_height: u64,
75+
) -> Result<(), WalletError> {
76+
// Check if bond lock time has expired
77+
let lock_time = LockTime::from_height(current_height as u32)?;
78+
if lock_time > proof.bond.lock_time {
79+
return Err(FidelityError::BondLocktimeExpired.into());
80+
}
81+
82+
// Verify certificate hash
83+
let expected_cert_hash = proof
84+
.bond
85+
.generate_cert_hash(addr)
86+
.expect("Bond is not yet confirmed");
87+
if proof.cert_hash != expected_cert_hash {
88+
return Err(FidelityError::InvalidCertHash.into());
89+
}
90+
91+
let networks = vec![
92+
bitcoin::network::Network::Regtest,
93+
bitcoin::network::Network::Testnet,
94+
bitcoin::network::Network::Bitcoin,
95+
bitcoin::network::Network::Signet,
96+
];
97+
98+
let mut all_failed = true;
99+
100+
for network in networks {
101+
// Validate redeem script and corresponding address
102+
let fidelity_redeem_script =
103+
fidelity_redeemscript(&proof.bond.lock_time, &proof.bond.pubkey);
104+
let expected_address = Address::p2wsh(fidelity_redeem_script.as_script(), network);
105+
106+
let derived_script_pubkey = expected_address.script_pubkey();
107+
let tx_out = tx
108+
.tx_out(proof.bond.outpoint.vout as usize)
109+
.map_err(|_| WalletError::General("Outputs index error".to_string()))?;
110+
111+
if tx_out.script_pubkey == derived_script_pubkey {
112+
all_failed = false;
113+
break; // No need to continue checking once we find a successful match
114+
}
115+
}
116+
117+
// Only throw error if all checks fail
118+
if all_failed {
119+
return Err(FidelityError::BondDoesNotExist.into());
120+
}
121+
122+
// Verify ECDSA signature
123+
let secp = Secp256k1::new();
124+
let cert_message = Message::from_digest_slice(proof.cert_hash.as_byte_array())?;
125+
secp.verify_ecdsa(&cert_message, &proof.cert_sig, &proof.bond.pubkey.inner)?;
126+
127+
Ok(())
128+
}
129+
68130
#[allow(unused)]
69131
/// Reads the locktime from a fidelity redeemscript.
70132
fn read_locktime_from_fidelity_script(redeemscript: &ScriptBuf) -> Result<LockTime, FidelityError> {

src/wallet/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub use api::{Balances, UTXOSpendInfo, Wallet};
1717
pub use backup::WalletBackup;
1818
pub use error::WalletError;
1919
pub use fidelity::FidelityBond;
20-
pub(crate) use fidelity::{fidelity_redeemscript, FidelityError};
20+
pub(crate) use fidelity::{verify_fidelity_checks, FidelityError};
2121
pub use rpc::RPCConfig;
2222
pub use spend::Destination;
2323
pub use storage::AddressType;

0 commit comments

Comments
 (0)