Skip to content

Commit 92f42b6

Browse files
committed
Add ChannelScheduler
Add a new struct to schedule new lightning channels, tracking them in different states mirroring the original protocol flow: Created, Accepted, FundingCreated, FundingSigned. Functions start with `mark_` are used in order to change the channel state. This can be utilised by users who want to schedule a channel opening for a future incoming payjoin transaction.
1 parent d016565 commit 92f42b6

File tree

3 files changed

+338
-0
lines changed

3 files changed

+338
-0
lines changed

lightning-payjoin/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ resolver = "2"
1010
version = "0.0.1"
1111

1212
[dependencies]
13+
bitcoin = "0.30.2"
14+
lightning = { git = "https://github.com/jbesraa/rust-lightning.git", rev = "d3e2d5a", features = ["std"] }

lightning-payjoin/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod scheduler;

lightning-payjoin/src/scheduler.rs

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/// A lightning network channel scheduler.
2+
use bitcoin::{psbt::Psbt, secp256k1::PublicKey, ScriptBuf, Txid};
3+
use lightning::ln::ChannelId;
4+
5+
#[derive(Clone)]
6+
pub struct ChannelScheduler {
7+
channels: Vec<ScheduledChannel>,
8+
}
9+
10+
impl ChannelScheduler {
11+
/// Create a new empty channel scheduler.
12+
pub fn new() -> Self {
13+
Self { channels: vec![] }
14+
}
15+
/// Schedule a new channel.
16+
///
17+
/// The channel will be created with `ScheduledChannelState::ChannelCreated` state.
18+
pub fn schedule(
19+
&mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey,
20+
channel_id: u128,
21+
) {
22+
let channel =
23+
ScheduledChannel::new(channel_value_satoshi, counterparty_node_id, channel_id);
24+
match channel.state {
25+
ScheduledChannelState::ChannelCreated => {
26+
self.channels.push(channel);
27+
},
28+
_ => {},
29+
}
30+
}
31+
/// Mark a channel as accepted.
32+
///
33+
/// The channel will be updated to `ScheduledChannelState::ChannelAccepted` state.
34+
pub fn set_channel_accepted(
35+
&mut self, channel_id: u128, output_script: ScriptBuf, temporary_channel_id: ChannelId,
36+
) {
37+
for channel in &mut self.channels {
38+
if channel.channel_id() == channel_id {
39+
channel.state.set_channel_accepted(output_script, temporary_channel_id);
40+
break;
41+
}
42+
}
43+
}
44+
/// Mark a channel as funding tx created.
45+
///
46+
/// The channel will be updated to `ScheduledChannelState::FundingTxCreated` state.
47+
pub fn set_funding_tx_created(&mut self, channel_id: u128, funding_tx: Psbt) {
48+
for channel in &mut self.channels {
49+
if channel.channel_id() == channel_id {
50+
channel.state.set_channel_funding_tx_created(funding_tx);
51+
break;
52+
}
53+
}
54+
}
55+
/// Mark a channel as funding tx signed.
56+
///
57+
/// The channel will be updated to `ScheduledChannelState::FundingTxSigned` state.
58+
pub fn set_funding_tx_signed(&mut self, txid: Txid) {
59+
for channel in &mut self.channels {
60+
if channel.txid() == Some(txid) {
61+
channel.state.set_channel_funding_tx_signed(txid);
62+
break;
63+
}
64+
}
65+
}
66+
/// Check if a channel is in the created state.
67+
pub fn is_channel_created(&self, channel_id: u128) -> bool {
68+
if let Some(c) = self.internal_find_channel(channel_id) {
69+
return c.is_channel_created();
70+
}
71+
false
72+
}
73+
/// Check if a channel is in the funding tx created state.
74+
pub fn is_funding_tx_created(&self, txid: &Txid) -> bool {
75+
self.internal_find_channel_by_txid(txid)
76+
.and_then(|ch| Some(ch.is_funding_tx_created()))
77+
.unwrap_or(false)
78+
}
79+
/// Check if a channel is in the funding tx signed state.
80+
pub fn is_funding_tx_signed(&self, txid: &Txid) -> bool {
81+
self.internal_find_channel_by_txid(txid)
82+
.and_then(|ch| Some(ch.is_funding_tx_signed()))
83+
.unwrap_or(false)
84+
}
85+
/// Get the next channel matching the given channel amount.
86+
///
87+
/// The channel must be in the accepted state.
88+
///
89+
/// If more than one channel matches the given channel amount, the channel with the oldest
90+
/// creation date will be returned.
91+
pub fn get_next_channel(&self, channel_amount: bitcoin::Amount) -> Option<&ScheduledChannel> {
92+
self.channels
93+
.iter()
94+
.filter(|channel| {
95+
channel.channel_value_satoshi() == channel_amount && channel.is_channel_accepted()
96+
})
97+
.min_by_key(|channel| channel.created_at())
98+
}
99+
/// List all channels.
100+
pub fn list_channels(&self) -> &Vec<ScheduledChannel> {
101+
&self.channels
102+
}
103+
fn internal_find_channel(&self, channel_id: u128) -> Option<&ScheduledChannel> {
104+
self.channels.iter().find(|channel| channel.channel_id() == channel_id)
105+
}
106+
fn internal_find_channel_by_txid(&self, txid: &Txid) -> Option<&ScheduledChannel> {
107+
let channel = self.channels.iter().find(|channel| {
108+
let ch_txid = match channel.txid() {
109+
Some(txid) => txid,
110+
None => return false,
111+
};
112+
return &ch_txid == txid;
113+
});
114+
channel
115+
}
116+
#[cfg(test)]
117+
fn is_channel_accepted(&self, channel_id: u128) -> bool {
118+
if let Some(c) = self.internal_find_channel(channel_id) {
119+
return c.is_channel_accepted();
120+
}
121+
false
122+
}
123+
}
124+
125+
/// A struct representing a scheduled channel.
126+
#[derive(Clone, Debug)]
127+
pub struct ScheduledChannel {
128+
state: ScheduledChannelState,
129+
channel_value_satoshi: bitcoin::Amount,
130+
channel_id: u128,
131+
counterparty_node_id: PublicKey,
132+
created_at: u64,
133+
}
134+
135+
impl ScheduledChannel {
136+
fn new(
137+
channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128,
138+
) -> Self {
139+
ScheduledChannel {
140+
state: ScheduledChannelState::ChannelCreated,
141+
channel_value_satoshi,
142+
channel_id,
143+
counterparty_node_id,
144+
created_at: 0,
145+
}
146+
}
147+
148+
fn is_channel_created(&self) -> bool {
149+
match self.state {
150+
ScheduledChannelState::ChannelCreated => true,
151+
_ => false,
152+
}
153+
}
154+
155+
fn is_channel_accepted(&self) -> bool {
156+
match self.state {
157+
ScheduledChannelState::ChannelAccepted(..) => true,
158+
_ => false,
159+
}
160+
}
161+
162+
fn is_funding_tx_created(&self) -> bool {
163+
match self.state {
164+
ScheduledChannelState::FundingTxCreated(..) => true,
165+
_ => false,
166+
}
167+
}
168+
169+
fn is_funding_tx_signed(&self) -> bool {
170+
match self.state {
171+
ScheduledChannelState::FundingTxSigned(..) => true,
172+
_ => false,
173+
}
174+
}
175+
176+
fn channel_value_satoshi(&self) -> bitcoin::Amount {
177+
self.channel_value_satoshi
178+
}
179+
180+
/// Get the user channel id.
181+
pub(crate) fn channel_id(&self) -> u128 {
182+
self.channel_id
183+
}
184+
185+
/// Get the counterparty node id.
186+
pub(crate) fn node_id(&self) -> PublicKey {
187+
self.counterparty_node_id
188+
}
189+
190+
/// Get the output script.
191+
pub(crate) fn output_script(&self) -> Option<&ScriptBuf> {
192+
self.state.output_script()
193+
}
194+
195+
/// Get the temporary channel id.
196+
pub(crate) fn temporary_channel_id(&self) -> Option<ChannelId> {
197+
self.state.temporary_channel_id()
198+
}
199+
200+
fn created_at(&self) -> u64 {
201+
self.created_at
202+
}
203+
204+
fn txid(&self) -> Option<Txid> {
205+
self.state.txid()
206+
}
207+
}
208+
209+
#[derive(Clone, Debug)]
210+
struct FundingTxParams {
211+
output_script: ScriptBuf,
212+
temporary_channel_id: ChannelId,
213+
}
214+
215+
impl FundingTxParams {
216+
fn new(output_script: ScriptBuf, temporary_channel_id: ChannelId) -> Self {
217+
Self { output_script, temporary_channel_id }
218+
}
219+
}
220+
221+
#[derive(Clone, Debug)]
222+
enum ScheduledChannelState {
223+
ChannelCreated,
224+
ChannelAccepted(FundingTxParams),
225+
FundingTxCreated(FundingTxParams, Psbt),
226+
FundingTxSigned(FundingTxParams, (), Txid),
227+
}
228+
229+
impl ScheduledChannelState {
230+
fn output_script(&self) -> Option<&ScriptBuf> {
231+
match self {
232+
ScheduledChannelState::ChannelAccepted(funding_tx_params) => {
233+
Some(&funding_tx_params.output_script)
234+
},
235+
ScheduledChannelState::FundingTxCreated(funding_tx_params, _) => {
236+
Some(&funding_tx_params.output_script)
237+
},
238+
ScheduledChannelState::FundingTxSigned(funding_tx_params, _, _) => {
239+
Some(&funding_tx_params.output_script)
240+
},
241+
_ => None,
242+
}
243+
}
244+
245+
fn temporary_channel_id(&self) -> Option<ChannelId> {
246+
match self {
247+
ScheduledChannelState::ChannelAccepted(funding_tx_params) => {
248+
Some(funding_tx_params.temporary_channel_id)
249+
},
250+
ScheduledChannelState::FundingTxCreated(funding_tx_params, _) => {
251+
Some(funding_tx_params.temporary_channel_id)
252+
},
253+
ScheduledChannelState::FundingTxSigned(funding_tx_params, _, _) => {
254+
Some(funding_tx_params.temporary_channel_id)
255+
},
256+
_ => None,
257+
}
258+
}
259+
260+
fn txid(&self) -> Option<Txid> {
261+
match self {
262+
ScheduledChannelState::FundingTxCreated(_, funding_tx) => {
263+
Some(funding_tx.clone().extract_tx().txid())
264+
},
265+
ScheduledChannelState::FundingTxSigned(_, _, txid) => Some(*txid),
266+
_ => None,
267+
}
268+
}
269+
270+
fn set_channel_accepted(&mut self, output_script: ScriptBuf, temporary_channel_id: ChannelId) {
271+
if let ScheduledChannelState::ChannelCreated = self {
272+
*self = ScheduledChannelState::ChannelAccepted(FundingTxParams::new(
273+
output_script,
274+
temporary_channel_id,
275+
));
276+
}
277+
}
278+
279+
fn set_channel_funding_tx_created(&mut self, funding_tx: Psbt) {
280+
if let ScheduledChannelState::ChannelAccepted(funding_tx_params) = self {
281+
*self = ScheduledChannelState::FundingTxCreated(funding_tx_params.clone(), funding_tx);
282+
}
283+
}
284+
285+
fn set_channel_funding_tx_signed(&mut self, txid: Txid) {
286+
if let ScheduledChannelState::FundingTxCreated(funding_tx_params, psbt) = self {
287+
if txid == psbt.unsigned_tx.txid() {
288+
*self = ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), (), txid);
289+
}
290+
}
291+
}
292+
}
293+
294+
#[cfg(test)]
295+
mod tests {
296+
use std::str::FromStr;
297+
298+
use super::*;
299+
use bitcoin::{
300+
psbt::Psbt,
301+
secp256k1::{self, Secp256k1},
302+
};
303+
304+
#[tokio::test]
305+
async fn test_channel_scheduler() {
306+
let create_pubkey = || -> PublicKey {
307+
let secp = Secp256k1::new();
308+
PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap())
309+
};
310+
let channel_value_satoshi = 100;
311+
let node_id = create_pubkey();
312+
let channel_id: u128 = 0;
313+
let mut channel_scheduler = ChannelScheduler::new();
314+
channel_scheduler.schedule(
315+
bitcoin::Amount::from_sat(channel_value_satoshi),
316+
node_id,
317+
channel_id,
318+
);
319+
assert_eq!(channel_scheduler.channels.len(), 1);
320+
assert_eq!(channel_scheduler.is_channel_created(channel_id), true);
321+
channel_scheduler.set_channel_accepted(
322+
channel_id,
323+
ScriptBuf::from(vec![1, 2, 3]),
324+
ChannelId::new_zero(),
325+
);
326+
assert_eq!(channel_scheduler.is_channel_accepted(channel_id), true);
327+
let str_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
328+
let mock_transaction = Psbt::from_str(str_psbt).unwrap();
329+
channel_scheduler.set_funding_tx_created(channel_id, mock_transaction.clone());
330+
let tx_id = mock_transaction.extract_tx().txid();
331+
assert_eq!(channel_scheduler.is_funding_tx_created(&tx_id), true);
332+
channel_scheduler.set_funding_tx_signed(tx_id);
333+
assert_eq!(channel_scheduler.is_funding_tx_signed(&tx_id), true);
334+
}
335+
}

0 commit comments

Comments
 (0)