Skip to content

Commit 0e4ccd2

Browse files
committed
sim-lib: add simulated channel representation
Add a single representation of a simulated lightning channel which uses our state representation to add and remove htlcs. For simplicity, each side of the channel is represented as a separate state, with the only interaction between the two through the changes to local balance that happen when we settle htlcs.
1 parent 0028e30 commit 0e4ccd2

File tree

2 files changed

+149
-1
lines changed

2 files changed

+149
-1
lines changed

sim-lib/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ impl std::fmt::Display for NodeId {
8686
}
8787

8888
/// Represents a short channel ID, expressed as a struct so that we can implement display for the trait.
89-
#[derive(Debug)]
89+
#[derive(Debug, Clone)]
9090
pub struct ShortChannelID(u64);
9191

9292
/// Utility function to easily convert from u64 to `ShortChannelID`

sim-lib/src/sim_node.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,151 @@ impl ChannelState {
190190
}
191191
}
192192
}
193+
194+
/// Represents a simulated channel, and is responsible for managing addition and removal of HTLCs from the channel and
195+
/// sanity checks. Channel state is tracked *unidirectionally* for each participant in the channel.
196+
///
197+
/// Each node represented in the channel tracks only its outgoing HTLCs, and balance is transferred between the two
198+
/// nodes as they settle or fail. Given some channel: node_1 <----> node_2:
199+
/// * HTLC sent node_1 -> node_2: added to in-flight outgoing htlcs on node_1.
200+
/// * HTLC sent node_2 -> node_1: added to in-flight outgoing htlcs on node_2.
201+
///
202+
/// Rules for managing balance are as follows:
203+
/// * When an HTLC is in flight, the channel's local outgoing liquidity decreases (as it's locked up).
204+
/// * When an HTLC fails, the balance is returned to the local node (the one that it was in-flight / outgoing on).
205+
/// * When an HTLC succeeds, the balance is sent to the remote node (the one that did not track it as in-flight).
206+
///
207+
/// With each state transition, the simulated channel checks that the sum of its local balances and in-flight equal the
208+
/// total channel capacity. Failure of this sanity check represents a critical failure in the state machine.
209+
#[derive(Clone)]
210+
pub struct SimulatedChannel {
211+
capacity_msat: u64,
212+
short_channel_id: ShortChannelID,
213+
node_1: ChannelState,
214+
node_2: ChannelState,
215+
}
216+
217+
impl SimulatedChannel {
218+
/// Creates a new channel with the capacity and policies provided. The total capacity of the channel is evenly split
219+
/// between the channel participants (this is an arbitrary decision).
220+
pub fn new(
221+
capacity_msat: u64,
222+
short_channel_id: ShortChannelID,
223+
node_1: ChannelPolicy,
224+
node_2: ChannelPolicy,
225+
) -> Self {
226+
SimulatedChannel {
227+
capacity_msat,
228+
short_channel_id,
229+
node_1: ChannelState::new(node_1, capacity_msat / 2),
230+
node_2: ChannelState::new(node_2, capacity_msat / 2),
231+
}
232+
}
233+
234+
fn get_node_mut(&mut self, pubkey: &PublicKey) -> Result<&mut ChannelState, ForwardingError> {
235+
if pubkey == &self.node_1.policy.pubkey {
236+
Ok(&mut self.node_1)
237+
} else if pubkey == &self.node_2.policy.pubkey {
238+
Ok(&mut self.node_2)
239+
} else {
240+
Err(ForwardingError::NodeNotFound(*pubkey))
241+
}
242+
}
243+
244+
fn get_node(&self, pubkey: &PublicKey) -> Result<&ChannelState, ForwardingError> {
245+
if pubkey == &self.node_1.policy.pubkey {
246+
Ok(&self.node_1)
247+
} else if pubkey == &self.node_2.policy.pubkey {
248+
Ok(&self.node_2)
249+
} else {
250+
Err(ForwardingError::NodeNotFound(*pubkey))
251+
}
252+
}
253+
254+
/// Adds an htlc to the appropriate side of the simulated channel, checking its policy and balance are okay. The
255+
/// public key of the node sending the HTLC (ie, the party that would send update_add_htlc in the protocol)
256+
/// must be provided to add the outgoing htlc to its side of the channel.
257+
fn add_htlc(
258+
&mut self,
259+
sending_node: &PublicKey,
260+
hash: PaymentHash,
261+
htlc: Htlc,
262+
) -> Result<(), ForwardingError> {
263+
if htlc.amount_msat == 0 {
264+
return Err(ForwardingError::ZeroAmountHtlc);
265+
}
266+
267+
self.get_node_mut(sending_node)?
268+
.add_outgoing_htlc(hash, htlc)?;
269+
self.sanity_check()
270+
}
271+
272+
/// Performs a sanity check on the total balances in a channel. Note that we do not currently include on-chain
273+
/// fees or reserve so these values should exactly match.
274+
fn sanity_check(&self) -> Result<(), ForwardingError> {
275+
let node_1_total = self.node_1.local_balance_msat + self.node_1.in_flight_total();
276+
let node_2_total = self.node_2.local_balance_msat + self.node_2.in_flight_total();
277+
278+
fail_forwarding_inequality!(node_1_total + node_2_total, !=, self.capacity_msat, SanityCheckFailed);
279+
280+
Ok(())
281+
}
282+
283+
/// Removes an htlc from the appropriate side of the simulated channel, settling balances across channel sides
284+
/// based on the success of the htlc. The public key of the node that originally sent the HTLC (ie, the party
285+
/// that would send update_add_htlc in the protocol) must be provided to remove the htlc from its side of the
286+
/// channel.
287+
fn remove_htlc(
288+
&mut self,
289+
sending_node: &PublicKey,
290+
hash: &PaymentHash,
291+
success: bool,
292+
) -> Result<(), ForwardingError> {
293+
let htlc = self
294+
.get_node_mut(sending_node)?
295+
.remove_outgoing_htlc(hash, success)?;
296+
self.settle_htlc(sending_node, htlc.amount_msat, success)?;
297+
self.sanity_check()
298+
}
299+
300+
/// Updates the local balance of each node in the channel once a htlc has been resolved, pushing funds to the
301+
/// receiving nodes in the case of a successful payment and returning balance to the sender in the case of a
302+
/// failure.
303+
fn settle_htlc(
304+
&mut self,
305+
sending_node: &PublicKey,
306+
amount_msat: u64,
307+
success: bool,
308+
) -> Result<(), ForwardingError> {
309+
// Successful payments push balance to the receiver, failures return it to the sender.
310+
let (sender_delta_msat, receiver_delta_msat) = if success {
311+
(0, amount_msat)
312+
} else {
313+
(amount_msat, 0)
314+
};
315+
316+
if sending_node == &self.node_1.policy.pubkey {
317+
self.node_1.local_balance_msat += sender_delta_msat;
318+
self.node_2.local_balance_msat += receiver_delta_msat;
319+
Ok(())
320+
} else if sending_node == &self.node_2.policy.pubkey {
321+
self.node_2.local_balance_msat += sender_delta_msat;
322+
self.node_1.local_balance_msat += receiver_delta_msat;
323+
Ok(())
324+
} else {
325+
Err(ForwardingError::NodeNotFound(*sending_node))
326+
}
327+
}
328+
329+
/// Checks an htlc forward against the outgoing policy of the node provided.
330+
fn check_htlc_forward(
331+
&self,
332+
forwarding_node: &PublicKey,
333+
cltv_delta: u32,
334+
amount_msat: u64,
335+
fee_msat: u64,
336+
) -> Result<(), ForwardingError> {
337+
self.get_node(forwarding_node)?
338+
.check_htlc_forward(cltv_delta, amount_msat, fee_msat)
339+
}
340+
}

0 commit comments

Comments
 (0)