Skip to content

Commit f81b8a0

Browse files
benthecarmanclaude
andcommitted
Add PostgreSQL storage backend
Add a PostgresStore implementation behind the "postgres" feature flag, mirroring the existing SqliteStore. Uses tokio-postgres (async-native) with an internal tokio runtime for the sync KVStoreSync trait, following the VssStore pattern. Includes unit tests, integration tests (channel full cycle and node restart), and a CI workflow that runs both against a PostgreSQL service container. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b24a905 commit f81b8a0

9 files changed

Lines changed: 1701 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: CI Checks - PostgreSQL Integration Tests
2+
3+
on: [ push, pull_request ]
4+
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ubuntu-latest
12+
13+
services:
14+
postgres:
15+
image: postgres:latest
16+
ports:
17+
- 5432:5432
18+
env:
19+
POSTGRES_DB: postgres
20+
POSTGRES_USER: postgres
21+
POSTGRES_PASSWORD: postgres
22+
options: >-
23+
--health-cmd pg_isready
24+
--health-interval 10s
25+
--health-timeout 5s
26+
--health-retries 5
27+
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@v6
31+
- name: Install Rust stable toolchain
32+
run: |
33+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
34+
- name: Enable caching for bitcoind
35+
id: cache-bitcoind
36+
uses: actions/cache@v4
37+
with:
38+
path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
39+
key: bitcoind-27_2-${{ runner.os }}-${{ runner.arch }}
40+
- name: Enable caching for electrs
41+
id: cache-electrs
42+
uses: actions/cache@v4
43+
with:
44+
path: bin/electrs-${{ runner.os }}-${{ runner.arch }}
45+
key: electrs-esplora_a33e97e1-${{ runner.os }}-${{ runner.arch }}
46+
- name: Download bitcoind/electrs
47+
if: "steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true'"
48+
run: |
49+
source ./scripts/download_bitcoind_electrs.sh
50+
mkdir -p bin
51+
mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
52+
mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }}
53+
- name: Set bitcoind/electrs environment variables
54+
run: |
55+
echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
56+
echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
57+
- name: Run PostgreSQL store tests
58+
env:
59+
TEST_POSTGRES_URL: "host=localhost user=postgres password=postgres"
60+
run: cargo test --features postgres io::postgres_store
61+
- name: Run PostgreSQL integration tests
62+
env:
63+
TEST_POSTGRES_URL: "host=localhost user=postgres password=postgres"
64+
run: |
65+
RUSTFLAGS="--cfg no_download --cfg cycle_tests" cargo test --features postgres --test integration_tests_postgres

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ panic = 'abort' # Abort on panic
2525

2626
[features]
2727
default = []
28+
postgres = ["dep:tokio-postgres", "dep:native-tls", "dep:postgres-native-tls"]
2829

2930
[dependencies]
3031
#lightning = { version = "0.2.0", features = ["std"] }
@@ -78,6 +79,9 @@ serde_json = { version = "1.0.128", default-features = false, features = ["std"]
7879
log = { version = "0.4.22", default-features = false, features = ["std"]}
7980

8081
async-trait = { version = "0.1", default-features = false }
82+
tokio-postgres = { version = "0.7", default-features = false, features = ["runtime"], optional = true }
83+
native-tls = { version = "0.2", default-features = false, optional = true }
84+
postgres-native-tls = { version = "0.5", default-features = false, features = ["runtime"], optional = true }
8185
vss-client = { package = "vss-client-ng", version = "0.5" }
8286
prost = { version = "0.11.6", default-features = false}
8387
#bitcoin-payment-instructions = { version = "0.6" }

bindings/ldk_node.udl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ interface Builder {
6464
[Throws=BuildError]
6565
Node build(NodeEntropy node_entropy);
6666
[Throws=BuildError]
67+
Node build_with_postgres_store(NodeEntropy node_entropy, string connection_string, string? db_name, string? kv_table_name, PostgresTlsConfig? tls_config);
68+
[Throws=BuildError]
6769
Node build_with_fs_store(NodeEntropy node_entropy);
6870
[Throws=BuildError]
6971
Node build_with_vss_store(NodeEntropy node_entropy, string vss_url, string store_id, record<string, string> fixed_headers);
@@ -267,6 +269,11 @@ dictionary RouteParametersConfig {
267269
u8 max_channel_saturation_power_of_half;
268270
};
269271

272+
[Remote]
273+
dictionary PostgresTlsConfig {
274+
string? certificate_pem;
275+
};
276+
270277
[Remote]
271278
dictionary LSPS1OrderStatus {
272279
LSPS1OrderId order_id;

src/builder.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,44 @@ impl NodeBuilder {
640640
self.build_with_store(node_entropy, kv_store)
641641
}
642642

643+
/// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options
644+
/// previously configured.
645+
///
646+
/// Connects to the PostgreSQL database at the given `connection_string`, e.g.,
647+
/// `"postgres://user:password@localhost/ldk_db"`.
648+
///
649+
/// The given `db_name` will be used or default to
650+
/// [`DEFAULT_DB_NAME`](io::postgres_store::DEFAULT_DB_NAME). The `connection_string` must
651+
/// not include a `dbname` when `db_name` is set, providing both is an error. The database
652+
/// will be created automatically if it doesn't already exist. The initial connection is
653+
/// made to the target database, and if it fails we fall back to the default `postgres`
654+
/// database to create it.
655+
///
656+
/// The given `kv_table_name` will be used or default to
657+
/// [`DEFAULT_KV_TABLE_NAME`](io::postgres_store::DEFAULT_KV_TABLE_NAME).
658+
///
659+
/// If `tls_config` is `Some`, TLS will be used for database connections. A custom CA
660+
/// certificate can be provided via
661+
/// [`PostgresTlsConfig::certificate_pem`](io::postgres_store::PostgresTlsConfig::certificate_pem),
662+
/// which will be added to the system's default root certificates (not replace them).
663+
/// If `tls_config` is `None`, connections will be unencrypted.
664+
///
665+
/// [PostgreSQL]: https://www.postgresql.org
666+
#[cfg(feature = "postgres")]
667+
pub fn build_with_postgres_store(
668+
&self, node_entropy: NodeEntropy, connection_string: String, db_name: Option<String>,
669+
kv_table_name: Option<String>, tls_config: Option<io::postgres_store::PostgresTlsConfig>,
670+
) -> Result<Node, BuildError> {
671+
let kv_store = io::postgres_store::PostgresStore::new(
672+
connection_string,
673+
db_name,
674+
kv_table_name,
675+
tls_config,
676+
)
677+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
678+
self.build_with_store(node_entropy, kv_store)
679+
}
680+
643681
/// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options
644682
/// previously configured.
645683
pub fn build_with_fs_store(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
@@ -1102,6 +1140,60 @@ impl ArcedNodeBuilder {
11021140
self.inner.read().expect("lock").build(*node_entropy).map(Arc::new)
11031141
}
11041142

1143+
/// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options
1144+
/// previously configured.
1145+
///
1146+
/// Connects to the PostgreSQL database at the given `connection_string`, e.g.,
1147+
/// `"postgres://user:password@localhost/ldk_db"`.
1148+
///
1149+
/// The given `db_name` will be used or default to
1150+
/// [`DEFAULT_DB_NAME`](io::postgres_store::DEFAULT_DB_NAME). The `connection_string` must
1151+
/// not include a `dbname` when `db_name` is set, providing both is an error. The database
1152+
/// will be created automatically if it doesn't already exist. The initial connection is
1153+
/// made to the target database, and if it fails we fall back to the default `postgres`
1154+
/// database to create it.
1155+
///
1156+
/// The given `kv_table_name` will be used or default to
1157+
/// [`DEFAULT_KV_TABLE_NAME`](io::postgres_store::DEFAULT_KV_TABLE_NAME).
1158+
///
1159+
/// If `tls_config` is `Some`, TLS will be used for database connections. A custom CA
1160+
/// certificate can be provided via
1161+
/// [`PostgresTlsConfig::certificate_pem`](io::postgres_store::PostgresTlsConfig::certificate_pem),
1162+
/// which will be added to the system's default root certificates (not replace them).
1163+
/// If `tls_config` is `None`, connections will be unencrypted.
1164+
///
1165+
/// [PostgreSQL]: https://www.postgresql.org
1166+
#[cfg(feature = "postgres")]
1167+
pub fn build_with_postgres_store(
1168+
&self, node_entropy: Arc<NodeEntropy>, connection_string: String, db_name: Option<String>,
1169+
kv_table_name: Option<String>, tls_config: Option<io::postgres_store::PostgresTlsConfig>,
1170+
) -> Result<Arc<Node>, BuildError> {
1171+
self.inner
1172+
.read()
1173+
.unwrap()
1174+
.build_with_postgres_store(
1175+
*node_entropy,
1176+
connection_string,
1177+
db_name,
1178+
kv_table_name,
1179+
tls_config,
1180+
)
1181+
.map(Arc::new)
1182+
}
1183+
1184+
/// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options
1185+
/// previously configured.
1186+
///
1187+
/// This requires the `postgres` crate feature.
1188+
#[cfg(not(feature = "postgres"))]
1189+
pub fn build_with_postgres_store(
1190+
&self, _node_entropy: Arc<NodeEntropy>, _connection_string: String,
1191+
_db_name: Option<String>, _kv_table_name: Option<String>,
1192+
_tls_config: Option<io::PostgresTlsConfig>,
1193+
) -> Result<Arc<Node>, BuildError> {
1194+
Err(BuildError::KVStoreSetupFailed)
1195+
}
1196+
11051197
/// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options
11061198
/// previously configured.
11071199
pub fn build_with_fs_store(

src/ffi/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub use lightning_liquidity::lsps1::msgs::{
4646
};
4747
pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
4848
pub use lightning_types::string::UntrustedString;
49+
50+
pub use crate::io::PostgresTlsConfig;
51+
4952
use vss_client::headers::{
5053
VssHeaderProvider as VssClientHeaderProvider,
5154
VssHeaderProviderError as VssClientHeaderProviderError,

src/io/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
//! Objects and traits for data persistence.
99
10+
#[cfg(feature = "postgres")]
11+
pub mod postgres_store;
1012
pub mod sqlite_store;
1113
#[cfg(test)]
1214
pub(crate) mod test_utils;
@@ -82,3 +84,13 @@ pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices
8284
/// The pending payment information will be persisted under this prefix.
8385
pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_payments";
8486
pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
87+
88+
/// TLS configuration for PostgreSQL connections.
89+
#[cfg(any(feature = "postgres", feature = "uniffi"))]
90+
#[derive(Debug, Clone)]
91+
pub struct PostgresTlsConfig {
92+
/// PEM-encoded CA certificate. When set, the certificate is added to the system's default
93+
/// root certificates — it does not replace them. If `None`, only the system's default root
94+
/// certificates are used.
95+
pub certificate_pem: Option<String>,
96+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
use lightning::io;
9+
use tokio_postgres::Client;
10+
11+
pub(super) async fn migrate_schema(
12+
_client: &Client, _kv_table_name: &str, from_version: u16, to_version: u16,
13+
) -> io::Result<()> {
14+
assert!(from_version < to_version);
15+
// Future migrations go here, e.g.:
16+
// if from_version == 1 && to_version >= 2 {
17+
// migrate_v1_to_v2(client, kv_table_name).await?;
18+
// from_version = 2;
19+
// }
20+
Ok(())
21+
}

0 commit comments

Comments
 (0)