|
| 1 | +use bitcoin::secp256k1::PublicKey; |
| 2 | +use lightning::ln::PaymentHash; |
| 3 | +use std::collections::HashMap; |
| 4 | + |
| 5 | +use crate::ShortChannelID; |
| 6 | + |
| 7 | +/// ForwardingError represents the various errors that we can run into when forwarding payments in a simulated network. |
| 8 | +/// Since we're not using real lightning nodes, these errors are not obfuscated and can be propagated to the sending |
| 9 | +/// node and used for analysis. |
| 10 | +#[derive(Debug)] |
| 11 | +pub enum ForwardingError { |
| 12 | + /// Zero amount htlcs are invalid in the protocol. |
| 13 | + ZeroAmountHtlc, |
| 14 | + /// The outgoing channel id was not found in the network graph. |
| 15 | + ChannelNotFound(ShortChannelID), |
| 16 | + /// The node pubkey provided was not associated with the channel in the network graph. |
| 17 | + NodeNotFound(PublicKey), |
| 18 | + /// The channel has already forwarded an HTLC with the payment hash provided. |
| 19 | + /// TODO: remove if MPP support is added. |
| 20 | + PaymentHashExists(PaymentHash), |
| 21 | + /// An htlc with the payment hash provided could not be found to resolve. |
| 22 | + PaymentHashNotFound(PaymentHash), |
| 23 | + /// The forwarding node did not have sufficient outgoing balance to forward the htlc (htlc amount / balance). |
| 24 | + InsufficientBalance(u64, u64), |
| 25 | + /// The htlc forwarded is less than the channel's advertised minimum htlc amount (htlc amount / minimum). |
| 26 | + LessThanMinimum(u64, u64), |
| 27 | + /// The htlc forwarded is more than the chanenl's advertised maximum htlc amount (htlc amount / maximum). |
| 28 | + MoreThanMaximum(u64, u64), |
| 29 | + /// The channel has reached its maximum allowable number of htlcs in flight (total in flight / maximim). |
| 30 | + ExceedsInFlightCount(u64, u64), |
| 31 | + /// The forwarded htlc's amount would push the channel over its maximum allowable in flight total |
| 32 | + /// (total in flight / maximum). |
| 33 | + ExceedsInFlightTotal(u64, u64), |
| 34 | + /// The forwarded htlc's cltv expiry exceeds the maximum value used to express block heights in Bitcoin. |
| 35 | + ExpiryInSeconds(u32, u32), |
| 36 | + /// The forwarded htlc has insufficient cltv delta for the channel's minimum delta (cltv delta / minimum). |
| 37 | + InsufficientCltvDelta(u32, u32), |
| 38 | + /// The forwarded htlc has insufficient fee for the channel's policy (fee / expected fee / base fee / prop fee). |
| 39 | + InsufficientFee(u64, u64, u64, u64), |
| 40 | + /// The fee policy for a htlc amount would overflow with the given fee policy (htlc amount / base fee / prop fee). |
| 41 | + FeeOverflow(u64, u64, u64), |
| 42 | + /// Sanity check on channel balances failed (node balances / channel capacity). |
| 43 | + SanityCheckFailed(u64, u64), |
| 44 | +} |
| 45 | + |
| 46 | +/// Represents an in-flight htlc that has been forwarded over a channel that is awaiting resolution. |
| 47 | +#[derive(Copy, Clone)] |
| 48 | +struct Htlc { |
| 49 | + amount_msat: u64, |
| 50 | + cltv_expiry: u32, |
| 51 | +} |
| 52 | + |
| 53 | +/// Represents one node in the channel's forwarding policy and restrictions. Note that this doesn't directly map to |
| 54 | +/// a single concept in the protocol, a few things have been combined for the sake of simplicity. Used to manage the |
| 55 | +/// lightning "state machine" and check that HTLCs are added in accordance of the advertised policy. |
| 56 | +#[derive(Clone)] |
| 57 | +pub struct ChannelPolicy { |
| 58 | + pub pubkey: PublicKey, |
| 59 | + pub max_htlc_count: u64, |
| 60 | + pub max_in_flight_msat: u64, |
| 61 | + pub min_htlc_size_msat: u64, |
| 62 | + pub max_htlc_size_msat: u64, |
| 63 | + pub cltv_expiry_delta: u32, |
| 64 | + pub base_fee: u64, |
| 65 | + pub fee_rate_prop: u64, |
| 66 | +} |
| 67 | + |
| 68 | +/// Fails with the forwarding error provided if the value provided fails its inequality check. |
| 69 | +macro_rules! fail_forwarding_inequality { |
| 70 | + ($value_1:expr, $op:tt, $value_2:expr, $error_variant:ident $(, $opt:expr)*) => { |
| 71 | + if $value_1 $op $value_2 { |
| 72 | + return Err(ForwardingError::$error_variant( |
| 73 | + $value_1, |
| 74 | + $value_2 |
| 75 | + $( |
| 76 | + , $opt |
| 77 | + )* |
| 78 | + )); |
| 79 | + } |
| 80 | + }; |
| 81 | +} |
| 82 | + |
| 83 | +/// The internal state of one side of a simulated channel, including its forwarding parameters. This struct is |
| 84 | +/// primarily responsible for handling our view of what's currently in-flight on the channel, and how much |
| 85 | +/// liquidity we have. |
| 86 | +#[derive(Clone)] |
| 87 | +struct ChannelState { |
| 88 | + local_balance_msat: u64, |
| 89 | + in_flight: HashMap<PaymentHash, Htlc>, |
| 90 | + policy: ChannelPolicy, |
| 91 | +} |
| 92 | + |
| 93 | +impl ChannelState { |
| 94 | + /// Creates a new channel with local liquidity as allocated by the caller. The responsibility of ensuring that the |
| 95 | + /// local balance of each side of the channel equals its total capacity is on the caller, as we are only dealing |
| 96 | + /// with a one-sided view of the channel's state. |
| 97 | + fn new(policy: ChannelPolicy, local_balance_msat: u64) -> Self { |
| 98 | + ChannelState { |
| 99 | + local_balance_msat, |
| 100 | + in_flight: HashMap::new(), |
| 101 | + policy, |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + /// Returns the sum of all the *in flight outgoing* HTLCs on the channel. |
| 106 | + fn in_flight_total(&self) -> u64 { |
| 107 | + self.in_flight.values().map(|h| h.amount_msat).sum() |
| 108 | + } |
| 109 | + |
| 110 | + /// Checks whether the proposed HTLC abides by the channel policy advertised for using this channel as the |
| 111 | + /// *outgoing* link in a forward. |
| 112 | + fn check_htlc_forward( |
| 113 | + &self, |
| 114 | + cltv_delta: u32, |
| 115 | + amt: u64, |
| 116 | + fee: u64, |
| 117 | + ) -> Result<(), ForwardingError> { |
| 118 | + fail_forwarding_inequality!(cltv_delta, <, self.policy.cltv_expiry_delta, InsufficientCltvDelta); |
| 119 | + |
| 120 | + let expected_fee = amt |
| 121 | + .checked_mul(self.policy.fee_rate_prop) |
| 122 | + .and_then(|prop_fee| (prop_fee / 1000000).checked_add(self.policy.base_fee)) |
| 123 | + .ok_or(ForwardingError::FeeOverflow( |
| 124 | + amt, |
| 125 | + self.policy.base_fee, |
| 126 | + self.policy.fee_rate_prop, |
| 127 | + ))?; |
| 128 | + |
| 129 | + fail_forwarding_inequality!( |
| 130 | + fee, <, expected_fee, InsufficientFee, self.policy.base_fee, self.policy.fee_rate_prop |
| 131 | + ); |
| 132 | + |
| 133 | + Ok(()) |
| 134 | + } |
| 135 | + |
| 136 | + /// Checks whether the proposed HTLC can be added to the channel as an outgoing HTLC. This requires that we have |
| 137 | + /// sufficient liquidity, and that the restrictions on our in flight htlc balance and count are not violated by |
| 138 | + /// the addition of the HTLC. Specification sanity checks (such as reasonable CLTV) are also included, as this |
| 139 | + /// is where we'd check it in real life. |
| 140 | + fn check_outgoing_addition(&self, htlc: &Htlc) -> Result<(), ForwardingError> { |
| 141 | + fail_forwarding_inequality!(htlc.amount_msat, >, self.policy.max_htlc_size_msat, MoreThanMaximum); |
| 142 | + fail_forwarding_inequality!(htlc.amount_msat, <, self.policy.min_htlc_size_msat, LessThanMinimum); |
| 143 | + fail_forwarding_inequality!( |
| 144 | + self.in_flight.len() as u64 + 1, >, self.policy.max_htlc_count, ExceedsInFlightCount |
| 145 | + ); |
| 146 | + fail_forwarding_inequality!( |
| 147 | + self.in_flight_total() + htlc.amount_msat, >, self.policy.max_in_flight_msat, ExceedsInFlightTotal |
| 148 | + ); |
| 149 | + fail_forwarding_inequality!(htlc.amount_msat, >, self.local_balance_msat, InsufficientBalance); |
| 150 | + fail_forwarding_inequality!(htlc.cltv_expiry, >, 500000000, ExpiryInSeconds); |
| 151 | + |
| 152 | + Ok(()) |
| 153 | + } |
| 154 | + |
| 155 | + /// Adds the HTLC to our set of outgoing in-flight HTLCs. [`check_outgoing_addition`] must be called before |
| 156 | + /// this to ensure that the restrictions on outgoing HTLCs are not violated. Local balance is decreased by the |
| 157 | + /// HTLC amount, as this liquidity is no longer available. |
| 158 | + /// |
| 159 | + /// Note: MPP payments are not currently supported, so this function will fail if a duplicate payment hash is |
| 160 | + /// reported. |
| 161 | + fn add_outgoing_htlc(&mut self, hash: PaymentHash, htlc: Htlc) -> Result<(), ForwardingError> { |
| 162 | + self.check_outgoing_addition(&htlc)?; |
| 163 | + if self.in_flight.get(&hash).is_some() { |
| 164 | + return Err(ForwardingError::PaymentHashExists(hash)); |
| 165 | + } |
| 166 | + self.local_balance_msat -= htlc.amount_msat; |
| 167 | + self.in_flight.insert(hash, htlc); |
| 168 | + Ok(()) |
| 169 | + } |
| 170 | + |
| 171 | + /// Removes the HTLC from our set of outgoing in-flight HTLCs, failing if the payment hash is not found. If the |
| 172 | + /// HTLC failed, the balance is returned to our local liquidity. Note that this function is not responsible for |
| 173 | + /// reflecting that the balance has moved to the other side of the channel in the success-case, calling code is |
| 174 | + /// responsible for that. |
| 175 | + fn remove_outgoing_htlc( |
| 176 | + &mut self, |
| 177 | + hash: &PaymentHash, |
| 178 | + success: bool, |
| 179 | + ) -> Result<Htlc, ForwardingError> { |
| 180 | + match self.in_flight.remove(hash) { |
| 181 | + Some(v) => { |
| 182 | + // If the HTLC failed, pending balance returns to local balance. |
| 183 | + if !success { |
| 184 | + self.local_balance_msat += v.amount_msat; |
| 185 | + } |
| 186 | + |
| 187 | + Ok(v) |
| 188 | + }, |
| 189 | + None => Err(ForwardingError::PaymentHashNotFound(*hash)), |
| 190 | + } |
| 191 | + } |
| 192 | +} |
0 commit comments