|
1 | 1 | use bitcoin::hashes::{sha256d::Hash as Sha256dHash, Hash}; |
2 | 2 | use core::time::Duration; |
3 | 3 | use hashbrown::hash_map::Entry; |
| 4 | +use std::time::{SystemTime, UNIX_EPOCH}; |
4 | 5 |
|
5 | 6 | use crate::prelude::{new_hash_map, HashMap}; |
6 | 7 |
|
| 8 | +#[derive(Clone, PartialEq, Eq, Debug)] |
| 9 | +enum BucketAssigned { |
| 10 | + General, |
| 11 | + Congestion, |
| 12 | + Protected, |
| 13 | +} |
| 14 | + |
7 | 15 | struct GeneralBucket { |
8 | 16 | /// Our SCID |
9 | 17 | scid: u64, |
@@ -230,6 +238,156 @@ impl BucketResources { |
230 | 238 | } |
231 | 239 | } |
232 | 240 |
|
| 241 | +#[derive(Debug, Clone)] |
| 242 | +struct PendingHTLC { |
| 243 | + incoming_channel: u64, |
| 244 | + incoming_amount_msat: u64, |
| 245 | + fee: u64, |
| 246 | + outgoing_channel: u64, |
| 247 | + outgoing_accountable: bool, |
| 248 | + added_at_unix_seconds: u64, |
| 249 | + in_flight_risk: u64, |
| 250 | + bucket: BucketAssigned, |
| 251 | +} |
| 252 | + |
| 253 | +#[derive(Debug, PartialEq, Eq, Hash)] |
| 254 | +struct HtlcRef { |
| 255 | + incoming_channel_id: u64, |
| 256 | + htlc_id: u64, |
| 257 | +} |
| 258 | + |
| 259 | +struct Channel { |
| 260 | + /// The reputation this channel has accrued as an outgoing link. |
| 261 | + outgoing_reputation: DecayingAverage, |
| 262 | + |
| 263 | + /// The revenue this channel has earned us as an incoming link. |
| 264 | + incoming_revenue: RevenueAverage, |
| 265 | + |
| 266 | + /// Pending HTLCs as an outgoing channel. |
| 267 | + pending_htlcs: HashMap<HtlcRef, PendingHTLC>, |
| 268 | + |
| 269 | + general_bucket: GeneralBucket, |
| 270 | + congestion_bucket: BucketResources, |
| 271 | + last_congestion_misuse: HashMap<u64, u64>, |
| 272 | + protected_bucket: BucketResources, |
| 273 | +} |
| 274 | + |
| 275 | +impl Channel { |
| 276 | + fn new( |
| 277 | + scid: u64, max_htlc_value_in_flight_msat: u64, max_accepted_htlcs: u16, |
| 278 | + general_bucket_pct: u8, congestion_bucket_pct: u8, protected_bucket_pct: u8, |
| 279 | + window: Duration, window_count: u8, |
| 280 | + ) -> Self { |
| 281 | + let general_bucket_slots_allocated = max_accepted_htlcs * general_bucket_pct as u16 / 100; |
| 282 | + let general_bucket_liquidity_allocated = |
| 283 | + max_htlc_value_in_flight_msat * general_bucket_pct as u64 / 100; |
| 284 | + |
| 285 | + let congestion_bucket_slots_allocated = |
| 286 | + max_accepted_htlcs * congestion_bucket_pct as u16 / 100; |
| 287 | + let congestion_bucket_liquidity_allocated = |
| 288 | + max_htlc_value_in_flight_msat * congestion_bucket_pct as u64 / 100; |
| 289 | + |
| 290 | + let protected_bucket_slots_allocated = |
| 291 | + max_accepted_htlcs * protected_bucket_pct as u16 / 100; |
| 292 | + let protected_bucket_liquidity_allocated = |
| 293 | + max_htlc_value_in_flight_msat * protected_bucket_pct as u64 / 100; |
| 294 | + |
| 295 | + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); |
| 296 | + Channel { |
| 297 | + outgoing_reputation: DecayingAverage::new(now, window * window_count.into()), |
| 298 | + incoming_revenue: RevenueAverage::new(window, window_count, now), |
| 299 | + pending_htlcs: new_hash_map(), |
| 300 | + general_bucket: GeneralBucket::new( |
| 301 | + scid, |
| 302 | + general_bucket_slots_allocated, |
| 303 | + general_bucket_liquidity_allocated, |
| 304 | + ), |
| 305 | + congestion_bucket: BucketResources::new( |
| 306 | + congestion_bucket_slots_allocated, |
| 307 | + congestion_bucket_liquidity_allocated, |
| 308 | + ), |
| 309 | + last_congestion_misuse: new_hash_map(), |
| 310 | + protected_bucket: BucketResources::new( |
| 311 | + protected_bucket_slots_allocated, |
| 312 | + protected_bucket_liquidity_allocated, |
| 313 | + ), |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + fn general_available( |
| 318 | + &mut self, incoming_amount_msat: u64, outgoing_channel_id: u64, salt: Option<[u8; 32]>, |
| 319 | + ) -> Result<bool, ()> { |
| 320 | + Ok(self.general_bucket.can_add_htlc(outgoing_channel_id, incoming_amount_msat, salt)?) |
| 321 | + } |
| 322 | + |
| 323 | + fn congestion_eligible( |
| 324 | + &mut self, pending_htlcs_in_congestion: bool, incoming_amount_msat: u64, |
| 325 | + outgoing_channel_id: u64, revenue_window: Duration, at_timestamp: u64, |
| 326 | + ) -> bool { |
| 327 | + !pending_htlcs_in_congestion |
| 328 | + && self.can_add_htlc_congestion( |
| 329 | + outgoing_channel_id, |
| 330 | + incoming_amount_msat, |
| 331 | + revenue_window, |
| 332 | + at_timestamp, |
| 333 | + ) |
| 334 | + } |
| 335 | + |
| 336 | + fn misused_congestion(&mut self, channel_id: u64, misuse_timestamp: u64) { |
| 337 | + self.last_congestion_misuse.insert(channel_id, misuse_timestamp); |
| 338 | + } |
| 339 | + |
| 340 | + // Returns whether the outgoing channel has misused the congestion bucket during our last |
| 341 | + // revenue window (two weeks by default). |
| 342 | + fn has_misused_congestion( |
| 343 | + &mut self, outgoing_scid: u64, at_timestamp: u64, revenue_window: Duration, |
| 344 | + ) -> bool { |
| 345 | + match self.last_congestion_misuse.entry(outgoing_scid) { |
| 346 | + Entry::Vacant(_) => false, |
| 347 | + Entry::Occupied(last_misuse) => { |
| 348 | + // If the last misuse of the congestion bucket was over 2 weeks ago, remove |
| 349 | + // the entry. |
| 350 | + debug_assert!(at_timestamp >= *last_misuse.get()); |
| 351 | + let since_last_misuse = Duration::from_secs(at_timestamp - last_misuse.get()); |
| 352 | + if since_last_misuse < revenue_window { |
| 353 | + return true; |
| 354 | + } else { |
| 355 | + last_misuse.remove(); |
| 356 | + return false; |
| 357 | + } |
| 358 | + }, |
| 359 | + } |
| 360 | + } |
| 361 | + |
| 362 | + fn can_add_htlc_congestion( |
| 363 | + &mut self, channel_id: u64, htlc_amount_msat: u64, revenue_window: Duration, |
| 364 | + at_timestamp: u64, |
| 365 | + ) -> bool { |
| 366 | + let congestion_resources_available = |
| 367 | + self.congestion_bucket.resources_available(htlc_amount_msat); |
| 368 | + let misused_congestion = |
| 369 | + self.has_misused_congestion(channel_id, at_timestamp, revenue_window); |
| 370 | + |
| 371 | + let below_slot_limit = htlc_amount_msat |
| 372 | + <= self.congestion_bucket.liquidity_allocated |
| 373 | + / self.congestion_bucket.slots_allocated as u64; |
| 374 | + |
| 375 | + congestion_resources_available && !misused_congestion && below_slot_limit |
| 376 | + } |
| 377 | + |
| 378 | + fn sufficient_reputation( |
| 379 | + &mut self, in_flight_htlc_risk: u64, outgoing_reputation: i64, |
| 380 | + outgoing_in_flight_risk: u64, at_timestamp: u64, |
| 381 | + ) -> Result<bool, ()> { |
| 382 | + let incoming_revenue_threshold = self.incoming_revenue.value_at_timestamp(at_timestamp)?; |
| 383 | + |
| 384 | + Ok(outgoing_reputation |
| 385 | + .saturating_sub(i64::try_from(outgoing_in_flight_risk).unwrap_or(i64::MAX)) |
| 386 | + .saturating_sub(i64::try_from(in_flight_htlc_risk).unwrap_or(i64::MAX)) |
| 387 | + >= incoming_revenue_threshold) |
| 388 | + } |
| 389 | +} |
| 390 | + |
233 | 391 | struct DecayingAverage { |
234 | 392 | value: i64, |
235 | 393 | last_updated: u64, |
|
0 commit comments