diff --git a/Cargo.lock b/Cargo.lock index 581879bb..215bf5bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,6 +359,15 @@ dependencies = [ "webpki-roots 0.25.4", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -495,12 +504,52 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -591,6 +640,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -627,6 +682,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -731,6 +801,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -740,7 +820,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -869,6 +949,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.12" @@ -1297,12 +1386,15 @@ dependencies = [ "lightning-transaction-sync", "lightning-types", "log", + "native-tls", + "postgres-native-tls", "prost", "rusqlite", "rustls 0.23.34", "serde", "serde_json", "tokio", + "tokio-postgres", "vss-client-ng", "winapi", ] @@ -1406,6 +1498,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + [[package]] name = "libsqlite3-sys" version = "0.28.0" @@ -1579,6 +1680,15 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -1597,6 +1707,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1627,7 +1747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -1637,6 +1757,23 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1652,6 +1789,72 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1674,6 +1877,25 @@ dependencies = [ "indexmap 2.12.0", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -1720,6 +1942,47 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "postgres-native-tls" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef4de47bb81477e0c3deaf153a1b10ae176484713ff1640969f4cb96b653ebc" +dependencies = [ + "native-tls", + "tokio", + "tokio-native-tls", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac", + "md-5", + "memchr", + "rand 0.9.2", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "postgres-protocol", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1940,6 +2203,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "regex" version = "1.12.2" @@ -2069,7 +2341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ "bitflags 2.10.0", - "fallible-iterator", + "fallible-iterator 0.3.0", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", @@ -2189,6 +2461,21 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sct" version = "0.7.1" @@ -2220,6 +2507,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -2284,6 +2594,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2299,6 +2620,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.11" @@ -2337,6 +2664,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2404,7 +2742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2513,6 +2851,42 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.2", + "socket2 0.6.1", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -2721,6 +3095,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.20" @@ -2736,6 +3122,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "untrusted" version = "0.9.0" @@ -2808,6 +3200,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -2817,6 +3218,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + [[package]] name = "wasm-bindgen" version = "0.2.105" @@ -2922,6 +3332,17 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "whoami" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fae98cf96deed1b7572272dfc777713c249ae40aa1cf8862e091e8b745f5361" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/contrib/ldk-server-config.toml b/contrib/ldk-server-config.toml index 32f74c8a..0eb84d50 100644 --- a/contrib/ldk-server-config.toml +++ b/contrib/ldk-server-config.toml @@ -11,7 +11,14 @@ alias = "ldk_server" # Lightning node alias # Storage settings [storage.disk] -dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persistence, optional, defaults to ~/Library/Application Support/ldk-server/ on macOS, ~/.ldk-server/ on Linux +dir_path = "/tmp/ldk-server/" # Path for local ldk-server data, optional, defaults to ~/Library/Application Support/ldk-server/ on macOS, ~/.ldk-server/ on Linux + +# Optional LDK Node PostgreSQL storage for wallet and channel state. +#[storage.postgres] +#connection_string = "host=localhost user=postgres password=postgres" # PostgreSQL connection string. Do not include dbname if db_name is set. +#db_name = "ldk_node" # Optional database name. Defaults to ldk-node's default. +#kv_table_name = "ldk_node_kv" # Optional KV table name. Defaults to ldk-node's default. +#certificate_path = "/path/to/postgres-ca.pem" # Optional CA certificate PEM file for TLS PostgreSQL connections. [log] level = "Debug" # Log level (Error, Warn, Info, Debug, Trace) diff --git a/docs/configuration.md b/docs/configuration.md index 548ab7c2..c437c2fe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -61,8 +61,27 @@ Leave the field unset to disable async payments. ### `[storage.disk]` -Where persistent data is stored. Defaults to `~/.ldk-server/` on Linux and -`~/Library/Application Support/ldk-server/` on macOS. +Where local ldk-server data is stored. Defaults to `~/.ldk-server/` on Linux and +`~/Library/Application Support/ldk-server/` on macOS. This directory is still used for +the node mnemonic, API key, TLS material, logs, and ldk-server's payment history when +LDK Node wallet/channel state uses PostgreSQL. + +### `[storage.postgres]` + +Optional PostgreSQL storage for LDK Node wallet and channel state. + +```toml +[storage.postgres] +connection_string = "host=localhost user=postgres password=postgres" +db_name = "ldk_node" +kv_table_name = "ldk_node_kv" +certificate_path = "/path/to/postgres-ca.pem" +``` + +Only `connection_string` is required. `db_name`, `kv_table_name`, and `certificate_path` +are optional. If `db_name` is set, do not also include a database name in the connection +string. If `certificate_path` is set, the file must contain a PEM-encoded CA certificate +for TLS PostgreSQL connections. ### `[log]` @@ -173,3 +192,7 @@ The mnemonic is the node's master secret, required to recover on-chain funds. On ldk-server generates a fresh 24-word BIP39 mnemonic at `/keys_mnemonic` if the file does not already exist. `ldk_node_data.sqlite` holds channel state, both are required to recover channel funds. See [Operations - Backups](operations.md#backups) for backup guidance. + +When `[storage.postgres]` is configured, LDK Node wallet/channel state is stored in +PostgreSQL instead of `ldk_node_data.sqlite`. The storage directory remains required for +the mnemonic, API key, TLS material, logs, and `ldk_server_data.sqlite`. diff --git a/docs/operations.md b/docs/operations.md index c32390e3..36b6cdff 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -53,7 +53,7 @@ the following config to `/etc/logrotate.d/ldk-server` (adjust the log path to ma | File | Priority | Description | | -------------------------------------- | ------------ | -------------------------------------------------------------------------- | | `/keys_mnemonic` | **Critical** | BIP39 mnemonic. Required to recover on-chain funds. Default for new installs. | -| `/ldk_node_data.sqlite` | **Critical** | Channel state and on-chain wallet data. Required to recover channel funds. | +| `/ldk_node_data.sqlite` or configured PostgreSQL database | **Critical** | Channel state and on-chain wallet data. Required to recover channel funds. | | `/ldk_server_data.sqlite` | Nice-to-have | Payment and forwarding history | ### What is Reconstructable diff --git a/ldk-server/Cargo.toml b/ldk-server/Cargo.toml index d27e4943..2ddd07bd 100644 --- a/ldk-server/Cargo.toml +++ b/ldk-server/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["bitcoin", "lightning", "ldk", "server"] categories = ["cryptography::cryptocurrencies"] [dependencies] -ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "f2e44fdd21490ff3fa90445d7a28b19cfd020c3d" } +ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "f2e44fdd21490ff3fa90445d7a28b19cfd020c3d", features = ["postgres"] } serde = { version = "1.0.203", default-features = false, features = ["derive"] } hyper = { version = "1", default-features = false, features = ["server", "http2"] } http-body-util = { version = "0.1", default-features = false } diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 123c35fb..4895fe39 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -49,7 +49,7 @@ use crate::io::persist::{ PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::service::NodeService; -use crate::util::config::{load_config, ArgsConfig, ChainSource}; +use crate::util::config::{load_config, ArgsConfig, ChainSource, LdkNodeStorageConfig}; use crate::util::logger::{LogConfig, ServerLogger}; use crate::util::metrics::Metrics; use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto}; @@ -253,7 +253,7 @@ fn main() { }, }; - let node = match builder.build(node_entropy) { + let node = match build_node(builder, node_entropy, config_file.ldk_node_storage) { Ok(node) => Arc::new(node), Err(e) => { error!("Failed to build LDK Node: {e}"); @@ -688,6 +688,27 @@ fn main() { log::logger().flush(); } +fn build_node( + builder: Builder, node_entropy: ldk_node::entropy::NodeEntropy, + ldk_node_storage: LdkNodeStorageConfig, +) -> Result { + match ldk_node_storage { + LdkNodeStorageConfig::Sqlite => builder.build(node_entropy), + LdkNodeStorageConfig::Postgres { + connection_string, + db_name, + kv_table_name, + certificate_pem, + } => builder.build_with_postgres_store( + node_entropy, + connection_string, + db_name, + kv_table_name, + certificate_pem, + ), + } +} + fn send_event_and_upsert_payment( payment_id: &PaymentId, payment_to_event: impl FnOnce(&Payment) -> event_envelope::Event, event_node: &Node, event_sender: &broadcast::Sender, diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index fc2ee45b..06db3393 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -52,6 +52,7 @@ pub struct Config { pub tls_config: Option, pub grpc_service_addr: SocketAddr, pub storage_dir_path: Option, + pub ldk_node_storage: LdkNodeStorageConfig, pub chain_source: ChainSource, pub rgs_server_url: Option, pub lsps2_client_config: Option, @@ -87,6 +88,17 @@ pub struct TlsConfig { pub hosts: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LdkNodeStorageConfig { + Sqlite, + Postgres { + connection_string: String, + db_name: Option, + kv_table_name: Option, + certificate_pem: Option, + }, +} + #[derive(Debug, PartialEq, Eq)] pub enum ChainSource { Rpc { @@ -121,6 +133,7 @@ struct ConfigBuilder { tls_config: Option, grpc_service_address: Option, storage_dir_path: Option, + ldk_node_postgres: Option, electrum_url: Option, esplora_url: Option, bitcoind_rpc_address: Option, @@ -165,8 +178,12 @@ impl ConfigBuilder { } if let Some(storage) = toml.storage { - self.storage_dir_path = - storage.disk.and_then(|d| d.dir_path).or(self.storage_dir_path.clone()); + self.storage_dir_path = storage + .disk + .as_ref() + .and_then(|d| d.dir_path.clone()) + .or(self.storage_dir_path.clone()); + self.ldk_node_postgres = storage.postgres.or(self.ldk_node_postgres.clone()); } if let Some(bitcoind) = toml.bitcoind { @@ -268,6 +285,27 @@ impl ConfigBuilder { self.storage_dir_path = Some(storage_dir_path.clone()); } + if args.storage_postgres_connection_string.is_some() + || args.storage_postgres_db_name.is_some() + || args.storage_postgres_kv_table_name.is_some() + || args.storage_postgres_certificate_path.is_some() + { + let mut postgres = self.ldk_node_postgres.take().unwrap_or_default(); + if let Some(connection_string) = &args.storage_postgres_connection_string { + postgres.connection_string = Some(connection_string.clone()); + } + if let Some(db_name) = &args.storage_postgres_db_name { + postgres.db_name = Some(db_name.clone()); + } + if let Some(kv_table_name) = &args.storage_postgres_kv_table_name { + postgres.kv_table_name = Some(kv_table_name.clone()); + } + if let Some(certificate_path) = &args.storage_postgres_certificate_path { + postgres.certificate_path = Some(certificate_path.clone()); + } + self.ldk_node_postgres = Some(postgres); + } + if let Some(pathfinding_scores_source_url) = &args.pathfinding_scores_source_url { self.pathfinding_scores_source_url = Some(pathfinding_scores_source_url.clone()); } @@ -459,6 +497,32 @@ impl ConfigBuilder { let log_max_files = self.log_max_files.unwrap_or(DEFAULT_LOG_MAX_FILES); let log_to_file = self.log_to_file.unwrap_or(true); + let ldk_node_storage = if let Some(postgres) = self.ldk_node_postgres { + LdkNodeStorageConfig::Postgres { + connection_string: postgres + .connection_string + .ok_or_else(|| missing_field_err("storage_postgres_connection_string"))?, + db_name: postgres.db_name, + kv_table_name: postgres.kv_table_name, + certificate_pem: postgres + .certificate_path + .map(|path| { + fs::read_to_string(&path).map_err(|e| { + io::Error::new( + e.kind(), + format!( + "Failed to read PostgreSQL certificate file '{}': {}", + path, e + ), + ) + }) + }) + .transpose()?, + } + } else { + LdkNodeStorageConfig::Sqlite + }; + let lsps2_client_config = self .lsps2 .as_ref() @@ -533,6 +597,7 @@ impl ConfigBuilder { tls_config: self.tls_config, grpc_service_addr, storage_dir_path: self.storage_dir_path, + ldk_node_storage, chain_source, rgs_server_url: self.rgs_server_url, lsps2_client_config, @@ -589,6 +654,7 @@ struct NodeConfig { #[serde(deny_unknown_fields)] struct StorageConfig { disk: Option, + postgres: Option, } #[derive(Deserialize, Serialize)] @@ -597,6 +663,15 @@ struct DiskConfig { dir_path: Option, } +#[derive(Clone, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct PostgresStorageConfig { + connection_string: Option, + db_name: Option, + kv_table_name: Option, + certificate_path: Option, +} + #[derive(Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct BitcoindConfig { @@ -957,6 +1032,34 @@ pub struct ArgsConfig { )] storage_dir_path: Option, + #[arg( + long, + env = "LDK_SERVER_STORAGE_POSTGRES_CONNECTION_STRING", + help = "PostgreSQL connection string for LDK Node wallet and channel state." + )] + storage_postgres_connection_string: Option, + + #[arg( + long, + env = "LDK_SERVER_STORAGE_POSTGRES_DB_NAME", + help = "Optional PostgreSQL database name for LDK Node storage." + )] + storage_postgres_db_name: Option, + + #[arg( + long, + env = "LDK_SERVER_STORAGE_POSTGRES_KV_TABLE_NAME", + help = "Optional PostgreSQL key-value table name for LDK Node storage." + )] + storage_postgres_kv_table_name: Option, + + #[arg( + long, + env = "LDK_SERVER_STORAGE_POSTGRES_CERTIFICATE_PATH", + help = "Optional path to a PEM-encoded CA certificate for TLS PostgreSQL connections." + )] + storage_postgres_certificate_path: Option, + #[arg( long, env = "LDK_SERVER_PATHFINDING_SCORES_SOURCE_URL", @@ -1145,6 +1248,10 @@ mod tests { rescan_from_height: None, force_wallet_full_scan: false, storage_dir_path: Some(String::from("/tmp_cli")), + storage_postgres_connection_string: None, + storage_postgres_db_name: None, + storage_postgres_kv_table_name: None, + storage_postgres_certificate_path: None, node_alias: Some(String::from("LDK Server CLI")), pathfinding_scores_source_url: Some(String::from("https://example.com/")), node_async_payments_role: Some(String::from("server")), @@ -1174,6 +1281,10 @@ mod tests { rescan_from_height: None, force_wallet_full_scan: false, storage_dir_path: None, + storage_postgres_connection_string: None, + storage_postgres_db_name: None, + storage_postgres_kv_table_name: None, + storage_postgres_certificate_path: None, pathfinding_scores_source_url: None, node_async_payments_role: None, metrics_enabled: false, @@ -1240,6 +1351,7 @@ mod tests { network: Network::Regtest, grpc_service_addr: SocketAddr::from_str("127.0.0.1:3002").unwrap(), storage_dir_path: Some("/tmp".to_string()), + ldk_node_storage: LdkNodeStorageConfig::Sqlite, tls_config: Some(TlsConfig { cert_path: Some("/path/to/tls.crt".to_string()), key_path: Some("/path/to/tls.key".to_string()), @@ -1298,6 +1410,7 @@ mod tests { assert_eq!(config.network, expected.network); assert_eq!(config.grpc_service_addr, expected.grpc_service_addr); assert_eq!(config.storage_dir_path, expected.storage_dir_path); + assert_eq!(config.ldk_node_storage, expected.ldk_node_storage); assert_eq!(config.chain_source, expected.chain_source); assert_eq!(config.rgs_server_url, expected.rgs_server_url); assert_eq!(config.lsps2_client_config, expected.lsps2_client_config); @@ -1309,7 +1422,6 @@ mod tests { assert!(matches!(config.async_payments_role, Some(AsyncPaymentsRole::Client))); assert_eq!(config.metrics_enabled, expected.metrics_enabled); assert_eq!(config.tor_config, expected.tor_config); - // Test case where only electrum is set let toml_config = r#" @@ -1482,6 +1594,249 @@ mod tests { assert_eq!(error.to_string(), "Must set a single chain source, multiple were configured"); } + #[test] + fn test_postgres_storage_config_from_file() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_postgres_storage_config_from_file.toml"; + let cert_path = storage_path.join("test_postgres_storage_cert.pem"); + fs::write(&cert_path, "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----").unwrap(); + let toml_config = format!( + r#" + [node] + network = "regtest" + + [storage.disk] + dir_path = "/tmp" + + [storage.postgres] + connection_string = "host=localhost user=postgres password=postgres" + db_name = "ldk_node" + kv_table_name = "ldk_node_kv" + certificate_path = "{}" + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + {} + "#, + cert_path.display(), + lsps2_service_config_for_feature() + ); + + fs::write(storage_path.join(config_file_name), toml_config).unwrap(); + + let mut args_config = empty_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + let config = load_config(&args_config).unwrap(); + assert_eq!(config.storage_dir_path, Some("/tmp".to_string())); + assert_eq!( + config.ldk_node_storage, + LdkNodeStorageConfig::Postgres { + connection_string: "host=localhost user=postgres password=postgres".to_string(), + db_name: Some("ldk_node".to_string()), + kv_table_name: Some("ldk_node_kv".to_string()), + certificate_pem: Some( + "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----".to_string() + ), + } + ); + } + + #[test] + fn test_postgres_storage_config_from_args() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_postgres_storage_config_from_args.toml"; + let cert_path = storage_path.join("test_postgres_storage_args_cert.pem"); + fs::write(&cert_path, "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----").unwrap(); + let mut args_config = default_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + fs::write( + storage_path.join(config_file_name), + format!( + r#" + [node] + network = "regtest" + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + {} + "#, + lsps2_service_config_for_feature() + ), + ) + .unwrap(); + args_config.storage_postgres_connection_string = + Some("host=localhost user=postgres password=postgres".to_string()); + args_config.storage_postgres_db_name = Some("ldk_node".to_string()); + args_config.storage_postgres_kv_table_name = Some("ldk_node_kv".to_string()); + args_config.storage_postgres_certificate_path = + Some(cert_path.to_string_lossy().to_string()); + + let config = load_config(&args_config).unwrap(); + assert_eq!( + config.ldk_node_storage, + LdkNodeStorageConfig::Postgres { + connection_string: "host=localhost user=postgres password=postgres".to_string(), + db_name: Some("ldk_node".to_string()), + kv_table_name: Some("ldk_node_kv".to_string()), + certificate_pem: Some( + "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----".to_string() + ), + } + ); + } + + #[test] + fn test_postgres_storage_config_partial_combinations_from_file() { + let storage_path = std::env::temp_dir(); + let cert_path = storage_path.join("test_postgres_storage_partial_cert.pem"); + let cert_pem = "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"; + fs::write(&cert_path, cert_pem).unwrap(); + + for mask in 0..8 { + let config_file_name = + format!("test_postgres_storage_config_partial_combinations_{mask}.toml"); + let include_db_name = mask & 0b001 != 0; + let include_kv_table_name = mask & 0b010 != 0; + let include_certificate_path = mask & 0b100 != 0; + + let db_name_line = if include_db_name { "db_name = \"ldk_node\"\n" } else { "" }; + let kv_table_name_line = + if include_kv_table_name { "kv_table_name = \"ldk_node_kv\"\n" } else { "" }; + let certificate_path_line = if include_certificate_path { + format!("certificate_path = \"{}\"\n", cert_path.display()) + } else { + String::new() + }; + + let toml_config = format!( + r#" + [node] + network = "regtest" + + [storage.postgres] + connection_string = "host=localhost user=postgres password=postgres" + {db_name_line}{kv_table_name_line}{certificate_path_line} + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + {} + "#, + lsps2_service_config_for_feature() + ); + + fs::write(storage_path.join(&config_file_name), toml_config).unwrap(); + + let mut args_config = empty_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + let config = load_config(&args_config).unwrap(); + assert_eq!( + config.ldk_node_storage, + LdkNodeStorageConfig::Postgres { + connection_string: "host=localhost user=postgres password=postgres".to_string(), + db_name: include_db_name.then(|| "ldk_node".to_string()), + kv_table_name: include_kv_table_name.then(|| "ldk_node_kv".to_string()), + certificate_pem: include_certificate_path.then(|| cert_pem.to_string()), + } + ); + } + } + + #[test] + fn test_postgres_storage_config_requires_connection_string_for_partial_file_configs() { + let storage_path = std::env::temp_dir(); + let cert_path = storage_path.join("test_postgres_storage_missing_connection_cert.pem"); + fs::write(&cert_path, "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----").unwrap(); + + let cases = [ + ("", "empty_section"), + ("db_name = \"ldk_node\"", "db_name"), + ("kv_table_name = \"ldk_node_kv\"", "kv_table_name"), + (&format!("certificate_path = \"{}\"", cert_path.display()), "certificate_path"), + ( + &format!( + "db_name = \"ldk_node\"\nkv_table_name = \"ldk_node_kv\"\ncertificate_path = \"{}\"", + cert_path.display() + ), + "all_optional", + ), + ]; + + for (postgres_fields, case_name) in cases { + let config_file_name = + format!("test_postgres_storage_missing_connection_{case_name}.toml"); + let toml_config = format!( + r#" + [node] + network = "regtest" + + [storage.postgres] + {postgres_fields} + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + {} + "#, + lsps2_service_config_for_feature() + ); + + fs::write(storage_path.join(&config_file_name), toml_config).unwrap(); + + let mut args_config = empty_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.to_string(), missing_field_msg("storage_postgres_connection_string")); + } + } + + #[test] + fn test_postgres_storage_config_requires_connection_string_for_partial_args_configs() { + let storage_path = std::env::temp_dir(); + let cert_path = storage_path.join("test_postgres_storage_missing_args_connection_cert.pem"); + fs::write(&cert_path, "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----").unwrap(); + + let mut cases = Vec::new(); + + let mut db_name_args = default_args_config(); + db_name_args.storage_postgres_db_name = Some("ldk_node".to_string()); + cases.push(db_name_args); + + let mut kv_table_name_args = default_args_config(); + kv_table_name_args.storage_postgres_kv_table_name = Some("ldk_node_kv".to_string()); + cases.push(kv_table_name_args); + + let mut certificate_path_args = default_args_config(); + certificate_path_args.storage_postgres_certificate_path = + Some(cert_path.to_string_lossy().to_string()); + cases.push(certificate_path_args); + + let mut all_optional_args = default_args_config(); + all_optional_args.storage_postgres_db_name = Some("ldk_node".to_string()); + all_optional_args.storage_postgres_kv_table_name = Some("ldk_node_kv".to_string()); + all_optional_args.storage_postgres_certificate_path = + Some(cert_path.to_string_lossy().to_string()); + cases.push(all_optional_args); + + for args_config in cases { + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.to_string(), missing_field_msg("storage_postgres_connection_string")); + } + } + #[test] fn test_config_optional_values() { let storage_path = std::env::temp_dir(); @@ -1668,6 +2023,7 @@ mod tests { .unwrap(), alias: Some(parse_alias(args_config.node_alias.as_deref().unwrap()).unwrap()), storage_dir_path: Some(args_config.storage_dir_path.unwrap()), + ldk_node_storage: LdkNodeStorageConfig::Sqlite, tls_config: None, chain_source: ChainSource::Rpc { rpc_host: host, @@ -1700,6 +2056,7 @@ mod tests { assert_eq!(config.network, expected.network); assert_eq!(config.grpc_service_addr, expected.grpc_service_addr); assert_eq!(config.storage_dir_path, expected.storage_dir_path); + assert_eq!(config.ldk_node_storage, expected.ldk_node_storage); assert_eq!(config.chain_source, expected.chain_source); assert_eq!(config.rgs_server_url, expected.rgs_server_url); assert!(config.lsps2_service_config.is_none()); @@ -1764,6 +2121,7 @@ mod tests { .unwrap(), alias: Some(parse_alias(args_config.node_alias.as_deref().unwrap()).unwrap()), storage_dir_path: Some(args_config.storage_dir_path.unwrap()), + ldk_node_storage: LdkNodeStorageConfig::Sqlite, tls_config: Some(TlsConfig { cert_path: Some("/path/to/tls.crt".to_string()), key_path: Some("/path/to/tls.key".to_string()), @@ -1821,6 +2179,7 @@ mod tests { assert_eq!(config.network, expected.network); assert_eq!(config.grpc_service_addr, expected.grpc_service_addr); assert_eq!(config.storage_dir_path, expected.storage_dir_path); + assert_eq!(config.ldk_node_storage, expected.ldk_node_storage); assert_eq!(config.chain_source, expected.chain_source); assert_eq!(config.rgs_server_url, expected.rgs_server_url); assert_eq!(config.lsps2_client_config, expected.lsps2_client_config);