feat: remove fedimint tonic lnd (#831)

* feat: remove fedimint tonic lnd
This commit is contained in:
thesimplekid
2025-06-23 10:59:44 +01:00
committed by GitHub
parent 32009c174c
commit e016687d20
15 changed files with 6429 additions and 74 deletions

View File

@@ -93,14 +93,12 @@ instant = { version = "0.1", default-features = false }
rand = "0.9.1"
regex = "1"
home = "0.5.5"
tonic = { version = "0.13.1", features = [
"channel",
"tls-webpki-roots",
] }
tonic = { version = "0.13.1", features = ["tls-ring", "codegen", "prost", "transport"], default-features = false }
prost = "0.13.1"
tonic-build = "0.13.1"
strum = "0.27.1"
strum_macros = "0.27.1"
rustls = { version = "0.23.28", default-features = false, features = ["ring"] }

View File

@@ -14,10 +14,20 @@ readme = "README.md"
async-trait.workspace = true
anyhow.workspace = true
cdk-common = { workspace = true, features = ["mint"] }
fedimint-tonic-lnd = "0.2.0"
futures.workspace = true
tokio.workspace = true
tokio = { workspace = true, default-features = false, features = ["fs"] }
tokio-util.workspace = true
tracing.workspace = true
thiserror.workspace = true
serde_json.workspace = true
prost.workspace = true
tonic = { workspace = true, features = ["transport"] }
http = "1.3.1"
hyper = { version = "1.6.0", features = ["http2", "client"] }
hyper-util = { version = "0.1.14", features = ["client"] }
hyper-rustls = { version = "0.27.7", features = ["http2", "tls12"] }
rustls.workspace = true
rustls-pemfile = "2.2.0"
[build-dependencies]
tonic-build.workspace = true

19
crates/cdk-lnd/build.rs Normal file
View File

@@ -0,0 +1,19 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=src/proto/lnrpc.proto");
println!("cargo:rerun-if-changed=src/proto/routerrpc.proto");
// Tell cargo to tell rustc to allow missing docs in generated code
println!("cargo:rustc-env=RUSTDOC_ARGS=--allow-missing-docs");
// Configure tonic build to generate code with documentation
tonic_build::configure()
.protoc_arg("--experimental_allow_proto3_optional")
.type_attribute(".", "#[allow(missing_docs)]")
.field_attribute(".", "#[allow(missing_docs)]")
.compile_protos(
&["src/proto/lnrpc.proto", "src/proto/routerrpc.proto"],
&["src/proto"],
)?;
Ok(())
}

View File

@@ -0,0 +1,227 @@
//! GRPC Client
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use cdk_common::util::hex;
use hyper_rustls::HttpsConnectorBuilder;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client as HyperClient;
use hyper_util::rt::TokioExecutor;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::crypto::ring::default_provider;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, Error as TLSError, SignatureScheme};
use tokio::fs;
use tonic::body::Body;
use tonic::codegen::InterceptedService;
use tonic::metadata::MetadataValue;
use tonic::service::Interceptor;
use tonic::{Request, Status};
use crate::{lnrpc, routerrpc, Error};
/// Custom certificate verifier for LND's self-signed certificates
#[derive(Debug)]
pub(crate) struct LndCertVerifier {
certs: Vec<Vec<u8>>,
provider: Arc<rustls::crypto::CryptoProvider>,
}
impl LndCertVerifier {
pub(crate) async fn load(path: impl AsRef<Path>) -> Result<Self, Error> {
let provider = default_provider();
let contents = fs::read(path).await.map_err(|_| Error::ReadFile)?;
let mut reader = std::io::Cursor::new(contents);
// Parse PEM certificates
let certs: Vec<CertificateDer<'static>> =
rustls_pemfile::certs(&mut reader).flatten().collect();
Ok(LndCertVerifier {
certs: certs.into_iter().map(|c| c.to_vec()).collect(),
provider: Arc::new(provider),
})
}
}
impl ServerCertVerifier for LndCertVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
_server_name: &ServerName,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, TLSError> {
let mut certs = intermediates
.iter()
.map(|c| c.as_ref().to_vec())
.collect::<Vec<Vec<u8>>>();
certs.push(end_entity.as_ref().to_vec());
certs.sort();
let mut our_certs = self.certs.clone();
our_certs.sort();
if self.certs.len() != certs.len() {
return Err(TLSError::General(format!(
"Mismatched number of certificates (Expected: {}, Presented: {})",
self.certs.len(),
certs.len()
)));
}
for (c, p) in our_certs.iter().zip(certs.iter()) {
if p != c {
return Err(TLSError::General(
"Server certificates do not match ours".to_string(),
));
}
}
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TLSError> {
rustls::crypto::verify_tls12_signature(
message,
cert,
dss,
&self.provider.signature_verification_algorithms,
)
.map(|_| HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TLSError> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&self.provider.signature_verification_algorithms,
)
.map(|_| HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.provider
.signature_verification_algorithms
.supported_schemes()
}
}
pub type RouterClient = routerrpc::router_client::RouterClient<
InterceptedService<
HyperClient<hyper_rustls::HttpsConnector<HttpConnector>, Body>,
MacaroonInterceptor,
>,
>;
/// The client returned by `connect` function
#[derive(Clone)]
pub struct Client {
lightning: lnrpc::lightning_client::LightningClient<
InterceptedService<
HyperClient<hyper_rustls::HttpsConnector<HttpConnector>, Body>,
MacaroonInterceptor,
>,
>,
router: RouterClient,
}
/// Supplies requests with macaroon
#[derive(Clone)]
pub struct MacaroonInterceptor {
macaroon: String,
}
impl Interceptor for MacaroonInterceptor {
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
request.metadata_mut().insert(
"macaroon",
MetadataValue::from_str(&self.macaroon)
.map_err(|e| Status::internal(format!("Invalid macaroon: {e}")))?,
);
Ok(request)
}
}
async fn load_macaroon(path: impl AsRef<Path>) -> Result<String, Error> {
let macaroon = fs::read(path).await.map_err(|_| Error::ReadFile)?;
Ok(hex::encode(macaroon))
}
pub async fn connect<P: AsRef<Path>>(
address: &str,
cert_path: P,
macaroon_path: P,
) -> Result<Client, Error> {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
let config = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(LndCertVerifier::load(cert_path).await?))
.with_no_client_auth();
// Create HTTPS connector
let https = HttpsConnectorBuilder::new()
.with_tls_config(config)
.https_only()
.enable_http2()
.build();
// Create hyper client
let client = HyperClient::builder(TokioExecutor::new())
.http2_only(true)
.build(https);
// Load macaroon
let macaroon = load_macaroon(macaroon_path).await?;
// Create service with macaroon interceptor
let service = InterceptedService::new(client, MacaroonInterceptor { macaroon });
// Create URI for the service
let address = address
.trim_start_matches("http://")
.trim_start_matches("https://");
let uri = http::Uri::from_str(&format!("https://{address}"))
.map_err(|e| Error::InvalidConfig(format!("Invalid URI: {e}")))?;
// Create LND client
let lightning =
lnrpc::lightning_client::LightningClient::with_origin(service.clone(), uri.clone());
let router = RouterClient::with_origin(service, uri);
Ok(Client { lightning, router })
}
impl Client {
pub fn lightning(
&mut self,
) -> &mut lnrpc::lightning_client::LightningClient<
InterceptedService<
HyperClient<hyper_rustls::HttpsConnector<HttpConnector>, Body>,
MacaroonInterceptor,
>,
> {
&mut self.lightning
}
pub fn router(&mut self) -> &mut RouterClient {
&mut self.router
}
}

View File

@@ -1,7 +1,7 @@
//! LND Errors
use fedimint_tonic_lnd::tonic::Status;
use thiserror::Error;
use tonic::Status;
/// LND Error
#[derive(Debug, Error)]
@@ -36,6 +36,9 @@ pub enum Error {
/// Errors invalid config
#[error("LND invalid config: `{0}`")]
InvalidConfig(String),
/// Could not read file
#[error("Could not read file")]
ReadFile,
}
impl From<Error> for cdk_common::payment::Error {
@@ -43,3 +46,9 @@ impl From<Error> for cdk_common::payment::Error {
Self::Lightning(Box::new(e))
}
}
impl From<tonic::transport::Error> for Error {
fn from(e: tonic::transport::Error) -> Self {
Error::InvalidConfig(format!("Transport error: {e}"))
}
}

View File

@@ -26,25 +26,26 @@ use cdk_common::payment::{
use cdk_common::util::hex;
use cdk_common::{mint, Bolt11Invoice};
use error::Error;
use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus;
use fedimint_tonic_lnd::lnrpc::{FeeLimit, Hop, MppRecord};
use fedimint_tonic_lnd::tonic::Code;
use fedimint_tonic_lnd::Client;
use futures::{Stream, StreamExt};
use tokio::sync::Mutex;
use lnrpc::fee_limit::Limit;
use lnrpc::payment::PaymentStatus;
use lnrpc::{FeeLimit, Hop, MppRecord};
use tokio_util::sync::CancellationToken;
use tracing::instrument;
mod client;
pub mod error;
mod proto;
pub(crate) use proto::{lnrpc, routerrpc};
/// Lnd mint backend
#[derive(Clone)]
pub struct Lnd {
address: String,
cert_file: PathBuf,
macaroon_file: PathBuf,
client: Arc<Mutex<Client>>,
_address: String,
_cert_file: PathBuf,
_macaroon_file: PathBuf,
lnd_client: client::Client,
fee_reserve: FeeReserve,
wait_invoice_cancel_token: CancellationToken,
wait_invoice_is_active: Arc<AtomicBool>,
@@ -86,18 +87,19 @@ impl Lnd {
)));
}
let client = fedimint_tonic_lnd::connect(address.to_string(), &cert_file, &macaroon_file)
let lnd_client = client::connect(&address, &cert_file, &macaroon_file)
.await
.map_err(|err| {
tracing::error!("Connection error: {}", err.to_string());
Error::Connection
})?;
})
.unwrap();
Ok(Self {
address,
cert_file,
macaroon_file,
client: Arc::new(Mutex::new(client)),
_address: address,
_cert_file: cert_file,
_macaroon_file: macaroon_file,
lnd_client,
fee_reserve,
wait_invoice_cancel_token: CancellationToken::new(),
wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
@@ -134,17 +136,14 @@ impl MintPayment for Lnd {
async fn wait_any_incoming_payment(
&self,
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
let mut client =
fedimint_tonic_lnd::connect(self.address.clone(), &self.cert_file, &self.macaroon_file)
.await
.map_err(|_| Error::Connection)?;
let mut lnd_client = self.lnd_client.clone();
let stream_req = fedimint_tonic_lnd::lnrpc::InvoiceSubscription {
let stream_req = lnrpc::InvoiceSubscription {
add_index: 0,
settle_index: 0,
};
let stream = client
let stream = lnd_client
.lightning()
.subscribe_invoices(stream_req)
.await
@@ -283,9 +282,11 @@ impl MintPayment for Lnd {
let payer_addr = invoice.payment_secret().0.to_vec();
let payment_hash = invoice.payment_hash();
let mut lnd_client = self.lnd_client.clone();
for attempt in 0..Self::MAX_ROUTE_RETRIES {
// Create a request for the routes
let route_req = fedimint_tonic_lnd::lnrpc::QueryRoutesRequest {
let route_req = lnrpc::QueryRoutesRequest {
pub_key: hex::encode(pub_key.serialize()),
amt_msat: u64::from(partial_amount_msat) as i64,
fee_limit: max_fee.map(|f| {
@@ -297,10 +298,7 @@ impl MintPayment for Lnd {
};
// Query the routes
let mut routes_response: fedimint_tonic_lnd::lnrpc::QueryRoutesResponse = self
.client
.lock()
.await
let mut routes_response = lnd_client
.lightning()
.query_routes(route_req)
.await
@@ -319,12 +317,9 @@ impl MintPayment for Lnd {
};
last_hop.mpp_record = Some(mpp_record);
let payment_response = self
.client
.lock()
.await
let payment_response = lnd_client
.router()
.send_to_route_v2(fedimint_tonic_lnd::routerrpc::SendToRouteRequest {
.send_to_route_v2(routerrpc::SendToRouteRequest {
payment_hash: payment_hash.to_byte_array().to_vec(),
route: Some(routes_response.routes[0].clone()),
..Default::default()
@@ -375,23 +370,21 @@ impl MintPayment for Lnd {
Err(Error::PaymentFailed.into())
}
None => {
let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
let mut lnd_client = self.lnd_client.clone();
let pay_req = lnrpc::SendRequest {
payment_request,
fee_limit: max_fee.map(|f| {
let limit = Limit::Fixed(u64::from(f) as i64);
FeeLimit { limit: Some(limit) }
}),
amt_msat: amount_msat as i64,
..Default::default()
};
let payment_response = self
.client
.lock()
.await
let payment_response = lnd_client
.lightning()
.send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req))
.send_payment_sync(tonic::Request::new(pay_req))
.await
.map_err(|err| {
tracing::warn!("Lightning payment failed: {}", err);
@@ -433,20 +426,19 @@ impl MintPayment for Lnd {
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice {
let invoice_request = lnrpc::Invoice {
value_msat: u64::from(amount) as i64,
memo: description,
..Default::default()
};
let invoice = self
.client
.lock()
.await
let mut lnd_client = self.lnd_client.clone();
let invoice = lnd_client
.lightning()
.add_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
.add_invoice(tonic::Request::new(invoice_request))
.await
.unwrap()
.map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
.into_inner();
let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
@@ -463,19 +455,18 @@ impl MintPayment for Lnd {
&self,
request_lookup_id: &str,
) -> Result<MintQuoteState, Self::Err> {
let invoice_request = fedimint_tonic_lnd::lnrpc::PaymentHash {
let mut lnd_client = self.lnd_client.clone();
let invoice_request = lnrpc::PaymentHash {
r_hash: hex::decode(request_lookup_id).unwrap(),
..Default::default()
};
let invoice = self
.client
.lock()
.await
let invoice = lnd_client
.lightning()
.lookup_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
.lookup_invoice(tonic::Request::new(invoice_request))
.await
.unwrap()
.map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
.into_inner();
match invoice.state {
@@ -496,24 +487,20 @@ impl MintPayment for Lnd {
&self,
payment_hash: &str,
) -> Result<MakePaymentResponse, Self::Err> {
let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest {
let mut lnd_client = self.lnd_client.clone();
let track_request = routerrpc::TrackPaymentRequest {
payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
no_inflight_updates: true,
};
let payment_response = self
.client
.lock()
.await
.router()
.track_payment_v2(track_request)
.await;
let payment_response = lnd_client.router().track_payment_v2(track_request).await;
let mut payment_stream = match payment_response {
Ok(stream) => stream.into_inner(),
Err(err) => {
let err_code = err.code();
if err_code == Code::NotFound {
if err_code == tonic::Code::NotFound {
return Ok(MakePaymentResponse {
payment_lookup_id: payment_hash.to_string(),
payment_proof: None,
@@ -540,7 +527,7 @@ impl MintPayment for Lnd {
total_spent: Amount::ZERO,
unit: self.settings.unit.clone(),
},
PaymentStatus::InFlight => {
PaymentStatus::InFlight | PaymentStatus::Initiated => {
// Continue waiting for the next update
continue;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
#[allow(clippy::all, clippy::pedantic, clippy::restriction, clippy::nursery)]
pub(crate) mod lnrpc {
tonic::include_proto!("lnrpc");
}
#[allow(clippy::all, clippy::pedantic, clippy::restriction, clippy::nursery)]
pub(crate) mod routerrpc {
tonic::include_proto!("routerrpc");
}

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ cdk = { workspace = true, features = [
"mint",
] }
clap.workspace = true
tonic.workspace = true
tonic = { workspace = true, features = ["transport"] }
tracing.workspace = true
tracing-subscriber.workspace = true
tokio.workspace = true
@@ -33,6 +33,7 @@ serde.workspace = true
thiserror.workspace = true
prost.workspace = true
home.workspace = true
rustls.workspace = true
[build-dependencies]

View File

@@ -94,6 +94,10 @@ async fn main() -> Result<()> {
tracing::debug!("Using work dir: {}", work_dir.display());
let channel = if work_dir.join("tls").is_dir() {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
// TLS directory exists, configure TLS
let server_root_ca_cert = std::fs::read_to_string(work_dir.join("tls/ca.pem")).unwrap();
let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert);

View File

@@ -39,7 +39,7 @@ utoipa = { workspace = true, optional = true }
futures.workspace = true
serde_json.workspace = true
serde_with.workspace = true
tonic.workspace = true
tonic = { workspace = true, features = ["router"] }
prost.workspace = true
tokio-stream.workspace = true
tokio-util = { workspace = true, default-features = false }

View File

@@ -22,7 +22,7 @@ cdk-common = { workspace = true, default-features = false, features = [
"mint",
"auth",
] }
tonic = { workspace = true, optional = true }
tonic = { workspace = true, optional = true, features = ["router"] }
prost = { workspace = true, optional = true }
tracing.workspace = true

View File

@@ -12,7 +12,7 @@ license.workspace = true
[features]
default = ["mint", "wallet", "auth"]
wallet = ["dep:reqwest", "cdk-common/wallet"]
wallet = ["dep:reqwest", "cdk-common/wallet", "dep:rustls"]
mint = ["dep:futures", "dep:reqwest", "cdk-common/mint", "cdk-signatory"]
auth = ["dep:jsonwebtoken", "cdk-common/auth", "cdk-common/auth"]
# We do not commit to a MSRV with swagger enabled
@@ -61,6 +61,7 @@ tokio-tungstenite = { workspace = true, features = [
"rustls-tls-native-roots",
"connect"
] }
rustls = { workspace = true, optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }

View File

@@ -34,6 +34,11 @@ struct HttpClientCore {
impl HttpClientCore {
fn new() -> Self {
#[cfg(not(target_arch = "wasm32"))]
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
Self {
inner: Client::new(),
}