Skip to content

Commit 0028e30

Browse files
committed
sim-node: add channel state tracking for channels
This commit adds a ChannelState struct which is used to track the policy and state of a channel in the *outgoing* direction. This will be used to check forwards against the node's advertised policy and track the movement of outgoing HTLCs through the channel. Note that we choose to implement this state *unidirectionally*, so a single channel will be represented by two ChannelState structs (one in each direction).
1 parent 2447510 commit 0028e30

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

sim-lib/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod defined_activity;
2727
pub mod lnd;
2828
mod random_activity;
2929
mod serializers;
30+
pub mod sim_node;
3031
#[cfg(test)]
3132
mod test_utils;
3233

@@ -84,6 +85,37 @@ impl std::fmt::Display for NodeId {
8485
}
8586
}
8687

88+
/// Represents a short channel ID, expressed as a struct so that we can implement display for the trait.
89+
#[derive(Debug)]
90+
pub struct ShortChannelID(u64);
91+
92+
/// Utility function to easily convert from u64 to `ShortChannelID`
93+
impl From<u64> for ShortChannelID {
94+
fn from(value: u64) -> Self {
95+
ShortChannelID(value)
96+
}
97+
}
98+
99+
/// Utility function to easily convert `ShortChannelID` into u64
100+
impl From<ShortChannelID> for u64 {
101+
fn from(scid: ShortChannelID) -> Self {
102+
scid.0
103+
}
104+
}
105+
106+
/// See https://github.com/lightning/bolts/blob/60de4a09727c20dea330f9ee8313034de6e50594/07-routing-gossip.md#definition-of-short_channel_id.
107+
impl std::fmt::Display for ShortChannelID {
108+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109+
write!(
110+
f,
111+
"{}:{}:{}",
112+
(self.0 >> 40) as u32,
113+
((self.0 >> 16) & 0xFFFFFF) as u32,
114+
(self.0 & 0xFFFF) as u16,
115+
)
116+
}
117+
}
118+
87119
#[derive(Debug, Serialize, Deserialize, Clone)]
88120
pub struct SimParams {
89121
pub nodes: Vec<NodeConnection>,

sim-lib/src/sim_node.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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

Comments
 (0)