Skip to content

Commit 1ab48ab

Browse files
committed
Add LightningPayjoin struct and Receiver trait
Add new trait `Receiver` that should be implemented in order to utilise the library as a payjoin receiver. This trait covers functions that are used in the payjoin request validation steps and also in the channel opening phase. `LightningPayjoin` utilise the `Receiver` trait in order to handle incoming payjoin requests and either accept a normal payjoin transaction or open a channel with the funds.
1 parent 92f42b6 commit 1ab48ab

File tree

3 files changed

+386
-0
lines changed

3 files changed

+386
-0
lines changed

lightning-payjoin/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ version = "0.0.1"
1212
[dependencies]
1313
bitcoin = "0.30.2"
1414
lightning = { git = "https://github.com/jbesraa/rust-lightning.git", rev = "d3e2d5a", features = ["std"] }
15+
payjoin = { git = "https://github.com/jbesraa/rust-payjoin.git", rev = "9e4f454", features = ["v2", "receive"] }
16+
tokio = { version = "1", features = ["full"] }
17+
reqwest = { version = "0.11", default-features = false, features = ["blocking"] }

lightning-payjoin/src/error.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use lightning::util::errors::APIError;
2+
3+
#[derive(Debug)]
4+
pub enum Error {
5+
Internal(InternalError),
6+
Payjoin(payjoin::Error),
7+
}
8+
9+
#[derive(Debug)]
10+
pub enum InternalError {
11+
TokioJoinError(tokio::task::JoinError),
12+
WalletOperationFailed,
13+
OnchainTxSigningFailed,
14+
FundingTxNotSigned,
15+
FundingTxGenerationFailed(String),
16+
NodeFailed,
17+
}
18+
19+
impl From<tokio::task::JoinError> for InternalError {
20+
fn from(value: tokio::task::JoinError) -> Self {
21+
Self::TokioJoinError(value)
22+
}
23+
}
24+
25+
impl From<tokio::task::JoinError> for Error {
26+
fn from(value: tokio::task::JoinError) -> Self {
27+
Self::Internal(InternalError::TokioJoinError(value))
28+
}
29+
}
30+
31+
impl From<InternalError> for Error {
32+
fn from(value: InternalError) -> Self {
33+
Self::Internal(value)
34+
}
35+
}
36+
37+
impl From<payjoin::Error> for Error {
38+
fn from(value: payjoin::Error) -> Self {
39+
Self::Payjoin(value)
40+
}
41+
}
42+
43+
impl From<Error> for payjoin::Error {
44+
fn from(value: Error) -> Self {
45+
match value {
46+
Error::Payjoin(e) => e,
47+
_ => unreachable!(),
48+
}
49+
}
50+
}
51+
52+
impl std::fmt::Display for InternalError {
53+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
54+
match *self {
55+
Self::TokioJoinError(ref e) => write!(f, "Tokio join error: {}", e),
56+
Self::WalletOperationFailed => write!(f, "Wallet operation failed"),
57+
Self::OnchainTxSigningFailed => write!(f, "Onchain transaction signing failed"),
58+
Self::FundingTxNotSigned => write!(f, "Funding transaction not signed"),
59+
Self::FundingTxGenerationFailed(ref e) => {
60+
write!(f, "Failed to generate funding transaction: {}", e)
61+
},
62+
Self::NodeFailed => write!(f, "Node failed"),
63+
}
64+
}
65+
}
66+
67+
impl std::fmt::Display for Error {
68+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
69+
match *self {
70+
Self::Internal(ref e) => write!(f, "Internal lightning_payjoin error: {}", e),
71+
Self::Payjoin(ref e) => write!(f, "Payjoin error: {}", e),
72+
}
73+
}
74+
}
75+
76+
// Maybe this should live in LDK-Node instead?
77+
impl From<APIError> for Error {
78+
fn from(value: APIError) -> Self {
79+
match value {
80+
APIError::APIMisuseError { err } => {
81+
Self::Internal(InternalError::FundingTxGenerationFailed(err))
82+
},
83+
APIError::FeeRateTooHigh { err, feerate } => Self::Internal(
84+
InternalError::FundingTxGenerationFailed(format!("{} feerate: {}", err, feerate)),
85+
),
86+
APIError::InvalidRoute { err } => {
87+
Self::Internal(InternalError::FundingTxGenerationFailed(format!(
88+
"Invalid route provided: {}",
89+
err
90+
)))
91+
},
92+
APIError::ChannelUnavailable { err } => Self::Internal(
93+
InternalError::FundingTxGenerationFailed(format!("Channel unavailable: {}", err)),
94+
),
95+
APIError::MonitorUpdateInProgress => {
96+
Self::Internal(InternalError::FundingTxGenerationFailed(
97+
"Client indicated a channel monitor update is in progress but not yet complete"
98+
.to_string(),
99+
))
100+
},
101+
APIError::IncompatibleShutdownScript { script } => {
102+
Self::Internal(InternalError::FundingTxGenerationFailed(format!(
103+
"Provided a scriptpubkey format not accepted by peer: {}",
104+
script
105+
)))
106+
},
107+
}
108+
}
109+
}
110+
111+
impl std::error::Error for Error {}

lightning-payjoin/src/lib.rs

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,273 @@
1+
// 1. Get payjoin_drectory and payjoin_relay urls
2+
// 2. Fetch payjoin_directory's ohttp_keys
3+
// 3. Create enroller from payjoin_directory, ohttp_keys and payjoin_relay
4+
// 4. Extract request from enroller
5+
// 5. Send request to payjoin_directory via payjoin_relay to enroll receiver
6+
// 6. Process response from payjoin_directory with `enroller.process_res`
7+
// 7. Store the enrolled receiver in `receive_store`(optional)
8+
// -- At this stage the receiver is enrolled and can receive payjoin requests but is actively polling for them --
9+
// -- When constructing payjoin uri use enrolled.fallback_target as payjoin_endpoint and ohttp_keys fetch in step 2 --
10+
//
11+
// For polling payjoin requests
12+
// 1. start loop { }
13+
// 2. Extract request from enrolled using `enrolled.extract_req`
14+
// 3. Send post request with request.url and request.body
15+
// 4. Process response from payjoin_directory with `enrolled.process_res`
16+
// 5. If proposal is received(return by `process_res`) break loop
17+
pub mod error;
118
pub mod scheduler;
19+
20+
use std::{ops::Deref, sync::Arc};
21+
22+
use bitcoin::{address::NetworkChecked, psbt::Psbt, Address, Amount, ScriptBuf, Transaction};
23+
use lightning::ln::ChannelId;
24+
pub use payjoin::Url;
25+
use payjoin::{
26+
receive::v2::{Enrolled, Enroller, PayjoinProposal, ProvisionalProposal},
27+
PjUriBuilder,
28+
};
29+
use tokio::sync::Mutex;
30+
31+
use crate::{
32+
error::{Error, InternalError},
33+
scheduler::{ChannelScheduler, ScheduledChannel},
34+
};
35+
36+
/// Payjoin `Receiver` trait defines the behavior of the receiver side in a payjoin transaction
37+
/// withing a lightning network context.
38+
///
39+
/// Utilising payjoin transactions, a lightning node setup as a receiver can fund a channel from an
40+
/// incoming payjoin transaction saving additional on-chain transaction.
41+
pub trait Receiver {
42+
fn get_new_address(&self) -> Result<Address<NetworkChecked>, Error>;
43+
/// Used to validate that the inputs of the incoming payjoin transaction are not owned by the
44+
/// receiver. i.e make sure the sender of the transaction is not trying to pay the receiver
45+
/// using the receiver's own funds.
46+
fn check_inputs_not_owned(&self, script: &ScriptBuf) -> Result<bool, Error>;
47+
/// Used to identify ouputs of the incoming payjoin request that are targeted to the receiver.
48+
fn identify_my_outputs(&self, script: &ScriptBuf) -> Result<bool, Error>;
49+
/// Sign PSBT with receivers wallet.
50+
fn sign_tx(&self, psbt: &Psbt) -> Result<Psbt, Error>;
51+
/// Check if the transaction can be broadcasted.
52+
///
53+
/// This is to make sure the original psbt proposed by the payjoin sender can be broadcasted to
54+
/// the network. This checks if the transaction violates the consensus or policy rules.
55+
fn can_broadcast(&self, transaction: &Transaction) -> Result<bool, Error>;
56+
/// Notify the receiver `ChannelManager` that the funding transaction has been generated.
57+
///
58+
/// This is used after we have created the funding transaction from an incoming payjoin
59+
/// request, we notify the `ChannelManager` so it can proceed with the channel opening process.
60+
fn funding_transaction_generated(
61+
&self, temporary_channel_id: &ChannelId,
62+
counterparty_node_id: bitcoin::secp256k1::PublicKey, funding_tx: bitcoin::Transaction,
63+
) -> Result<(), Error>;
64+
}
65+
66+
pub struct LightningPayjoin<P: Deref>
67+
where
68+
P::Target: Receiver,
69+
{
70+
receiver_handler: P,
71+
channel_scheduler: Arc<Mutex<ChannelScheduler>>,
72+
ohttp_keys: payjoin::OhttpKeys,
73+
enrolled: Enrolled,
74+
}
75+
76+
impl<P: Deref> LightningPayjoin<P>
77+
where
78+
P::Target: Receiver,
79+
{
80+
pub fn new(
81+
receiver_handler: P, channel_scheduler: Arc<Mutex<ChannelScheduler>>,
82+
payjoin_directory: Url, payjoin_relay: Url,
83+
) -> Self {
84+
let ohttp_keys =
85+
fetch_ohttp_keys(payjoin_directory.clone(), payjoin_relay.clone()).unwrap();
86+
let mut enroller = Enroller::from_directory_config(
87+
payjoin_directory.clone(),
88+
ohttp_keys.clone(),
89+
payjoin_relay.clone(),
90+
);
91+
let (req, ctx) = enroller.extract_req().unwrap();
92+
let agent = reqwest::blocking::Client::new();
93+
let ohttp_response = agent
94+
.post(req.url)
95+
.body(req.body)
96+
.header("Content-Type", payjoin::V2_REQ_CONTENT_TYPE)
97+
.send()
98+
.unwrap();
99+
let mut ohttp_response = ohttp_response.error_for_status().unwrap();
100+
let mut body = Vec::new();
101+
let _ = ohttp_response.copy_to(&mut body).unwrap();
102+
let enrolled = enroller.process_res(body.as_slice(), ctx).unwrap();
103+
104+
Self { receiver_handler, channel_scheduler, ohttp_keys, enrolled }
105+
}
106+
107+
pub async fn process_request(&mut self) -> Result<(), Error> {
108+
let proposal = match self.fetch_payjoin_proposal().await {
109+
Ok(Some(proposal)) => proposal,
110+
Ok(None) => return Ok(()),
111+
Err(e) => return Err(e),
112+
};
113+
114+
let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1);
115+
let provisional_proposal = self.validate_payjoin_request(proposal, min_fee_rate)?;
116+
let channel = self
117+
.channel_scheduler
118+
.lock()
119+
.await
120+
.get_next_channel(bitcoin::Amount::from_sat(provisional_proposal.payjoin_amount()))
121+
.cloned();
122+
let finalized_proposal = if channel.is_some() {
123+
self.accept_payjoin_with_channel_opening(provisional_proposal, channel.unwrap())
124+
.await
125+
.unwrap()
126+
} else {
127+
self.accept_payjoin_without_channel_opening(provisional_proposal).unwrap()
128+
};
129+
self.send_response(finalized_proposal).await
130+
}
131+
132+
async fn send_response(&self, mut proposal: PayjoinProposal) -> Result<(), Error> {
133+
let (res, _ctx) = proposal.extract_v2_req()?;
134+
let agent = reqwest::Client::new();
135+
let ohttp_response = agent
136+
.post(res.url.as_str())
137+
.header("Content-Type", payjoin::V2_REQ_CONTENT_TYPE)
138+
.body(res.body)
139+
.send()
140+
.await
141+
.unwrap();
142+
assert!(ohttp_response.status().is_success());
143+
Ok(())
144+
}
145+
146+
async fn fetch_payjoin_proposal(
147+
&mut self,
148+
) -> Result<Option<payjoin::receive::v2::UncheckedProposal>, Error> {
149+
let (req, context) = self.enrolled.extract_req().unwrap();
150+
let agent = reqwest::Client::new();
151+
let ohttp_response = agent
152+
.post(req.clone().url.clone().as_str())
153+
.header("Content-Type", payjoin::V2_REQ_CONTENT_TYPE)
154+
.body(req.body)
155+
.send()
156+
.await;
157+
match ohttp_response {
158+
Ok(ohttp_response) => {
159+
let ohttp_response = ohttp_response.error_for_status().unwrap();
160+
let body = ohttp_response.bytes().await.unwrap();
161+
let body = body.as_ref();
162+
match self.enrolled.process_res(body, context).map_err(Error::from) {
163+
Ok(Some(proposal)) => {
164+
return Ok(Some(proposal));
165+
},
166+
Ok(None) => Ok(None),
167+
Err(_) => panic!("Error processing response"),
168+
}
169+
},
170+
Err(_e) => Ok(None),
171+
}
172+
}
173+
174+
pub fn construct_payjoin_uri(&self, amount: Amount) -> String {
175+
let pj_receiver_address = self.receiver_handler.get_new_address().unwrap();
176+
let pj_part = payjoin::Url::parse(&self.enrolled.fallback_target()).unwrap();
177+
let pj_uri = PjUriBuilder::new(pj_receiver_address, pj_part, Some(self.ohttp_keys.clone()))
178+
.amount(amount)
179+
.build();
180+
pj_uri.to_string()
181+
}
182+
183+
fn validate_payjoin_request(
184+
&self, proposal: payjoin::receive::v2::UncheckedProposal,
185+
min_fee_rate: Option<bitcoin::FeeRate>,
186+
) -> Result<ProvisionalProposal, Error> {
187+
let proposal = proposal.check_broadcast_suitability(min_fee_rate, |t| {
188+
Ok(self.receiver_handler.can_broadcast(t)?)
189+
})?;
190+
let proposal = proposal.check_inputs_not_owned(|script| {
191+
Ok(self.receiver_handler.check_inputs_not_owned(&script.to_owned())?)
192+
})?;
193+
let proposal = proposal.check_no_mixed_input_scripts()?;
194+
let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?;
195+
let original_proposal = proposal.clone().identify_receiver_outputs(|script| {
196+
Ok(self.receiver_handler.identify_my_outputs(&script.to_owned())?)
197+
})?;
198+
Ok(original_proposal)
199+
}
200+
201+
fn accept_payjoin_without_channel_opening(
202+
&self, provisional_proposal: ProvisionalProposal,
203+
) -> Result<PayjoinProposal, Box<dyn std::error::Error>> {
204+
// fixme: instead of psbt.clone() we should sign the psbt,
205+
// but BDK adds bip32 derivation paths to the psbt which is not supported by payjoin
206+
let finalized_proposal =
207+
provisional_proposal.finalize_proposal(|psbt| Ok(psbt.clone()), None).unwrap();
208+
Ok(finalized_proposal)
209+
}
210+
211+
async fn accept_payjoin_with_channel_opening(
212+
&self, provisional_proposal: ProvisionalProposal, channel: ScheduledChannel,
213+
) -> Result<PayjoinProposal, Error> {
214+
let mut proposal = provisional_proposal.clone();
215+
let output_script = channel.output_script().expect("Output script should exist");
216+
// substitute the output script with the channel output script
217+
proposal.substitute_output_script(output_script.clone());
218+
let finalized_proposal = proposal
219+
.clone()
220+
.finalize_proposal(|psbt| Ok(self.receiver_handler.sign_tx(&psbt)?), None)?;
221+
let psbt = finalized_proposal.psbt();
222+
let funding_tx = psbt.clone().extract_tx();
223+
// tell the channel_scheduler that the funding tx has been created
224+
self.channel_scheduler
225+
.lock()
226+
.await
227+
.set_funding_tx_created(channel.channel_id(), psbt.clone());
228+
let temporary_channel_id =
229+
channel.temporary_channel_id().expect("Temporary channel id should exist");
230+
// tell the counterparty node that the funding tx has been created
231+
let _ = self.receiver_handler.funding_transaction_generated(
232+
&temporary_channel_id,
233+
channel.node_id(),
234+
funding_tx.clone(),
235+
)?;
236+
// wait for the counterparty node to return FundingSigned message
237+
let channel_scheduler = self.channel_scheduler.clone();
238+
let is_funding_signed =
239+
tokio::time::timeout(tokio::time::Duration::from_secs(3), async move {
240+
loop {
241+
let txid = funding_tx.txid();
242+
if channel_scheduler.lock().await.is_funding_tx_signed(&txid) {
243+
break;
244+
}
245+
}
246+
})
247+
.await;
248+
match is_funding_signed {
249+
Ok(_) => {
250+
return Ok(finalized_proposal);
251+
},
252+
Err(_) => {
253+
return Err(Error::Internal(InternalError::FundingTxNotSigned));
254+
},
255+
}
256+
}
257+
}
258+
259+
// should be used from payjoin_defaults crate, which will be published in the next release.
260+
// https://github.com/payjoin/rust-payjoin/tree/master/payjoin-defaults
261+
fn fetch_ohttp_keys(
262+
payjoin_directory: Url, payjoin_relay: Url,
263+
) -> Result<payjoin::OhttpKeys, Error> {
264+
let ohttp_keys_url = payjoin_directory.join("/ohttp-keys").unwrap();
265+
let proxy = reqwest::blocking::Client::builder()
266+
.proxy(reqwest::Proxy::all(payjoin_relay).unwrap())
267+
.build()
268+
.unwrap();
269+
let mut res = proxy.get(ohttp_keys_url.as_str()).send().unwrap();
270+
let mut body = Vec::new();
271+
let _ = res.copy_to(&mut body).unwrap();
272+
Ok(payjoin::OhttpKeys::decode(body.as_slice()).unwrap())
273+
}

0 commit comments

Comments
 (0)