diff --git a/Cargo.lock b/Cargo.lock index 7ecd83ae16da..5f1965482408 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", "synstructure", ] @@ -68,7 +68,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -90,7 +90,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -101,7 +101,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -170,12 +170,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -236,17 +230,11 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" @@ -262,7 +250,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cln-grpc" -version = "0.1.9" +version = "0.2.0" dependencies = [ "anyhow", "bitcoin", @@ -282,7 +270,7 @@ dependencies = [ [[package]] name = "cln-grpc-plugin" -version = "0.1.6" +version = "0.2.0" dependencies = [ "anyhow", "cln-grpc", @@ -298,7 +286,7 @@ dependencies = [ [[package]] name = "cln-plugin" -version = "0.1.9" +version = "0.2.0" dependencies = [ "anyhow", "bytes", @@ -316,7 +304,7 @@ dependencies = [ [[package]] name = "cln-rpc" -version = "0.1.9" +version = "0.2.0" dependencies = [ "anyhow", "bitcoin", @@ -369,7 +357,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -481,7 +469,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -586,15 +574,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys", -] - [[package]] name = "http" version = "0.2.12" @@ -717,15 +696,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -937,7 +907,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -966,12 +936,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.1.25" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 1.0.109", + "syn", ] [[package]] @@ -985,9 +955,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.9" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -995,44 +965,43 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.11.9" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", "heck", "itertools", - "lazy_static", "log", "multimap", + "once_cell", "petgraph", "prettyplease", "prost", "prost-types", "regex", - "syn 1.0.109", + "syn", "tempfile", - "which", ] [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "prost-types" -version = "0.11.9" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] @@ -1083,7 +1052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" dependencies = [ "pem", - "ring 0.17.8", + "ring", "rustls-pki-types", "time", "x509-parser", @@ -1134,21 +1103,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -1159,8 +1113,8 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", - "untrusted 0.9.0", + "spin", + "untrusted", "windows-sys", ] @@ -1194,23 +1148,26 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.9" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", - "ring 0.16.20", - "sct", - "webpki", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", + "rustls-pki-types", ] [[package]] @@ -1219,6 +1176,17 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1231,16 +1199,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "secp256k1" version = "0.27.0" @@ -1278,7 +1236,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -1327,12 +1285,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -1340,21 +1292,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "syn" -version = "1.0.109" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.72" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1375,7 +1322,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -1416,7 +1363,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -1494,18 +1441,18 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", + "rustls-pki-types", "tokio", - "webpki", ] [[package]] @@ -1548,17 +1495,15 @@ dependencies = [ [[package]] name = "tonic" -version = "0.8.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.13.1", + "base64 0.21.7", "bytes", - "futures-core", - "futures-util", "h2", "http", "http-body", @@ -1567,30 +1512,28 @@ dependencies = [ "percent-encoding", "pin-project", "prost", - "prost-derive", "rustls-pemfile", + "rustls-pki-types", "tokio", "tokio-rustls", "tokio-stream", - "tokio-util", "tower", "tower-layer", "tower-service", "tracing", - "tracing-futures", ] [[package]] name = "tonic-build" -version = "0.8.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" +checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -1645,7 +1588,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn", ] [[package]] @@ -1658,16 +1601,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -1709,12 +1642,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -1742,92 +1669,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.72", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1944,7 +1785,7 @@ dependencies = [ "lazy_static", "nom", "oid-registry", - "ring 0.17.8", + "ring", "rusticata-macros", "thiserror", "time", @@ -1958,3 +1799,9 @@ checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ "time", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Makefile b/Makefile index 77043ff9b18f..80b61324dc24 100644 --- a/Makefile +++ b/Makefile @@ -456,6 +456,11 @@ else PYTEST_OPTS += -x endif +# Allow for targeting specific tests by setting the PYTEST_TESTS environment variable. +ifeq ($(PYTEST_TESTS),) +PYTEST_TESTS = "tests/" +endif + check-units: check: check-units installcheck pytest @@ -466,7 +471,7 @@ ifeq ($(PYTEST),) exit 1 else # Explicitly hand VALGRIND so you can override on make cmd line. - PYTHONPATH=$(MY_CHECK_PYTHONPATH) TEST_DEBUG=1 VALGRIND=$(VALGRIND) $(PYTEST) tests/ $(PYTEST_OPTS) + PYTHONPATH=$(MY_CHECK_PYTHONPATH) TEST_DEBUG=1 VALGRIND=$(VALGRIND) $(PYTEST) $(PYTEST_TESTS) $(PYTEST_OPTS) endif check-fuzz: $(ALL_FUZZ_TARGETS) diff --git a/cln-grpc/Cargo.toml b/cln-grpc/Cargo.toml index ff36a319237a..2b96b9c194b5 100644 --- a/cln-grpc/Cargo.toml +++ b/cln-grpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cln-grpc" -version = "0.1.9" +version = "0.2.0" edition = "2021" license = "MIT" description = "The Core Lightning API as grpc primitives. Provides the bindings used to expose the API over the network." @@ -15,10 +15,10 @@ server = ["cln-rpc"] [dependencies] anyhow = "1.0" log = "0.4" -cln-rpc = { path="../cln-rpc/", version = "^0.1", optional = true } -tonic = { version = "0.8", features = ["tls", "transport"] } -prost = "0.11" +cln-rpc = { path="../cln-rpc/", version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"] } +tonic = { version = "0.11", features = ["tls", "transport"] } +prost = "0.12" hex = "0.4.3" bitcoin = { version = "0.30", features = [ "serde" ] } tokio-stream = { version = "0.1.14", features = ["sync"] } @@ -28,7 +28,7 @@ tokio-util = "0.7.10" [dev-dependencies] serde_json = "1.0.72" -cln-rpc = { path="../cln-rpc/", version = "^0.1" } +cln-rpc = { path="../cln-rpc/", version = "0.2" } [build-dependencies] -tonic-build = "0.8" +tonic-build = "0.11" diff --git a/cln-grpc/proto/primitives.proto b/cln-grpc/proto/primitives.proto index 8916b84f20c1..f592172bcae4 100644 --- a/cln-grpc/proto/primitives.proto +++ b/cln-grpc/proto/primitives.proto @@ -127,6 +127,7 @@ enum ChannelTypeName { anchors_zero_fee_htlc_tx_even = 2; scid_alias_even = 3; zeroconf_even = 4; + anchors_even = 5; } enum AutocleanSubsystem { diff --git a/cln-rpc/Cargo.toml b/cln-rpc/Cargo.toml index 3d4b760f1e44..3f38145ef03b 100644 --- a/cln-rpc/Cargo.toml +++ b/cln-rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cln-rpc" -version = "0.1.9" +version = "0.2.0" edition = "2021" license = "MIT" description = "An async RPC client for Core Lightning." diff --git a/cln-rpc/src/model.rs b/cln-rpc/src/model.rs index 26a25e53a9a4..2704515ce60e 100644 --- a/cln-rpc/src/model.rs +++ b/cln-rpc/src/model.rs @@ -3,7 +3,7 @@ // This file was automatically generated using the following command: // // ```bash -// contrib/msggen/msggen/__main__.py generate +// contrib/msggen/msggen/__main__.py -s generate // ``` // // Do not edit this file, it'll be overwritten. Rather edit the schema that diff --git a/cln-rpc/src/primitives.rs b/cln-rpc/src/primitives.rs index 8d7ef03bb48f..0d05c3aa2a10 100644 --- a/cln-rpc/src/primitives.rs +++ b/cln-rpc/src/primitives.rs @@ -67,6 +67,8 @@ pub enum ChannelTypeName { SCID_ALIAS_EVEN = 3, #[serde(rename = "zeroconf/even")] ZEROCONF_EVEN = 4, + #[serde(rename = "anchors/even")] + ANCHORS_EVEN = 5, } #[derive(Copy, Clone, Serialize, Deserialize, Debug)] @@ -407,6 +409,7 @@ impl From for ChannelTypeName { 2 => ChannelTypeName::ANCHORS_ZERO_FEE_HTLC_TX_EVEN, 3 => ChannelTypeName::SCID_ALIAS_EVEN, 4 => ChannelTypeName::ZEROCONF_EVEN, + 5 => ChannelTypeName::ANCHORS_EVEN, o => panic!("Unmapped ChannelTypeName {}", o), } } @@ -420,6 +423,7 @@ impl From for i32 { ChannelTypeName::ANCHORS_ZERO_FEE_HTLC_TX_EVEN => 2, ChannelTypeName::SCID_ALIAS_EVEN => 3, ChannelTypeName::ZEROCONF_EVEN => 4, + ChannelTypeName::ANCHORS_EVEN => 5, } } } diff --git a/common/gossmap.c b/common/gossmap.c index 068a25c41566..990cb421cbca 100644 --- a/common/gossmap.c +++ b/common/gossmap.c @@ -474,8 +474,8 @@ static struct gossmap_chan *add_channel(struct gossmap *map, return NULL; } - /* gossipd writes WIRE_GOSSIP_STORE_CHANNEL_AMOUNT after this (not for - * local channels), so ignore channel_announcement until that appears */ + /* gossipd writes WIRE_GOSSIP_STORE_CHANNEL_AMOUNT after this, + * so ignore channel_announcement until that appears */ if (msglen && (map->map_size < cannounce_off + msglen + sizeof(struct gossip_hdr) + sizeof(u16) + sizeof(u64))) return NULL; @@ -760,24 +760,34 @@ static void destroy_map(struct gossmap *map) } /* Local modifications. We only expect a few, so we use a simple - * array. */ + * array. If this changes, use a hashtable and a storage area for all + * those pointers to avoid dynamic allocation overhead! */ struct localmod { struct short_channel_id scid; /* If this is an entirely-local channel, here's its offset. * Otherwise, 0xFFFFFFFF. */ u32 local_off; - /* Are updates in either direction set? */ - bool updates_set[2]; - /* hc[n] defined if updates_set[n]. */ - struct half_chan hc[2]; - /* orig[n] defined if updates_set[n] and local_off == 0xFFFFFFFF */ + /* Non-NULL values mean change existing ones */ + struct localmod_changes { + const bool *enabled; + const fp16_t *htlc_min, *htlc_max; + const u32 *base_fee, *proportional_fee; + const u16 *delay; + } changes[2]; + + /* orig[n] defined if local_off == 0xFFFFFFFF */ struct half_chan orig[2]; /* Original update offsets */ u32 orig_cupdate_off[2]; }; +static bool localmod_is_local_chan(const struct localmod *mod) +{ + return mod->local_off != 0xFFFFFFFF; +} + struct gossmap_localmods { struct localmod *mods; /* This is the local array to be used by the gossmap */ @@ -818,6 +828,7 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, const struct node_id *n1, const struct node_id *n2, struct short_channel_id scid, + struct amount_msat capacity, const u8 *features) { be16 be16; @@ -837,15 +848,23 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, * compressed keys sorted in ascending lexicographic order. */ if (node_id_cmp(n1, n2) > 0) - return gossmap_local_addchan(localmods, n2, n1, scid, features); + return gossmap_local_addchan(localmods, n2, n1, scid, capacity, + features); mod.scid = scid; - mod.updates_set[0] = mod.updates_set[1] = false; + memset(&mod.changes, 0, sizeof(mod.changes)); - /* We create fake local channel_announcement. */ + /* We create amount, then fake local channel_announcement */ off = insert_local_space(localmods, - 2 + 64 * 4 + 2 + tal_bytelen(features) + 8 + 2 + 64 * 4 + 2 + tal_bytelen(features) + 32 + 8 + 33 + 33); + + /* Write amount */ + be64 = be64_to_cpu(capacity.millisatoshis / 1000 /* Raw: gossmap */); + memcpy(localmods->local + off, &be64, sizeof(be64)); + off += sizeof(be64); + + /* From here is a channel-announcment, with only the fields we use */ mod.local_off = off; /* Set type to be kosher. */ @@ -885,53 +904,100 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, return true; }; -/* Insert a local-only channel_update. */ +/* Insert a local-only channel_update: false if can't represent. */ bool gossmap_local_updatechan(struct gossmap_localmods *localmods, - struct short_channel_id scid, - struct amount_msat htlc_min, - struct amount_msat htlc_max, - u32 base_fee, - u32 proportional_fee, - u16 delay, - bool enabled, - int dir) + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay) { struct localmod *mod; + struct localmod_changes *lc; + struct half_chan test; + + /* Check fit before making any changes. */ + if (base_fee) { + test.base_fee = base_fee->millisatoshis /* Raw: localmod */; + if (!amount_msat_eq(amount_msat(test.base_fee), *base_fee)) + return false; + } + if (proportional_fee) { + test.proportional_fee = *proportional_fee; + if (test.proportional_fee != *proportional_fee) + return false; + } + if (delay) { + test.delay = *delay; + if (test.delay != *delay) + return false; + } - mod = find_localmod(localmods, scid); + mod = find_localmod(localmods, scidd->scid); if (!mod) { /* Create new reference to (presumably) existing channel. */ size_t nmods = tal_count(localmods->mods); tal_resize(&localmods->mods, nmods + 1); mod = &localmods->mods[nmods]; - mod->scid = scid; - mod->updates_set[0] = mod->updates_set[1] = false; + mod->scid = scidd->scid; + memset(&mod->changes, 0, sizeof(mod->changes)); mod->local_off = 0xFFFFFFFF; } - assert(dir == 0 || dir == 1); - mod->updates_set[dir] = true; - mod->hc[dir].enabled = enabled; - /* node_idx needs to be set once we're in the gossmap. */ - mod->hc[dir].htlc_min - = u64_to_fp16(htlc_min.millisatoshis, /* Raw: to fp16 */ - false); - mod->hc[dir].htlc_max - = u64_to_fp16(htlc_max.millisatoshis, /* Raw: to fp16 */ - true); - mod->hc[dir].base_fee = base_fee; - mod->hc[dir].proportional_fee = proportional_fee; - mod->hc[dir].delay = delay; - - /* Check they fit */ - if (mod->hc[dir].base_fee != base_fee - || mod->hc[dir].proportional_fee != proportional_fee - || mod->hc[dir].delay != delay) - return false; + lc = &mod->changes[scidd->dir]; + if (enabled) { + tal_free(lc->enabled); + lc->enabled = tal_dup(localmods, bool, enabled); + } + if (htlc_min) { + fp16_t min = u64_to_fp16(htlc_min->millisatoshis, /* Raw: to fp16 */ + false); + tal_free(lc->htlc_min); + lc->htlc_min = tal_dup(localmods, fp16_t, &min); + } + if (htlc_max) { + fp16_t max = u64_to_fp16(htlc_max->millisatoshis, /* Raw: to fp16 */ + true); + tal_free(lc->htlc_max); + lc->htlc_max = tal_dup(localmods, fp16_t, &max); + } + if (base_fee) { + u32 base_as_u32 = base_fee->millisatoshis; /* Raw: localmod */ + tal_free(lc->base_fee); + lc->base_fee = tal_dup(localmods, u32, &base_as_u32); + } + if (proportional_fee) { + tal_free(lc->proportional_fee); + lc->proportional_fee = tal_dup(localmods, u32, proportional_fee); + } + if (delay) { + tal_free(lc->delay); + lc->delay = tal_dup(localmods, u16, delay); + } return true; } +bool gossmap_local_setchan(struct gossmap_localmods *localmods, + struct short_channel_id scid, + struct amount_msat htlc_min, + struct amount_msat htlc_max, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + bool enabled, + int dir) +{ + struct short_channel_id_dir scidd = {scid, dir}; + return gossmap_local_updatechan(localmods, &scidd, + &enabled, + &htlc_min, &htlc_max, + &base_fee, &proportional_fee, + &delay); +} + /* Apply localmods to this map */ void gossmap_apply_localmods(struct gossmap *map, struct gossmap_localmods *localmods) @@ -949,22 +1015,56 @@ void gossmap_apply_localmods(struct gossmap *map, chan = gossmap_find_chan(map, &mod->scid); /* If it doesn't exist, are we supposed to create a local one? */ if (!chan) { - if (mod->local_off == 0xFFFFFFFF) + if (!localmod_is_local_chan(mod)) continue; /* Create new channel, pointing into local. */ chan = add_channel(map, map->map_size + mod->local_off, 0); } - /* Save old, overwrite (keep nodeidx) */ + /* Save old, update any fields they wanted to change */ for (size_t h = 0; h < 2; h++) { - if (!mod->updates_set[h]) - continue; + bool was_set, all_changed; + const struct localmod_changes *c = &mod->changes[h]; + /* Save existing versions */ mod->orig[h] = chan->half[h]; mod->orig_cupdate_off[h] = chan->cupdate_off[h]; - chan->half[h] = mod->hc[h]; - chan->half[h].nodeidx = mod->orig[h].nodeidx; - chan->cupdate_off[h] = 0xFFFFFFFF; + + was_set = gossmap_chan_set(chan, h); + + /* Override specified fields. */ + all_changed = true; + if (c->enabled) + chan->half[h].enabled = *c->enabled; + else + all_changed = false; + if (c->htlc_min) + chan->half[h].htlc_min = *c->htlc_min; + else + all_changed = false; + if (c->htlc_max) + chan->half[h].htlc_max = *c->htlc_max; + else + all_changed = false; + if (c->base_fee) + chan->half[h].base_fee = *c->base_fee; + else + all_changed = false; + if (c->proportional_fee) + chan->half[h].proportional_fee = *c->proportional_fee; + else + all_changed = false; + if (c->delay) + chan->half[h].delay = *c->delay; + else + all_changed = false; + + /* Is it all defined? + * This controls gossmap_chan_set(chan, h); */ + if (was_set || all_changed) + chan->cupdate_off[h] = 0xFFFFFFFF; + else + chan->cupdate_off[h] = 0; } } } @@ -980,19 +1080,17 @@ void gossmap_remove_localmods(struct gossmap *map, const struct localmod *mod = &localmods->mods[i]; struct gossmap_chan *chan = gossmap_find_chan(map, &mod->scid); + /* If there was no channel, ignore */ + if (!chan) + continue; + /* If that's a local channel, remove it now. */ if (chan->cann_off >= map->map_size) { gossmap_remove_chan(map, chan); } else { - /* Restore (keep nodeidx). */ + /* Restore. */ for (size_t h = 0; h < 2; h++) { - u32 nodeidx; - if (!mod->updates_set[h]) - continue; - - nodeidx = chan->half[h].nodeidx; chan->half[h] = mod->orig[h]; - chan->half[h].nodeidx = nodeidx; chan->cupdate_off[h] = mod->orig_cupdate_off[h]; } } @@ -1135,34 +1233,39 @@ bool gossmap_chan_is_dying(const struct gossmap *map, return ghdr.flags & CPU_TO_BE16(GOSSIP_STORE_DYING_BIT); } -bool gossmap_chan_get_capacity(const struct gossmap *map, - const struct gossmap_chan *c, - struct amount_sat *amount) +struct amount_msat gossmap_chan_get_capacity(const struct gossmap *map, + const struct gossmap_chan *c) { struct gossip_hdr ghdr; size_t off; u16 type; + struct amount_sat sat; + struct amount_msat msat; - /* Fail for local channels */ - if (gossmap_chan_is_localmod(map, c)) - return false; + if (gossmap_chan_is_localmod(map, c)) { + /* Amount is *before* c->cann_off */ + off = c->cann_off - sizeof(u64); + goto get_amount; + } /* Skip over this record to next; expect a gossip_store_channel_amount */ off = c->cann_off - sizeof(ghdr); map_copy(map, off, &ghdr, sizeof(ghdr)); off += sizeof(ghdr) + be16_to_cpu(ghdr.len); - /* Partial write, this can happen. */ - if (off + sizeof(ghdr) + sizeof(u16) + sizeof(u64) > map->map_size) - return false; - - /* Get type of next field. */ + /* We don't allow loading a channel unless it has capacity field! */ type = map_be16(map, off + sizeof(ghdr)); - if (type != WIRE_GOSSIP_STORE_CHANNEL_AMOUNT) - return false; + assert(type == WIRE_GOSSIP_STORE_CHANNEL_AMOUNT); - *amount = amount_sat(map_be64(map, off + sizeof(ghdr) + sizeof(be16))); - return true; + off += sizeof(ghdr) + sizeof(be16); + +get_amount: + /* Shouldn't overflow */ + sat = amount_sat(map_be64(map, off)); + if (!amount_sat_to_msat(&msat, sat)) + errx(1, "invalid capacity %s", fmt_amount_sat(tmpctx, sat)); + + return msat; } struct gossmap_chan *gossmap_nth_chan(const struct gossmap *map, diff --git a/common/gossmap.h b/common/gossmap.h index ade384f16465..6a4a5308a924 100644 --- a/common/gossmap.h +++ b/common/gossmap.h @@ -94,22 +94,33 @@ bool gossmap_local_addchan(struct gossmap_localmods *localmods, const struct node_id *n1, const struct node_id *n2, struct short_channel_id scid, + struct amount_msat capacity, const u8 *features) NON_NULL_ARGS(1,2,3); /* Create a local-only channel_update: can apply to lcoal-only or * normal channels. Returns false if amounts don't fit in our - * internal representation (implies channel unusable anyway). */ + * internal representation (implies channel unusable anyway). Any + * NULL arguments mean "leave as is". */ bool gossmap_local_updatechan(struct gossmap_localmods *localmods, - struct short_channel_id scid, - struct amount_msat htlc_min, - struct amount_msat htlc_max, - u32 base_fee, - u32 proportional_fee, - u16 delay, - bool enabled, - int dir) - NO_NULL_ARGS; + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay); + +/* Convenience version which sets everything (older API) */ +bool gossmap_local_setchan(struct gossmap_localmods *localmods, + struct short_channel_id scid, + struct amount_msat htlc_min, + struct amount_msat htlc_max, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + bool enabled, + int dir); /* Apply localmods to this map */ void gossmap_apply_localmods(struct gossmap *map, @@ -161,10 +172,9 @@ static inline bool gossmap_chan_set(const struct gossmap_chan *chan, int dir) return chan->cupdate_off[dir] != 0; } -/* Return capacity if it's known (fails on a local mod) */ -bool gossmap_chan_get_capacity(const struct gossmap *map, - const struct gossmap_chan *c, - struct amount_sat *amount); +/* Return capacity (in msat). */ +struct amount_msat gossmap_chan_get_capacity(const struct gossmap *map, + const struct gossmap_chan *c); /* Get the announcement msg which created this chan (NULL for localmods) */ u8 *gossmap_chan_get_announce(const tal_t *ctx, diff --git a/common/gossmods_listpeerchannels.c b/common/gossmods_listpeerchannels.c index 73864241302e..df0b97133132 100644 --- a/common/gossmods_listpeerchannels.c +++ b/common/gossmods_listpeerchannels.c @@ -9,12 +9,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, @@ -26,14 +27,15 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, max = spendable; /* FIXME: features? */ - gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); - - gossmap_local_updatechan(mods, scidd->scid, min, max, - fee_base.millisatoshis, /* Raw: gossmap */ - fee_proportional, - cltv_delta, - enabled, - scidd->dir); + gossmap_local_addchan(mods, self, peer, scidd->scid, capacity_msat, + NULL); + + gossmap_local_updatechan(mods, scidd, + &enabled, + &min, &max, + &fee_base, + &fee_proportional, + &cltv_delta); } struct gossmap_localmods * @@ -46,12 +48,13 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat sr_able, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, @@ -68,7 +71,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, struct short_channel_id alias; bool enabled; struct node_id dst; - struct amount_msat spendable, receivable, fee_base[NUM_SIDES], htlc_min[NUM_SIDES], htlc_max[NUM_SIDES]; + struct amount_msat capacity_msat, spendable, receivable, fee_base[NUM_SIDES], htlc_min[NUM_SIDES], htlc_max[NUM_SIDES]; u32 fee_proportional[NUM_SIDES], cltv_delta[NUM_SIDES]; const char *state, *err; @@ -87,6 +90,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, "peer_connected:%," "state:%," "peer_id:%," + "total_msat?:%," "updates?:{" "local" ":{fee_base_msat:%," @@ -108,6 +112,7 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, JSON_SCAN(json_to_bool, &enabled), JSON_SCAN_TAL(tmpctx, json_strdup, &state), JSON_SCAN(json_to_node_id, &dst), + JSON_SCAN(json_to_msat, &capacity_msat), JSON_SCAN(json_to_msat, &fee_base[LOCAL]), JSON_SCAN(json_to_u32, &fee_proportional[LOCAL]), JSON_SCAN(json_to_msat, &htlc_min[LOCAL]), @@ -148,7 +153,8 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, } /* We add both directions */ - cb(mods, self, &dst, &scidd, htlc_min[LOCAL], htlc_max[LOCAL], + cb(mods, self, &dst, &scidd, capacity_msat, + htlc_min[LOCAL], htlc_max[LOCAL], spendable, fee_base[LOCAL], fee_proportional[LOCAL], cltv_delta[LOCAL], enabled, buf, channel, cbarg); @@ -158,7 +164,8 @@ gossmods_from_listpeerchannels_(const tal_t *ctx, scidd.dir = !scidd.dir; - cb(mods, self, &dst, &scidd, htlc_min[REMOTE], htlc_max[REMOTE], + cb(mods, self, &dst, &scidd, capacity_msat, + htlc_min[REMOTE], htlc_max[REMOTE], receivable, fee_base[REMOTE], fee_proportional[REMOTE], cltv_delta[REMOTE], enabled, buf, channel, cbarg); } diff --git a/common/gossmods_listpeerchannels.h b/common/gossmods_listpeerchannels.h index a9847c882645..22eafdd3d143 100644 --- a/common/gossmods_listpeerchannels.h +++ b/common/gossmods_listpeerchannels.h @@ -30,12 +30,13 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, const struct node_id *self_, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf_, const jsmntok_t *chantok, @@ -53,8 +54,9 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx, struct amount_msat, \ struct amount_msat, \ struct amount_msat, \ + struct amount_msat, \ u32, \ - u32, \ + u16, \ bool, \ const char *, \ const jsmntok_t *), \ @@ -65,12 +67,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, diff --git a/common/json_param.c b/common/json_param.c index 3a25e1684b2f..216451271803 100644 --- a/common/json_param.c +++ b/common/json_param.c @@ -642,6 +642,19 @@ struct command_result *param_short_channel_id(struct command *cmd, "should be a short_channel_id of form NxNxN"); } +struct command_result *param_short_channel_id_dir(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct short_channel_id_dir **scidd) +{ + *scidd = tal(cmd, struct short_channel_id_dir); + if (!json_to_short_channel_id_dir(buffer, tok, *scidd)) + return command_fail_badparam(cmd, name, buffer, tok, + "should be a short_channel_id_dir of form NxNxN/N"); + return NULL; +} + struct command_result *param_secret(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, struct secret **secret) diff --git a/common/json_param.h b/common/json_param.h index 83e4f3b3a70a..07db22f3d2cf 100644 --- a/common/json_param.h +++ b/common/json_param.h @@ -275,6 +275,12 @@ struct command_result *param_short_channel_id(struct command *cmd, const jsmntok_t *tok, struct short_channel_id **scid); +struct command_result *param_short_channel_id_dir(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct short_channel_id_dir **scidd); + /* Ignore the token. Not usually used. */ struct command_result *param_ignore(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, diff --git a/common/json_parse.c b/common/json_parse.c index 639c35b2e70b..1d272b1f0c6c 100644 --- a/common/json_parse.c +++ b/common/json_parse.c @@ -109,6 +109,17 @@ bool json_to_int(const char *buffer, const jsmntok_t *tok, int *num) return true; } +bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) +{ + u32 v32; + if (!json_to_u32(buffer, tok, &v32)) + return false; + if (v32 != 0 && v32 != 1) + return false; + *num = v32; + return true; +} + bool json_to_jsonrpc_errcode(const char *buffer, const jsmntok_t *tok, enum jsonrpc_errcode *errcode) { @@ -577,7 +588,6 @@ bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, struct short_channel_id_dir *scidd) { jsmntok_t scidtok, numtok; - u32 dir; if (!split_tok(buffer, tok, '/', &scidtok, &numtok)) return false; @@ -585,10 +595,9 @@ bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, if (!json_to_short_channel_id(buffer, &scidtok, &scidd->scid)) return false; - if (!json_to_u32(buffer, &numtok, &dir) || (dir > 1)) + if (!json_to_zero_or_one(buffer, &numtok, &scidd->dir)) return false; - scidd->dir = dir; return true; } diff --git a/common/json_parse.h b/common/json_parse.h index 7eca7284c2d6..4bd23cbddcd7 100644 --- a/common/json_parse.h +++ b/common/json_parse.h @@ -31,6 +31,9 @@ u8 *json_tok_bin_from_hex(const tal_t *ctx, const char *buffer, const jsmntok_t bool json_to_number(const char *buffer, const jsmntok_t *tok, unsigned int *num); +/* Extract 0/1 from this */ +bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num); + /* Extract signed 64 bit integer from this (may be a string, or a number literal) */ bool json_to_s64(const char *buffer, const jsmntok_t *tok, s64 *num); @@ -86,6 +89,10 @@ bool json_to_bitcoin_amount(const char *buffer, const jsmntok_t *tok, bool json_to_short_channel_id(const char *buffer, const jsmntok_t *tok, struct short_channel_id *scid); +/* Extract a short_channel_id_dir from this */ +bool json_to_short_channel_id_dir(const char *buffer, const jsmntok_t *tok, + struct short_channel_id_dir *scidd); + /* Extract a satoshis amount from this */ bool json_to_sat(const char *buffer, const jsmntok_t *tok, struct amount_sat *sat); diff --git a/common/test/run-gossmap_canned.c b/common/test/run-gossmap_canned.c index 1b267f65737d..e7f0662284cd 100644 --- a/common/test/run-gossmap_canned.c +++ b/common/test/run-gossmap_canned.c @@ -314,7 +314,7 @@ int main(int argc, char *argv[]) struct gossmap *map; struct node_id l1, l2; struct short_channel_id scid12; - struct amount_sat capacity; + struct amount_msat capacity; u32 timestamp, fee_base_msat, fee_proportional_millionths; u8 message_flags, channel_flags; struct amount_msat htlc_minimum_msat, htlc_maximum_msat; @@ -337,9 +337,8 @@ int main(int argc, char *argv[]) assert(short_channel_id_from_str("110x1x0", 7, &scid12)); assert(gossmap_find_chan(map, &scid12)); assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid12))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); gossmap_chan_get_update_details(map, gossmap_find_chan(map, &scid12), 0, diff --git a/common/test/run-gossmap_local.c b/common/test/run-gossmap_local.c index 4debfc47c418..b583495c0439 100644 --- a/common/test/run-gossmap_local.c +++ b/common/test/run-gossmap_local.c @@ -333,10 +333,10 @@ int main(int argc, char *argv[]) char *gossfile; struct gossmap *map; struct node_id l1, l2, l3, l4; - struct short_channel_id scid23, scid12, scid_local; + struct short_channel_id scid23, scid12, scid_local, scid_nonexisting; struct gossmap_chan *chan; struct gossmap_localmods *mods; - struct amount_sat capacity; + struct amount_msat capacity; u32 timestamp, fee_base_msat, fee_proportional_millionths; u8 message_flags, channel_flags; struct amount_msat htlc_minimum_msat, htlc_maximum_msat; @@ -365,12 +365,10 @@ int main(int argc, char *argv[]) assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid23))); assert(gossmap_find_chan(map, &scid12)); assert(!gossmap_chan_is_localmod(map, gossmap_find_chan(map, &scid12))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid23), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); - assert(gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12), - &capacity)); - assert(amount_sat_eq(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid23)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); + capacity = gossmap_chan_get_capacity(map, gossmap_find_chan(map, &scid12)); + assert(amount_msat_eq_sat(capacity, AMOUNT_SAT(1000000))); gossmap_chan_get_update_details(map, gossmap_find_chan(map, &scid23), 0, @@ -467,7 +465,7 @@ int main(int argc, char *argv[]) assert(node_id_from_hexstr("0382ce59ebf18be7d84677c2e35f23294b9992ceca95491fcf8a56c6cb2d9de199", 66, &l4)); assert(short_channel_id_from_str("111x1x1", 7, &scid_local)); - assert(gossmap_local_addchan(mods, &l1, &l4, scid_local, NULL)); + assert(gossmap_local_addchan(mods, &l1, &l4, scid_local, AMOUNT_MSAT(100000), NULL)); /* Apply changes, check they work. */ gossmap_apply_localmods(map, mods); @@ -478,6 +476,10 @@ int main(int argc, char *argv[]) assert(!gossmap_chan_set(chan, 0)); assert(!gossmap_chan_set(chan, 1)); + /* Capacity is correct */ + assert(amount_msat_eq(gossmap_chan_get_capacity(map, chan), + AMOUNT_MSAT(100000))); + /* Remove, no longer can find. */ gossmap_remove_localmods(map, mods); @@ -485,18 +487,25 @@ int main(int argc, char *argv[]) assert(!gossmap_find_node(map, &l4)); /* Now update it both local, and an existing one. */ - gossmap_local_updatechan(mods, scid_local, - AMOUNT_MSAT(1), - AMOUNT_MSAT(100000), - 2, 3, 4, true, 0); + gossmap_local_setchan(mods, scid_local, + AMOUNT_MSAT(1), + AMOUNT_MSAT(100000), + AMOUNT_MSAT(2), 3, 4, true, 0); /* Adding an existing channel is a noop. */ - assert(gossmap_local_addchan(mods, &l2, &l3, scid23, NULL)); + assert(gossmap_local_addchan(mods, &l2, &l3, scid23, AMOUNT_MSAT(100000), NULL)); + + gossmap_local_setchan(mods, scid23, + AMOUNT_MSAT(99), + AMOUNT_MSAT(100), + AMOUNT_MSAT(101), 102, 103, true, 0); - gossmap_local_updatechan(mods, scid23, - AMOUNT_MSAT(99), - AMOUNT_MSAT(100), - 101, 102, 103, true, 0); + /* We can "update" a channel which doesn't exist, and it's a noop */ + scid_nonexisting.u64 = 1; + gossmap_local_setchan(mods, scid_nonexisting, + AMOUNT_MSAT(1), + AMOUNT_MSAT(100000), + AMOUNT_MSAT(2), 3, 4, false, 0); gossmap_apply_localmods(map, mods); chan = gossmap_find_chan(map, &scid_local); @@ -510,6 +519,8 @@ int main(int argc, char *argv[]) assert(chan->half[0].proportional_fee == 3); assert(chan->half[0].delay == 4); + assert(!gossmap_find_chan(map, &scid_nonexisting)); + chan = gossmap_find_chan(map, &scid23); assert(chan->half[0].enabled); assert(chan->half[0].htlc_min == u64_to_fp16(99, false)); diff --git a/common/test/run-json_filter.c b/common/test/run-json_filter.c index d4e68801a637..54138cbe6999 100644 --- a/common/test/run-json_filter.c +++ b/common/test/run-json_filter.c @@ -120,6 +120,10 @@ bool json_to_pubkey(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct short_channel_id *scid UNNEEDED) { fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id_dir */ +bool json_to_short_channel_id_dir(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id_dir *scidd UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_to_txid */ bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct bitcoin_txid *txid UNNEEDED) diff --git a/common/test/run-json_remove.c b/common/test/run-json_remove.c index 93c844143415..f1c16a28cbe4 100644 --- a/common/test/run-json_remove.c +++ b/common/test/run-json_remove.c @@ -146,6 +146,10 @@ bool json_to_pubkey(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct short_channel_id *scid UNNEEDED) { fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id_dir */ +bool json_to_short_channel_id_dir(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id_dir *scidd UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id_dir called!\n"); abort(); } /* Generated stub for json_to_txid */ bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct bitcoin_txid *txid UNNEEDED) diff --git a/common/test/run-route-infloop.c b/common/test/run-route-infloop.c index f4c3f9071f7a..bec98e232486 100644 --- a/common/test/run-route-infloop.c +++ b/common/test/run-route-infloop.c @@ -56,15 +56,13 @@ static double capacity_bias(const struct gossmap *map, int dir, struct amount_msat amount) { - struct amount_sat capacity; + struct amount_msat msat; u64 amtmsat = amount.millisatoshis; /* Raw: lengthy math */ double capmsat; - /* Can fail in theory if gossmap changed underneath. */ - if (!gossmap_chan_get_capacity(map, c, &capacity)) - return 0; + msat = gossmap_chan_get_capacity(map, c); - capmsat = (double)capacity.satoshis * 1000; /* Raw: lengthy math */ + capmsat = (double)msat.millisatoshis; /* Raw: log */ return -log((capmsat + 1 - amtmsat) / (capmsat + 1)); } @@ -138,17 +136,18 @@ int main(int argc, char *argv[]) /* We overlay our own channels as zero fee & delay, since we don't pay fees */ struct gossmap_localmods *localmods = gossmap_localmods_new(gossmap); for (size_t i = 0; i < me->num_chans; i++) { - int dir; - struct short_channel_id scid; - struct gossmap_chan *c = gossmap_nth_chan(gossmap, me, i, &dir); + struct short_channel_id_dir scidd; + const struct amount_msat base_fee = AMOUNT_MSAT(0); + const u32 proportional_fee = 0; + struct gossmap_chan *c = gossmap_nth_chan(gossmap, me, i, &scidd.dir); - if (!c->half[dir].enabled) + if (!c->half[scidd.dir].enabled) continue; - scid = gossmap_chan_scid(gossmap, c); - assert(gossmap_local_updatechan(localmods, scid, - amount_msat(fp16_to_u64(c->half[dir].htlc_min)), - amount_msat(fp16_to_u64(c->half[dir].htlc_max)), - 0, 0, 0, true, dir)); + scidd.scid = gossmap_chan_scid(gossmap, c); + assert(gossmap_local_updatechan(localmods, &scidd, + NULL, NULL, NULL, + &base_fee, &proportional_fee, + NULL)); } gossmap_apply_localmods(gossmap, localmods); @@ -173,13 +172,13 @@ int main(int argc, char *argv[]) } else { double probability = 1; for (size_t j = 0; j < tal_count(r); j++) { - struct amount_sat capacity_sat; + struct amount_msat msat; u64 cap_msat; struct gossmap_chan *c = gossmap_find_chan(gossmap, &r[j].scid); assert(c); - assert(gossmap_chan_get_capacity(gossmap, c, &capacity_sat)); + msat = gossmap_chan_get_capacity(gossmap, c); - cap_msat = capacity_sat.satoshis * 1000; + cap_msat = msat.millisatoshis; /* Assume linear distribution, implying probability depends on * amount we would leave in channel */ assert(cap_msat >= r[0].amount.millisatoshis); diff --git a/contrib/keys/sfarooqui.txt b/contrib/keys/sfarooqui.txt index 7e6d51c188d3..04125380495f 100644 --- a/contrib/keys/sfarooqui.txt +++ b/contrib/keys/sfarooqui.txt @@ -39,3 +39,44 @@ dOm4LuC5X/Ygx+zc5s8aTEYRGjlGYOJczZy6a5azaYUZMbiNSc4pEVOXJgUnbgC+ /JD0CXztohXlKEQYwx0PpModgSRhMFmDDtU8 =meZK -----END PGP PUBLIC KEY BLOCK----- +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGTw6DcBDAC2MNGWMfZ/Ufakd6Ruz9P34PqiT+uSSDPTU7HwGQGBBcpIKbc1 +egv3Tk/tU/ZfGmn1BgZRP1IxmajLkp/cub2Xlils1ec8+gBSIhYi/Rl0KcZbjiNn +9/gLyLFuCfQySVPzRAcdvt0zPTK+zvd9ActZhEC0FriFOowloiZbqT6QpRttx8Sv +4hTRm3vj3gMKB1eP5/QWK8zdpKU5T4YflToVN0nFtaaX57ZeWBAE1SmqbodZ7bYq +9tI5uwFdw/idqIJHwVxaR5f3YQ6q9fk0X7ldWjShb/P2fGTvNTlJhzQ0Z3N+wb/X +duYUu19SWFnj05mhBN6CrFiPKUtCm8hbBOiaBw6rQHpLbvOQ/vVonvucXoAcqQaG +MfaacbhtLAhLmnHK9Y1J0dv9sDxaSjrcztrOWKYZmVHQeBnWmYbFhXFbFtoVPQP1 +M1MiCh/oM2SsKk3qOzGNtGnqKJCRsIbA2cCilef5EOWX37GSHv/fPvf2cnjuN8o9 +R6h/ZpfIqLE96VkAEQEAAbQtU2hhaGFuYSBGYXJvb3F1aSA8c2hhaGFuYS5mYXJv +b3F1aUBnbWFpbC5jb20+iQHUBBMBCgA+FiEEtWtEU9qMbff8m8/L3KQLcSjaYqgF +AmTw6DcCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ3KQLcSja +Yqhe/gv+IWkbMd0plNznBLVxO3h2AvvQ1B2kN704dMf98rTGA4Fo3wwKUBFb3zu7 +f7t4IKUMm45f3/QGJ4bxMZiq7o3LpepBbnKmhMZM3op6OAgH1mDTGDMyhmLOCCRG +FQMcBbGDI4gfB9QQtUKTyvC+uwuXlou+mR72AsqAmaPHbkjl3UkZtwOM0wEWgzCh +D2h8T8iVDex9j+IU3fJWMyMesJ1WIDqV3AJa/CuxD6myNCF5kJ9cZZGlGD67ICPW +GyeiAz0QTCcPr0tLNfArEuOA0iNpBt6akaPFyuNcYbnOVNfzgbdgZUiFbmB/AEsi +ODbmLcEMRGwkZ7G6/udAkehNO1ER32RVKTqMxcqHQyrCl9zMpXEHDBRy6tgSqd7B +9aQTlu66asvDcrJfLGy7cB/Yg09+5T8eGf/rBgPn2z6LCmKPlozsAObQNKxGPCwv +Gcr5UThCJGl11qTDt8S8Ty3JVMOXKn1x0emIUWYR9C9fuKTXtFNuTZD5EVM8LYOC +q6iWFh77uQGNBGTw6DcBDACflWHK6bsvst16EMqR7YrSRcETj3l+Fz+ztNdYz0Jy +zdpNgOjW1G9Xp/YF0JbAmnmqlFPLnY44gWITfbINN9dGC+RRAmjjrFHLeeYoY2ox +xN0GiSx6UWl3kXxhMowHSZnF3vwrupaoRFbkY/q3p8Q7uNzfEeiUj5n5Gcr58FCQ +SrztlJP+brXd+xQc9nL2vQREZIqisOJ2nwxlMEzCimreTJIXpOMc/A3cp4gVNeX8 +GnwmXXTSEUEV1MvlQWZFAuvdR/iCcUmktuw3bJAKBzeE9hqaOxAznuV69PFZC66k +Yfav5X10FLXDM5hVNgqZjnq4m68y4U0tzJoW4BXPGiG5qofK1HYuGn2oqPccJEso +8ibTTiQcv7i8PGlDyH184PBVQGSQyJ2w8h3ytHWAw+3yI7+2vNTmWXqztwfrvveK +GA98xNGSgTK78V1/p9p1TwbyAuNKNqyK5bVadIi7LOqDfbJPvLry/63WJozDJsaR +eWMcGFybCVP4HZapNOSjBisAEQEAAYkBvAQYAQoAJhYhBLVrRFPajG33/JvPy9yk +C3Eo2mKoBQJk8Og3AhsMBQkDwmcAAAoJENykC3Eo2mKoVCYL/1NRDd3zkd03cMum +RXcApsY4KiErw+fd146lTJ8p7RLNJrPERtwCCKYxKK0vSBRIMtwRFOM2mRQMsenI +1nu4n2KUEAF7aul2fhtKIG7Hs6Nzbklvt6Hj9Jtga9JrfGk5hPQ2ndg8qG0XU5JT +NqVLT94P5m3hSQD9Y9O2uoblBc3YmIkaEAEHirJd2u92+sneAZ4VeaUMnhYZKyJf +H0ydP+iDyYWWQjg5R9rzIfgL6kvORyUuwrbmGaoqcEHH649Wj9/4jMNvCTKrK0G1 +hT/IDj7U3R7Admb1F4wb47oHZyO1SFdZ6fp3F6rc66587a/KlylDAHT/Pfpx6Mlt +tyVNOM1nSorzYhnomdaQ7WzIf0zxHbTLLv9seGbV/kbGIWS9rK0Q6u4vjlICtRro +8L9EEq40A+zMrJsav31bw+jdwVNe4kRVmrchDzbZl4aqiHkfhUJt1UcBRNyiKLji +jEWDGbNfzrtZQbMoe2UMpld9IePwtGMpxAEUWvzLx8osqbGlkw== +=gFP5 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index a9b6d1572ac4..66058efe5f9b 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -265,7 +265,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + "The **askrene-create-channel** RPC command tells askrene create a channel in the given layer. To actually populate the channel use *askrene-update-channel* in each direction." ], "request": { "required": [ @@ -273,12 +273,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_min", - "htlc_max", - "base_fee", - "proportional_fee", - "delay" + "capacity_msat" ], "properties": { "layer": { @@ -308,37 +303,8 @@ "capacity_msat": { "type": "msat", "description": [ - "The capacity (onchain size) of the channel." - ] - }, - "htlc_min": { - "type": "msat", - "description": [ - "The minimum value allowed in this direction." - ] - }, - "htlc_max": { - "type": "msat", - "description": [ - "The maximum value allowed in this direction." - ] - }, - "base_fee": { - "type": "msat", - "description": [ - "The base fee to apply to use the channel in this direction." - ] - }, - "proportional_fee": { - "type": "u32", - "description": [ - "The proportional fee (in parts per million) to apply to use the channel in this direction." - ] - }, - "delay": { - "type": "u16", - "description": [ - "The CLTV delay required for this direction." + "The capacity (onchain size) of the channel.", + "NOTE: this is in millisatoshis!" ] } } @@ -350,6 +316,208 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", + "lightning-askrene-update-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-create-layer.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-layer", + "title": "Command to create a new layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-layer** RPC command tells askrene to create a new, empty layer. This layer can then be populated with `askrene-create-channel` and `askrene-inform-channel`, and be used in `getroutes`." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to create." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "created_channels", + "channel_updates", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-remove-layer(7)", + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" @@ -419,13 +587,14 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + "The **askrene-inform-channel** RPC command tells askrene about channels we used so it can update its capacity estimates. For most accuracy, you should remove your own reservations before calling this. It can be applied whether the current channel exists or not." ], "request": { "required": [ "layer", - "short_channel_id", - "direction" + "short_channel_id_dir", + "amount_msat", + "inform" ], "properties": { "layer": { @@ -434,74 +603,62 @@ "The name of the layer to apply this change to." ] }, - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The short channel id to apply this change to." + "The short channel id and direction to apply this change to." ] }, - "direction": { - "type": "u32", - "description": [ - "The direction to apply this change to." - ] - }, - "minimum_msat": { + "amount_msat": { "type": "msat", "description": [ - "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "The amount we used on the channel" ] }, - "maximum_msat": { - "type": "msat", + "inform": { + "type": "string", + "enum": [ + "constrained", + "unconstrained", + "succeeded" + ], "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "Whether this payment passed (implying capacity of at least that amount), failed (implying maximum capacity of one msat less), or succeeded (implying capacity has been reduced in this direction)" ] } } }, "response": { "required": [ - "constraint" + "constraints" ], "properties": { - "constraint": { - "type": "object", - "required": [ - "short_channel_id", - "direction", - "timestamp" - ], - "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The *short_channel_id* specified." - ] - }, - "direction": { - "type": "u32", - "description": [ - "The *direction* specified." - ] - }, - "timestamp": { - "type": "u64", - "description": [ - "The UNIX time (seconds since 1970) this was created." - ] - }, - "maximum_msat": { - "type": "msat", - "description": [ - "The *minimum_msat* (if specified)" - ] - }, - "minimum_msat": { - "type": "msat", - "description": [ - "The *maximum_msat* (if specified)" - ] + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id and direction" + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass." + ] + } } } } @@ -557,6 +714,7 @@ "layer", "disabled_nodes", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -575,6 +733,15 @@ ] } }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, "created_channels": { "type": "array", "items": { @@ -583,12 +750,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -614,7 +776,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ @@ -653,32 +826,25 @@ "items": { "type": "object", "required": [ - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The short channel id." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The direction." + "The short channel id and direction" ] }, "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } @@ -703,6 +869,117 @@ "Main web site: " ] }, + "lightning-askrene-listreservations.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listreservations", + "title": "Command to display information about reservations (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reservations** RPC command reports outstanding reservations made with `askrene-reserve`, mainly for debugging." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "reservations" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id_dir", + "amount_msat", + "age_in_seconds", + "command_id" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction that is reserved." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount reserved." + ] + }, + "age_in_seconds": { + "type": "u64", + "description": [ + "The age of this reservation." + ] + }, + "command_id": { + "type": "string", + "description": [ + "The JSON id of the command used to make the reservation." + ] + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-remove-layer.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-remove-layer", + "title": "Command to destroy a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-remove-layer** RPC command tells askrene to forget a layer." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to remove." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-askrene-create-layer(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-askrene-reserve.json": { "$schema": "../rpc-schema-draft.json", "type": "object", @@ -712,7 +989,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call *askrene-unreserve* after the attempt has completed (and before calling *askrene-inform*).", "", "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." ], @@ -727,21 +1004,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -762,11 +1032,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-unreserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." @@ -799,21 +1065,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -834,11 +1093,83 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-reserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-update-channel.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-update-channel", + "title": "Command to manipulate channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-update-channel** RPC command overrides updates for an existing channel when the layer is applied." + ], + "request": { + "required": [ + "layer", + "short_channel_id_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to apply the change to." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "cltv_expiry_delta": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)" ], "author": [ "Rusty Russell <> is mainly responsible." @@ -13099,6 +13430,54 @@ } ] }, + "lightning-getemergencyrecoverdata.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "getemergencyrecoverdata", + "title": "Command to fetch data from the emergency.recover file", + "description": [ + "The **getemergencyrecoverdata** RPC command is used to fetch data from the emergency.recover file, which contains encrypted data." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "filedata" + ], + "properties": { + "filedata": { + "type": "hex", + "description": [ + "The raw, hex-encoded, emergency.recover file" + ] + } + } + }, + "author": [ + "Aditya <> is mainly responsible." + ], + "see_also": [ + "lightning-getsharedsecret(7)" + ], + "resources": [ + "Main web site: " + ], + "examples": [ + { + "request": { + "id": "example:getemergencyrecoverdata#1", + "method": "getemergencyrecoverdata", + "params": {} + }, + "response": { + "filedata": "5b3142fa0dd1115c29654b44780dcd9cf56cd53f9168061e964b39f3ce596962594b25660cba5d90ef07cfccbe1620f378ef284c7d1afed49d" + } + } + ] + }, "lightning-getinfo.json": { "$schema": "../rpc-schema-draft.json", "type": "object", @@ -14375,23 +14754,16 @@ "type": "object", "additionalProperties": false, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "next_node_id", "amount_msat", "delay" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/contrib/pyln-grpc-proto/pyln/grpc/primitives_pb2.py b/contrib/pyln-grpc-proto/pyln/grpc/primitives_pb2.py index 955eb91afcdb..29aa74c044d0 100644 --- a/contrib/pyln-grpc-proto/pyln/grpc/primitives_pb2.py +++ b/contrib/pyln-grpc-proto/pyln/grpc/primitives_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10primitives.proto\x12\x03\x63ln\"\x16\n\x06\x41mount\x12\x0c\n\x04msat\x18\x01 \x01(\x04\"D\n\x0b\x41mountOrAll\x12\x1d\n\x06\x61mount\x18\x01 \x01(\x0b\x32\x0b.cln.AmountH\x00\x12\r\n\x03\x61ll\x18\x02 \x01(\x08H\x00\x42\x07\n\x05value\"D\n\x0b\x41mountOrAny\x12\x1d\n\x06\x61mount\x18\x01 \x01(\x0b\x32\x0b.cln.AmountH\x00\x12\r\n\x03\x61ny\x18\x02 \x01(\x08H\x00\x42\x07\n\x05value\"\x19\n\x17\x43hannelStateChangeCause\"(\n\x08Outpoint\x12\x0c\n\x04txid\x18\x01 \x01(\x0c\x12\x0e\n\x06outnum\x18\x02 \x01(\r\"h\n\x07\x46\x65\x65rate\x12\x0e\n\x04slow\x18\x01 \x01(\x08H\x00\x12\x10\n\x06normal\x18\x02 \x01(\x08H\x00\x12\x10\n\x06urgent\x18\x03 \x01(\x08H\x00\x12\x0f\n\x05perkb\x18\x04 \x01(\rH\x00\x12\x0f\n\x05perkw\x18\x05 \x01(\rH\x00\x42\x07\n\x05style\":\n\nOutputDesc\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x1b\n\x06\x61mount\x18\x02 \x01(\x0b\x32\x0b.cln.Amount\"h\n\x08RouteHop\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x0c\n\x04scid\x18\x02 \x01(\t\x12\x1c\n\x07\x66\x65\x65\x62\x61se\x18\x03 \x01(\x0b\x32\x0b.cln.Amount\x12\x0f\n\x07\x66\x65\x65prop\x18\x04 \x01(\r\x12\x13\n\x0b\x65xpirydelta\x18\x05 \x01(\r\"(\n\tRoutehint\x12\x1b\n\x04hops\x18\x01 \x03(\x0b\x32\r.cln.RouteHop\".\n\rRoutehintList\x12\x1d\n\x05hints\x18\x02 \x03(\x0b\x32\x0e.cln.Routehint\"\x9e\x01\n\x0e\x44\x65\x63odeRouteHop\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x18\n\x10short_channel_id\x18\x02 \x01(\t\x12\"\n\rfee_base_msat\x18\x03 \x01(\x0b\x32\x0b.cln.Amount\x12#\n\x1b\x66\x65\x65_proportional_millionths\x18\x04 \x01(\r\x12\x19\n\x11\x63ltv_expiry_delta\x18\x05 \x01(\r\"4\n\x0f\x44\x65\x63odeRoutehint\x12!\n\x04hops\x18\x01 \x03(\x0b\x32\x13.cln.DecodeRouteHop\":\n\x13\x44\x65\x63odeRoutehintList\x12#\n\x05hints\x18\x02 \x03(\x0b\x32\x14.cln.DecodeRoutehint\"\'\n\x08TlvEntry\x12\x0c\n\x04type\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x0c\"+\n\tTlvStream\x12\x1e\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\r.cln.TlvEntry*$\n\x0b\x43hannelSide\x12\t\n\x05LOCAL\x10\x00\x12\n\n\x06REMOTE\x10\x01*\xa0\x02\n\x0c\x43hannelState\x12\x0c\n\x08Openingd\x10\x00\x12\x1a\n\x16\x43hanneldAwaitingLockin\x10\x01\x12\x12\n\x0e\x43hanneldNormal\x10\x02\x12\x18\n\x14\x43hanneldShuttingDown\x10\x03\x12\x17\n\x13\x43losingdSigexchange\x10\x04\x12\x14\n\x10\x43losingdComplete\x10\x05\x12\x16\n\x12\x41waitingUnilateral\x10\x06\x12\x14\n\x10\x46undingSpendSeen\x10\x07\x12\x0b\n\x07Onchain\x10\x08\x12\x15\n\x11\x44ualopendOpenInit\x10\t\x12\x1b\n\x17\x44ualopendAwaitingLockin\x10\n\x12\x1a\n\x16\x43hanneldAwaitingSplice\x10\x0b*\xd5\x03\n\tHtlcState\x12\x0f\n\x0bSentAddHtlc\x10\x00\x12\x11\n\rSentAddCommit\x10\x01\x12\x15\n\x11RcvdAddRevocation\x10\x02\x12\x14\n\x10RcvdAddAckCommit\x10\x03\x12\x18\n\x14SentAddAckRevocation\x10\x04\x12\x18\n\x14RcvdAddAckRevocation\x10\x05\x12\x12\n\x0eRcvdRemoveHtlc\x10\x06\x12\x14\n\x10RcvdRemoveCommit\x10\x07\x12\x18\n\x14SentRemoveRevocation\x10\x08\x12\x17\n\x13SentRemoveAckCommit\x10\t\x12\x1b\n\x17RcvdRemoveAckRevocation\x10\n\x12\x0f\n\x0bRcvdAddHtlc\x10\x0b\x12\x11\n\rRcvdAddCommit\x10\x0c\x12\x15\n\x11SentAddRevocation\x10\r\x12\x14\n\x10SentAddAckCommit\x10\x0e\x12\x12\n\x0eSentRemoveHtlc\x10\x0f\x12\x14\n\x10SentRemoveCommit\x10\x10\x12\x18\n\x14RcvdRemoveRevocation\x10\x11\x12\x17\n\x13RcvdRemoveAckCommit\x10\x12\x12\x1b\n\x17SentRemoveAckRevocation\x10\x13*\x90\x01\n\x0f\x43hannelTypeName\x12\x19\n\x15static_remotekey_even\x10\x00\x12\x17\n\x13\x61nchor_outputs_even\x10\x01\x12!\n\x1d\x61nchors_zero_fee_htlc_tx_even\x10\x02\x12\x13\n\x0fscid_alias_even\x10\x03\x12\x11\n\rzeroconf_even\x10\x04*\x89\x01\n\x12\x41utocleanSubsystem\x12\x15\n\x11SUCCEEDEDFORWARDS\x10\x00\x12\x12\n\x0e\x46\x41ILEDFORWARDS\x10\x01\x12\x11\n\rSUCCEEDEDPAYS\x10\x02\x12\x0e\n\nFAILEDPAYS\x10\x03\x12\x10\n\x0cPAIDINVOICES\x10\x04\x12\x13\n\x0f\x45XPIREDINVOICES\x10\x05*K\n\x10PluginSubcommand\x12\t\n\x05START\x10\x00\x12\x08\n\x04STOP\x10\x01\x12\n\n\x06RESCAN\x10\x02\x12\x0c\n\x08STARTDIR\x10\x03\x12\x08\n\x04LIST\x10\x04\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10primitives.proto\x12\x03\x63ln\"\x16\n\x06\x41mount\x12\x0c\n\x04msat\x18\x01 \x01(\x04\"D\n\x0b\x41mountOrAll\x12\x1d\n\x06\x61mount\x18\x01 \x01(\x0b\x32\x0b.cln.AmountH\x00\x12\r\n\x03\x61ll\x18\x02 \x01(\x08H\x00\x42\x07\n\x05value\"D\n\x0b\x41mountOrAny\x12\x1d\n\x06\x61mount\x18\x01 \x01(\x0b\x32\x0b.cln.AmountH\x00\x12\r\n\x03\x61ny\x18\x02 \x01(\x08H\x00\x42\x07\n\x05value\"\x19\n\x17\x43hannelStateChangeCause\"(\n\x08Outpoint\x12\x0c\n\x04txid\x18\x01 \x01(\x0c\x12\x0e\n\x06outnum\x18\x02 \x01(\r\"h\n\x07\x46\x65\x65rate\x12\x0e\n\x04slow\x18\x01 \x01(\x08H\x00\x12\x10\n\x06normal\x18\x02 \x01(\x08H\x00\x12\x10\n\x06urgent\x18\x03 \x01(\x08H\x00\x12\x0f\n\x05perkb\x18\x04 \x01(\rH\x00\x12\x0f\n\x05perkw\x18\x05 \x01(\rH\x00\x42\x07\n\x05style\":\n\nOutputDesc\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x1b\n\x06\x61mount\x18\x02 \x01(\x0b\x32\x0b.cln.Amount\"h\n\x08RouteHop\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x0c\n\x04scid\x18\x02 \x01(\t\x12\x1c\n\x07\x66\x65\x65\x62\x61se\x18\x03 \x01(\x0b\x32\x0b.cln.Amount\x12\x0f\n\x07\x66\x65\x65prop\x18\x04 \x01(\r\x12\x13\n\x0b\x65xpirydelta\x18\x05 \x01(\r\"(\n\tRoutehint\x12\x1b\n\x04hops\x18\x01 \x03(\x0b\x32\r.cln.RouteHop\".\n\rRoutehintList\x12\x1d\n\x05hints\x18\x02 \x03(\x0b\x32\x0e.cln.Routehint\"\x9e\x01\n\x0e\x44\x65\x63odeRouteHop\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x18\n\x10short_channel_id\x18\x02 \x01(\t\x12\"\n\rfee_base_msat\x18\x03 \x01(\x0b\x32\x0b.cln.Amount\x12#\n\x1b\x66\x65\x65_proportional_millionths\x18\x04 \x01(\r\x12\x19\n\x11\x63ltv_expiry_delta\x18\x05 \x01(\r\"4\n\x0f\x44\x65\x63odeRoutehint\x12!\n\x04hops\x18\x01 \x03(\x0b\x32\x13.cln.DecodeRouteHop\":\n\x13\x44\x65\x63odeRoutehintList\x12#\n\x05hints\x18\x02 \x03(\x0b\x32\x14.cln.DecodeRoutehint\"\'\n\x08TlvEntry\x12\x0c\n\x04type\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x0c\"+\n\tTlvStream\x12\x1e\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\r.cln.TlvEntry*$\n\x0b\x43hannelSide\x12\t\n\x05LOCAL\x10\x00\x12\n\n\x06REMOTE\x10\x01*\xa0\x02\n\x0c\x43hannelState\x12\x0c\n\x08Openingd\x10\x00\x12\x1a\n\x16\x43hanneldAwaitingLockin\x10\x01\x12\x12\n\x0e\x43hanneldNormal\x10\x02\x12\x18\n\x14\x43hanneldShuttingDown\x10\x03\x12\x17\n\x13\x43losingdSigexchange\x10\x04\x12\x14\n\x10\x43losingdComplete\x10\x05\x12\x16\n\x12\x41waitingUnilateral\x10\x06\x12\x14\n\x10\x46undingSpendSeen\x10\x07\x12\x0b\n\x07Onchain\x10\x08\x12\x15\n\x11\x44ualopendOpenInit\x10\t\x12\x1b\n\x17\x44ualopendAwaitingLockin\x10\n\x12\x1a\n\x16\x43hanneldAwaitingSplice\x10\x0b*\xd5\x03\n\tHtlcState\x12\x0f\n\x0bSentAddHtlc\x10\x00\x12\x11\n\rSentAddCommit\x10\x01\x12\x15\n\x11RcvdAddRevocation\x10\x02\x12\x14\n\x10RcvdAddAckCommit\x10\x03\x12\x18\n\x14SentAddAckRevocation\x10\x04\x12\x18\n\x14RcvdAddAckRevocation\x10\x05\x12\x12\n\x0eRcvdRemoveHtlc\x10\x06\x12\x14\n\x10RcvdRemoveCommit\x10\x07\x12\x18\n\x14SentRemoveRevocation\x10\x08\x12\x17\n\x13SentRemoveAckCommit\x10\t\x12\x1b\n\x17RcvdRemoveAckRevocation\x10\n\x12\x0f\n\x0bRcvdAddHtlc\x10\x0b\x12\x11\n\rRcvdAddCommit\x10\x0c\x12\x15\n\x11SentAddRevocation\x10\r\x12\x14\n\x10SentAddAckCommit\x10\x0e\x12\x12\n\x0eSentRemoveHtlc\x10\x0f\x12\x14\n\x10SentRemoveCommit\x10\x10\x12\x18\n\x14RcvdRemoveRevocation\x10\x11\x12\x17\n\x13RcvdRemoveAckCommit\x10\x12\x12\x1b\n\x17SentRemoveAckRevocation\x10\x13*\xa2\x01\n\x0f\x43hannelTypeName\x12\x19\n\x15static_remotekey_even\x10\x00\x12\x17\n\x13\x61nchor_outputs_even\x10\x01\x12!\n\x1d\x61nchors_zero_fee_htlc_tx_even\x10\x02\x12\x13\n\x0fscid_alias_even\x10\x03\x12\x11\n\rzeroconf_even\x10\x04\x12\x10\n\x0c\x61nchors_even\x10\x05*\x89\x01\n\x12\x41utocleanSubsystem\x12\x15\n\x11SUCCEEDEDFORWARDS\x10\x00\x12\x12\n\x0e\x46\x41ILEDFORWARDS\x10\x01\x12\x11\n\rSUCCEEDEDPAYS\x10\x02\x12\x0e\n\nFAILEDPAYS\x10\x03\x12\x10\n\x0cPAIDINVOICES\x10\x04\x12\x13\n\x0f\x45XPIREDINVOICES\x10\x05*K\n\x10PluginSubcommand\x12\t\n\x05START\x10\x00\x12\x08\n\x04STOP\x10\x01\x12\n\n\x06RESCAN\x10\x02\x12\x0c\n\x08STARTDIR\x10\x03\x12\x08\n\x04LIST\x10\x04\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -28,11 +28,11 @@ _globals['_HTLCSTATE']._serialized_start=1311 _globals['_HTLCSTATE']._serialized_end=1780 _globals['_CHANNELTYPENAME']._serialized_start=1783 - _globals['_CHANNELTYPENAME']._serialized_end=1927 - _globals['_AUTOCLEANSUBSYSTEM']._serialized_start=1930 - _globals['_AUTOCLEANSUBSYSTEM']._serialized_end=2067 - _globals['_PLUGINSUBCOMMAND']._serialized_start=2069 - _globals['_PLUGINSUBCOMMAND']._serialized_end=2144 + _globals['_CHANNELTYPENAME']._serialized_end=1945 + _globals['_AUTOCLEANSUBSYSTEM']._serialized_start=1948 + _globals['_AUTOCLEANSUBSYSTEM']._serialized_end=2085 + _globals['_PLUGINSUBCOMMAND']._serialized_start=2087 + _globals['_PLUGINSUBCOMMAND']._serialized_end=2162 _globals['_AMOUNT']._serialized_start=25 _globals['_AMOUNT']._serialized_end=47 _globals['_AMOUNTORALL']._serialized_start=49 diff --git a/contrib/pyln-testing/pyln/testing/fixtures.py b/contrib/pyln-testing/pyln/testing/fixtures.py index 13631c1be10d..dede9e88d272 100644 --- a/contrib/pyln-testing/pyln/testing/fixtures.py +++ b/contrib/pyln-testing/pyln/testing/fixtures.py @@ -164,6 +164,8 @@ def bitcoind(directory, teardown_checks): bitcoind.proc.kill() bitcoind.proc.wait() + bitcoind.cleanup_files() + class TeardownErrors(object): def __init__(self): diff --git a/contrib/pyln-testing/pyln/testing/utils.py b/contrib/pyln-testing/pyln/testing/utils.py index 6eda2d9ffc09..9d1ac4a77d45 100644 --- a/contrib/pyln-testing/pyln/testing/utils.py +++ b/contrib/pyln-testing/pyln/testing/utils.py @@ -256,6 +256,14 @@ def kill(self): self.proc.kill() self.proc.wait() + def cleanup_files(self): + """Ensure files are closed.""" + for f in ["stdout_write", "stderr_write", "stdout_read", "stderr_read"]: + try: + getattr(self, f).close() + except Exception: + pass + def logs_catchup(self): """Save the latest stdout / stderr contents; return true if we got anything. """ diff --git a/devtools/gossmap-compress.c b/devtools/gossmap-compress.c index 426dde9c9575..d41672a54b25 100644 --- a/devtools/gossmap-compress.c +++ b/devtools/gossmap-compress.c @@ -84,11 +84,12 @@ static unsigned int verbose = 0; #define GC_HEADERLEN (sizeof(GC_HEADER)) #define GOSSIP_STORE_VER ((0 << 5) | 14) +/* Backwards, we want larger first */ static int cmp_node_num_chans(struct gossmap_node *const *a, struct gossmap_node *const *b, void *unused) { - return (int)(*a)->num_chans - (int)(*b)->num_chans; + return (int)(*b)->num_chans - (int)(*a)->num_chans; } static void write_bigsize(gzFile outf, u64 val) @@ -233,18 +234,17 @@ static u64 get_htlc_max(struct gossmap *gossmap, int dir) { struct amount_msat msat, capacity_msat; - struct amount_sat capacity_sats; - gossmap_chan_get_capacity(gossmap, chan, &capacity_sats); + + capacity_msat = gossmap_chan_get_capacity(gossmap, chan); gossmap_chan_get_update_details(gossmap, chan, dir, NULL, NULL, NULL, NULL, NULL, NULL, &msat); /* Special value for the common case of "max_htlc == capacity" */ - if (amount_msat_eq_sat(msat, capacity_sats)) { + if (amount_msat_eq(msat, capacity_msat)) { return 0; } /* Other common case: "max_htlc == 99% capacity" */ - if (amount_sat_to_msat(&capacity_msat, capacity_sats) - && amount_msat_scale(&capacity_msat, capacity_msat, 0.99) + if (amount_msat_scale(&capacity_msat, capacity_msat, 0.99) && amount_msat_eq(msat, capacity_msat)) { return 1; } @@ -495,10 +495,44 @@ static char *opt_node(const char *optarg, const struct pubkey ***node_ids) return NULL; } +static const char *get_alias(const tal_t *ctx, + const struct gossmap *gossmap, + const struct gossmap_node *n) +{ + const u8 *ann = gossmap_node_get_announce(tmpctx, gossmap, n); + secp256k1_ecdsa_signature signature; + u8 *features; + u32 timestamp; + struct node_id node_id; + u8 rgb_color[3]; + u8 alias[32]; + u8 *addresses; + struct tlv_node_ann_tlvs *tlvs; + + if (!fromwire_node_announcement(tmpctx, ann, &signature, &features, ×tamp, + &node_id, rgb_color, alias, &addresses, + &tlvs)) + return ""; + return tal_strndup(ctx, (const char *)alias, 32); +} + +static void cupdate_bad(struct gossmap *map, + const struct short_channel_id_dir *scidd, + u16 cltv_expiry_delta, + u32 fee_base_msat, + u32 fee_proportional_millionths, + void *unused) +{ + warnx("Bad cupdate for %s, ignoring (delta=%u, fee=%u/%u)", + fmt_short_channel_id_dir(tmpctx, scidd), + cltv_expiry_delta, fee_base_msat, fee_proportional_millionths); +} + int main(int argc, char *argv[]) { int infd, outfd; const struct pubkey **node_ids; + bool print_nodes = false; common_setup(argv[0]); setup_locale(); @@ -507,7 +541,9 @@ int main(int argc, char *argv[]) opt_register_noarg("--verbose|-v", opt_add_one, &verbose, "Print details (each additional gives more!)."); opt_register_arg("--node-map=num=", opt_node, NULL, &node_ids, - "Map node num to "); + "Map node num to (decompress only)"); + opt_register_noarg("--output-node-map", opt_set_bool, &print_nodes, + "Output nodenumber:nodeid:alias for each node (compress only)"); opt_register_noarg("--help|-h", opt_usage_and_exit, "[decompress|compress] infile outfile\n" "Compress or decompress a gossmap file", @@ -515,25 +551,26 @@ int main(int argc, char *argv[]) opt_parse(&argc, argv, opt_log_stderr_exit); if (argc != 4) - opt_usage_and_exit("Needs 4 arguments"); + opt_usage_exit_fail("Needs 4 arguments"); infd = open(argv[2], O_RDONLY); if (infd < 0) - opt_usage_and_exit(tal_fmt(tmpctx, "Cannot open %s for reading: %s", - argv[2], strerror(errno))); + opt_usage_exit_fail(tal_fmt(tmpctx, "Cannot open %s for reading: %s", + argv[2], strerror(errno))); outfd = open(argv[3], O_WRONLY|O_CREAT|O_TRUNC, 0666); if (outfd < 0) - opt_usage_and_exit(tal_fmt(tmpctx, "Cannot open %s for writing: %s", - argv[3], strerror(errno))); + opt_usage_exit_fail(tal_fmt(tmpctx, "Cannot open %s for writing: %s", + argv[3], strerror(errno))); if (streq(argv[1], "compress")) { struct gossmap_node **nodes, *n; size_t *node_to_compr_idx; size_t node_count, channel_count; struct gossmap_chan **chans, *c; + bool *dirs; gzFile outf = gzdopen(outfd, "wb9"); - struct gossmap *gossmap = gossmap_load_fd(tmpctx, infd, NULL, NULL, NULL); + struct gossmap *gossmap = gossmap_load_fd(tmpctx, infd, cupdate_bad, NULL, NULL); if (!gossmap) opt_usage_and_exit("Cannot read gossmap"); @@ -552,8 +589,18 @@ int main(int argc, char *argv[]) /* Create map of gossmap index to compression index */ node_to_compr_idx = tal_arr(nodes, size_t, gossmap_max_node_idx(gossmap)); - for (size_t i = 0; i < tal_count(nodes); i++) + for (size_t i = 0; i < tal_count(nodes); i++) { node_to_compr_idx[gossmap_node_idx(gossmap, nodes[i])] = i; + if (print_nodes) { + struct node_id node_id; + gossmap_node_get_id(gossmap, nodes[i], &node_id); + + printf("%zu:%s:%s\n", + i, + fmt_node_id(tmpctx, &node_id), + get_alias(tmpctx, gossmap, nodes[i])); + } + } if (gzwrite(outf, GC_HEADER, GC_HEADERLEN) == 0) err(1, "Writing header"); @@ -568,27 +615,42 @@ int main(int argc, char *argv[]) if (verbose) printf("%zu channels\n", channel_count); chans = tal_arr(gossmap, struct gossmap_chan *, channel_count); + dirs = tal_arr(gossmap, bool, channel_count); /* * := {channel_count} {start_nodeidx}*{channel_count} {end_nodeidx}*{channel_count} */ write_bigsize(outf, channel_count); size_t chanidx = 0; /* We iterate nodes to get to channels. This gives us nicer ordering for compression */ - for (size_t wanted_dir = 0; wanted_dir < 2; wanted_dir++) { - for (n = gossmap_first_node(gossmap); n; n = gossmap_next_node(gossmap, n)) { - for (size_t i = 0; i < n->num_chans; i++) { - int dir; - c = gossmap_nth_chan(gossmap, n, i, &dir); - if (dir != wanted_dir) - continue; - - write_bigsize(outf, - node_to_compr_idx[gossmap_node_idx(gossmap, n)]); - /* First time reflects channel index for reader */ - if (wanted_dir == 0) - chans[chanidx++] = c; - } + for (size_t i = 0; i < tal_count(nodes); i++) { + n = nodes[i]; + for (size_t j = 0; j < n->num_chans; j++) { + const struct gossmap_node *peer; + int dir; + c = gossmap_nth_chan(gossmap, n, j, &dir); + + peer = gossmap_nth_node(gossmap, c, !dir); + /* Don't write if peer already wrote it! */ + /* FIXME: What about self-channels? */ + if (node_to_compr_idx[gossmap_node_idx(gossmap, peer)] < i) + continue; + + write_bigsize(outf, node_to_compr_idx[gossmap_node_idx(gossmap, n)]); + + assert(chanidx < channel_count); + dirs[chanidx] = dir; + chans[chanidx] = c; + chanidx++; } } + assert(chanidx == channel_count); + + /* Now write out the other ends of the channels */ + for (size_t i = 0; i < channel_count; i++) { + const struct gossmap_node *peer; + + peer = gossmap_nth_node(gossmap, chans[i], !dirs[i]); + write_bigsize(outf, node_to_compr_idx[gossmap_node_idx(gossmap, peer)]); + } /* := * {channel_count*2} */ /* := {chanidx}*2+{direction} */ @@ -612,9 +674,9 @@ int main(int argc, char *argv[]) /* := {capacity_count} {capacity_count}*{capacity} */ u64 *vals = tal_arr(chans, u64, channel_count); for (size_t i = 0; i < channel_count; i++) { - struct amount_sat sats; - gossmap_chan_get_capacity(gossmap, chans[i], &sats); - vals[i] = sats.satoshis; /* Raw: compression format */ + struct amount_msat cap; + cap = gossmap_chan_get_capacity(gossmap, chans[i]); + vals[i] = cap.millisatoshis / 1000; /* Raw: compression format */ } write_template_and_values(outf, vals, "capacities"); diff --git a/doc/Makefile b/doc/Makefile index fd59c4182646..ccae17e9eaa5 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,10 +6,14 @@ doc-wrongdir: GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-addpsbtoutput.7 \ + doc/lightning-askrene-create-layer.7 \ + doc/lightning-askrene-remove-layer.7 \ doc/lightning-askrene-create-channel.7 \ + doc/lightning-askrene-update-channel.7 \ doc/lightning-askrene-disable-node.7 \ doc/lightning-askrene-inform-channel.7 \ doc/lightning-askrene-listlayers.7 \ + doc/lightning-askrene-listreservations.7 \ doc/lightning-askrene-reserve.7 \ doc/lightning-askrene-unreserve.7 \ doc/lightning-autoclean-once.7 \ @@ -56,6 +60,7 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-fundchannel_start.7 \ doc/lightning-funderupdate.7 \ doc/lightning-fundpsbt.7 \ + doc/lightning-getemergencyrecoverdata.7 \ doc/lightning-getinfo.7 \ doc/lightning-getlog.7 \ doc/lightning-getroute.7 \ diff --git a/doc/contribute-to-core-lightning/release-checklist.md b/doc/contribute-to-core-lightning/release-checklist.md index 3b5aade0e389..59fb0376a328 100644 --- a/doc/contribute-to-core-lightning/release-checklist.md +++ b/doc/contribute-to-core-lightning/release-checklist.md @@ -105,3 +105,23 @@ Here's a checklist for the release process. 3. Look through PRs which were delayed for release and merge them. 4. Close out the Milestone for the now-shipped release. 5. Update this file with any missing or changed instructions. + +## Performing the Point (hotfix) Release + +1. Create a new branch named `release-.`, where each new branch is based on the commit from the previous release tag. For example, `release-.1` is based on `release-`, `release-.2` is based on `release-.1`, and so on. +2. Cherry-pick all necessary commits for the hotfix into the new branch. +3. Add entries for changes and fixed issues in `CHANGELOG.md` under a new heading for `v.`. +4. Update the python package versions by running `make update-py-versions NEW_VERSION=.` +5. Create a new commit that includes the updates from `update-py-versions` and `CHANGELOG.md`. +6. Tag the release with `git pull && git tag -s v.`. You will be prompted to enter a tag message, ensure this is filled out. +7. Confirm that the tag is properly set up for builds by running `git describe`. +8. Push the tag to the remote repository `git push --tags`. +9. Create a new release draft for `v.` on GitHub, ensuring to check the `Set as a pre-release` option. +10. Follow the [reproducible build](https://docs.corelightning.org/docs/repro) instructions for [Builder image setup](https://docs.corelightning.org/docs/repro#builder-image-setup) to create builder images named `cl-repro-` required for the next step. +11. Run the following script to prepare the required builds `tools/build-release.sh bin-Fedora-28-amd64 bin-Ubuntu sign`. +12. Upload the reproducible builds along with `SHA256SUMS` and `SHA256SUMS.asc` files from the release folder to the newly drafted release. +13. Share the `SHA256SUMS` and `SHA256SUMS.asc` files with the team for verification and signing. +14. Append the signatures received from the team to the `SHA256SUMS.asc` file. Verify the file using `gpg --verify SHA256SUMS.asc`. Then re-upload the file. +15. Finalize and publish the release (change it from draft to public). +16. Ensure that the GitHub Actions for `Publish Python 🐍 distributions 📦 to PyPI and TestPyPI` and `Build and push multi-platform docker images` are functioning correctly. Check that the `PyPI` modules published on `https://pypi.org/project/pyln-*` and that the Docker image has been uploaded to Docker Hub. +17. Announce the hotfix release in the core-lightning release-chat channel on Discord and on [BuildOnL2](https://community.corelightning.org/c/general-questions/). diff --git a/doc/contribute-to-core-lightning/testing.md b/doc/contribute-to-core-lightning/testing.md index c1ca8082b5a9..2de963756a0f 100644 --- a/doc/contribute-to-core-lightning/testing.md +++ b/doc/contribute-to-core-lightning/testing.md @@ -43,6 +43,10 @@ There are four kinds of tests: You can also append `-k TESTNAME` to run a single test. Environment variables `DEBUG_SUBD=` and `TIMEOUT=` can be useful for debugging subdaemons on individual tests, and `DEBUG_LIGHTNINGD` for attaching a debugger to each `lightningd` instance created. + Alternatively, to run a specific test via the `Makefile`, you can specify the test by setting the environment variable `PYTEST_TESTS`: + + `PYTEST_TESTS="tests/test_askrene.py::test_layers" make pytest` + - **pylightning tests** - will check contrib pylightning for codestyle and run the tests in `contrib/pylightning/tests` afterwards: `make check-python` @@ -52,14 +56,28 @@ Our Github Actions instance (see `.github/workflows/*.yml`) runs all these for e #### Additional Environment Variables ```text -TEST_CHECK_DBSTMTS=[0|1] - When running blackbox tests, this will - load a plugin that logs all compiled - and expanded database statements. - Note: Only SQLite3. -TEST_DB_PROVIDER=[sqlite3|postgres] - Selects the database to use when running - blackbox tests. -EXPERIMENTAL_DUAL_FUND=[0|1] - Enable dual-funding tests. -EXPERIMENTAL_SPLICING=[0|1] - Enable splicing tests. +EXPERIMENTAL_DUAL_FUND=[0|1] - Enable dual-funding tests. +EXPERIMENTAL_SPLICING=[0|1] - Enable splicing tests. +TEST_CHECK_DBSTMTS=[0|1] - When running blackbox tests, this will + load a plugin that logs all compiled + and expanded database statements. + Note: Only SQLite3. +TEST_DB_PROVIDER=[sqlite3|postgres] - Selects the database to use when running + blackbox tests. +TEST_DEBUG=[0|1] - Enable additional debug logging output + during tests. +TEST_NETWORK=[regtest|liquid-regtest] - Select the test network to use. Default is + to 'regtest'. +TIMEOUT - Override the default timeout value for + API calls. +PYTEST_PAR=[1-n] - Number of processes to use when running + the blackbox the tests in parallel. +PYTEST_TESTS="tests/" - Target a specific set of blackbox tests + when running 'make pytest'. Pass a string + of Pytest test targets. +SLOW_MACHINE=[0|1] - Set sensible defaults for running tests + in resource-constrained environments. +VALGRIND=[0|1] - Run the tests with Valgrind. ``` #### Troubleshooting diff --git a/doc/index.rst b/doc/index.rst index 5515229db141..bb48bce62bb6 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,11 +15,15 @@ Core Lightning Documentation lightning-addgossip lightning-addpsbtoutput lightning-askrene-create-channel + lightning-askrene-create-layer lightning-askrene-disable-node lightning-askrene-inform-channel lightning-askrene-listlayers + lightning-askrene-listreservations + lightning-askrene-remove-layer lightning-askrene-reserve lightning-askrene-unreserve + lightning-askrene-update-channel lightning-autoclean-once lightning-autoclean-status lightning-batching @@ -65,6 +69,7 @@ Core Lightning Documentation lightning-fundchannel_start lightning-funderupdate lightning-fundpsbt + lightning-getemergencyrecoverdata lightning-getinfo lightning-getlog lightning-getroute diff --git a/doc/lightning-hsmtool.8.md b/doc/lightning-hsmtool.8.md index cabe5dc6bd9f..d10f7b3b3ee6 100644 --- a/doc/lightning-hsmtool.8.md +++ b/doc/lightning-hsmtool.8.md @@ -20,38 +20,38 @@ as well as derive secrets used in channel commitments. METHODS ------- -**encrypt** *hsm\_secret* *password* +**encrypt** *hsm\_secret\_path* *password* - Encrypt the `hsm_secret` file so that it can only be decrypted at + Encrypt the `hsm_secret_path` file so that it can only be decrypted at **lightningd** startup. You must give the option **--encrypted-hsm** to **lightningd**. -The password of the `hsm_secret` file will be asked whenever you +The password of the `hsm_secret_path` file will be asked whenever you start **lightningd**. -**decrypt** *hsm\_secret* *password* +**decrypt** *hsm\_secret\_path* *password* - Decrypt the `hsm_secret` file that was encrypted with the **encrypt** + Decrypt the `hsm_secret_path` file that was encrypted with the **encrypt** method. -**dumpcommitments** *node\_id* *channel\_dbid* *depth* *hsm\_secret* \[*password*\] +**dumpcommitments** *node\_id* *channel\_dbid* *depth* *hsm\_secret\_path* \[*password*\] Show the per-commitment secret and point of up to *depth* commitments, of the specified channel with the specified peer, identified by the channel database index. -Specify *password* if the `hsm_secret` is encrypted. +Specify *password* if the `hsm_secret_path` is encrypted. -**guesstoremote** *p2wpkh* *node\_id* *max\_channel\_dbid* *hsm\_secret* \[*password*\] +**guesstoremote** *p2wpkh* *node\_id* *max\_channel\_dbid* *hsm\_secret\_path* \[*password*\] Brute-force the private key to our funds from a remote unilateral close of a channel, in a case where we have lost all database data except for -our `hsm_secret`. +our `hsm_secret_path`. The peer must be the one to close the channel (and the funds will remain unrecoverable until the channel is closed). *max\_channel\_dbid* is your own guess on what the *channel\_dbid* was, or at least the maximum possible value, and is usually no greater than the number of channels that the node has ever had. -Specify *password* if the `hsm_secret` is encrypted. +Specify *password* if the `hsm_secret_path` is encrypted. **generatehsm** *hsm\_secret\_path* Generates a new hsm\_secret using BIP39. @@ -59,7 +59,7 @@ Specify *password* if the `hsm_secret` is encrypted. **checkhsm** *hsm\_secret\_path* Checks that hsm\_secret matches a BIP39 passphrase. -**dumponchaindescriptors** \[*--show-secrets*\] *hsm\_secret* \[*network*\] +**dumponchaindescriptors** \[*--show-secrets*\] *hsm\_secret\_path* \[*network*\] Dump output descriptors for our onchain wallet. This command requires the path to the hsm\_secret containing the wallet seed. If the flag *--show-secrets* is set the command will show the BIP32 extended private @@ -74,7 +74,7 @@ password. To generate descriptors using testnet master keys, you may specify *testnet* as the last parameter. By default, mainnet-encoded keys are generated. -**makerune** *hsm\_secret* +**makerune** *hsm\_secret\_path* Make a master rune for this node (with `uniqueid` 0) This produces the same results as lightning-commando-rune(7) on a fresh node. You will still need to create a rune once the node starts, if you want commando to work (as it is only activated once it has generated one). @@ -85,6 +85,9 @@ You will still need to create a rune once the node starts, if you want commando **getemergencyrecover** *emergency.recover\_path* Print out the bech32 encoded emergency.recover file. +**getnodeid** *hsm\_secret\_path* + Print out the node id that a node using this hsm secret would have: useful for verifying that you are accessing the correct secret! + BUGS ---- diff --git a/doc/schemas/lightning-askrene-create-channel.json b/doc/schemas/lightning-askrene-create-channel.json index ff43fdf7eb99..f1acd0c8299b 100644 --- a/doc/schemas/lightning-askrene-create-channel.json +++ b/doc/schemas/lightning-askrene-create-channel.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + "The **askrene-create-channel** RPC command tells askrene create a channel in the given layer. To actually populate the channel use *askrene-update-channel* in each direction." ], "request": { "required": [ @@ -15,12 +15,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_min", - "htlc_max", - "base_fee", - "proportional_fee", - "delay" + "capacity_msat" ], "properties": { "layer": { @@ -50,37 +45,8 @@ "capacity_msat": { "type": "msat", "description": [ - "The capacity (onchain size) of the channel." - ] - }, - "htlc_min": { - "type": "msat", - "description": [ - "The minimum value allowed in this direction." - ] - }, - "htlc_max": { - "type": "msat", - "description": [ - "The maximum value allowed in this direction." - ] - }, - "base_fee": { - "type": "msat", - "description": [ - "The base fee to apply to use the channel in this direction." - ] - }, - "proportional_fee": { - "type": "u32", - "description": [ - "The proportional fee (in parts per million) to apply to use the channel in this direction." - ] - }, - "delay": { - "type": "u16", - "description": [ - "The CLTV delay required for this direction." + "The capacity (onchain size) of the channel.", + "NOTE: this is in millisatoshis!" ] } } @@ -92,6 +58,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-disable-node(7)", + "lightning-askrene-update-channel(7)", "lightning-askrene-inform-channel(7)", "lightning-askrene-listlayers(7)", "lightning-askrene-age(7)" diff --git a/doc/schemas/lightning-askrene-create-layer.json b/doc/schemas/lightning-askrene-create-layer.json new file mode 100644 index 000000000000..566a2a1e947a --- /dev/null +++ b/doc/schemas/lightning-askrene-create-layer.json @@ -0,0 +1,201 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-layer", + "title": "Command to create a new layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-layer** RPC command tells askrene to create a new, empty layer. This layer can then be populated with `askrene-create-channel` and `askrene-inform-channel`, and be used in `getroutes`." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to create." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "created_channels", + "channel_updates", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-remove-layer(7)", + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json index 8ba0f90f62e3..b12de57fef3a 100644 --- a/doc/schemas/lightning-askrene-inform-channel.json +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -7,13 +7,14 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + "The **askrene-inform-channel** RPC command tells askrene about channels we used so it can update its capacity estimates. For most accuracy, you should remove your own reservations before calling this. It can be applied whether the current channel exists or not." ], "request": { "required": [ "layer", - "short_channel_id", - "direction" + "short_channel_id_dir", + "amount_msat", + "inform" ], "properties": { "layer": { @@ -22,74 +23,62 @@ "The name of the layer to apply this change to." ] }, - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The short channel id to apply this change to." + "The short channel id and direction to apply this change to." ] }, - "direction": { - "type": "u32", - "description": [ - "The direction to apply this change to." - ] - }, - "minimum_msat": { + "amount_msat": { "type": "msat", "description": [ - "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "The amount we used on the channel" ] }, - "maximum_msat": { - "type": "msat", + "inform": { + "type": "string", + "enum": [ + "constrained", + "unconstrained", + "succeeded" + ], "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + "Whether this payment passed (implying capacity of at least that amount), failed (implying maximum capacity of one msat less), or succeeded (implying capacity has been reduced in this direction)" ] } } }, "response": { "required": [ - "constraint" + "constraints" ], "properties": { - "constraint": { - "type": "object", - "required": [ - "short_channel_id", - "direction", - "timestamp" - ], - "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The *short_channel_id* specified." - ] - }, - "direction": { - "type": "u32", - "description": [ - "The *direction* specified." - ] - }, - "timestamp": { - "type": "u64", - "description": [ - "The UNIX time (seconds since 1970) this was created." - ] - }, - "maximum_msat": { - "type": "msat", - "description": [ - "The *minimum_msat* (if specified)" - ] - }, - "minimum_msat": { - "type": "msat", - "description": [ - "The *maximum_msat* (if specified)" - ] + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The short channel id and direction" + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass." + ] + } } } } diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json index a0a5cba70511..c206f8fe60be 100644 --- a/doc/schemas/lightning-askrene-listlayers.json +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -34,6 +34,7 @@ "layer", "disabled_nodes", "created_channels", + "channel_updates", "constraints" ], "properties": { @@ -52,6 +53,15 @@ ] } }, + "disabled_channels": { + "type": "array", + "items": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction which is disabled." + ] + } + }, "created_channels": { "type": "array", "items": { @@ -60,12 +70,7 @@ "source", "destination", "short_channel_id", - "capacity_msat", - "htlc_minimum_msat", - "htlc_maximum_msat", - "fee_base_msat", - "fee_proportional_millionths", - "delay" + "capacity_msat" ], "properties": { "source": { @@ -91,7 +96,18 @@ "description": [ "The capacity (onchain size) of the channel." ] - }, + } + } + } + }, + "channel_updates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id_dir" + ], + "properties": { "htlc_minimum_msat": { "type": "msat", "description": [ @@ -130,32 +146,25 @@ "items": { "type": "object", "required": [ - "short_channel_id", - "direction" + "short_channel_id_dir" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", - "description": [ - "The short channel id." - ] - }, - "direction": { - "type": "u32", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The direction." + "The short channel id and direction" ] }, "maximum_msat": { "type": "msat", "description": [ - "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The maximum value which this channel could pass." ] }, "minimum_msat": { "type": "msat", "description": [ - "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + "The minimum value which this channel could pass." ] } } diff --git a/doc/schemas/lightning-askrene-listreservations.json b/doc/schemas/lightning-askrene-listreservations.json new file mode 100644 index 000000000000..1886d9e1c19e --- /dev/null +++ b/doc/schemas/lightning-askrene-listreservations.json @@ -0,0 +1,72 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listreservations", + "title": "Command to display information about reservations (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reservations** RPC command reports outstanding reservations made with `askrene-reserve`, mainly for debugging." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "reservations" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id_dir", + "amount_msat", + "age_in_seconds", + "command_id" + ], + "properties": { + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction that is reserved." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount reserved." + ] + }, + "age_in_seconds": { + "type": "u64", + "description": [ + "The age of this reservation." + ] + }, + "command_id": { + "type": "string", + "description": [ + "The JSON id of the command used to make the reservation." + ] + } + } + } + } + } + }, + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-remove-layer.json b/doc/schemas/lightning-askrene-remove-layer.json new file mode 100644 index 000000000000..541cf9529002 --- /dev/null +++ b/doc/schemas/lightning-askrene-remove-layer.json @@ -0,0 +1,39 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-remove-layer", + "title": "Command to destroy a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-remove-layer** RPC command tells askrene to forget a layer." + ], + "request": { + "required": [ + "layer" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to remove." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-askrene-create-layer(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-reserve.json b/doc/schemas/lightning-askrene-reserve.json index 7b91e3800998..880eff109dfd 100644 --- a/doc/schemas/lightning-askrene-reserve.json +++ b/doc/schemas/lightning-askrene-reserve.json @@ -7,7 +7,7 @@ "description": [ "WARNING: experimental, so API may change.", "", - "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call *askrene-unreserve* after the attempt has completed (and before calling *askrene-inform*).", "", "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." ], @@ -22,21 +22,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -57,11 +50,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-unreserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." diff --git a/doc/schemas/lightning-askrene-unreserve.json b/doc/schemas/lightning-askrene-unreserve.json index 377595a5caa5..c214e56ddc75 100644 --- a/doc/schemas/lightning-askrene-unreserve.json +++ b/doc/schemas/lightning-askrene-unreserve.json @@ -22,21 +22,14 @@ "type": "object", "additionalProperties": true, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "amount_msat" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { @@ -57,11 +50,7 @@ "see_also": [ "lightning-getroutes(7)", "lightning-askrene-reserve(7)", - "lightning-askrene-disable-node(7)", - "lightning-askrene-create-channel(7)", - "lightning-askrene-inform-channel(7)", - "lightning-askrene-listlayers(7)", - "lightning-askrene-age(7)" + "lightning-askrene-listreservations(7)" ], "author": [ "Rusty Russell <> is mainly responsible." diff --git a/doc/schemas/lightning-askrene-update-channel.json b/doc/schemas/lightning-askrene-update-channel.json new file mode 100644 index 000000000000..c0f82dcdee0f --- /dev/null +++ b/doc/schemas/lightning-askrene-update-channel.json @@ -0,0 +1,76 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-update-channel", + "title": "Command to manipulate channel in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-update-channel** RPC command overrides updates for an existing channel when the layer is applied." + ], + "request": { + "required": [ + "layer", + "short_channel_id_dir" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id_dir": { + "type": "short_channel_id_dir", + "description": [ + "The channel and direction to apply the change to." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "cltv_expiry_delta": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-getemergencyrecoverdata.json b/doc/schemas/lightning-getemergencyrecoverdata.json new file mode 100644 index 000000000000..3fc4bee3de86 --- /dev/null +++ b/doc/schemas/lightning-getemergencyrecoverdata.json @@ -0,0 +1,48 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "getemergencyrecoverdata", + "title": "Command to fetch data from the emergency.recover file", + "description": [ + "The **getemergencyrecoverdata** RPC command is used to fetch data from the emergency.recover file, which contains encrypted data." + ], + "request": { + "required": [], + "properties": {} + }, + "response": { + "required": [ + "filedata" + ], + "properties": { + "filedata": { + "type": "hex", + "description": [ + "The raw, hex-encoded, emergency.recover file" + ] + } + } + }, + "author": [ + "Aditya <> is mainly responsible." + ], + "see_also": [ + "lightning-getsharedsecret(7)" + ], + "resources": [ + "Main web site: " + ], + "examples": [ + { + "request": { + "id": "example:getemergencyrecoverdata#1", + "method": "getemergencyrecoverdata", + "params": {} + }, + "response": { + "filedata": "5b3142fa0dd1115c29654b44780dcd9cf56cd53f9168061e964b39f3ce596962594b25660cba5d90ef07cfccbe1620f378ef284c7d1afed49d" + } + } + ] +} diff --git a/doc/schemas/lightning-getroutes.json b/doc/schemas/lightning-getroutes.json index 5b09bda28c57..aeaeb2216b8a 100644 --- a/doc/schemas/lightning-getroutes.json +++ b/doc/schemas/lightning-getroutes.json @@ -121,23 +121,16 @@ "type": "object", "additionalProperties": false, "required": [ - "short_channel_id", - "direction", + "short_channel_id_dir", "next_node_id", "amount_msat", "delay" ], "properties": { - "short_channel_id": { - "type": "short_channel_id", + "short_channel_id_dir": { + "type": "short_channel_id_dir", "description": [ - "The channel joining these nodes." - ] - }, - "direction": { - "type": "u32", - "description": [ - "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + "The channel and direction joining these nodes." ] }, "amount_msat": { diff --git a/lightningd/channel.c b/lightningd/channel.c index af5dae06ed9f..59c5be0a2d0e 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -310,6 +310,9 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->ignore_fee_limits = ld->config.ignore_fee_limits; channel->last_stable_connection = 0; channel->stable_conn_timer = NULL; + /* Nothing happened yet */ + memset(&channel->stats, 0, sizeof(channel->stats)); + channel->state_changes = tal_arr(channel, struct channel_state_change *, 0); /* No shachain yet */ channel->their_shachain.id = 0; @@ -445,7 +448,9 @@ struct channel *new_channel(struct peer *peer, u64 dbid, bool ignore_fee_limits, /* NULL or stolen */ struct peer_update *peer_update STEALS, - u64 last_stable_connection) + u64 last_stable_connection, + const struct channel_stats *stats, + struct channel_state_change **state_changes STEALS) { struct channel *channel = tal(peer->ld, struct channel); struct amount_msat htlc_min, htlc_max; @@ -602,6 +607,9 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->ignore_fee_limits = ignore_fee_limits; channel->last_stable_connection = last_stable_connection; channel->stable_conn_timer = NULL; + channel->stats = *stats; + channel->state_changes = tal_steal(channel, state_changes); + /* Populate channel->channel_gossip */ channel_gossip_init(channel, take(peer_update)); @@ -826,14 +834,28 @@ void channel_set_last_tx(struct channel *channel, channel->last_tx = tal_steal(channel, tx); } +struct channel_state_change *new_channel_state_change(const tal_t *ctx, + struct timeabs timestamp, + enum channel_state old_state, + enum channel_state new_state, + enum state_change cause, + const char *message TAKES) +{ + struct channel_state_change *c = tal(ctx, struct channel_state_change); + c->timestamp = timestamp; + c->old_state = old_state; + c->new_state = new_state; + c->cause = cause; + c->message = tal_strdup(c, message); + return c; +} + void channel_set_state(struct channel *channel, enum channel_state old_state, enum channel_state state, enum state_change reason, char *why) { - struct timeabs timestamp; - /* set closer, if known */ if (channel_state_closing(state) && channel->closer == NUM_SIDES) { if (reason == REASON_LOCAL) channel->closer = LOCAL; @@ -861,10 +883,19 @@ void channel_set_state(struct channel *channel, /* plugin notification channel_state_changed and DB entry */ if (state != old_state) { /* see issue #4029 */ - timestamp = time_now(); + struct channel_state_change *change; + + change = new_channel_state_change(channel->state_changes, + time_now(), + old_state, + state, + reason, + why); + tal_arr_expand(&channel->state_changes, change); + wallet_state_change_add(channel->peer->ld->wallet, channel->dbid, - timestamp, + change->timestamp, old_state, state, reason, @@ -873,7 +904,7 @@ void channel_set_state(struct channel *channel, &channel->peer->id, &channel->cid, channel->scid, - timestamp, + change->timestamp, old_state, state, reason, diff --git a/lightningd/channel.h b/lightningd/channel.h index b04e68d84dc8..4b40356990c9 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -107,6 +107,23 @@ struct open_attempt { const u8 *open_msg; }; +/* Statistics for a channel */ +struct channel_stats { + u64 in_payments_offered, in_payments_fulfilled; + struct amount_msat in_msatoshi_offered, in_msatoshi_fulfilled; + u64 out_payments_offered, out_payments_fulfilled; + struct amount_msat out_msatoshi_offered, out_msatoshi_fulfilled; +}; + + +struct channel_state_change { + struct timeabs timestamp; + enum channel_state old_state; + enum channel_state new_state; + enum state_change cause; + const char *message; +}; + struct channel { /* Inside peer->channels. */ struct list_node list; @@ -314,6 +331,12 @@ struct channel { /* Last time we had a stable connection, if any (0 = none) */ u64 last_stable_connection; struct oneshot *stable_conn_timer; + + /* Our stats */ + struct channel_stats stats; + + /* Our change history. */ + struct channel_state_change **state_changes; }; /* Is channel owned (and should be talking to peer) */ @@ -395,7 +418,9 @@ struct channel *new_channel(struct peer *peer, u64 dbid, bool ignore_fee_limits, /* NULL or stolen */ struct peer_update *peer_update STEALS, - u64 last_stable_connection); + u64 last_stable_connection, + const struct channel_stats *stats, + struct channel_state_change **state_changes STEALS); /* new_inflight - Create a new channel_inflight for a channel */ struct channel_inflight *new_inflight(struct channel *channel, @@ -415,6 +440,13 @@ struct channel_inflight *new_inflight(struct channel *channel, bool i_am_initiator, bool force_sign_first); +struct channel_state_change *new_channel_state_change(const tal_t *ctx, + struct timeabs timestamp, + enum channel_state old_state, + enum channel_state new_state, + enum state_change cause, + const char *message TAKES); + /* Add a last_tx and sig to an inflight */ void inflight_set_last_tx(struct channel_inflight *inflight, struct bitcoin_tx *last_tx STEALS, diff --git a/lightningd/channel_state.h b/lightningd/channel_state.h index 2c655bc8b0ef..8ef3da3bebe7 100644 --- a/lightningd/channel_state.h +++ b/lightningd/channel_state.h @@ -73,13 +73,4 @@ enum state_change { /* Note: This is very likely a conscious remote decision. */ REASON_ONCHAIN }; - -struct state_change_entry { - struct timeabs timestamp; - enum channel_state old_state; - enum channel_state new_state; - enum state_change cause; - char *message; -}; - #endif /* LIGHTNING_LIGHTNINGD_CHANNEL_STATE_H */ diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index a1066eb166e8..d0a2c7e6eed6 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -109,6 +109,10 @@ wallet_commit_channel(struct lightningd *ld, u32 lease_start_blockheight = 0; /* No leases on v1 */ struct timeabs timestamp; bool any_active = peer_any_channel(uc->peer, channel_state_wants_peercomms, NULL); + struct channel_stats zero_channel_stats; + + /* We can't have any payments yet */ + memset(&zero_channel_stats, 0, sizeof(zero_channel_stats)); /* We cannot both be the fundee *and* have a `fundchannel_start` * command running! @@ -215,7 +219,9 @@ wallet_commit_channel(struct lightningd *ld, ld->config.htlc_maximum_msat, ld->config.ignore_fee_limits, NULL, - 0); + 0, + &zero_channel_stats, + tal_arr(NULL, struct channel_state_change *, 0)); /* Now we finally put it in the database. */ wallet_channel_insert(ld->wallet, channel); @@ -1456,6 +1462,7 @@ static struct channel *stub_chan(struct command *cmd, struct short_channel_id *scid; u32 blockht; u32 feerate; + struct channel_stats zero_channel_stats; u8 *dummy_sig = tal_hexdata(cmd, "30450221009b2e0eef267b94c3899fb0dc73750" "12e2cee4c10348a068fe78d1b82b4b1403602207" @@ -1530,6 +1537,8 @@ static struct channel *stub_chan(struct command *cmd, if (!mk_short_channel_id(scid, 1, 1, 1)) fatal("Failed to make short channel 1x1x1!"); + memset(&zero_channel_stats, 0, sizeof(zero_channel_stats)); + /* Channel Shell with Dummy data(mostly) */ channel = new_channel(peer, id, NULL, /* No shachain yet */ @@ -1589,7 +1598,9 @@ static struct channel *stub_chan(struct command *cmd, ld->config.htlc_maximum_msat, false, NULL, - 0); + 0, + &zero_channel_stats, + tal_arr(NULL, struct channel_state_change *, 0)); /* We don't want to gossip about this, ever. */ channel->channel_gossip = tal_free(channel->channel_gossip); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index aaf7897754e3..755a06f9d9d3 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -818,10 +818,8 @@ static void NON_NULL_ARGS(1, 2, 4, 5) json_add_channel(struct command *cmd, const struct peer *peer) { struct lightningd *ld = cmd->ld; - struct channel_stats channel_stats; struct amount_msat funding_msat; struct amount_sat peer_funded_sats; - struct state_change_entry *state_changes; const struct peer_update *peer_update; u32 feerate; @@ -1162,19 +1160,19 @@ static void NON_NULL_ARGS(1, 2, 4, 5) json_add_channel(struct command *cmd, json_add_num(response, "max_accepted_htlcs", channel->our_config.max_accepted_htlcs); - state_changes = wallet_state_change_get(tmpctx, ld->wallet, channel->dbid); json_array_start(response, "state_changes"); - for (size_t i = 0; i < tal_count(state_changes); i++) { + for (size_t i = 0; i < tal_count(channel->state_changes); i++) { + const struct channel_state_change *change + = channel->state_changes[i]; json_object_start(response, NULL); - json_add_timeiso(response, "timestamp", - state_changes[i].timestamp); + json_add_timeiso(response, "timestamp", change->timestamp); json_add_string(response, "old_state", - channel_state_str(state_changes[i].old_state)); + channel_state_str(change->old_state)); json_add_string(response, "new_state", - channel_state_str(state_changes[i].new_state)); + channel_state_str(change->new_state)); json_add_string(response, "cause", - channel_change_state_reason_str(state_changes[i].cause)); - json_add_string(response, "message", state_changes[i].message); + channel_change_state_reason_str(change->cause)); + json_add_string(response, "message", change->message); json_object_end(response); } json_array_end(response); @@ -1191,27 +1189,26 @@ static void NON_NULL_ARGS(1, 2, 4, 5) json_add_channel(struct command *cmd, json_array_end(response); /* Provide channel statistics */ - wallet_channel_stats_load(ld->wallet, channel->dbid, &channel_stats); json_add_u64(response, "in_payments_offered", - channel_stats.in_payments_offered); + channel->stats.in_payments_offered); json_add_amount_msat(response, "in_offered_msat", - channel_stats.in_msatoshi_offered); + channel->stats.in_msatoshi_offered); json_add_u64(response, "in_payments_fulfilled", - channel_stats.in_payments_fulfilled); + channel->stats.in_payments_fulfilled); json_add_amount_msat(response, "in_fulfilled_msat", - channel_stats.in_msatoshi_fulfilled); + channel->stats.in_msatoshi_fulfilled); json_add_u64(response, "out_payments_offered", - channel_stats.out_payments_offered); + channel->stats.out_payments_offered); json_add_amount_msat(response, "out_offered_msat", - channel_stats.out_msatoshi_offered); + channel->stats.out_msatoshi_offered); json_add_u64(response, "out_payments_fulfilled", - channel_stats.out_payments_fulfilled); + channel->stats.out_payments_fulfilled); json_add_amount_msat(response, "out_fulfilled_msat", - channel_stats.out_msatoshi_fulfilled); + channel->stats.out_msatoshi_fulfilled); json_add_htlcs(ld, response, channel); json_object_end(response); @@ -3170,7 +3167,7 @@ static struct command_result *param_dev_channel(struct command *cmd, const jsmntok_t *tok, struct channel **channel) { - struct peer *peer; + struct peer *peer COMPILER_WANTS_INIT("gcc version 12.3.0 -O3"); struct command_result *res; bool more_than_one; diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 97bb11b035b1..a9af74260cae 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -93,6 +93,48 @@ static bool htlc_out_update_state(struct channel *channel, return true; } +static void cstat_accumulate(struct channel *c, + struct amount_msat *val, + struct amount_msat msat) +{ + if (!amount_msat_add(val, *val, msat)) + log_broken(c->log, "Adding %s to stat %s overflowed!", + fmt_amount_msat(tmpctx, *val), + fmt_amount_msat(tmpctx, msat)); +} + +static void channel_stats_incr_in_fulfilled(struct channel *c, + struct amount_msat msat) +{ + c->stats.in_payments_fulfilled++; + cstat_accumulate(c, &c->stats.in_msatoshi_fulfilled, msat); + wallet_channel_stats_incr_in_fulfilled(c->peer->ld->wallet, c->dbid, msat); +} + +static void channel_stats_incr_out_fulfilled(struct channel *c, + struct amount_msat msat) +{ + c->stats.out_payments_fulfilled++; + cstat_accumulate(c, &c->stats.out_msatoshi_fulfilled, msat); + wallet_channel_stats_incr_out_fulfilled(c->peer->ld->wallet, c->dbid, msat); +} + +static void channel_stats_incr_in_offered(struct channel *c, + struct amount_msat msat) +{ + c->stats.in_payments_offered++; + cstat_accumulate(c, &c->stats.in_msatoshi_offered, msat); + wallet_channel_stats_incr_in_offered(c->peer->ld->wallet, c->dbid, msat); +} + +static void channel_stats_incr_out_offered(struct channel *c, + struct amount_msat msat) +{ + c->stats.out_payments_offered++; + cstat_accumulate(c, &c->stats.out_msatoshi_offered, msat); + wallet_channel_stats_incr_out_offered(c->peer->ld->wallet, c->dbid, msat); +} + /* BOLT #4: * - if `blinding_point` is set in the incoming `update_add_htlc`: * - MUST return an `invalid_onion_blinding` error. @@ -332,7 +374,6 @@ void fulfill_htlc(struct htlc_in *hin, const struct preimage *preimage) { u8 *msg; struct channel *channel = hin->key.channel; - struct wallet *wallet = channel->peer->ld->wallet; if (hin->hstate != RCVD_ADD_ACK_REVOCATION) { log_debug(channel->log, @@ -349,9 +390,7 @@ void fulfill_htlc(struct htlc_in *hin, const struct preimage *preimage) htlc_in_check(hin, __func__); /* Update channel stats */ - wallet_channel_stats_incr_in_fulfilled(wallet, - channel->dbid, - hin->msat); + channel_stats_incr_in_fulfilled(channel, hin->msat); /* No owner? We'll either send to channeld in peer_htlcs, or * onchaind in onchaind_tell_fulfill. */ @@ -1417,9 +1456,7 @@ static void fulfill_our_htlc_out(struct channel *channel, struct htlc_out *hout, 0, hout->failonion, hout->failmsg, &we_filled); /* Update channel stats */ - wallet_channel_stats_incr_out_fulfilled(ld->wallet, - channel->dbid, - hout->msat); + channel_stats_incr_out_fulfilled(channel, hout->msat); if (hout->am_origin) payment_succeeded(ld, &hout->payment_hash, hout->partid, hout->groupid, preimage); @@ -1892,9 +1929,7 @@ static bool update_out_htlc(struct channel *channel, if (!hout->dbid) { wallet_htlc_save_out(ld->wallet, channel, hout); /* Update channel stats */ - wallet_channel_stats_incr_out_offered(ld->wallet, - channel->dbid, - hout->msat); + channel_stats_incr_out_offered(channel, hout->msat); if (hout->in) { struct short_channel_id scid; @@ -2131,8 +2166,7 @@ static bool channel_added_their_htlc(struct channel *channel, /* Save an incoming htlc to the wallet */ wallet_htlc_save_in(ld->wallet, channel, hin); /* Update channel stats */ - wallet_channel_stats_incr_in_offered(ld->wallet, channel->dbid, - added->amount); + channel_stats_incr_in_offered(channel, added->amount); log_debug(channel->log, "Adding their HTLC %"PRIu64, added->id); connect_htlc_in(channel->peer->ld->htlcs_in, hin); diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 154684d3d10a..54fe654ac139 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -1023,9 +1023,6 @@ const char *version(void) /* Generated stub for wallet_channel_save */ void wallet_channel_save(struct wallet *w UNNEEDED, struct channel *chan UNNEEDED) { fprintf(stderr, "wallet_channel_save called!\n"); abort(); } -/* Generated stub for wallet_channel_stats_load */ -void wallet_channel_stats_load(struct wallet *w UNNEEDED, u64 cdbid UNNEEDED, struct channel_stats *stats UNNEEDED) -{ fprintf(stderr, "wallet_channel_stats_load called!\n"); abort(); } /* Generated stub for wallet_channeltxs_add */ void wallet_channeltxs_add(struct wallet *w UNNEEDED, struct channel *chan UNNEEDED, const int type UNNEEDED, const struct bitcoin_txid *txid UNNEEDED, @@ -1062,11 +1059,6 @@ char *wallet_offer_find(const tal_t *ctx UNNEEDED, enum offer_status *status) { fprintf(stderr, "wallet_offer_find called!\n"); abort(); } -/* Generated stub for wallet_state_change_get */ -struct state_change_entry *wallet_state_change_get(const tal_t *ctx UNNEEDED, - struct wallet *w UNNEEDED, - u64 channel_id UNNEEDED) -{ fprintf(stderr, "wallet_state_change_get called!\n"); abort(); } /* Generated stub for wallet_total_forward_fees */ struct amount_msat wallet_total_forward_fees(struct wallet *w UNNEEDED) { fprintf(stderr, "wallet_total_forward_fees called!\n"); abort(); } diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml index 1b3dea946920..a753f01b9297 100644 --- a/plugins/Cargo.toml +++ b/plugins/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cln-plugin" -version = "0.1.9" +version = "0.2.0" edition = "2021" license = "MIT" description = "A CLN plugin library. Write your plugin in Rust." @@ -27,4 +27,4 @@ tracing = { version = "^0.1", features = ["async-await", "log"] } [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread", ] } -cln-grpc = { version = "0.1", path = "../cln-grpc" } +cln-grpc = { version = "0.2", path = "../cln-grpc" } diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile index a55136f75753..989905aee192 100644 --- a/plugins/askrene/Makefile +++ b/plugins/askrene/Makefile @@ -1,5 +1,5 @@ -PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c plugins/askrene/mcf.c plugins/askrene/dijkstra.c plugins/askrene/flow.c plugins/askrene/refine.c -PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h plugins/askrene/mcf.h plugins/askrene/dijkstra.h plugins/askrene/flow.h plugins/askrene/refine.h +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c plugins/askrene/mcf.c plugins/askrene/dijkstra.c plugins/askrene/flow.c plugins/askrene/refine.c plugins/askrene/explain_failure.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h plugins/askrene/mcf.h plugins/askrene/dijkstra.h plugins/askrene/flow.h plugins/askrene/refine.h plugins/askrene/explain_failure.h PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) @@ -7,4 +7,4 @@ $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) ALL_C_SOURCES += $(PLUGIN_ASKRENE_SRC) ALL_C_HEADERS += $(PLUGIN_ASKRENE_HEADER) -plugins/cln-askrene: $(PLUGIN_ASKRENE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/sciddir_or_pubkey.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12_wiregen.o wire/onion_wiregen.o +plugins/cln-askrene: $(PLUGIN_ASKRENE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/sciddir_or_pubkey.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12_wiregen.o wire/onion_wiregen.o common/route.o diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 751b687416ef..db720f0d96ba 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -9,6 +9,7 @@ #include "config.h" #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -35,12 +37,6 @@ per_htlc_cost_key(const struct per_htlc_cost *phc) return &phc->scidd; } -static size_t hash_scidd(const struct short_channel_id_dir *scidd) -{ - /* scids cost money to generate, so simple hash works here */ - return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 << 1) ^ scidd->dir; -} - static inline bool per_htlc_cost_eq_key(const struct per_htlc_cost *phc, const struct short_channel_id_dir *scidd) { @@ -62,24 +58,35 @@ static bool have_layer(const char **layers, const char *name) return false; } -/* JSON helpers */ -static struct command_result *param_string_array(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - const char ***arr) +/* Valid, known layers */ +static struct command_result *param_layer_names(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + const char ***arr) { size_t i; const jsmntok_t *t; if (tok->type != JSMN_ARRAY) - return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); + return command_fail_badparam(cmd, name, buffer, tok, + "should be an array"); *arr = tal_arr(cmd, const char *, tok->size); json_for_each_arr(i, t, tok) { if (t->type != JSMN_STRING) - return command_fail_badparam(cmd, name, buffer, t, "should be a string"); + return command_fail_badparam(cmd, name, buffer, t, + "should be a string"); (*arr)[i] = json_strdup(*arr, buffer, t); + + /* Must be a known layer name */ + if (streq((*arr)[i], "auto.localchans") + || streq((*arr)[i], "auto.sourcefree")) + continue; + if (!find_layer(get_askrene(cmd->plugin), (*arr)[i])) { + return command_fail_badparam(cmd, name, buffer, t, + "unknown layer"); + } } return NULL; } @@ -102,49 +109,17 @@ static struct command_result *param_known_layer(struct command *cmd, return NULL; } -static bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) -{ - u32 v32; - if (!json_to_u32(buffer, tok, &v32)) - return false; - if (v32 != 0 && v32 != 1) - return false; - *num = v32; - return true; -} - -static struct command_result *param_zero_or_one(struct command *cmd, +static struct command_result *parse_reserve_hop(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, - int **num) -{ - *num = tal(cmd, int); - if (json_to_zero_or_one(buffer, tok, *num)) - return NULL; - - return command_fail_badparam(cmd, name, buffer, tok, - "should be 0 or 1"); -} - -struct reserve_path { - struct short_channel_id_dir *scidds; - struct amount_msat *amounts; -}; - -static struct command_result *parse_reserve_path(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - struct short_channel_id_dir *scidd, - struct amount_msat *amount) + struct reserve_hop *rhop) { const char *err; - err = json_scan(tmpctx, buffer, tok, "{short_channel_id:%,direction:%,amount_msat:%}", - JSON_SCAN(json_to_short_channel_id, &scidd->scid), - JSON_SCAN(json_to_zero_or_one, &scidd->dir), - JSON_SCAN(json_to_msat, amount)); + err = json_scan(tmpctx, buffer, tok, "{short_channel_id_dir:%,amount_msat:%}", + JSON_SCAN(json_to_short_channel_id_dir, &rhop->scidd), + JSON_SCAN(json_to_msat, &rhop->amount)); if (err) return command_fail_badparam(cmd, name, buffer, tok, err); return NULL; @@ -154,7 +129,7 @@ static struct command_result *param_reserve_path(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, - struct reserve_path **path) + struct reserve_hop **path) { size_t i; const jsmntok_t *t; @@ -162,15 +137,11 @@ static struct command_result *param_reserve_path(struct command *cmd, if (tok->type != JSMN_ARRAY) return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); - *path = tal(cmd, struct reserve_path); - (*path)->scidds = tal_arr(cmd, struct short_channel_id_dir, tok->size); - (*path)->amounts = tal_arr(cmd, struct amount_msat, tok->size); + *path = tal_arr(cmd, struct reserve_hop, tok->size); json_for_each_arr(i, t, tok) { struct command_result *ret; - ret = parse_reserve_path(cmd, name, buffer, t, - &(*path)->scidds[i], - &(*path)->amounts[i]); + ret = parse_reserve_hop(cmd, name, buffer, t, &(*path)[i]); if (ret) return ret; } @@ -188,16 +159,12 @@ static fp16_t *get_capacities(const tal_t *ctx, for (c = gossmap_first_chan(gossmap); c; c = gossmap_next_chan(gossmap, c)) { - struct amount_sat cap; + struct amount_msat cap; - if (!gossmap_chan_get_capacity(gossmap, c, &cap)) { - plugin_log(plugin, LOG_BROKEN, - "get_capacity failed for channel?"); - cap = AMOUNT_SAT(0); - } + cap = gossmap_chan_get_capacity(gossmap, c); /* Pessimistic: round down! */ caps[gossmap_chan_idx(gossmap, c)] - = u64_to_fp16(cap.satoshis, false); /* Raw: fp16 */ + = u64_to_fp16(cap.millisatoshis/1000, false); /* Raw: fp16 */ } return caps; } @@ -214,11 +181,11 @@ static void add_free_source(struct plugin *plugin, /* We apply existing localmods, save up mods we want, then append * them: it's not safe to modify localmods while they are applied! */ const struct gossmap_node *srcnode; - struct mod { - struct short_channel_id_dir scidd; - fp16_t htlc_min, htlc_max; - bool enabled; - } *mods = tal_arr(tmpctx, struct mod, 0); + const struct amount_msat zero_base_fee = AMOUNT_MSAT(0); + const u16 zero_delay = 0; + const u32 zero_prop_fee = 0; + struct short_channel_id_dir *scidds + = tal_arr(tmpctx, struct short_channel_id_dir, 0); gossmap_apply_localmods(gossmap, localmods); @@ -226,35 +193,24 @@ static void add_free_source(struct plugin *plugin, srcnode = gossmap_find_node(gossmap, source); for (size_t i = 0; srcnode && i < srcnode->num_chans; i++) { + struct short_channel_id_dir scidd; const struct gossmap_chan *c; - const struct half_chan *h; - struct mod mod; - c = gossmap_nth_chan(gossmap, srcnode, i, &mod.scidd.dir); - h = &c->half[mod.scidd.dir]; - - mod.scidd.scid = gossmap_chan_scid(gossmap, c); - mod.htlc_min = h->htlc_min; - mod.htlc_max = h->htlc_max; - mod.enabled = h->enabled; - tal_arr_expand(&mods, mod); + c = gossmap_nth_chan(gossmap, srcnode, i, &scidd.dir); + scidd.scid = gossmap_chan_scid(gossmap, c); + tal_arr_expand(&scidds, scidd); } gossmap_remove_localmods(gossmap, localmods); - /* Now we can update localmods */ - for (size_t i = 0; i < tal_count(mods); i++) { + /* Now we can update localmods: we only change fee levels and delay */ + for (size_t i = 0; i < tal_count(scidds); i++) { if (!gossmap_local_updatechan(localmods, - mods[i].scidd.scid, - /* Keep min and max */ - /* FIXME: lossy conversion! */ - amount_msat(fp16_to_u64(mods[i].htlc_min)), - amount_msat(fp16_to_u64(mods[i].htlc_max)), - 0, 0, 0, - /* Keep enabled flag */ - mods[i].enabled, - mods[i].scidd.dir)) - plugin_err(plugin, "Could not zero fee on %s", - fmt_short_channel_id_dir(tmpctx, &mods[i].scidd)); + &scidds[i], + NULL, NULL, NULL, + &zero_base_fee, &zero_prop_fee, + &zero_delay)) + plugin_err(plugin, "Could not zero fee/delay on %s", + fmt_short_channel_id_dir(tmpctx, &scidds[i])); } } @@ -269,9 +225,10 @@ struct amount_msat get_additional_per_htlc_cost(const struct route_query *rq, return AMOUNT_MSAT(0); } + /* Returns an error message, or sets *routes */ static const char *get_routes(const tal_t *ctx, - struct plugin *plugin, + struct command *cmd, const struct node_id *source, const struct node_id *dest, struct amount_msat amount, @@ -285,7 +242,7 @@ static const char *get_routes(const tal_t *ctx, const struct additional_cost_htable *additional_costs, double *probability) { - struct askrene *askrene = get_askrene(plugin); + struct askrene *askrene = get_askrene(cmd->plugin); struct route_query *rq = tal(ctx, struct route_query); struct flow **flows; const struct gossmap_node *srcnode, *dstnode; @@ -300,7 +257,8 @@ static const char *get_routes(const tal_t *ctx, askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); } - rq->plugin = plugin; + rq->cmd = cmd; + rq->plugin = cmd->plugin; rq->gossmap = askrene->gossmap; rq->reserved = askrene->reserved; rq->layers = tal_arr(rq, const struct layer *, 0); @@ -311,16 +269,19 @@ static const char *get_routes(const tal_t *ctx, for (size_t i = 0; i < tal_count(layers); i++) { const struct layer *l = find_layer(askrene, layers[i]); if (!l) { - if (local_layer && streq(layers[i], "auto.localchans")) { - plugin_log(plugin, LOG_DBG, "Adding auto.localchans"); + if (streq(layers[i], "auto.localchans")) { + plugin_log(rq->plugin, LOG_DBG, "Adding auto.localchans"); l = local_layer; - } else + } else { + /* Handled below, after other layers */ + assert(streq(layers[i], "auto.sourcefree")); continue; + } } tal_arr_expand(&rq->layers, l); /* FIXME: Implement localmods_merge, and cache this in layer? */ - layer_add_localmods(l, rq->gossmap, false, localmods); + layer_add_localmods(l, rq->gossmap, localmods); /* Clear any entries in capacities array if we * override them (incl local channels) */ @@ -329,7 +290,7 @@ static const char *get_routes(const tal_t *ctx, /* This also looks into localmods, to zero them */ if (have_layer(layers, "auto.sourcefree")) - add_free_source(plugin, askrene->gossmap, localmods, source); + add_free_source(rq->plugin, askrene->gossmap, localmods, source); /* Clear scids with reservations, too, so we don't have to look up * all the time! */ @@ -340,13 +301,13 @@ static const char *get_routes(const tal_t *ctx, srcnode = gossmap_find_node(askrene->gossmap, source); if (!srcnode) { ret = tal_fmt(ctx, "Unknown source node %s", fmt_node_id(tmpctx, source)); - goto out; + goto fail; } dstnode = gossmap_find_node(askrene->gossmap, dest); if (!dstnode) { ret = tal_fmt(ctx, "Unknown destination node %s", fmt_node_id(tmpctx, dest)); - goto out; + goto fail; } delay_feefactor = 1.0/1000000; @@ -369,11 +330,8 @@ static const char *get_routes(const tal_t *ctx, flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows) { - /* FIXME: disjktra here to see if there is any route, and - * diagnose problem (offline peers? Not enough capacity at - * our end? Not enough at theirs?) */ - ret = tal_fmt(ctx, "Could not find route"); - goto out; + ret = explain_failure(ctx, rq, srcnode, dstnode, amount); + goto fail; } /* Too much delay? */ @@ -390,24 +348,24 @@ static const char *get_routes(const tal_t *ctx, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || delay_feefactor > 10) { ret = tal_fmt(ctx, "Could not find route without excessive delays"); - goto out; + goto fail; } } /* Too expensive? */ - while (amount_msat_greater(flowset_fee(plugin, flows), maxfee)) { + while (amount_msat_greater(flowset_fee(rq->plugin, flows), maxfee)) { mu += 10; flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || mu == 100) { ret = tal_fmt(ctx, "Could not find route without excessive cost"); - goto out; + goto fail; } } if (finalcltv + flows_worst_delay(flows) > 2016) { ret = tal_fmt(ctx, "Could not find route without excessive cost or delays"); - goto out; + goto fail; } /* The above did not take into account the extra funds to pay @@ -416,7 +374,7 @@ static const char *get_routes(const tal_t *ctx, * still possible */ ret = refine_with_fees_and_limits(ctx, rq, amount, &flows); if (ret) - goto out; + goto fail; /* Convert back into routes, with delay and other information fixed */ *routes = tal_arr(ctx, struct route *, tal_count(flows)); @@ -440,7 +398,7 @@ static const char *get_routes(const tal_t *ctx, const struct half_chan *h = flow_edge(flows[i], j); if (!amount_msat_add_fee(&msat, h->base_fee, h->proportional_fee)) - plugin_err(plugin, "Adding fee to amount"); + plugin_err(rq->plugin, "Adding fee to amount"); delay += h->delay; rh->scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); @@ -454,9 +412,13 @@ static const char *get_routes(const tal_t *ctx, } *probability = flowset_probability(flows, rq); - ret = NULL; + gossmap_remove_localmods(askrene->gossmap, localmods); + return NULL; -out: + /* Explicit failure path keeps the compiler (gcc version 12.3.0 -O3) from + * warning about uninitialized variables in the caller */ +fail: + assert(ret != NULL); gossmap_remove_localmods(askrene->gossmap, localmods); return ret; } @@ -468,7 +430,6 @@ void get_constraints(const struct route_query *rq, struct amount_msat *max) { struct short_channel_id_dir scidd; - const struct reserve *reserve; size_t idx = gossmap_chan_idx(rq->gossmap, chan); *min = AMOUNT_MSAT(0); @@ -484,45 +445,18 @@ void get_constraints(const struct route_query *rq, scidd.dir = dir; *max = AMOUNT_MSAT(-1ULL); - /* Look through layers for any constraints */ - for (size_t i = 0; i < tal_count(rq->layers); i++) { - const struct constraint *cmin, *cmax; - cmin = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MIN); - if (cmin && amount_msat_greater(cmin->limit, *min)) - *min = cmin->limit; - cmax = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MAX); - if (cmax && amount_msat_less(cmax->limit, *max)) - *max = cmax->limit; - } + /* Look through layers for any constraints (might be dummy + * ones, for created channels!) */ + for (size_t i = 0; i < tal_count(rq->layers); i++) + layer_apply_constraints(rq->layers[i], &scidd, min, max); /* Might be here because it's reserved, but capacity is normal. */ - if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) { - struct amount_sat cap; - if (gossmap_chan_get_capacity(rq->gossmap, chan, &cap)) { - /* Shouldn't happen! */ - if (!amount_sat_to_msat(max, cap)) { - plugin_log(rq->plugin, LOG_BROKEN, - "Local channel %s with capacity %s?", - fmt_short_channel_id(tmpctx, scidd.scid), - fmt_amount_sat(tmpctx, cap)); - } - } else { - /* Shouldn't happen: local channels have explicit constraints */ - plugin_log(rq->plugin, LOG_BROKEN, - "Channel %s without capacity?", - fmt_short_channel_id(tmpctx, scidd.scid)); - } - } + if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) + *max = gossmap_chan_get_capacity(rq->gossmap, chan); /* Finally, if any is in use, subtract that! */ - reserve = find_reserve(rq->reserved, &scidd); - if (reserve) { - /* They can definitely *try* to push too much through a channel! */ - if (!amount_msat_sub(min, *min, reserve->amount)) - *min = AMOUNT_MSAT(0); - if (!amount_msat_sub(max, *max, reserve->amount)) - *max = AMOUNT_MSAT(0); - } + reserve_sub(rq->reserved, &scidd, min); + reserve_sub(rq->reserved, &scidd, max); } struct getroutes_info { @@ -546,7 +480,7 @@ static struct command_result *do_getroutes(struct command *cmd, struct route **routes; struct json_stream *response; - err = get_routes(cmd, cmd->plugin, + err = get_routes(cmd, cmd, info->source, info->dest, *info->amount, *info->maxfee, *info->finalcltv, info->layers, localmods, info->local_layer, @@ -564,10 +498,12 @@ static struct command_result *do_getroutes(struct command *cmd, json_add_u32(response, "final_cltv", *info->finalcltv); json_array_start(response, "path"); for (size_t j = 0; j < tal_count(routes[i]->hops); j++) { + struct short_channel_id_dir scidd; const struct route_hop *r = &routes[i]->hops[j]; json_object_start(response, NULL); - json_add_short_channel_id(response, "short_channel_id", r->scid); - json_add_u32(response, "direction", r->direction); + scidd.scid = r->scid; + scidd.dir = r->direction; + json_add_short_channel_id_dir(response, "short_channel_id_dir", scidd); json_add_node_id(response, "next_node_id", &r->node_id); json_add_amount_msat(response, "amount_msat", r->amount); json_add_u32(response, "delay", r->delay); @@ -584,12 +520,13 @@ static void add_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, @@ -599,7 +536,7 @@ static void add_localchan(struct gossmap_localmods *mods, const char *opener; const char *err; - gossmod_add_localchan(mods, self, peer, scidd, htlcmin, htlcmax, + gossmod_add_localchan(mods, self, peer, scidd, capacity_msat, htlcmin, htlcmax, spendable, fee_base, fee_proportional, cltv_delta, enabled, buf, chantok, info->local_layer); @@ -643,8 +580,7 @@ static void add_localchan(struct gossmap_localmods *mods, } /* Known capacity on local channels (ts = max) */ - layer_update_constraint(info->local_layer, scidd, CONSTRAINT_MIN, UINT64_MAX, spendable); - layer_update_constraint(info->local_layer, scidd, CONSTRAINT_MAX, UINT64_MAX, spendable); + layer_add_constraint(info->local_layer, scidd, UINT64_MAX, &spendable, &spendable); } static struct command_result * @@ -676,7 +612,7 @@ static struct command_result *json_getroutes(struct command *cmd, p_req("source", param_node_id, &info->source), p_req("destination", param_node_id, &info->dest), p_req("amount_msat", param_msat, &info->amount), - p_req("layers", param_string_array, &info->layers), + p_req("layers", param_layer_names, &info->layers), p_req("maxfee_msat", param_msat, &info->maxfee), p_req("final_cltv", param_u32, &info->finalcltv), NULL)) @@ -704,9 +640,8 @@ static struct command_result *json_askrene_reserve(struct command *cmd, const char *buffer, const jsmntok_t *params) { - struct reserve_path *path; + struct reserve_hop *path; struct json_stream *response; - size_t num; struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, @@ -714,17 +649,8 @@ static struct command_result *json_askrene_reserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_add(askrene->reserved, path->scidds, path->amounts, - tal_count(path->scidds)); - if (num != tal_count(path->scidds)) { - const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Overflow reserving %zu: %s amount %s (%s reserved already)", - num, - fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), - fmt_amount_msat(tmpctx, path->amounts[num]), - r ? fmt_amount_msat(tmpctx, r->amount) : "none"); - } + for (size_t i = 0; i < tal_count(path); i++) + reserve_add(askrene->reserved, &path[i], cmd->id); response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); @@ -734,9 +660,8 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, const char *buffer, const jsmntok_t *params) { - struct reserve_path *path; + struct reserve_hop *path; struct json_stream *response; - size_t num; struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, @@ -744,140 +669,181 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, NULL)) return command_param_failed(); - num = reserves_remove(askrene->reserved, path->scidds, path->amounts, - tal_count(path->scidds)); - if (num != tal_count(path->scidds)) { - const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Underflow unreserving %zu: %s amount %s (%zu reserved, amount %s)", - num, - fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), - fmt_amount_msat(tmpctx, path->amounts[num]), - r ? r->num_htlcs : 0, - r ? fmt_amount_msat(tmpctx, r->amount) : "none"); - } + for (size_t i = 0; i < tal_count(path); i++) { + if (!reserve_remove(askrene->reserved, &path[i])) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Unknown reservation for %s", + fmt_short_channel_id_dir(tmpctx, + &path[i].scidd)); + } + } response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } -static struct command_result *param_layername(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - const char **str) +static struct command_result *json_askrene_listreservations(struct command *cmd, + const char *buffer, + const jsmntok_t *params) { - *str = tal_strndup(cmd, buffer + tok->start, - tok->end - tok->start); - if (strstarts(*str, "auto.")) - return command_fail_badparam(cmd, name, buffer, tok, - "New layers cannot start with auto."); - return NULL; + struct askrene *askrene = get_askrene(cmd->plugin); + struct json_stream *response; + + if (!param(cmd, buffer, params, + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + json_add_reservations(response, askrene->reserved, "reservations"); + return command_finished(cmd, response); } static struct command_result *json_askrene_create_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) { - const char *layername; struct layer *layer; - const struct local_channel *lc; struct node_id *src, *dst; struct short_channel_id *scid; struct amount_msat *capacity; struct json_stream *response; - struct amount_msat *htlc_min, *htlc_max, *base_fee; - u32 *proportional_fee; - u16 *delay; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_layername, &layername), + p_req("layer", param_known_layer, &layer), p_req("source", param_node_id, &src), p_req("destination", param_node_id, &dst), p_req("short_channel_id", param_short_channel_id, &scid), p_req("capacity_msat", param_msat, &capacity), - p_req("htlc_minimum_msat", param_msat, &htlc_min), - p_req("htlc_maximum_msat", param_msat, &htlc_max), - p_req("fee_base_msat", param_msat, &base_fee), - p_req("fee_proportional_millionths", param_u32, &proportional_fee), - p_req("delay", param_u16, &delay), NULL)) return command_param_failed(); - /* If it exists, it must match */ - layer = find_layer(askrene, layername); - if (layer) { - lc = layer_find_local_channel(layer, *scid); - if (lc && !layer_check_local_channel(lc, src, dst, *capacity)) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "channel already exists with different values!"); - } - } else - lc = NULL; + if (layer_find_local_channel(layer, *scid)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "channel already exists"); + } if (command_check_only(cmd)) return command_check_done(cmd); - if (!layer) - layer = new_layer(askrene, layername); + layer_add_local_channel(layer, src, dst, *scid, *capacity); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} - layer_update_local_channel(layer, src, dst, *scid, *capacity, - *base_fee, *proportional_fee, *delay, - *htlc_min, *htlc_max); +static struct command_result *json_askrene_update_channel(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct layer *layer; + struct short_channel_id_dir *scidd; + bool *enabled; + struct amount_msat *htlc_min, *htlc_max, *base_fee; + u32 *proportional_fee; + u16 *delay; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_known_layer, &layer), + p_req("short_channel_id_dir", param_short_channel_id_dir, &scidd), + p_opt("enabled", param_bool, &enabled), + p_opt("htlc_minimum_msat", param_msat, &htlc_min), + p_opt("htlc_maximum_msat", param_msat, &htlc_max), + p_opt("fee_base_msat", param_msat, &base_fee), + p_opt("fee_proportional_millionths", param_u32, &proportional_fee), + p_opt("cltv_expiry_delta", param_u16, &delay), + NULL)) + return command_param_failed(); + + layer_add_update_channel(layer, scidd, + enabled, + htlc_min, htlc_max, + base_fee, proportional_fee, delay); response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } +enum inform { + INFORM_CONSTRAINED, + INFORM_UNCONSTRAINED, + INFORM_SUCCEEDED, +}; + +static struct command_result *param_inform(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + enum inform **inform) +{ + *inform = tal(cmd, enum inform); + if (json_tok_streq(buffer, tok, "constrained")) + **inform = INFORM_CONSTRAINED; + else if (json_tok_streq(buffer, tok, "unconstrained")) + **inform = INFORM_UNCONSTRAINED; + else if (json_tok_streq(buffer, tok, "succeeded")) + **inform = INFORM_SUCCEEDED; + else + command_fail_badparam(cmd, name, buffer, tok, + "must be constrained/unconstrained/succeeded"); + return NULL; +} + static struct command_result *json_askrene_inform_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) { + struct askrene *askrene = get_askrene(cmd->plugin); struct layer *layer; - const char *layername; - struct short_channel_id *scid; - int *direction; + struct short_channel_id_dir *scidd; struct json_stream *response; - struct amount_msat *max, *min; + struct amount_msat *amount; + enum inform *inform; const struct constraint *c; - struct short_channel_id_dir scidd; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_layername, &layername), - p_req("short_channel_id", param_short_channel_id, &scid), - p_req("direction", param_zero_or_one, &direction), - p_opt("minimum_msat", param_msat, &min), - p_opt("maximum_msat", param_msat, &max), + p_req("layer", param_known_layer, &layer), + p_req("short_channel_id_dir", param_short_channel_id_dir, &scidd), + p_req("amount_msat", param_msat, &amount), + p_req("inform", param_inform, &inform), NULL)) return command_param_failed(); - if ((!min && !max) || (min && max)) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Must specify exactly one of maximum_msat/minimum_msat"); + switch (*inform) { + case INFORM_CONSTRAINED: + /* It didn't pass, so minimal assumption is that reserve was all used + * then there we were one msat short. */ + if (!amount_msat_sub(amount, *amount, AMOUNT_MSAT(1))) + *amount = AMOUNT_MSAT(0); + if (!reserve_accumulate(askrene->reserved, scidd, amount)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Amount overflow with reserves"); + if (command_check_only(cmd)) + return command_check_done(cmd); + c = layer_add_constraint(layer, scidd, time_now().ts.tv_sec, + NULL, amount); + goto output; + case INFORM_UNCONSTRAINED: + /* It passed, so the capacity is at least this much (minimal assumption is + * that no reserves were used) */ + if (command_check_only(cmd)) + return command_check_done(cmd); + c = layer_add_constraint(layer, scidd, time_now().ts.tv_sec, + amount, NULL); + goto output; + case INFORM_SUCCEEDED: + /* FIXME: We could do something useful here! */ + c = NULL; + goto output; } + abort(); - if (command_check_only(cmd)) - return command_check_done(cmd); - - layer = find_layer(askrene, layername); - if (!layer) - layer = new_layer(askrene, layername); - - /* Calls expect a convenient short_channel_id_dir struct */ - scidd.scid = *scid; - scidd.dir = *direction; - - if (min) { - c = layer_update_constraint(layer, &scidd, CONSTRAINT_MIN, - time_now().ts.tv_sec, *min); - } else { - c = layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, - time_now().ts.tv_sec, *max); - } +output: response = jsonrpc_stream_success(cmd); - json_add_constraint(response, "constraint", c, layer); + json_array_start(response, "constraints"); + if (c) + json_add_constraint(response, NULL, c, layer); + json_array_end(response); return command_finished(cmd, response); } @@ -886,21 +852,15 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, const jsmntok_t *params) { struct node_id *node; - const char *layername; struct layer *layer; struct json_stream *response; - struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, - p_req("layer", param_layername, &layername), + p_req("layer", param_known_layer, &layer), p_req("node", param_node_id, &node), NULL)) return command_param_failed(); - layer = find_layer(askrene, layername); - if (!layer) - layer = new_layer(askrene, layername); - /* We save this in the layer, because they want us to disable all the channels * to the node at *use* time (a new channel might be gossiped!). */ layer_add_disabled_node(layer, node); @@ -909,21 +869,71 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *json_askrene_create_layer(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct askrene *askrene = get_askrene(cmd->plugin); + struct layer *layer; + const char *layername; + struct json_stream *response; + + if (!param_check(cmd, buffer, params, + p_req("layer", param_string, &layername), + NULL)) + return command_param_failed(); + + if (find_layer(askrene, layername)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Layer already exists"); + + if (strstarts(layername, "auto.")) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Cannot create auto layer"); + + if (command_check_only(cmd)) + return command_check_done(cmd); + + layer = new_layer(askrene, layername); + + response = jsonrpc_stream_success(cmd); + json_add_layers(response, askrene, "layers", layer); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_remove_layer(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct layer *layer; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_known_layer, &layer), + NULL)) + return command_param_failed(); + + tal_free(layer); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + static struct command_result *json_askrene_listlayers(struct command *cmd, const char *buffer, const jsmntok_t *params) { struct askrene *askrene = get_askrene(cmd->plugin); - const char *layername; + struct layer *layer; struct json_stream *response; if (!param(cmd, buffer, params, - p_opt("layer", param_string, &layername), + p_opt("layer", param_known_layer, &layer), NULL)) return command_param_failed(); response = jsonrpc_stream_success(cmd); - json_add_layers(response, askrene, "layers", layername); + json_add_layers(response, askrene, "layers", layer); return command_finished(cmd, response); } @@ -955,6 +965,10 @@ static const struct plugin_command commands[] = { "getroutes", json_getroutes, }, + { + "askrene-listreservations", + json_askrene_listreservations, + }, { "askrene-reserve", json_askrene_reserve, @@ -971,10 +985,22 @@ static const struct plugin_command commands[] = { "askrene-create-channel", json_askrene_create_channel, }, + { + "askrene-update-channel", + json_askrene_update_channel, + }, { "askrene-inform-channel", json_askrene_inform_channel, }, + { + "askrene-create-layer", + json_askrene_create_layer, + }, + { + "askrene-remove-layer", + json_askrene_remove_layer, + }, { "askrene-listlayers", json_askrene_listlayers, diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 7fb432838c8f..5ae9a4511ace 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -35,6 +35,9 @@ struct askrene { /* Information for a single route query. */ struct route_query { + /* Command pointer, mainly for command id. */ + struct command *cmd; + /* Plugin pointer, for logging mainly */ struct plugin *plugin; @@ -71,4 +74,11 @@ static inline struct askrene *get_askrene(struct plugin *plugin) return plugin_get_data(plugin, struct askrene); } +/* Convenience routine for hash tables */ +static inline size_t hash_scidd(const struct short_channel_id_dir *scidd) +{ + /* scids cost money to generate, so simple hash works here */ + return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 >> 16) ^ (scidd->scid.u64 << 1) ^ scidd->dir; +} + #endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ diff --git a/plugins/askrene/explain_failure.c b/plugins/askrene/explain_failure.c new file mode 100644 index 000000000000..b8ac907d132d --- /dev/null +++ b/plugins/askrene/explain_failure.c @@ -0,0 +1,291 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define NO_USABLE_PATHS_STRING "We could not find a usable set of paths." + +/* Dijkstra, reduced to ignore anything but connectivity */ +static bool always_true(const struct gossmap *map, + const struct gossmap_chan *c, + int dir, + struct amount_msat amount, + void *unused) +{ + return true; +} + +static u64 route_score_one(struct amount_msat fee UNUSED, + struct amount_msat risk UNUSED, + struct amount_msat total UNUSED, + int dir UNUSED, + const struct gossmap_chan *c UNUSED) +{ + return 1; +} + +/* This mirrors get_constraints() */ +static const char *why_max_constrained(const tal_t *ctx, + const struct route_query *rq, + struct short_channel_id_dir *scidd, + struct amount_msat amount) +{ + char *ret = NULL; + const char *reservations; + const struct layer *constrains = NULL; + struct amount_msat max = amount; + + /* Figure out the layer that constrains us (most) */ + for (size_t i = 0; i < tal_count(rq->layers); i++) { + struct amount_msat min = AMOUNT_MSAT(0), new_max = max; + + layer_apply_constraints(rq->layers[i], scidd, &min, &new_max); + if (!amount_msat_eq(new_max, max)) + constrains = rq->layers[i]; + max = new_max; + } + + if (constrains) { + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, ", "); + tal_append_fmt(&ret, "layer %s says max is %s", + layer_name(constrains), + fmt_amount_msat(tmpctx, max)); + } + + reservations = fmt_reservations(tmpctx, rq->reserved, scidd); + if (reservations) { + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, " and "); + tal_append_fmt(&ret, "already reserved %s", reservations); + } + + /* This seems unlikely, but don't return NULL. */ + if (!ret) + ret = tal_fmt(ctx, "is constrained"); + return ret; +} + +struct stat { + size_t num_channels; + struct amount_msat capacity; +}; + +struct node_stats { + struct stat total, gossip_known, enabled; +}; + +enum node_direction { + INTO_NODE, + OUT_OF_NODE, +}; + +static void add_stat(struct stat *stat, + struct amount_msat amount) +{ + stat->num_channels++; + if (!amount_msat_accumulate(&stat->capacity, amount)) + abort(); +} + +static void node_stats(const struct route_query *rq, + const struct gossmap_node *node, + enum node_direction node_direction, + struct node_stats *stats) +{ + memset(stats, 0, sizeof(*stats)); + for (size_t i = 0; i < node->num_chans; i++) { + int dir; + struct gossmap_chan *c; + struct amount_msat cap_msat; + + c = gossmap_nth_chan(rq->gossmap, node, i, &dir); + cap_msat = gossmap_chan_get_capacity(rq->gossmap, c); + + if (node_direction == INTO_NODE) + dir = !dir; + + add_stat(&stats->total, cap_msat); + if (gossmap_chan_set(c, dir)) + add_stat(&stats->gossip_known, cap_msat); + if (c->half[dir].enabled) + add_stat(&stats->enabled, cap_msat); + } +} + +static const char *check_capacity(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *node, + enum node_direction node_direction, + struct amount_msat amount, + const char *name) +{ + struct node_stats stats; + + node_stats(rq, node, node_direction, &stats); + if (amount_msat_greater(amount, stats.total.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " Total %s capacity is only %s" + " (in %zu channels).", + name, + fmt_amount_msat(tmpctx, stats.total.capacity), + stats.total.num_channels); + } + if (amount_msat_greater(amount, stats.gossip_known.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " Missing gossip for %s: only known %zu/%zu channels, leaving capacity only %s of %s.", + name, + stats.gossip_known.num_channels, + stats.total.num_channels, + fmt_amount_msat(tmpctx, stats.gossip_known.capacity), + fmt_amount_msat(tmpctx, stats.total.capacity)); + } + if (amount_msat_greater(amount, stats.enabled.capacity)) { + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " The %s has disabled %zu of %zu channels, leaving capacity only %s of %s.", + name, + stats.total.num_channels - stats.enabled.num_channels, + stats.total.num_channels, + fmt_amount_msat(tmpctx, stats.enabled.capacity), + fmt_amount_msat(tmpctx, stats.total.capacity)); + } + return NULL; +} + +/* Return description of why scidd is disabled scidd */ +static const char *describe_disabled(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_chan *c, + const struct short_channel_id_dir *scidd) +{ + for (int i = tal_count(rq->layers) - 1; i >= 0; i--) { + struct gossmap_node *dst = gossmap_nth_node(rq->gossmap, c, !scidd->dir); + struct node_id dstid; + + gossmap_node_get_id(rq->gossmap, dst, &dstid); + if (layer_disables_node(rq->layers[i], &dstid)) + return tal_fmt(ctx, "leads to node disabled by layer %s.", + layer_name(rq->layers[i])); + else if (layer_disables_chan(rq->layers[i], scidd)) { + return tal_fmt(ctx, "marked disabled by layer %s.", + layer_name(rq->layers[i])); + } + } + + return tal_fmt(ctx, "marked disabled by gossip message."); +} + +static const char *describe_capacity(const tal_t *ctx, + const struct route_query *rq, + const struct short_channel_id_dir *scidd, + struct amount_msat amount) +{ + for (int i = tal_count(rq->layers) - 1; i >= 0; i--) { + if (layer_created(rq->layers[i], scidd->scid)) { + return tal_fmt(ctx, " (created by layer %s) isn't big enough to carry %s.", + layer_name(rq->layers[i]), + fmt_amount_msat(tmpctx, amount)); + } + } + + return tal_fmt(ctx, "isn't big enough to carry %s.", + fmt_amount_msat(tmpctx, amount)); +} + +/* We failed to find a flow at all. Why? */ +const char *explain_failure(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount) +{ + const struct route_hop *hops; + const struct dijkstra *dij; + char *path; + const char *cap_check; + + /* Do we have enough funds? */ + cap_check = check_capacity(ctx, rq, srcnode, OUT_OF_NODE, + amount, "source"); + if (cap_check) + return cap_check; + + /* Does destination have enough capacity? */ + cap_check = check_capacity(ctx, rq, dstnode, INTO_NODE, + amount, "destination"); + if (cap_check) + return cap_check; + + /* OK, fall back to telling them why didn't shortest path + * work. This covers the "but I have a direct channel!" + * case. */ + dij = dijkstra(tmpctx, rq->gossmap, dstnode, AMOUNT_MSAT(0), 0, + always_true, route_score_one, NULL); + hops = route_from_dijkstra(tmpctx, rq->gossmap, dij, srcnode, + AMOUNT_MSAT(0), 0); + if (!hops) + return tal_fmt(ctx, "There is no connection between source and destination at all"); + + /* Description of shortest path */ + path = tal_strdup(tmpctx, ""); + for (size_t i = 0; i < tal_count(hops); i++) { + tal_append_fmt(&path, "%s%s", + i > 0 ? "->" : "", + fmt_short_channel_id(tmpctx, hops[i].scid)); + } + + /* Now walk through this: is it disabled? Insuff capacity? */ + for (size_t i = 0; i < tal_count(hops); i++) { + const char *explanation; + struct short_channel_id_dir scidd; + struct gossmap_chan *c; + struct amount_msat cap_msat, min, max, htlc_max; + + scidd.scid = hops[i].scid; + scidd.dir = hops[i].direction; + c = gossmap_find_chan(rq->gossmap, &scidd.scid); + cap_msat = gossmap_chan_get_capacity(rq->gossmap, c); + get_constraints(rq, c, scidd.dir, &min, &max); + htlc_max = amount_msat(fp16_to_u64(c->half[scidd.dir].htlc_max)); + + if (!gossmap_chan_set(c, scidd.dir)) + explanation = "has no gossip"; + else if (!c->half[scidd.dir].enabled) + explanation = describe_disabled(tmpctx, rq, c, &scidd); + else if (amount_msat_greater(amount, cap_msat)) + explanation = describe_capacity(tmpctx, rq, &scidd, amount); + else if (amount_msat_greater(amount, max)) + explanation = why_max_constrained(tmpctx, rq, + &scidd, amount); + else if (amount_msat_greater(amount, htlc_max)) + explanation = tal_fmt(tmpctx, + "exceeds htlc_maximum_msat ~%s", + fmt_amount_msat(tmpctx, htlc_max)); + else + continue; + + return tal_fmt(ctx, + NO_USABLE_PATHS_STRING + " The shortest path is %s, but %s %s", + path, + fmt_short_channel_id_dir(tmpctx, &scidd), + explanation); + } + + return tal_fmt(ctx, + "Actually, I'm not sure why we didn't find the" + " obvious route %s: perhaps this is a bug?", + path); +} diff --git a/plugins/askrene/explain_failure.h b/plugins/askrene/explain_failure.h new file mode 100644 index 000000000000..ed470f8298dc --- /dev/null +++ b/plugins/askrene/explain_failure.h @@ -0,0 +1,16 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H +#define LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H +#include "config.h" +#include + +struct route_query; +struct gossmap_node; + +/* When MCF returns nothing, try to explain why */ +const char *explain_failure(const tal_t *ctx, + const struct route_query *rq, + const struct gossmap_node *srcnode, + const struct gossmap_node *dstnode, + struct amount_msat amount); + +#endif /* LIGHTNING_PLUGINS_ASKRENE_EXPLAIN_FAILURE_H */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 3991716dc988..17cadc8e8983 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -14,38 +14,44 @@ struct local_channel { struct node_id n1, n2; struct short_channel_id scid; struct amount_msat capacity; +}; + +struct local_update { + struct short_channel_id_dir scidd; - struct added_channel_half { - /* Other fields only valid if this is true */ - bool enabled; - u16 delay; - u32 proportional_fee; - struct amount_msat base_fee; - struct amount_msat htlc_min, htlc_max; - } half[2]; + /* Non-null fields apply. */ + const bool *enabled; + const u16 *delay; + const u32 *proportional_fee; + const struct amount_msat *base_fee; + const struct amount_msat *htlc_min, *htlc_max; }; -static const struct constraint_key * -constraint_key(const struct constraint *c) -{ - return &c->key; -} +/* A constraint reflects something we learned about a channel */ +struct constraint { + struct short_channel_id_dir scidd; + /* Time this constraint was last updated */ + u64 timestamp; + /* Non-zero means set */ + struct amount_msat min; + /* Non-0xFFFFF.... means set */ + struct amount_msat max; +}; -static size_t hash_constraint_key(const struct constraint_key *key) +static const struct short_channel_id_dir * +constraint_scidd(const struct constraint *c) { - /* scids cost money to generate, so simple hash works here */ - return (key->scidd.scid.u64 >> 32) ^ (key->scidd.scid.u64) - ^ (key->scidd.dir << 1) ^ (key->type); + return &c->scidd; } -static inline bool constraint_eq_key(const struct constraint *c, - const struct constraint_key *key) +static inline bool constraint_eq_scidd(const struct constraint *c, + const struct short_channel_id_dir *scidd) { - return short_channel_id_dir_eq(&key->scidd, &c->key.scidd) && key->type == c->key.type; + return short_channel_id_dir_eq(scidd, &c->scidd); } -HTABLE_DEFINE_TYPE(struct constraint, constraint_key, hash_constraint_key, - constraint_eq_key, constraint_hash); +HTABLE_DEFINE_TYPE(struct constraint, constraint_scidd, hash_scidd, + constraint_eq_scidd, constraint_hash); static struct short_channel_id local_channel_scid(const struct local_channel *lc) @@ -68,6 +74,21 @@ static inline bool local_channel_eq_scid(const struct local_channel *lc, HTABLE_DEFINE_TYPE(struct local_channel, local_channel_scid, hash_scid, local_channel_eq_scid, local_channel_hash); +static const struct short_channel_id_dir * +local_update_scidd(const struct local_update *lu) +{ + return &lu->scidd; +} + +static inline bool local_update_eq_scidd(const struct local_update *lu, + const struct short_channel_id_dir *scidd) +{ + return short_channel_id_dir_eq(scidd, &lu->scidd); +} + +HTABLE_DEFINE_TYPE(struct local_update, local_update_scidd, hash_scidd, + local_update_eq_scidd, local_update_hash); + struct layer { /* Inside global list of layers */ struct list_node list; @@ -78,6 +99,9 @@ struct layer { /* Completely made up local additions, indexed by scid */ struct local_channel_hash *local_channels; + /* Modifications to channels, indexed by scidd */ + struct local_update_hash *local_updates; + /* Additional info, indexed by scid+dir */ struct constraint_hash *constraints; @@ -92,6 +116,8 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) l->name = tal_strdup(l, name); l->local_channels = tal(l, struct local_channel_hash); local_channel_hash_init(l->local_channels); + l->local_updates = tal(l, struct local_update_hash); + local_update_hash_init(l->local_updates); l->constraints = tal(l, struct constraint_hash); constraint_hash_init(l->constraints); l->disabled_nodes = tal_arr(l, struct node_id, 0); @@ -99,27 +125,19 @@ struct layer *new_temp_layer(const tal_t *ctx, const char *name) return l; } +static void destroy_layer(struct layer *l, struct askrene *askrene) +{ + list_del_from(&askrene->layers, &l->list); +} + struct layer *new_layer(struct askrene *askrene, const char *name) { struct layer *l = new_temp_layer(askrene, name); list_add(&askrene->layers, &l->list); + tal_add_destructor2(l, destroy_layer, askrene); return l; } -/* Swap if necessary to make into BOLT-7 order. Return direction. */ -static int canonicalize_node_order(const struct node_id **n1, - const struct node_id **n2) -{ - const struct node_id *tmp; - - if (node_id_cmp(*n1, *n2) < 0) - return 0; - tmp = *n2; - *n2 = *n1; - *n1 = tmp; - return 1; -} - struct layer *find_layer(struct askrene *askrene, const char *name) { struct layer *l; @@ -142,63 +160,77 @@ static struct local_channel *new_local_channel(struct layer *layer, struct amount_msat capacity) { struct local_channel *lc = tal(layer, struct local_channel); - lc->n1 = *n1; - lc->n2 = *n2; + + /* Swap if necessary to make into BOLT-7 order. */ + if (node_id_cmp(n1, n2) < 0) { + lc->n1 = *n1; + lc->n2 = *n2; + } else { + lc->n1 = *n2; + lc->n2 = *n1; + } lc->scid = scid; lc->capacity = capacity; - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) - lc->half[i].enabled = false; - local_channel_hash_add(layer->local_channels, lc); return lc; } -bool layer_check_local_channel(const struct local_channel *lc, - const struct node_id *n1, - const struct node_id *n2, - struct amount_msat capacity) +void layer_add_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity) { - canonicalize_node_order(&n1, &n2); - return node_id_eq(&lc->n1, n1) - && node_id_eq(&lc->n2, n2) - && amount_msat_eq(lc->capacity, capacity); + assert(!local_channel_hash_get(layer->local_channels, scid)); + new_local_channel(layer, src, dst, scid, capacity); } -/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ -void layer_update_local_channel(struct layer *layer, - const struct node_id *src, - const struct node_id *dst, - struct short_channel_id scid, - struct amount_msat capacity, - struct amount_msat base_fee, - u32 proportional_fee, - u16 delay, - struct amount_msat htlc_min, - struct amount_msat htlc_max) +void layer_add_update_channel(struct layer *layer, + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay) { - struct local_channel *lc = local_channel_hash_get(layer->local_channels, scid); - int dir = canonicalize_node_order(&src, &dst); - struct short_channel_id_dir scidd; - - if (lc) { - assert(layer_check_local_channel(lc, src, dst, capacity)); - } else { - lc = new_local_channel(layer, src, dst, scid, capacity); + struct local_update *lu; + + lu = local_update_hash_get(layer->local_updates, scidd); + if (!lu) { + lu = tal(layer, struct local_update); + lu->scidd = *scidd; + lu->enabled = NULL; + lu->delay = NULL; + lu->proportional_fee = NULL; + lu->base_fee = lu->htlc_min = lu->htlc_max = NULL; + local_update_hash_add(layer->local_updates, lu); + } + if (enabled) { + tal_free(lu->enabled); + lu->enabled = tal_dup(lu, bool, enabled); + } + if (htlc_min) { + tal_free(lu->htlc_min); + lu->htlc_min = tal_dup(lu, struct amount_msat, htlc_min); + } + if (htlc_max) { + tal_free(lu->htlc_max); + lu->htlc_max = tal_dup(lu, struct amount_msat, htlc_max); + } + if (base_fee) { + tal_free(lu->base_fee); + lu->base_fee = tal_dup(lu, struct amount_msat, base_fee); + } + if (proportional_fee) { + tal_free(lu->proportional_fee); + lu->proportional_fee = tal_dup(lu, u32, proportional_fee); + } + if (delay) { + tal_free(lu->delay); + lu->delay = tal_dup(lu, u16, delay); } - - lc->half[dir].enabled = true; - lc->half[dir].htlc_min = htlc_min; - lc->half[dir].htlc_max = htlc_max; - lc->half[dir].base_fee = base_fee; - lc->half[dir].proportional_fee = proportional_fee; - lc->half[dir].delay = delay; - - /* We always add an explicit constraint for local channels, to simplify - * lookups. You can tell it's a fake one by the timestamp. */ - scidd.scid = scid; - scidd.dir = dir; - layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, UINT64_MAX, capacity); } struct amount_msat local_channel_capacity(const struct local_channel *lc) @@ -212,50 +244,43 @@ const struct local_channel *layer_find_local_channel(const struct layer *layer, return local_channel_hash_get(layer->local_channels, scid); } -static struct constraint *layer_find_constraint_nonconst(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type) +void layer_apply_constraints(const struct layer *layer, + const struct short_channel_id_dir *scidd, + struct amount_msat *min, + struct amount_msat *max) { - struct constraint_key k = { *scidd, type }; - return constraint_hash_get(layer->constraints, &k); -} + struct constraint *c; + struct constraint_hash_iter cit; -/* Public one returns const */ -const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type) -{ - return layer_find_constraint_nonconst(layer, scidd, type); + /* We can have more than one: apply them all! */ + for (c = constraint_hash_getfirst(layer->constraints, scidd, &cit); + c; + c = constraint_hash_getnext(layer->constraints, scidd, &cit)) { + *min = amount_msat_max(*min, c->min); + *max = amount_msat_min(*max, c->max); + } } -const struct constraint *layer_update_constraint(struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type, - u64 timestamp, - struct amount_msat limit) +const struct constraint *layer_add_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + u64 timestamp, + const struct amount_msat *min, + const struct amount_msat *max) { - struct constraint *c = layer_find_constraint_nonconst(layer, scidd, type); - if (!c) { - c = tal(layer, struct constraint); - c->key.scidd = *scidd; - c->key.type = type; - c->limit = limit; - constraint_hash_add(layer->constraints, c); - } else { - switch (type) { - case CONSTRAINT_MIN: - /* Increase minimum? */ - if (amount_msat_greater(limit, c->limit)) - c->limit = limit; - break; - case CONSTRAINT_MAX: - /* Decrease maximum? */ - if (amount_msat_less(limit, c->limit)) - c->limit = limit; - break; - } - } + struct constraint *c = tal(layer, struct constraint); + c->scidd = *scidd; + + if (min) + c->min = *min; + else + c->min = AMOUNT_MSAT(0); + if (max) + c->max = *max; + else + c->max = AMOUNT_MSAT(UINT64_MAX); c->timestamp = timestamp; + + constraint_hash_add(layer->constraints, c); return c; } @@ -269,7 +294,7 @@ void layer_clear_overridden_capacities(const struct layer *layer, for (con = constraint_hash_first(layer->constraints, &conit); con; con = constraint_hash_next(layer->constraints, &conit)) { - struct gossmap_chan *c = gossmap_find_chan(gossmap, &con->key.scidd.scid); + struct gossmap_chan *c = gossmap_find_chan(gossmap, &con->scidd.scid); size_t idx; if (!c) continue; @@ -304,11 +329,12 @@ void layer_add_disabled_node(struct layer *layer, const struct node_id *node) void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, - bool zero_cost, struct gossmap_localmods *localmods) { const struct local_channel *lc; struct local_channel_hash_iter lcit; + const struct local_update *lu; + struct local_update_hash_iter luit; /* First, disable all channels into blocked nodes (local updates * can add new ones)! */ @@ -319,73 +345,79 @@ void layer_add_localmods(const struct layer *layer, if (!node) continue; for (size_t n = 0; n < node->num_chans; n++) { - struct short_channel_id scid; + struct short_channel_id_dir scidd; struct gossmap_chan *c; - int dir; - c = gossmap_nth_chan(gossmap, node, n, &dir); - scid = gossmap_chan_scid(gossmap, c); + bool enabled = false; + c = gossmap_nth_chan(gossmap, node, n, &scidd.dir); + scidd.scid = gossmap_chan_scid(gossmap, c); /* Disabled zero-capacity on incoming */ gossmap_local_updatechan(localmods, - scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(0), - 0, - 0, - 0, - false, - !dir); + &scidd, + &enabled, + NULL, NULL, NULL, NULL, NULL); } } + /* Now create new channels */ for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { gossmap_local_addchan(localmods, - &lc->n1, &lc->n2, lc->scid, NULL); - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { - u64 base, propfee, delay; - if (!lc->half[i].enabled) - continue; - if (zero_cost) { - base = propfee = delay = 0; - } else { - base = lc->half[i].base_fee.millisatoshis; /* Raw: gossmap */ - propfee = lc->half[i].proportional_fee; - delay = lc->half[i].delay; - } - gossmap_local_updatechan(localmods, lc->scid, - lc->half[i].htlc_min, - lc->half[i].htlc_max, - base, propfee, delay, - true, - i); - } + &lc->n1, &lc->n2, lc->scid, lc->capacity, + NULL); + } + + /* Now update channels */ + /* Now modify channels, if they exist */ + for (lu = local_update_hash_first(layer->local_updates, &luit); + lu; + lu = local_update_hash_next(layer->local_updates, &luit)) { + gossmap_local_updatechan(localmods, &lu->scidd, + lu->enabled, + lu->htlc_min, + lu->htlc_max, + lu->base_fee, + lu->proportional_fee, + lu->delay); } } static void json_add_local_channel(struct json_stream *response, const char *fieldname, - const struct local_channel *lc, - int dir) + const struct local_channel *lc) { json_object_start(response, fieldname); - - if (dir == 0) { - json_add_node_id(response, "source", &lc->n1); - json_add_node_id(response, "destination", &lc->n2); - } else { - json_add_node_id(response, "source", &lc->n2); - json_add_node_id(response, "destination", &lc->n1); - } + json_add_node_id(response, "source", &lc->n1); + json_add_node_id(response, "destination", &lc->n2); json_add_short_channel_id(response, "short_channel_id", lc->scid); json_add_amount_msat(response, "capacity_msat", lc->capacity); - json_add_amount_msat(response, "htlc_minimum_msat", lc->half[dir].htlc_min); - json_add_amount_msat(response, "htlc_maximum_msat", lc->half[dir].htlc_max); - json_add_amount_msat(response, "fee_base_msat", lc->half[dir].base_fee); - json_add_u32(response, "fee_proportional_millionths", lc->half[dir].proportional_fee); - json_add_u32(response, "delay", lc->half[dir].delay); + json_object_end(response); +} +static void json_add_local_update(struct json_stream *response, + const char *fieldname, + const struct local_update *lu) +{ + json_object_start(response, fieldname); + json_add_short_channel_id_dir(response, "short_channel_id_dir", + lu->scidd); + if (lu->enabled) + json_add_bool(response, "enabled", *lu->enabled); + if (lu->htlc_min) + json_add_amount_msat(response, + "htlc_minimum_msat", *lu->htlc_min); + if (lu->htlc_max) + json_add_amount_msat(response, + "htlc_maximum_msat", *lu->htlc_max); + if (lu->base_fee) + json_add_amount_msat(response, "fee_base_msat", *lu->base_fee); + if (lu->proportional_fee) + json_add_u32(response, + "fee_proportional_millionths", + *lu->proportional_fee); + if (lu->delay) + json_add_u32(response, "cltv_expiry_delta", *lu->delay); json_object_end(response); } @@ -397,17 +429,12 @@ void json_add_constraint(struct json_stream *js, json_object_start(js, fieldname); if (layer) json_add_string(js, "layer", layer->name); - json_add_short_channel_id(js, "short_channel_id", c->key.scidd.scid); - json_add_u32(js, "direction", c->key.scidd.dir); + json_add_short_channel_id_dir(js, "short_channel_id_dir", c->scidd); json_add_u64(js, "timestamp", c->timestamp); - switch (c->key.type) { - case CONSTRAINT_MIN: - json_add_amount_msat(js, "minimum_msat", c->limit); - break; - case CONSTRAINT_MAX: - json_add_amount_msat(js, "maximum_msat", c->limit); - break; - } + if (!amount_msat_is_zero(c->min)) + json_add_amount_msat(js, "minimum_msat", c->min); + if (!amount_msat_eq(c->max, AMOUNT_MSAT(UINT64_MAX))) + json_add_amount_msat(js, "maximum_msat", c->max); json_object_end(js); } @@ -417,6 +444,8 @@ static void json_add_layer(struct json_stream *js, { struct local_channel_hash_iter lcit; const struct local_channel *lc; + const struct local_update *lu; + struct local_update_hash_iter luit; struct constraint_hash_iter conit; const struct constraint *c; @@ -430,10 +459,14 @@ static void json_add_layer(struct json_stream *js, for (lc = local_channel_hash_first(layer->local_channels, &lcit); lc; lc = local_channel_hash_next(layer->local_channels, &lcit)) { - for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { - if (lc->half[i].enabled) - json_add_local_channel(js, NULL, lc, i); - } + json_add_local_channel(js, NULL, lc); + } + json_array_end(js); + json_array_start(js, "channel_updates"); + for (lu = local_update_hash_first(layer->local_updates, &luit); + lu; + lu = local_update_hash_next(layer->local_updates, &luit)) { + json_add_local_update(js, NULL, lu); } json_array_end(js); json_array_start(js, "constraints"); @@ -452,19 +485,44 @@ static void json_add_layer(struct json_stream *js, void json_add_layers(struct json_stream *js, struct askrene *askrene, const char *fieldname, - const char *layername) + const struct layer *layer) { struct layer *l; json_array_start(js, fieldname); list_for_each(&askrene->layers, l, list) { - if (layername && !streq(l->name, layername)) + if (layer && l != layer) continue; json_add_layer(js, NULL, l); } json_array_end(js); } +bool layer_created(const struct layer *layer, struct short_channel_id scid) +{ + return local_channel_hash_get(layer->local_channels, scid); +} + +bool layer_disables_chan(const struct layer *layer, + const struct short_channel_id_dir *scidd) +{ + const struct local_update *lu; + + lu = local_update_hash_get(layer->local_updates, scidd); + + return (lu && lu->enabled && *lu->enabled == false); +} + +bool layer_disables_node(const struct layer *layer, + const struct node_id *node) +{ + for (size_t i = 0; i < tal_count(layer->disabled_nodes); i++) { + if (node_id_eq(&layer->disabled_nodes[i], node)) + return true; + } + return false; +} + void layer_memleak_mark(struct askrene *askrene, struct htable *memtable) { struct layer *l; diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 7acea220c072..0bac5744a62b 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -17,24 +17,6 @@ struct askrene; struct layer; struct json_stream; -enum constraint_type { - CONSTRAINT_MIN, - CONSTRAINT_MAX, -}; - -struct constraint_key { - struct short_channel_id_dir scidd; - enum constraint_type type; -}; - -/* A constraint reflects something we learned about a channel */ -struct constraint { - struct constraint_key key; - /* Time this constraint was last updated */ - u64 timestamp; - struct amount_msat limit; -}; - /* Look up a layer by name. */ struct layer *find_layer(struct askrene *askrene, const char *name); @@ -60,17 +42,22 @@ bool layer_check_local_channel(const struct local_channel *lc, const struct node_id *n2, struct amount_msat capacity); -/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ -void layer_update_local_channel(struct layer *layer, - const struct node_id *src, - const struct node_id *dst, - struct short_channel_id scid, - struct amount_msat capacity, - struct amount_msat base_fee, - u32 proportional_fee, - u16 delay, - struct amount_msat htlc_min, - struct amount_msat htlc_max); +/* Add a local channel to a layer! */ +void layer_add_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity); + +/* Update details on a channel (could be in this layer, or another) */ +void layer_add_update_channel(struct layer *layer, + const struct short_channel_id_dir *scidd, + const bool *enabled, + const struct amount_msat *htlc_min, + const struct amount_msat *htlc_max, + const struct amount_msat *base_fee, + const u32 *proportional_fee, + const u16 *delay); /* If any capacities of channels are limited, unset the corresponding element in * the capacities[] array */ @@ -78,22 +65,23 @@ void layer_clear_overridden_capacities(const struct layer *layer, const struct gossmap *gossmap, fp16_t *capacities); -/* Find a constraint in a layer. */ -const struct constraint *layer_find_constraint(const struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type); - -/* Add/update a constraint on a layer. */ -const struct constraint *layer_update_constraint(struct layer *layer, - const struct short_channel_id_dir *scidd, - enum constraint_type type, - u64 timestamp, - struct amount_msat limit); - -/* Add local channels from this layer. zero_cost means set fees and delay to 0. */ +/* Apply constraints from a layer (reduce min, increase max). */ +void layer_apply_constraints(const struct layer *layer, + const struct short_channel_id_dir *scidd, + struct amount_msat *min, + struct amount_msat *max) + NO_NULL_ARGS; + +/* Add one or more constraints on a layer. */ +const struct constraint *layer_add_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + u64 timestamp, + const struct amount_msat *min, + const struct amount_msat *max); + +/* Add local channels from this layer. */ void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, - bool zero_cost, struct gossmap_localmods *localmods); /* Remove constraints older then cutoff: returns num removed. */ @@ -102,11 +90,11 @@ size_t layer_trim_constraints(struct layer *layer, u64 cutoff); /* Add a disabled node to a layer. */ void layer_add_disabled_node(struct layer *layer, const struct node_id *node); -/* Print out a json object per layer, or all if layer is NULL */ +/* Print out a json object for this layer, or all if layer is NULL */ void json_add_layers(struct json_stream *js, struct askrene *askrene, const char *fieldname, - const char *layername); + const struct layer *layer); /* Print a single constraint */ void json_add_constraint(struct json_stream *js, @@ -114,6 +102,15 @@ void json_add_constraint(struct json_stream *js, const struct constraint *c, const struct layer *layer); +/* For explain_failure: did this layer create this scid? */ +bool layer_created(const struct layer *layer, struct short_channel_id scid); + +/* For explain_failure: did this layer disable this channel? */ +bool layer_disables_chan(const struct layer *layer, const struct short_channel_id_dir *scidd); + +/* For explain_failure: did this layer disable this node? */ +bool layer_disables_node(const struct layer *layer, const struct node_id *node); + /* Scan for memleaks */ void layer_memleak_mark(struct askrene *askrene, struct htable *memtable); #endif /* LIGHTNING_PLUGINS_ASKRENE_LAYER_H */ diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 18b62ab7570e..aaa914b03afe 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -393,7 +392,7 @@ static s64 get_arc_flow( static u32 arc_tail(const struct linear_network *linear_network, const struct arc arc) { - assert(arc.idx < tal_count(linear_network->arc_tail_node)); + assert(arc.idx < linear_network->max_num_arcs); return linear_network->arc_tail_node[ arc.idx ]; } /* Helper function. @@ -402,7 +401,7 @@ static u32 arc_head(const struct linear_network *linear_network, const struct arc arc) { const struct arc dual = arc_dual(arc); - assert(dual.idx < tal_count(linear_network->arc_tail_node)); + assert(dual.idx < linear_network->max_num_arcs); return linear_network->arc_tail_node[dual.idx]; } @@ -413,7 +412,7 @@ static struct arc node_adjacency_begin( const struct linear_network * linear_network, const u32 node) { - assert(node < tal_count(linear_network->node_adjacency_first_arc)); + assert(node < linear_network->max_num_nodes); return linear_network->node_adjacency_first_arc[node]; } @@ -430,7 +429,7 @@ static struct arc node_adjacency_next( const struct linear_network *linear_network, const struct arc arc) { - assert(arc.idx < tal_count(linear_network->node_adjacency_next_arc)); + assert(arc.idx < linear_network->max_num_arcs); return linear_network->node_adjacency_next_arc[arc.idx]; } @@ -541,16 +540,13 @@ static void linear_network_add_adjacenct_arc( const u32 node_idx, const struct arc arc) { - assert(arc.idx < tal_count(linear_network->arc_tail_node)); + assert(arc.idx < linear_network->max_num_arcs); linear_network->arc_tail_node[arc.idx] = node_idx; - assert(node_idx < tal_count(linear_network->node_adjacency_first_arc)); + assert(node_idx < linear_network->max_num_nodes); const struct arc first_arc = linear_network->node_adjacency_first_arc[node_idx]; - assert(arc.idx < tal_count(linear_network->node_adjacency_next_arc)); linear_network->node_adjacency_next_arc[arc.idx]=first_arc; - - assert(node_idx < tal_count(linear_network->node_adjacency_first_arc)); linear_network->node_adjacency_first_arc[node_idx]=arc; } @@ -597,23 +593,23 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params) linear_network->max_num_nodes = max_num_nodes; linear_network->arc_tail_node = tal_arr(linear_network,u32,max_num_arcs); - for(size_t i=0;iarc_tail_node);++i) + for(size_t i=0;iarc_tail_node[i]=INVALID_INDEX; linear_network->node_adjacency_next_arc = tal_arr(linear_network,struct arc,max_num_arcs); - for(size_t i=0;inode_adjacency_next_arc);++i) + for(size_t i=0;inode_adjacency_next_arc[i].idx=INVALID_INDEX; linear_network->node_adjacency_first_arc = tal_arr(linear_network,struct arc,max_num_nodes); - for(size_t i=0;inode_adjacency_first_arc);++i) + for(size_t i=0;inode_adjacency_first_arc[i].idx=INVALID_INDEX; linear_network->arc_prob_cost = tal_arr(linear_network,s64,max_num_arcs); - for(size_t i=0;iarc_prob_cost);++i) + for(size_t i=0;iarc_prob_cost[i]=INFINITE; linear_network->arc_fee_cost = tal_arr(linear_network,s64,max_num_arcs); - for(size_t i=0;iarc_fee_cost);++i) + for(size_t i=0;iarc_fee_cost[i]=INFINITE; linear_network->capacity = tal_arrz(linear_network,s64,max_num_arcs); @@ -630,7 +626,7 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params) const struct gossmap_chan *c = gossmap_nth_chan(gossmap, node, j, &half); - if (!gossmap_chan_set(c, half)) + if (!gossmap_chan_set(c, half) || !c->half[half].enabled) continue; const u32 chan_id = gossmap_chan_idx(gossmap, c); @@ -687,13 +683,6 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params) return linear_network; } -/* Simple queue to traverse the network. */ -struct queue_data -{ - u32 idx; - struct lqueue_link ql; -}; - // TODO(eduardo): unit test this /* Finds an admissible path from source to target, traversing arcs in the * residual network with capacity greater than 0. @@ -705,25 +694,21 @@ find_admissible_path(const struct linear_network *linear_network, const u32 source, const u32 target, struct arc *prev) { bool target_found = false; + /* Simple linear queue of node indexes */ + u32 *queue = tal_arr(tmpctx, u32, linear_network->max_num_arcs); + size_t qstart, qend, prev_len = tal_count(prev); - for(size_t i=0;iidx = source; - lqueue_enqueue(&myqueue,qdata); + queue[0] = source; + qstart = 0; + qend = 1; - while(!lqueue_empty(&myqueue)) - { - qdata = lqueue_dequeue(&myqueue); - u32 cur = qdata->idx; - - tal_free(qdata); + while (qstart < qend) { + u32 cur = queue[qstart++]; if(cur==target) { @@ -741,17 +726,15 @@ find_admissible_path(const struct linear_network *linear_network, u32 next = arc_head(linear_network,arc); - assert(next < tal_count(prev)); + assert(next < prev_len); // if that node has been seen previously if(prev[next].idx!=INVALID_INDEX) continue; prev[next] = arc; - - qdata = tal(tmpctx, struct queue_data); - qdata->idx = next; - lqueue_enqueue(&myqueue,qdata); + assert(qend < linear_network->max_num_arcs); + queue[qend++] = next; } } return target_found; diff --git a/plugins/askrene/refine.c b/plugins/askrene/refine.c index e78eebf7e571..18596281594f 100644 --- a/plugins/askrene/refine.c +++ b/plugins/askrene/refine.c @@ -9,10 +9,6 @@ /* We (ab)use the reservation system to place temporary reservations * on channels while we are refining each flow. This has the effect * of making flows aware of each other. */ -struct reservations { - struct short_channel_id_dir *scidds; - struct amount_msat *amounts; -}; /* Get the scidd for the i'th hop in flow */ static void get_scidd(const struct gossmap *gossmap, @@ -24,50 +20,38 @@ static void get_scidd(const struct gossmap *gossmap, scidd->dir = flow->dirs[i]; } -static void destroy_reservations(struct reservations *r, struct askrene *askrene) +static void destroy_reservations(struct reserve_hop *rhops, struct askrene *askrene) { - assert(tal_count(r->scidds) == tal_count(r->amounts)); - if (reserves_remove(askrene->reserved, - r->scidds, r->amounts, - tal_count(r->scidds)) != tal_count(r->scidds)) { - plugin_err(askrene->plugin, "Failed to remove reservations?"); - } + for (size_t i = 0; i < tal_count(rhops); i++) + reserve_remove(askrene->reserved, &rhops[i]); } -static struct reservations *new_reservations(const tal_t *ctx, - struct route_query *rq) +static struct reserve_hop *new_reservations(const tal_t *ctx, + const struct route_query *rq) { - struct reservations *r = tal(ctx, struct reservations); - r->scidds = tal_arr(r, struct short_channel_id_dir, 0); - r->amounts = tal_arr(r, struct amount_msat, 0); + struct reserve_hop *rhops = tal_arr(ctx, struct reserve_hop, 0); /* Unreserve on free */ - tal_add_destructor2(r, destroy_reservations, get_askrene(rq->plugin)); - return r; + tal_add_destructor2(rhops, destroy_reservations, get_askrene(rq->plugin)); + return rhops; } /* Add reservation: we (ab)use this to temporarily avoid over-usage as * we refine. */ -static void add_reservation(struct reservations *r, +static void add_reservation(struct reserve_hop **reservations, struct route_query *rq, const struct flow *flow, size_t i, struct amount_msat amt) { - struct short_channel_id_dir scidd; + struct reserve_hop rhop; struct askrene *askrene = get_askrene(rq->plugin); size_t idx; - get_scidd(rq->gossmap, flow, i, &scidd); + get_scidd(rq->gossmap, flow, i, &rhop.scidd); + rhop.amount = amt; - /* This should not happen, but simply don't reserve if it does */ - if (!reserves_add(askrene->reserved, &scidd, &amt, 1)) { - plugin_log(rq->plugin, LOG_BROKEN, - "Failed to reserve %s in %s", - fmt_amount_msat(tmpctx, amt), - fmt_short_channel_id_dir(tmpctx, &scidd)); - return; - } + reserve_add(askrene->reserved, &rhop, rq->cmd->id); /* Set capacities entry to 0 so it get_constraints() looks in reserve. */ idx = gossmap_chan_idx(rq->gossmap, flow->path[i]); @@ -75,8 +59,7 @@ static void add_reservation(struct reservations *r, rq->capacities[idx] = 0; /* Record so destructor will unreserve */ - tal_arr_expand(&r->scidds, scidd); - tal_arr_expand(&r->amounts, amt); + tal_arr_expand(reservations, rhop); } /* We have a basic set of flows, but we need to add fees, and take into @@ -93,7 +76,7 @@ static void add_reservation(struct reservations *r, static const char *constrain_flow(const tal_t *ctx, struct route_query *rq, struct flow *flow, - struct reservations *reservations) + struct reserve_hop **reservations) { struct amount_msat msat; int decreased = -1; @@ -187,7 +170,7 @@ static const char *constrain_flow(const tal_t *ctx, /* Flow is now delivering `extra` additional msat, so modify reservations */ static void add_to_flow(struct flow *flow, struct route_query *rq, - struct reservations *reservations, + struct reserve_hop **reservations, struct amount_msat extra) { struct amount_msat orig, updated; @@ -299,7 +282,7 @@ refine_with_fees_and_limits(const tal_t *ctx, struct amount_msat deliver, struct flow ***flows) { - struct reservations *reservations = new_reservations(NULL, rq); + struct reserve_hop *reservations = new_reservations(NULL, rq); struct amount_msat more_to_deliver; const char *flow_constraint_error = NULL; const char *ret; @@ -317,7 +300,7 @@ refine_with_fees_and_limits(const tal_t *ctx, fmt_amount_msat(tmpctx, max)); } - flow_constraint_error = constrain_flow(tmpctx, rq, flow, reservations); + flow_constraint_error = constrain_flow(tmpctx, rq, flow, &reservations); if (!flow_constraint_error) { i++; continue; @@ -398,7 +381,7 @@ refine_with_fees_and_limits(const tal_t *ctx, } /* Make this flow deliver +extra, and modify reservations */ - add_to_flow(f, rq, reservations, extra); + add_to_flow(f, rq, &reservations, extra); /* Should not happen, since extra comes from div... */ if (!amount_msat_sub(&more_to_deliver, more_to_deliver, extra)) diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index fb822143c068..82c9096d3c27 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -1,28 +1,34 @@ #include "config.h" #include #include +#include #include +#include #include #include #include +/* Note! We can have multiple of these! */ +struct reserve { + /* What */ + struct reserve_hop rhop; + /* When */ + struct timemono timestamp; + /* ID of command which reserved it */ + const char *cmd_id; +}; + /* Hash table for reservations */ static const struct short_channel_id_dir * reserve_scidd(const struct reserve *r) { - return &r->scidd; -} - -static size_t hash_scidd(const struct short_channel_id_dir *scidd) -{ - /* scids cost money to generate, so simple hash works here */ - return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 >> 16) ^ (scidd->scid.u64 << 1) ^ scidd->dir; + return &r->rhop.scidd; } static bool reserve_eq_scidd(const struct reserve *r, const struct short_channel_id_dir *scidd) { - return short_channel_id_dir_eq(scidd, &r->scidd); + return short_channel_id_dir_eq(scidd, &r->rhop.scidd); } HTABLE_DEFINE_TYPE(struct reserve, reserve_scidd, hash_scidd, @@ -35,110 +41,140 @@ struct reserve_htable *new_reserve_htable(const tal_t *ctx) return reserved; } -/* Find a reservation for this scidd (if any!) */ -const struct reserve *find_reserve(const struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd) -{ - return reserve_htable_get(reserved, scidd); -} - -/* Create a new (empty) reservation */ -static struct reserve *new_reserve(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd) +void reserve_add(struct reserve_htable *reserved, + const struct reserve_hop *rhop, + const char *cmd_id TAKES) { struct reserve *r = tal(reserved, struct reserve); - - r->num_htlcs = 0; - r->amount = AMOUNT_MSAT(0); - r->scidd = *scidd; + r->rhop = *rhop; + r->timestamp = time_mono(); + r->cmd_id = tal_strdup(r, cmd_id); reserve_htable_add(reserved, r); - return r; } -static void del_reserve(struct reserve_htable *reserved, struct reserve *r) +bool reserve_remove(struct reserve_htable *reserved, + const struct reserve_hop *rhop) { - assert(r->num_htlcs == 0); + struct reserve *r; + struct reserve_htable_iter rit; - reserve_htable_del(reserved, r); - tal_free(r); -} + /* Note! This may remove the "wrong" one, but since they're only + * differentiated for debugging, that's OK */ + for (r = reserve_htable_getfirst(reserved, &rhop->scidd, &rit); + r; + r = reserve_htable_getnext(reserved, &rhop->scidd, &rit)) { + if (!amount_msat_eq(r->rhop.amount, rhop->amount)) + continue; -/* Add to existing reservation (false if would overflow). */ -static bool add(struct reserve *r, struct amount_msat amount) -{ - if (!amount_msat_accumulate(&r->amount, amount)) - return false; - r->num_htlcs++; - return true; + reserve_htable_del(reserved, r); + tal_free(r); + return true; + } + return false; } -static bool remove(struct reserve *r, struct amount_msat amount) +void reserves_clear_capacities(struct reserve_htable *reserved, + const struct gossmap *gossmap, + fp16_t *capacities) { - if (r->num_htlcs == 0) - return false; - if (!amount_msat_sub(&r->amount, r->amount, amount)) - return false; - r->num_htlcs--; - return true; + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_first(reserved, &rit); + r; + r = reserve_htable_next(reserved, &rit)) { + struct gossmap_chan *c = gossmap_find_chan(gossmap, &r->rhop.scidd.scid); + size_t idx; + if (!c) + continue; + idx = gossmap_chan_idx(gossmap, c); + if (idx < tal_count(capacities)) + capacities[idx] = 0; + } } -/* Atomically add to reserves, or fail. - * Returns offset of failure, or num on success */ -size_t reserves_add(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num) +void reserve_sub(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount) { - for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &scidds[i]); - if (!r) - r = new_reserve(reserved, &scidds[i]); - if (!add(r, amounts[i])) { - reserves_remove(reserved, scidds, amounts, i); - return i; - } + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + if (!amount_msat_sub(amount, *amount, r->rhop.amount)) + *amount = AMOUNT_MSAT(0); } - return num; } -/* Atomically remove from reserves, to fail. - * Returns offset of failure or tal_count(scidds) */ -size_t reserves_remove(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num) +bool reserve_accumulate(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount) { - for (size_t i = 0; i < num; i++) { - struct reserve *r = reserve_htable_get(reserved, &scidds[i]); - if (!r || !remove(r, amounts[i])) { - reserves_add(reserved, scidds, amounts, i); - return i; - } - if (r->num_htlcs == 0) - del_reserve(reserved, r); + struct reserve *r; + struct reserve_htable_iter rit; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + if (!amount_msat_add(amount, *amount, r->rhop.amount)) + return false; } - return num; + return true; } -void reserves_clear_capacities(struct reserve_htable *reserved, - const struct gossmap *gossmap, - fp16_t *capacities) +void json_add_reservations(struct json_stream *js, + const struct reserve_htable *reserved, + const char *fieldname) { struct reserve *r; struct reserve_htable_iter rit; + json_array_start(js, fieldname); for (r = reserve_htable_first(reserved, &rit); r; r = reserve_htable_next(reserved, &rit)) { - struct gossmap_chan *c = gossmap_find_chan(gossmap, &r->scidd.scid); - size_t idx; - if (!c) - continue; - idx = gossmap_chan_idx(gossmap, c); - if (idx < tal_count(capacities)) - capacities[idx] = 0; + json_object_start(js, NULL); + json_add_short_channel_id_dir(js, + "short_channel_id_dir", + r->rhop.scidd); + json_add_amount_msat(js, + "amount_msat", + r->rhop.amount); + json_add_u64(js, "age_in_seconds", + timemono_between(time_mono(), r->timestamp).ts.tv_sec); + json_add_string(js, "command_id", r->cmd_id); + json_object_end(js); + } + json_array_end(js); +} + +const char *fmt_reservations(const tal_t *ctx, + const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd) +{ + struct reserve *r; + struct reserve_htable_iter rit; + char *ret = NULL; + + for (r = reserve_htable_getfirst(reserved, scidd, &rit); + r; + r = reserve_htable_getnext(reserved, scidd, &rit)) { + u64 seconds; + if (!ret) + ret = tal_strdup(ctx, ""); + else + tal_append_fmt(&ret, ", "); + tal_append_fmt(&ret, "%s by command %s", + fmt_amount_msat(tmpctx, r->rhop.amount), r->cmd_id); + seconds = timemono_between(time_mono(), r->timestamp).ts.tv_sec; + /* Add a note if it's old */ + if (seconds > 0) + tal_append_fmt(&ret, " (%"PRIu64" seconds ago)", seconds); } + return ret; } void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable) diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index 7da617229c56..1c9e71eaaf60 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -8,39 +8,48 @@ #include #include -/* We reserve a path being used. This records how many and how much */ -struct reserve { - size_t num_htlcs; - struct short_channel_id_dir scidd; - struct amount_msat amount; -}; - /* Initialize hash table for reservations */ struct reserve_htable *new_reserve_htable(const tal_t *ctx); -/* Find a reservation for this scidd (if any!) */ -const struct reserve *find_reserve(const struct reserve_htable *reserved, - const struct short_channel_id_dir *scidd); +struct reserve_hop { + struct short_channel_id_dir scidd; + struct amount_msat amount; +}; -/* Atomically add to reserves, or fail. - * Returns offset of failure, or num on success */ -size_t reserves_add(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num); +/* Add a reservation */ +void reserve_add(struct reserve_htable *reserved, + const struct reserve_hop *rhop, + const char *cmd_id TAKES); -/* Atomically remove from reserves, to fail. - * Returns offset of failure or tal_count(scidds) */ -size_t reserves_remove(struct reserve_htable *reserved, - const struct short_channel_id_dir *scidds, - const struct amount_msat *amounts, - size_t num); +/* Try to remove a reservation, if it exists. */ +bool reserve_remove(struct reserve_htable *reserved, + const struct reserve_hop *rhop); /* Clear capacities array where we have reserves */ void reserves_clear_capacities(struct reserve_htable *reserved, const struct gossmap *gossmap, fp16_t *capacities); +/* Subtract any reserves for scidd from this amount */ +void reserve_sub(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount); + +/* Add any reserves for scidd to this amount */ +bool reserve_accumulate(const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd, + struct amount_msat *amount); + +/* To explain why we couldn't route */ +const char *fmt_reservations(const tal_t *ctx, + const struct reserve_htable *reserved, + const struct short_channel_id_dir *scidd); + +/* Print out a json object for all reservations */ +void json_add_reservations(struct json_stream *js, + const struct reserve_htable *reserved, + const char *fieldname); + /* Scan for memleaks */ void reserve_memleak_mark(struct askrene *askrene, struct htable *memtable); #endif /* LIGHTNING_PLUGINS_ASKRENE_RESERVE_H */ diff --git a/plugins/chanbackup.c b/plugins/chanbackup.c index ca8365495294..60efc5ce5344 100644 --- a/plugins/chanbackup.c +++ b/plugins/chanbackup.c @@ -757,6 +757,23 @@ static struct command_result *json_restorefrompeer(struct command *cmd, NULL); } +static struct command_result *json_getemergencyrecoverdata(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + u8 *filedata; + if (!param(cmd, buf, params, NULL)) + return command_param_failed(); + + struct json_stream *response; + + filedata = get_file_data(tmpctx, cmd->plugin); + response = jsonrpc_stream_success(cmd); + json_add_hex(response, "filedata", filedata, tal_bytelen(filedata)); + + return command_finished(cmd, response); +} + static const char *init(struct plugin *p, const char *buf UNUSED, const jsmntok_t *config UNUSED) @@ -817,6 +834,10 @@ static const struct plugin_command commands[] = { "emergencyrecover", json_emergencyrecover, }, + { + "getemergencyrecoverdata", + json_getemergencyrecoverdata, + }, { "restorefrompeer", json_restorefrompeer, diff --git a/plugins/clnrest/pyproject.toml b/plugins/clnrest/pyproject.toml index dd418ceca97f..8c2b15c03193 100644 --- a/plugins/clnrest/pyproject.toml +++ b/plugins/clnrest/pyproject.toml @@ -10,7 +10,7 @@ json5 = "^0.9.14" flask = "^2.3.3" flask-restx = "^1.1.0" gunicorn = "^21.2.0" -pyln-client = "^24.2" +pyln-client = "^24.8" flask-socketio = "^5.3.6" gevent = "^23.9.0.post1" gevent-websocket = "^0.10.1" diff --git a/plugins/establish_onion_path.c b/plugins/establish_onion_path.c index 9906a55ea28f..5aa723fb3093 100644 --- a/plugins/establish_onion_path.c +++ b/plugins/establish_onion_path.c @@ -95,7 +95,8 @@ gossmods_from_listpeers(const tal_t *ctx, struct node_id peer_id; const char *err; u8 *features = NULL; - struct short_channel_id fake_scid; + struct short_channel_id_dir fake_scidd; + bool enabled = true; err = json_scan(tmpctx, buf, peer, "{connected:%," @@ -112,13 +113,13 @@ gossmods_from_listpeers(const tal_t *ctx, continue; /* Add a fake channel */ - fake_scid.u64 = i; + fake_scidd.scid.u64 = i; + fake_scidd.dir = node_id_idx(self, &peer_id); - gossmap_local_addchan(mods, self, &peer_id, fake_scid, NULL); - gossmap_local_updatechan(mods, fake_scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(0), - 0, 0, 0, true, node_id_idx(self, &peer_id)); + gossmap_local_addchan(mods, self, &peer_id, fake_scidd.scid, + AMOUNT_MSAT(1000), NULL); + gossmap_local_updatechan(mods, &fake_scidd, &enabled, + NULL, NULL, NULL, NULL, NULL); } return mods; } diff --git a/plugins/grpc-plugin/Cargo.toml b/plugins/grpc-plugin/Cargo.toml index 32085e44c29a..5b8cd886b40c 100644 --- a/plugins/grpc-plugin/Cargo.toml +++ b/plugins/grpc-plugin/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "cln-grpc-plugin" -version = "0.1.6" +version = "0.2.0" description = "A Core Lightning plugin that re-exposes the JSON-RPC over grpc. Authentication is done via mTLS." license = "MIT" @@ -15,11 +15,11 @@ path = "src/main.rs" [dependencies] anyhow = "1.0" log = "0.4" -prost = "0.11" rcgen = { version = "0.13.1", features = ["pem", "x509-parser"] } -cln-grpc = { version = "0.1", features = ["server"], path = "../../cln-grpc"} -cln-plugin = { version = "0.1", path = "../../plugins" } -cln-rpc = { version = "0.1", path = "../../cln-rpc" } +prost = "0.12" +cln-grpc = { version = "0.2", features = ["server"], path = "../../cln-grpc"} +cln-plugin = { version = "0.2", path = "../../plugins" } +cln-rpc = { version = "0.2", path = "../../cln-rpc" } serde_json = "1.0.113" [dependencies.tokio] @@ -28,4 +28,4 @@ version = "1" [dependencies.tonic] features = ["tls", "transport"] -version = "0.8" +version = "0.11" diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index dd953661a019..572fcc9e3751 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -856,15 +856,12 @@ static double capacity_bias(const struct gossmap *map, int dir, struct amount_msat amount) { - struct amount_sat capacity; u64 amtmsat = amount.millisatoshis; /* Raw: lengthy math */ double capmsat; /* Can fail in theory if gossmap changed underneath. */ - if (!gossmap_chan_get_capacity(map, c, &capacity)) - return 0; + capmsat = (double)gossmap_chan_get_capacity(map, c).millisatoshis; /* Raw: log */ - capmsat = (double)capacity.satoshis * 1000; /* Raw: lengthy math */ return -log((capmsat + 1 - amtmsat) / (capmsat + 1)); } diff --git a/plugins/renepay/chan_extra.c b/plugins/renepay/chan_extra.c index 57183227ea3b..931463ca53d2 100644 --- a/plugins/renepay/chan_extra.c +++ b/plugins/renepay/chan_extra.c @@ -559,13 +559,8 @@ get_chan_extra_half_by_chan_verify(const struct gossmap *gossmap, struct chan_extra_half *h = get_chan_extra_half_by_scid(chan_extra_map, &scidd); if (!h) { - struct amount_sat cap; - struct amount_msat cap_msat; - - if (!gossmap_chan_get_capacity(gossmap, chan, &cap) || - !amount_sat_to_msat(&cap_msat, cap)) { - return NULL; - } + struct amount_msat cap_msat + = gossmap_chan_get_capacity(gossmap, chan); h = &new_chan_extra(chan_extra_map, scidd.scid, cap_msat) ->half[scidd.dir]; } diff --git a/plugins/renepay/mods.c b/plugins/renepay/mods.c index 49b420f66e9b..3d410cbce0d5 100644 --- a/plugins/renepay/mods.c +++ b/plugins/renepay/mods.c @@ -384,12 +384,13 @@ static void gossmod_cb(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat htlcmin, struct amount_msat htlcmax, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf, const jsmntok_t *chantok, @@ -408,14 +409,12 @@ static void gossmod_cb(struct gossmap_localmods *mods, } /* FIXME: features? */ - gossmap_local_addchan(mods, self, peer, scidd->scid, NULL); - - gossmap_local_updatechan(mods, scidd->scid, min, max, - fee_base.millisatoshis, /* Raw: gossmap */ - fee_proportional, - cltv_delta, - enabled, - scidd->dir); + gossmap_local_addchan(mods, self, peer, scidd->scid, capacity_msat, + NULL); + gossmap_local_updatechan(mods, scidd, + &enabled, + &min, &max, + &fee_base, &fee_proportional, &cltv_delta); /* Is it disabled? */ if (!enabled) @@ -516,13 +515,19 @@ static void add_hintchan(struct payment *payment, const struct node_id *src, assert(payment); assert(payment->local_gossmods); - int dir = node_id_idx(src, dst); - const char *errmsg; const struct chan_extra *ce = uncertainty_find_channel(pay_plugin->uncertainty, scid); if (!ce) { + struct short_channel_id_dir scidd; + /* We assume any HTLC is allowed */ + struct amount_msat htlc_min = AMOUNT_MSAT(0), htlc_max = MAX_CAPACITY; + struct amount_msat fee_base = amount_msat(fee_base_msat); + bool enabled = true; + scidd.scid = scid; + scidd.dir = node_id_idx(src, dst); + /* This channel is not public, we don't know his capacity One possible solution is set the capacity to MAX_CAP and the state to [0,MAX_CAP]. Alternatively we could @@ -540,13 +545,12 @@ static void add_hintchan(struct payment *payment, const struct node_id *src, } /* FIXME: features? */ if (!gossmap_local_addchan(payment->local_gossmods, src, dst, - scid, NULL) || + scid, MAX_CAPACITY, NULL) || !gossmap_local_updatechan( - payment->local_gossmods, scid, - /* We assume any HTLC is allowed */ - AMOUNT_MSAT(0), MAX_CAPACITY, fee_base_msat, - fee_proportional_millionths, cltv_expiry_delta, true, - dir)) { + payment->local_gossmods, &scidd, + &enabled, &htlc_min, &htlc_max, + &fee_base, &fee_proportional_millionths, + &cltv_expiry_delta)) { errmsg = tal_fmt( tmpctx, "Failed to update scid=%s in the local_gossmods.", diff --git a/plugins/renepay/test/common.h b/plugins/renepay/test/common.h index 3368bdb49e54..75d3b1f3aeb8 100644 --- a/plugins/renepay/test/common.h +++ b/plugins/renepay/test/common.h @@ -49,8 +49,7 @@ static void add_connection(int store_fd, struct amount_msat max, u32 base_fee, s32 proportional_fee, u32 delay, - struct amount_sat capacity, - bool add_capacity) + struct amount_sat capacity) { secp256k1_ecdsa_signature dummy_sig; struct secret not_a_secret; @@ -79,10 +78,8 @@ static void add_connection(int store_fd, &dummy_key, &dummy_key); write_to_store(store_fd, msg); - if (add_capacity) { - msg = towire_gossip_store_channel_amount(tmpctx, capacity); - write_to_store(store_fd, msg); - } + msg = towire_gossip_store_channel_amount(tmpctx, capacity); + write_to_store(store_fd, msg); u8 flags = node_id_idx(from, to); diff --git a/plugins/renepay/test/run-bottleneck.c b/plugins/renepay/test/run-bottleneck.c index 510ddebde5e7..2173ebd54392 100644 --- a/plugins/renepay/test/run-bottleneck.c +++ b/plugins/renepay/test/run-bottleneck.c @@ -113,32 +113,28 @@ int main(int argc, char *argv[]) AMOUNT_MSAT(0), AMOUNT_MSAT(60 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(60 * 1000), - true); + AMOUNT_SAT(60 * 1000)); assert(mk_short_channel_id(&scid, 1, 3, 0)); add_connection(fd, &nodes[0], &nodes[2], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(60 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(60 * 1000), - true); + AMOUNT_SAT(60 * 1000)); assert(mk_short_channel_id(&scid, 2, 4, 0)); add_connection(fd, &nodes[1], &nodes[3], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 3, 4, 0)); add_connection(fd, &nodes[2], &nodes[3], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 4, 5, 0)); add_connection(fd, &nodes[3], &nodes[4], scid, @@ -148,40 +144,35 @@ int main(int argc, char *argv[]) * through this channel. */ AMOUNT_MSAT(106 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(110 * 1000), - true); + AMOUNT_SAT(110 * 1000)); assert(mk_short_channel_id(&scid, 5, 6, 0)); add_connection(fd, &nodes[4], &nodes[5], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 100 * 1000 /* 10% */, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 5, 7, 0)); add_connection(fd, &nodes[4], &nodes[6], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 100 * 1000 /* 10% */, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 6, 8, 0)); add_connection(fd, &nodes[5], &nodes[7], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(mk_short_channel_id(&scid, 7, 8, 0)); add_connection(fd, &nodes[6], &nodes[7], scid, AMOUNT_MSAT(0), AMOUNT_MSAT(1000 * 1000 * 1000), 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - true); + AMOUNT_SAT(1000 * 1000)); assert(gossmap_refresh(gossmap, NULL)); struct uncertainty *uncertainty = uncertainty_new(tmpctx); diff --git a/plugins/renepay/test/run-mcf-diamond.c b/plugins/renepay/test/run-mcf-diamond.c index 57e90db1dca2..2db20d6e876a 100644 --- a/plugins/renepay/test/run-mcf-diamond.c +++ b/plugins/renepay/test/run-mcf-diamond.c @@ -133,36 +133,36 @@ int main(int argc, char *argv[]) mods = gossmap_localmods_new(tmpctx); /* 1->2->4 has capacity 10k sat, 1->3->4 has capacity 5k sat (lower fee!) */ - assert(gossmap_local_addchan(mods, &l1, &l2, scid12, NULL)); - assert(gossmap_local_updatechan(mods, scid12, - /*htlc_min=*/ AMOUNT_MSAT(0), - /*htlc_max=*/ AMOUNT_MSAT(10000000), - /*base_fee=*/ 0, - /*ppm_fee =*/ 1001, - /* delay =*/ 5, - /* enabled=*/ true, - /* dir =*/ 0)); - assert(gossmap_local_addchan(mods, &l2, &l4, scid24, NULL)); - assert(gossmap_local_updatechan(mods, scid24, - AMOUNT_MSAT(0), - AMOUNT_MSAT(10000000), - 0, 1002, 5, - true, - 0)); - assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); - assert(gossmap_local_updatechan(mods, scid13, - AMOUNT_MSAT(0), - AMOUNT_MSAT(5000000), - 0, 503, 5, - true, - 0)); - assert(gossmap_local_addchan(mods, &l3, &l4, scid34, NULL)); - assert(gossmap_local_updatechan(mods, scid34, - AMOUNT_MSAT(0), - AMOUNT_MSAT(5000000), - 0, 504, 5, - true, - 0)); + assert(gossmap_local_addchan(mods, &l1, &l2, scid12, AMOUNT_MSAT(10000000), NULL)); + assert(gossmap_local_setchan(mods, scid12, + /*htlc_min=*/ AMOUNT_MSAT(0), + /*htlc_max=*/ AMOUNT_MSAT(10000000), + /*base_fee=*/ AMOUNT_MSAT(0), + /*ppm_fee =*/ 1001, + /* delay =*/ 5, + /* enabled=*/ true, + /* dir =*/ 0)); + assert(gossmap_local_addchan(mods, &l2, &l4, scid24, AMOUNT_MSAT(10000000), NULL)); + assert(gossmap_local_setchan(mods, scid24, + AMOUNT_MSAT(0), + AMOUNT_MSAT(10000000), + AMOUNT_MSAT(0), 1002, 5, + true, + 0)); + assert(gossmap_local_addchan(mods, &l1, &l3, scid13, AMOUNT_MSAT(5000000), NULL)); + assert(gossmap_local_setchan(mods, scid13, + AMOUNT_MSAT(0), + AMOUNT_MSAT(5000000), + AMOUNT_MSAT(0), 503, 5, + true, + 0)); + assert(gossmap_local_addchan(mods, &l3, &l4, scid34, AMOUNT_MSAT(5000000), NULL)); + assert(gossmap_local_setchan(mods, scid34, + AMOUNT_MSAT(0), + AMOUNT_MSAT(5000000), + AMOUNT_MSAT(0), 504, 5, + true, + 0)); gossmap_apply_localmods(gossmap, mods); chan_extra_map = tal(tmpctx, struct chan_extra_map); diff --git a/plugins/renepay/test/run-mcf.c b/plugins/renepay/test/run-mcf.c index 79c291fe92c4..487304918580 100644 --- a/plugins/renepay/test/run-mcf.c +++ b/plugins/renepay/test/run-mcf.c @@ -480,13 +480,14 @@ int main(int argc, char *argv[]) assert(short_channel_id_from_str("111x1x1", 7, &scid13)); /* 400,000sat channel from 1->3, basefee 0, ppm 1000, delay 5 */ - assert(gossmap_local_addchan(mods, &l1, &l3, scid13, NULL)); - assert(gossmap_local_updatechan(mods, scid13, - AMOUNT_MSAT(0), - AMOUNT_MSAT(400000000), - 0, 1000, 5, - true, - 0)); + assert(gossmap_local_addchan(mods, &l1, &l3, scid13, + AMOUNT_MSAT(400000000), NULL)); + assert(gossmap_local_setchan(mods, scid13, + AMOUNT_MSAT(0), + AMOUNT_MSAT(400000000), + AMOUNT_MSAT(0), 1000, 5, + true, + 0)); /* Apply changes, check they work. */ gossmap_apply_localmods(gossmap, mods); @@ -494,6 +495,7 @@ int main(int argc, char *argv[]) assert(local_chan); /* The local chans have no "capacity", so set it manually. */ + /* FIXME: They do now! */ uncertainty_add_channel(uncertainty, scid13, AMOUNT_MSAT(400000000)); // flows = minflow(tmpctx, gossmap, diff --git a/plugins/renepay/test/run-missingcapacity.c b/plugins/renepay/test/run-missingcapacity.c deleted file mode 100644 index 4dc53ec4825f..000000000000 --- a/plugins/renepay/test/run-missingcapacity.c +++ /dev/null @@ -1,179 +0,0 @@ -/* Checks that uncertainty_update and get_routes can handle a gossmap where the - * capacity of some channels are missing. - * - * */ -#include "config.h" - -#include "../disabledmap.c" -#include "../errorcodes.c" -#include "../flow.c" -#include "../mcf.c" -#include "../route.c" -#include "../routebuilder.c" -#include "../uncertainty.c" -#include "common.h" - -#include -#include -#include -#include -#include -#include - -/* AUTOGENERATED MOCKS START */ -/* Generated stub for sciddir_or_pubkey_from_node_id */ -bool sciddir_or_pubkey_from_node_id(struct sciddir_or_pubkey *sciddpk UNNEEDED, - const struct node_id *node_id UNNEEDED) -{ fprintf(stderr, "sciddir_or_pubkey_from_node_id called!\n"); abort(); } -/* AUTOGENERATED MOCKS END */ - -static u8 empty_map[] = {10}; - -#define NUM_NODES 4 - -static void remove_file(char *fname) { assert(!remove(fname)); } - -int main(int argc, char *argv[]) -{ - int fd; - char *gossfile; - struct gossmap *gossmap; - struct node_id nodes[NUM_NODES]; - - common_setup(argv[0]); - chainparams = chainparams_for_network("regtest"); - - fd = tmpdir_mkstemp(tmpctx, "run-missingcapacity.XXXXXX", &gossfile); - tal_add_destructor(gossfile, remove_file); - assert(write(fd, empty_map, sizeof(empty_map)) == sizeof(empty_map)); - - gossmap = gossmap_load(tmpctx, gossfile, NULL); - assert(gossmap); - - for (size_t i = 0; i < NUM_NODES; i++) { - struct privkey tmp; - memset(&tmp, i+1, sizeof(tmp)); - node_id_from_privkey(&tmp, &nodes[i]); - } - - /* We will try a payment from 1 to 4. - * There are two possible routes 1->2->4 or 1->3->4. - * However, we will simulate that we don't have channel 3->4's capacity - * in the gossmap (see #7194). We expect that 3->4 it's simply ignored - * and only route through 1->2->4 is used. - * - * +--2--+ - * | | - * 1 4 - * | | - * +--3--+ - * - * */ - struct short_channel_id scid; - - assert(mk_short_channel_id(&scid, 1, 2, 0)); - add_connection(fd, &nodes[0], &nodes[1], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 2, 4, 0)); - add_connection(fd, &nodes[1], &nodes[3], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 1, 3, 0)); - add_connection(fd, &nodes[0], &nodes[2], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ true); - - assert(mk_short_channel_id(&scid, 3, 4, 0)); - add_connection(fd, &nodes[2], &nodes[3], scid, - AMOUNT_MSAT(0), - AMOUNT_MSAT(1000 * 1000 * 1000), - 0, 0, 5, - AMOUNT_SAT(1000 * 1000), - /* add capacity? = */ false); - - assert(gossmap_refresh(gossmap, NULL)); - struct uncertainty *uncertainty = uncertainty_new(tmpctx); - int skipped_count = - uncertainty_update(uncertainty, gossmap); - assert(skipped_count==1); - - struct preimage preimage; - - struct amount_msat maxfee = AMOUNT_MSAT(20*1000); - struct payment_info pinfo; - pinfo.invstr = NULL; - pinfo.label = NULL; - pinfo.description = NULL; - pinfo.payment_secret = NULL; - pinfo.payment_metadata = NULL; - pinfo.routehints = NULL; - pinfo.destination = nodes[3]; - pinfo.amount = AMOUNT_MSAT(100 * 1000 * 1000); - - assert(amount_msat_add(&pinfo.maxspend, maxfee, pinfo.amount)); - pinfo.maxdelay = 100; - pinfo.final_cltv = 5; - - pinfo.start_time = time_now(); - pinfo.stop_time = timeabs_add(pinfo.start_time, time_from_sec(10000)); - - pinfo.base_fee_penalty = 1e-5; - pinfo.prob_cost_factor = 1e-5; - pinfo.delay_feefactor = 1e-6; - pinfo.min_prob_success = 0.9; - pinfo.base_prob_success = 1.0; - pinfo.use_shadow = false; - - randombytes_buf(&preimage, sizeof(preimage)); - sha256(&pinfo.payment_hash, &preimage, sizeof(preimage)); - - struct disabledmap *disabledmap = disabledmap_new(tmpctx); - - enum jsonrpc_errcode errcode; - const char *err_msg; - - u64 groupid = 1; - u64 next_partid=1; - - struct route **routes = get_routes( - /* ctx */tmpctx, - /* payment */&pinfo, - /* source */&nodes[0], - /* destination */&nodes[3], - /* gossmap */gossmap, - /* uncertainty */uncertainty, - disabledmap, - /* amount */ pinfo.amount, - /* feebudget */maxfee, - &next_partid, - groupid, - &errcode, - &err_msg); - - assert(routes); - - if (!routes) { - printf("get_route failed with error %d: %s", errcode, err_msg); - } else { - printf("get_routes: %s\n", print_routes(tmpctx, routes)); - assert(tal_count(routes) == 1); - assert(tal_count(routes[0]->hops) == 2); - assert(node_id_eq(&routes[0]->hops[0].node_id, &nodes[1])); - assert(node_id_eq(&routes[0]->hops[1].node_id, &nodes[3])); - } - - common_shutdown(); -} - diff --git a/plugins/renepay/test/run-testflow.c b/plugins/renepay/test/run-testflow.c index 5d907bae1547..a570fd9eb7d0 100644 --- a/plugins/renepay/test/run-testflow.c +++ b/plugins/renepay/test/run-testflow.c @@ -578,12 +578,12 @@ static void test_flow_to_route(void) struct chan_extra_half *h0,*h1; struct gossmap_chan *c; - struct amount_sat cap; + struct amount_msat cap; struct amount_msat sum_min1_max0,sum_min0_max1; // check the bounds channel 1--2 c = gossmap_find_chan(gossmap,&scid12); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -596,17 +596,17 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(1000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 2--3 c = gossmap_find_chan(gossmap,&scid23); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h1 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,1); assert(h1); @@ -619,17 +619,17 @@ static void test_flow_to_route(void) h0->known_max = AMOUNT_MSAT(2000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 3--4 c = gossmap_find_chan(gossmap,&scid34); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -642,17 +642,17 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(500000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); // check the bounds channel 4--5 c = gossmap_find_chan(gossmap,&scid45); - assert(gossmap_chan_get_capacity(gossmap,c,&cap)); + cap = gossmap_chan_get_capacity(gossmap,c); h0 = get_chan_extra_half_by_chan_verify(gossmap,chan_extra_map,c,0); assert(h0); @@ -665,13 +665,13 @@ static void test_flow_to_route(void) h1->known_max = AMOUNT_MSAT(2000000000); assert(amount_msat_less_eq(h0->known_min,h0->known_max)); - assert(amount_msat_less_eq_sat(h0->known_max,cap)); + assert(amount_msat_less_eq(h0->known_max,cap)); assert(amount_msat_less_eq(h1->known_min,h1->known_max)); - assert(amount_msat_less_eq_sat(h1->known_max,cap)); + assert(amount_msat_less_eq(h1->known_max,cap)); assert(amount_msat_add(&sum_min1_max0,h1->known_min,h0->known_max)); assert(amount_msat_add(&sum_min0_max1,h0->known_min,h1->known_max)); - assert(amount_msat_eq_sat(sum_min1_max0,cap)); - assert(amount_msat_eq_sat(sum_min0_max1,cap)); + assert(amount_msat_eq(sum_min1_max0,cap)); + assert(amount_msat_eq(sum_min0_max1,cap)); struct flow *F; struct route *route; diff --git a/plugins/renepay/uncertainty.c b/plugins/renepay/uncertainty.c index 00c50ebc1134..d151471229a0 100644 --- a/plugins/renepay/uncertainty.c +++ b/plugins/renepay/uncertainty.c @@ -104,12 +104,10 @@ int uncertainty_update(struct uncertainty *uncertainty, struct gossmap *gossmap) struct chan_extra *ce = chan_extra_map_get(chan_extra_map, scid); if (!ce) { - struct amount_sat cap; struct amount_msat cap_msat; - if (!gossmap_chan_get_capacity(gossmap, chan, &cap) || - !amount_sat_to_msat(&cap_msat, cap) || - !new_chan_extra(chan_extra_map, scid, + cap_msat = gossmap_chan_get_capacity(gossmap, chan); + if (!new_chan_extra(chan_extra_map, scid, cap_msat)) { /* If the new chan_extra cannot be created we * skip this channel. */ diff --git a/plugins/test/Makefile b/plugins/test/Makefile index 0165ee8c646c..2ecba481c65a 100644 --- a/plugins/test/Makefile +++ b/plugins/test/Makefile @@ -19,7 +19,8 @@ plugins/test/run-route-overlong: \ common/fp16.o \ common/gossmap.o \ common/node_id.o \ - common/route.o + common/route.o \ + gossipd/gossip_store_wiregen.o plugins/test/run-route-calc: \ common/fp16.o \ diff --git a/plugins/test/run-route-calc.c b/plugins/test/run-route-calc.c index da73c25a9a8b..7a8d23a18fbf 100644 --- a/plugins/test/run-route-calc.c +++ b/plugins/test/run-route-calc.c @@ -42,12 +42,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, const struct node_id *self UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf UNUSED UNNEEDED, const jsmntok_t *chantok UNUSED UNNEEDED, @@ -63,12 +64,13 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE const struct node_id *self_ UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf_ UNNEEDED, const jsmntok_t *chantok UNNEEDED, diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c index 4a7d94b3f7d2..9655104413ed 100644 --- a/plugins/test/run-route-overlong.c +++ b/plugins/test/run-route-overlong.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -39,12 +40,13 @@ void gossmod_add_localchan(struct gossmap_localmods *mods UNNEEDED, const struct node_id *self UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf UNUSED UNNEEDED, const jsmntok_t *chantok UNUSED UNNEEDED, @@ -60,12 +62,13 @@ struct gossmap_localmods *gossmods_from_listpeerchannels_(const tal_t *ctx UNNEE const struct node_id *self_ UNNEEDED, const struct node_id *peer UNNEEDED, const struct short_channel_id_dir *scidd UNNEEDED, + struct amount_msat capacity_msat UNNEEDED, struct amount_msat htlcmin UNNEEDED, struct amount_msat htlcmax UNNEEDED, struct amount_msat spendable UNNEEDED, struct amount_msat fee_base UNNEEDED, u32 fee_proportional UNNEEDED, - u32 cltv_delta UNNEEDED, + u16 cltv_delta UNNEEDED, bool enabled UNNEEDED, const char *buf_ UNNEEDED, const jsmntok_t *chantok UNNEEDED, @@ -381,6 +384,8 @@ static void add_connection(int store_fd, ids[0], ids[1], &dummy_key, &dummy_key); write_to_store(store_fd, msg); + msg = towire_gossip_store_channel_amount(tmpctx, AMOUNT_SAT(10000)); + write_to_store(store_fd, msg); update_connection(store_fd, from, to, scid, min, max, base_fee, proportional_fee, diff --git a/plugins/topology.c b/plugins/topology.c index 972853e3826b..5cc00aa894b2 100644 --- a/plugins/topology.c +++ b/plugins/topology.c @@ -224,7 +224,7 @@ static void json_add_halfchan(struct json_stream *response, struct short_channel_id scid; struct node_id node_id[2]; const u8 *chanfeatures; - struct amount_sat capacity; + struct amount_msat capacity_msat; bool local_disable; /* These are channel (not per-direction) properties */ @@ -234,9 +234,7 @@ static void json_add_halfchan(struct json_stream *response, gossmap_node_get_id(gossmap, gossmap_nth_node(gossmap, c, i), &node_id[i]); - /* This can theoretically happen on partial write races. */ - if (!gossmap_chan_get_capacity(gossmap, c, &capacity)) - capacity = AMOUNT_SAT(0); + capacity_msat = gossmap_chan_get_capacity(gossmap, c); /* Deprecated: local channels are not "active" unless peer is connected. */ if (connected && node_id_eq(&node_id[0], &local_id)) @@ -287,7 +285,7 @@ static void json_add_halfchan(struct json_stream *response, &htlc_maximum_msat); } - json_add_amount_sat_msat(response, "amount_msat", capacity); + json_add_amount_msat(response, "amount_msat", capacity_msat); json_add_num(response, "message_flags", message_flags); json_add_num(response, "channel_flags", channel_flags); @@ -362,12 +360,13 @@ static void gossmod_add_unknown_localchan(struct gossmap_localmods *mods, const struct node_id *self, const struct node_id *peer, const struct short_channel_id_dir *scidd, + struct amount_msat capacity_msat, struct amount_msat min, struct amount_msat max, struct amount_msat spendable, struct amount_msat fee_base, u32 fee_proportional, - u32 cltv_delta, + u16 cltv_delta, bool enabled, const char *buf UNUSED, const jsmntok_t *chantok UNUSED, @@ -376,7 +375,8 @@ static void gossmod_add_unknown_localchan(struct gossmap_localmods *mods, if (gossmap_find_chan(gossmap, &scidd->scid)) return; - gossmod_add_localchan(mods, self, peer, scidd, min, max, spendable, + gossmod_add_localchan(mods, self, peer, scidd, capacity_msat, + min, max, spendable, fee_base, fee_proportional, cltv_delta, enabled, buf, chantok, gossmap); } diff --git a/tests/data/gossip-store-2024-09-22-node-map.xz b/tests/data/gossip-store-2024-09-22-node-map.xz new file mode 100644 index 000000000000..a12cf619a92b Binary files /dev/null and b/tests/data/gossip-store-2024-09-22-node-map.xz differ diff --git a/tests/data/gossip-store-2024-09-22.compressed b/tests/data/gossip-store-2024-09-22.compressed new file mode 100644 index 000000000000..3f955dd09edb Binary files /dev/null and b/tests/data/gossip-store-2024-09-22.compressed differ diff --git a/tests/test_askrene.py b/tests/test_askrene.py index a87ae3c6ec53..5a7e7abe7c2a 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -8,51 +8,193 @@ import time +def direction(src, dst): + """BOLT 7 direction: 0 means from lesser encoded id""" + if src < dst: + return 0 + return 1 + + +def test_reserve(node_factory): + """Test reserving channels""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + assert l1.rpc.askrene_listreservations() == {'reservations': []} + scid12 = first_scid(l1, l2) + scid23 = first_scid(l2, l3) + scid12dir = f"{scid12}/{direction(l1.info['id'], l2.info['id'])}" + scid23dir = f"{scid23}/{direction(l2.info['id'], l3.info['id'])}" + + initial_prob = l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] + + # Reserve 1000 sats on path. This should reduce probability! + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_001}]) + listres = l1.rpc.askrene_listreservations()['reservations'] + if listres[0]['short_channel_id_dir'] == scid12dir: + assert listres[0]['amount_msat'] == 1000_000 + assert listres[1]['short_channel_id_dir'] == scid23dir + assert listres[1]['amount_msat'] == 1000_001 + else: + assert listres[0]['short_channel_id_dir'] == scid23dir + assert listres[0]['amount_msat'] == 1000_001 + assert listres[1]['short_channel_id_dir'] == scid12dir + assert listres[1]['amount_msat'] == 1000_000 + assert len(listres) == 2 + + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] < initial_prob + + # Now reserve so much there's nothing left. + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000_000_000}]) + + # Keep it consistent: the below will mention a time if >= 1 seconds old, + # which might happen without the sleep on slow machines. + time.sleep(2) + + # Reservations can be in either order. + with pytest.raises(RpcError, match=rf'We could not find a usable set of paths. The shortest path is {scid12}->{scid23}, but {scid12dir} already reserved 10000000*msat by command ".*" \([0-9]* seconds ago\), 10000000*msat by command ".*" \([0-9]* seconds ago\)'): + l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] + + # Can't remove wrong amounts: that's user error + with pytest.raises(RpcError, match="Unknown reservation"): + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_001}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000}]) + + # Remove, it's all ok. + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_001}]) + l1.rpc.askrene_unreserve(path=[{'short_channel_id_dir': scid12dir, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23dir, + 'amount_msat': 1000_000_000_000}]) + assert l1.rpc.askrene_listreservations() == {'reservations': []} + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] == initial_prob + + # Reserving in reverse makes no difference! + scid12rev = f"{first_scid(l1, l2)}/{direction(l2.info['id'], l1.info['id'])}" + scid23rev = f"{first_scid(l2, l3)}/{direction(l3.info['id'], l2.info['id'])}" + l1.rpc.askrene_reserve(path=[{'short_channel_id_dir': scid12rev, + 'amount_msat': 1000_000_000_000}, + {'short_channel_id_dir': scid23rev, + 'amount_msat': 1000_000_000_000}]) + assert l1.rpc.getroutes(source=l1.info['id'], + destination=l3.info['id'], + amount_msat=1000000, + layers=[], + maxfee_msat=100000, + final_cltv=0)['probability_ppm'] == initial_prob + + def test_layers(node_factory): """Test manipulating information in layers""" l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) assert l2.rpc.askrene_listlayers() == {'layers': []} - assert l2.rpc.askrene_listlayers('test_layers') == {'layers': []} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_listlayers('test_layers') expect = {'layer': 'test_layers', 'disabled_nodes': [], 'created_channels': [], + 'channel_updates': [], 'constraints': []} + l2.rpc.askrene_create_layer('test_layers') l2.rpc.askrene_disable_node('test_layers', l1.info['id']) expect['disabled_nodes'].append(l1.info['id']) assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} assert l2.rpc.askrene_listlayers() == {'layers': [expect]} - assert l2.rpc.askrene_listlayers('test_layers2') == {'layers': []} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_listlayers('test_layers2') + + l2.rpc.askrene_update_channel('test_layers', "0x0x1/0", False) + expect['channel_updates'].append({'short_channel_id_dir': "0x0x1/0", + 'enabled': False}) + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + with pytest.raises(RpcError, match="Layer already exists"): + l2.rpc.askrene_create_layer('test_layers') # Tell it l3 connects to l1! l2.rpc.askrene_create_channel('test_layers', l3.info['id'], l1.info['id'], '0x0x1', - '1000000sat', - 100, '900000sat', - 1, 2, 18) - expect['created_channels'].append({'source': l3.info['id'], - 'destination': l1.info['id'], + '1000000sat') + # src/dst gets turned into BOLT 7 order + expect['created_channels'].append({'source': l1.info['id'], + 'destination': l3.info['id'], 'short_channel_id': '0x0x1', - 'capacity_msat': 1000000000, - 'htlc_minimum_msat': 100, - 'htlc_maximum_msat': 900000000, - 'fee_base_msat': 1, - 'fee_proportional_millionths': 2, - 'delay': 18}) + 'capacity_msat': 1000000000}) + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + + # And give details. + l2.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir='0x0x1/0', + htlc_minimum_msat=100, + htlc_maximum_msat=900000000, + fee_base_msat=1, + fee_proportional_millionths=2, + cltv_expiry_delta=18) + # This is *still* disabled, since we disabled it above! + expect['channel_updates'] = [{'short_channel_id_dir': '0x0x1/0', + 'enabled': False, + 'htlc_minimum_msat': 100, + 'htlc_maximum_msat': 900000000, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 2, + 'cltv_expiry_delta': 18}] + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + + # Now enable (and change another value for good measure! + l2.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir='0x0x1/0', + enabled=True, + cltv_expiry_delta=19) + expect['channel_updates'] = [{'short_channel_id_dir': '0x0x1/0', + 'enabled': True, + 'htlc_minimum_msat': 100, + 'htlc_maximum_msat': 900000000, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 2, + 'cltv_expiry_delta': 19}] assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} # We can tell it about made up channels... first_timestamp = int(time.time()) l2.rpc.askrene_inform_channel('test_layers', - '0x0x1', - 1, - 100000) + '0x0x1/1', + 100000, + 'unconstrained') last_timestamp = int(time.time()) + 1 - expect['constraints'].append({'short_channel_id': '0x0x1', - 'direction': 1, + expect['constraints'].append({'short_channel_id_dir': '0x0x1/1', 'minimum_msat': 100000}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') @@ -67,25 +209,24 @@ def test_layers(node_factory): # We can tell it about existing channels... scid12 = first_scid(l1, l2) first_timestamp = int(time.time()) + scid12dir = f"{scid12}/{direction(l2.info['id'], l1.info['id'])}" l2.rpc.askrene_inform_channel(layer='test_layers', - short_channel_id=scid12, - # This is l2 -> l1 - direction=0, - maximum_msat=12341234) + short_channel_id_dir=scid12dir, + amount_msat=12341235, + inform='constrained') last_timestamp = int(time.time()) + 1 - expect['constraints'].append({'short_channel_id': scid12, - 'direction': 0, + expect['constraints'].append({'short_channel_id_dir': scid12dir, 'timestamp': first_timestamp, 'maximum_msat': 12341234}) # Check timestamp first. listlayers = l2.rpc.askrene_listlayers('test_layers') - ts2 = only_one([c['timestamp'] for c in only_one(listlayers['layers'])['constraints'] if c['short_channel_id'] == scid12]) + ts2 = only_one([c['timestamp'] for c in only_one(listlayers['layers'])['constraints'] if c['short_channel_id_dir'] == scid12dir]) assert first_timestamp <= ts2 <= last_timestamp expect['constraints'][1]['timestamp'] = ts2 # Could be either order! actual = expect.copy() - if only_one(listlayers['layers'])['constraints'][0]['short_channel_id'] == scid12: + if only_one(listlayers['layers'])['constraints'][0]['short_channel_id_dir'] == scid12dir: actual['constraints'] = [expect['constraints'][1], expect['constraints'][0]] assert listlayers == {'layers': [actual]} @@ -106,6 +247,12 @@ def test_layers(node_factory): listlayers = l2.rpc.askrene_listlayers('test_layers') assert listlayers == {'layers': [expect]} + with pytest.raises(RpcError, match="Unknown layer"): + l2.rpc.askrene_remove_layer('test_layers_unknown') + + assert l2.rpc.askrene_remove_layer('test_layers') == {} + assert l2.rpc.askrene_listlayers() == {'layers': []} + def check_route_as_expected(routes, paths): """Make sure all fields in paths are match those in routes""" @@ -164,6 +311,46 @@ def test_getroutes(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + # Too much should give a decent explanation. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. The shortest path is 0x1x0, but 0x1x0/1 isn't big enough to carry 1000000001msat\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000000001, + layers=[], + maxfee_msat=100000000, + final_cltv=99) + + # This should tell us source doesn't have enough. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. Total source capacity is only 1019000000msat \(in 3 channels\)\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=2000000001, + layers=[], + maxfee_msat=20000000, + final_cltv=99) + + # This should tell us dest doesn't have enough. + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. Total destination capacity is only 1000000000msat \(in 1 channels\)\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[4], + amount_msat=1000000001, + layers=[], + maxfee_msat=30000000, + final_cltv=99) + + # Disabling channels makes getroutes fail + l1.rpc.askrene_create_layer('chans_disabled') + l1.rpc.askrene_update_channel(layer="chans_disabled", + short_channel_id_dir='0x1x0/1', + enabled=False) + with pytest.raises(RpcError, match=r"We could not find a usable set of paths\. The shortest path is 0x1x0, but 0x1x0/1 marked disabled by layer chans_disabled\."): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=["chans_disabled"], + maxfee_msat=1000, + final_cltv=99) + # Start easy assert l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], @@ -174,8 +361,7 @@ def test_getroutes(node_factory): 'routes': [{'probability_ppm': 999999, 'final_cltv': 99, 'amount_msat': 1000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 1010, 'delay': 99 + 6}]}]} @@ -189,13 +375,11 @@ def test_getroutes(node_factory): 'routes': [{'probability_ppm': 999798, 'final_cltv': 99, 'amount_msat': 100000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 103020, 'delay': 99 + 6 + 6}, - {'short_channel_id': '1x3x2', - 'direction': 1, + {'short_channel_id_dir': '1x3x2/1', 'next_node_id': nodemap[3], 'amount_msat': 102000, 'delay': 99 + 6} @@ -235,8 +419,7 @@ def test_getroutes(node_factory): 'routes': [{'probability_ppm': 900000, 'final_cltv': 99, 'amount_msat': 1000000, - 'path': [{'short_channel_id': '0x2x3', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x2x3/1', 'next_node_id': nodemap[2], 'amount_msat': 1000001, 'delay': 99 + 6}]}]} @@ -246,11 +429,11 @@ def test_getroutes(node_factory): nodemap[0], nodemap[2], 10000000, - [[{'short_channel_id': '0x2x1', + [[{'short_channel_id_dir': '0x2x1/1', 'next_node_id': nodemap[2], 'amount_msat': 500000, 'delay': 99 + 6}], - [{'short_channel_id': '0x2x3', + [{'short_channel_id_dir': '0x2x3/1', 'next_node_id': nodemap[2], 'amount_msat': 9500009, 'delay': 99 + 6}]]) @@ -280,8 +463,8 @@ def test_getroutes_fee_fallback(node_factory): nodemap[3], 10000, maxfee_msat=201, - paths=[[{'short_channel_id': '0x1x0'}, - {'short_channel_id': '1x3x2'}]]) + paths=[[{'short_channel_id_dir': '0x1x0/1'}, + {'short_channel_id_dir': '1x3x2/1'}]]) # maxfee exceeded? lower prob path. check_getroute_paths(l1, @@ -289,8 +472,8 @@ def test_getroutes_fee_fallback(node_factory): nodemap[3], 10000, maxfee_msat=200, - paths=[[{'short_channel_id': '0x2x1'}, - {'short_channel_id': '2x3x3'}]]) + paths=[[{'short_channel_id_dir': '0x2x1/1'}, + {'short_channel_id_dir': '2x3x3/0'}]]) def test_getroutes_auto_sourcefree(node_factory): @@ -304,6 +487,21 @@ def test_getroutes_auto_sourcefree(node_factory): # Set up l1 with this as the gossip_store l1 = node_factory.get_node(gossip_store_file=gsfile.name) + # Without sourcefree: + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=[], + maxfee_msat=1000, + final_cltv=99) == {'probability_ppm': 999999, + 'routes': [{'probability_ppm': 999999, + 'final_cltv': 99, + 'amount_msat': 1000, + 'path': [{'short_channel_id_dir': '0x1x0/1', + 'next_node_id': nodemap[1], + 'amount_msat': 1010, + 'delay': 105}]}]} + # Start easy assert l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], @@ -314,8 +512,7 @@ def test_getroutes_auto_sourcefree(node_factory): 'routes': [{'probability_ppm': 999999, 'final_cltv': 99, 'amount_msat': 1000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 1000, 'delay': 99}]}]} @@ -329,13 +526,11 @@ def test_getroutes_auto_sourcefree(node_factory): 'routes': [{'probability_ppm': 999798, 'final_cltv': 99, 'amount_msat': 100000, - 'path': [{'short_channel_id': '0x1x0', - 'direction': 1, + 'path': [{'short_channel_id_dir': '0x1x0/1', 'next_node_id': nodemap[1], 'amount_msat': 102000, 'delay': 99 + 6}, - {'short_channel_id': '1x3x2', - 'direction': 1, + {'short_channel_id_dir': '1x3x2/1', 'next_node_id': nodemap[3], 'amount_msat': 102000, 'delay': 99 + 6} @@ -389,15 +584,16 @@ def test_getroutes_auto_localchans(node_factory): final_cltv=99) # This should work + scid21dir = f"{scid12}/{direction(l2.info['id'], l1.info['id'])}" check_getroute_paths(l2, l2.info['id'], nodemap[2], 100000, maxfee_msat=100000, layers=['auto.localchans'], - paths=[[{'short_channel_id': scid12, 'amount_msat': 102012, 'delay': 99 + 6 + 6 + 6}, - {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, - {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]]) + paths=[[{'short_channel_id_dir': scid21dir, 'amount_msat': 102012, 'delay': 99 + 6 + 6 + 6}, + {'short_channel_id_dir': '0x1x0/0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '1x2x1/1', 'amount_msat': 101000, 'delay': 99 + 6}]]) # This should get self-discount correct check_getroute_paths(l2, @@ -406,9 +602,9 @@ def test_getroutes_auto_localchans(node_factory): 100000, maxfee_msat=100000, layers=['auto.localchans', 'auto.sourcefree'], - paths=[[{'short_channel_id': scid12, 'amount_msat': 102010, 'delay': 99 + 6 + 6}, - {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, - {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]]) + paths=[[{'short_channel_id_dir': scid21dir, 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '0x1x0/0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id_dir': '1x2x1/1', 'amount_msat': 101000, 'delay': 99 + 6}]]) def test_fees_dont_exceed_constraints(node_factory): @@ -424,10 +620,11 @@ def test_fees_dont_exceed_constraints(node_factory): l1 = node_factory.get_node(gossip_store_file=gsfile.name) chan = only_one([c for c in l1.rpc.listchannels(source=nodemap[0])['channels'] if c['destination'] == nodemap[1]]) + l1.rpc.askrene_create_layer('test_layers') l1.rpc.askrene_inform_channel(layer='test_layers', - short_channel_id=chan['short_channel_id'], - direction=chan['direction'], - maximum_msat=max_msat) + short_channel_id_dir=f"{chan['short_channel_id']}/{chan['direction']}", + amount_msat=max_msat + 1, + inform='constrained') routes = l1.rpc.getroutes(source=nodemap[0], destination=nodemap[3], @@ -437,7 +634,7 @@ def test_fees_dont_exceed_constraints(node_factory): final_cltv=99)['routes'] assert len(routes) == 2 for hop in routes[0]['path'] + routes[1]['path']: - if hop['short_channel_id'] == chan['short_channel_id']: + if hop['short_channel_id_dir'] == f"{chan['short_channel_id']}/{chan['direction']}": amount = hop['amount_msat'] assert amount <= max_msat @@ -450,13 +647,20 @@ def test_sourcefree_on_mods(node_factory, bitcoind): l1 = node_factory.get_node(gossip_store_file=gsfile.name) # Add a local channel from 0->l1 (we just needed a nodeid). + l1.rpc.askrene_create_layer('test_layers') l1.rpc.askrene_create_channel('test_layers', nodemap[0], l1.info['id'], '0x3x3', - '1000000sat', - 100, '900000sat', - 1000, 2000, 18) + '1000000sat') + l1.rpc.askrene_update_channel(layer='test_layers', + short_channel_id_dir=f'0x3x3/{direction(nodemap[0], l1.info["id"])}', + enabled=True, + htlc_minimum_msat=100, + htlc_maximum_msat='900000sat', + fee_base_msat=1000, + fee_proportional_millionths=2000, + cltv_expiry_delta=18) routes = l1.rpc.getroutes(source=nodemap[0], destination=l1.info['id'], amount_msat=1000000, @@ -464,7 +668,7 @@ def test_sourcefree_on_mods(node_factory, bitcoind): maxfee_msat=100000, final_cltv=99)['routes'] # Expect no fee. - check_route_as_expected(routes, [[{'short_channel_id': '0x3x3', + check_route_as_expected(routes, [[{'short_channel_id_dir': '0x3x3/1', 'amount_msat': 1000000, 'delay': 99}]]) # Same if we specify layers in the other order! @@ -475,7 +679,7 @@ def test_sourcefree_on_mods(node_factory, bitcoind): maxfee_msat=100000, final_cltv=99)['routes'] # Expect no fee. - check_route_as_expected(routes, [[{'short_channel_id': '0x3x3', + check_route_as_expected(routes, [[{'short_channel_id_dir': '0x3x3/1', 'amount_msat': 1000000, 'delay': 99}]]) @@ -517,9 +721,7 @@ def test_live_spendable(node_factory, bitcoind): path_total = {} num_htlcs = {} for r in routes["routes"]: - key = "{}/{}".format( - r["path"][0]["short_channel_id"], r["path"][0]["direction"] - ) + key = r["path"][0]["short_channel_id_dir"] path_total[key] = path_total.get(key, 0) + r["path"][0]["amount_msat"] num_htlcs[key] = num_htlcs.get(key, 0) + 1 @@ -538,9 +740,9 @@ def test_live_spendable(node_factory, bitcoind): # No duplicate paths! for i in range(0, len(routes["routes"])): - path_i = [(p['short_channel_id'], p['direction']) for p in routes["routes"][i]['path']] + path_i = [p['short_channel_id_dir'] for p in routes["routes"][i]['path']] for j in range(i + 1, len(routes["routes"])): - path_j = [(p['short_channel_id'], p['direction']) for p in routes["routes"][j]['path']] + path_j = [p['short_channel_id_dir'] for p in routes["routes"][j]['path']] assert path_i != path_j # Must deliver exact amount. @@ -572,14 +774,18 @@ def test_limits_fake_gossmap(node_factory, bitcoind): for scidd in spendable: assert scidd in [f"{c['short_channel_id']}/{c['direction']}" for c in l1.rpc.listchannels(source=nodemap[0])['channels']] + # We tell it we could get through amount, but not amount + 1. + # This makes min == max, just like we do for auto.localchans spendable. + l1.rpc.askrene_create_layer('localchans') for scidd, amount in spendable.items(): - chan, direction = scidd.split('/') l1.rpc.askrene_inform_channel(layer='localchans', - short_channel_id=chan, direction=int(direction), - minimum_msat=amount) + short_channel_id_dir=scidd, + amount_msat=amount, + inform='unconstrained') l1.rpc.askrene_inform_channel(layer='localchans', - short_channel_id=chan, direction=int(direction), - maximum_msat=amount) + short_channel_id_dir=scidd, + amount_msat=amount + 1, + inform='constrained') routes = l1.rpc.getroutes( source=nodemap[0], @@ -592,9 +798,7 @@ def test_limits_fake_gossmap(node_factory, bitcoind): path_total = {} for r in routes["routes"]: - key = "{}/{}".format( - r["path"][0]["short_channel_id"], r["path"][0]["direction"] - ) + key = r["path"][0]["short_channel_id_dir"] path_total[key] = path_total.get(key, 0) + r["path"][0]["amount_msat"] exceeded = {} @@ -607,9 +811,9 @@ def test_limits_fake_gossmap(node_factory, bitcoind): # No duplicate paths! for i in range(0, len(routes["routes"])): - path_i = [(p['short_channel_id'], p['direction']) for p in routes["routes"][i]['path']] + path_i = [p['short_channel_id_dir'] for p in routes["routes"][i]['path']] for j in range(i + 1, len(routes["routes"])): - path_j = [(p['short_channel_id'], p['direction']) for p in routes["routes"][j]['path']] + path_j = [p['short_channel_id_dir'] for p in routes["routes"][j]['path']] assert path_i != path_j # Must deliver exact amount. @@ -631,16 +835,17 @@ def test_max_htlc(node_factory, bitcoind): final_cltv=10) check_route_as_expected(routes['routes'], - [[{'short_channel_id': '0x1x0', 'amount_msat': 1_000_001, 'delay': 10 + 6}], - [{'short_channel_id': '0x1x1', 'amount_msat': 19_000_019, 'delay': 10 + 6}]]) + [[{'short_channel_id_dir': '0x1x0/1', 'amount_msat': 1_000_001, 'delay': 10 + 6}], + [{'short_channel_id_dir': '0x1x1/1', 'amount_msat': 19_000_019, 'delay': 10 + 6}]]) # If we can't use channel 2, we fail. + l1.rpc.askrene_create_layer('removechan2') l1.rpc.askrene_inform_channel(layer='removechan2', - short_channel_id='0x1x1', direction=1, - maximum_msat=0) + short_channel_id_dir='0x1x1/1', + amount_msat=1, + inform='constrained') - # FIXME: Better diag! - with pytest.raises(RpcError, match="Could not find route"): + with pytest.raises(RpcError, match="We could not find a usable set of paths. The shortest path is 0x1x0, but 0x1x0/1 exceeds htlc_maximum_msat ~1000448msat"): l1.rpc.getroutes(source=nodemap[0], destination=nodemap[1], amount_msat=20_000_000, diff --git a/tests/test_misc.py b/tests/test_misc.py index e4b4b148a5b9..c369f87e8082 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2869,6 +2869,18 @@ def test_recoverchannel(node_factory): assert stubs[0] == "c3a7b9d74a174497122bc52d74d6d69836acadc77e0429c6d8b68b48d5c9139a" +def test_getemergencyrecoverdata(node_factory): + """ + Test getemergencyrecoverdata + """ + l1 = node_factory.get_node() + filedata = l1.rpc.getemergencyrecoverdata()['filedata'] + + with open(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "emergency.recover"), "rb") as f: + lines = f.read().hex() + assert lines == filedata + + @unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3") def test_emergencyrecover(node_factory, bitcoind): """ diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 77ba5f38a983..cf197fdc0fc4 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1701,3 +1701,11 @@ def test_hsmtool_makerune(node_factory): # We have to generate a rune now, for commando to even start processing! rune = l1.rpc.commando_rune()['rune'] assert rune == out + + +def test_hsmtool_getnodeid(node_factory): + l1 = node_factory.get_node() + + cmd_line = ["tools/hsmtool", "getnodeid", os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")] + out = subprocess.check_output(cmd_line).decode('utf-8').strip() + assert out == l1.info['id'] diff --git a/tools/hsmtool.c b/tools/hsmtool.c index c3daa80d6420..4b3c16141b4e 100644 --- a/tools/hsmtool.c +++ b/tools/hsmtool.c @@ -49,6 +49,7 @@ static void show_usage(const char *progname) printf(" - makerune \n"); printf(" - getcodexsecret \n"); printf(" - getemergencyrecover \n"); + printf(" - getnodeid \n"); exit(0); } @@ -697,6 +698,38 @@ static int make_rune(const char *hsm_secret_path) return 0; } +static int get_node_id(const char *hsm_secret_path) +{ + u32 salt = 0; + struct secret hsm_secret; + struct privkey node_privkey; + struct pubkey node_id; + + secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY + | SECP256K1_CONTEXT_SIGN); + + /* Get hsm_secret */ + get_hsm_secret(&hsm_secret, hsm_secret_path); + + /*~ So, there is apparently a 1 in 2^127 chance that a random value is + * not a valid private key, so this never actually loops. */ + do { + /*~ ccan/crypto/hkdf_sha256 implements RFC5869 "Hardened Key + * Derivation Functions". That means that if a derived key + * leaks somehow, the other keys are not compromised. */ + hkdf_sha256(&node_privkey, sizeof(node_privkey), + &salt, sizeof(salt), + &hsm_secret, + sizeof(hsm_secret), + "nodeid", 6); + salt++; + } while (!secp256k1_ec_pubkey_create(secp256k1_ctx, &node_id.pubkey, + node_privkey.secret.data)); + + printf("%s\n", fmt_pubkey(tmpctx, &node_id)); + return 0; +} + int main(int argc, char *argv[]) { const char *method; @@ -838,5 +871,11 @@ int main(int argc, char *argv[]) return getemergencyrecover(argv[2]); } + if (streq(method, "getnodeid")) { + if (argc < 3) + show_usage(argv[0]); + return get_node_id(argv[2]); + } + show_usage(argv[0]); } diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c index fc73479c23ae..755b6318ee4c 100644 --- a/wallet/test/run-db.c +++ b/wallet/test/run-db.c @@ -191,8 +191,18 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED, bool ignore_fee_limits UNNEEDED, /* NULL or stolen */ struct peer_update *peer_update STEALS UNNEEDED, - u64 last_stable_connection UNNEEDED) + u64 last_stable_connection UNNEEDED, + const struct channel_stats *stats UNNEEDED, + struct channel_state_change **state_changes STEALS UNNEEDED) { fprintf(stderr, "new_channel called!\n"); abort(); } +/* Generated stub for new_channel_state_change */ +struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED, + struct timeabs timestamp UNNEEDED, + enum channel_state old_state UNNEEDED, + enum channel_state new_state UNNEEDED, + enum state_change cause UNNEEDED, + const char *message TAKES UNNEEDED) +{ fprintf(stderr, "new_channel_state_change called!\n"); abort(); } /* Generated stub for new_coin_wallet_deposit */ struct chain_coin_mvt *new_coin_wallet_deposit(const tal_t *ctx UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index b6f0791a9231..31006b1f7a6d 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -1918,6 +1918,7 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx) secp256k1_ecdsa_signature *lease_commit_sig; u32 feerate, lease_blockheight_start; u64 dbid; + struct channel_stats *stats = talz(w, struct channel_stats); pubkey_from_der(tal_hexdata(w, "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc", 66), 33, &pk); node_id_from_pubkey(&id, &pk); @@ -1995,7 +1996,9 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx) AMOUNT_MSAT(-1ULL), false, NULL, - 0); + 0, + stats, + tal_arr(NULL, struct channel_state_change *, 0)); db_begin_transaction(w->db); CHECK(!wallet_err); wallet_channel_insert(w, chan); diff --git a/wallet/wallet.c b/wallet/wallet.c index 6d0ee76a99b2..84a449d0ac91 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -37,21 +37,6 @@ /* 12 hours is usually enough reservation time */ #define RESERVATION_INC (6 * 12) -/* Possible channel state */ -enum channel_state_bucket { - IN_OFFERED = 0, - IN_FULLFILLED = 1, - OUT_OFFERED = 2, - OUT_FULLFILLED = 3, -}; - -/* channel state identifier */ -struct channel_state_param { - const char *dir_key; - const char *type_key; - const enum channel_state_bucket state; -}; - /* These go in db, so values cannot change (we can't put this into * lightningd/channel_state.h since it confuses cdump!) */ static enum state_change state_change_in_db(enum state_change s) @@ -484,6 +469,10 @@ struct utxo **wallet_utxo_boost(const tal_t *ctx, if (utxo_is_reserved(utxo, blockheight)) continue; + /* Don't add csv-locked ones */ + if (utxo_is_csv_locked(utxo, blockheight)) + continue; + /* UTXOs must be sane amounts */ if (!amount_sat_add(&new_fee_amount, fee_amount, utxo->amount)) @@ -1501,6 +1490,41 @@ static struct short_channel_id *db_col_optional_scid(const tal_t *ctx, return scid; } +static struct channel_state_change **wallet_state_change_get(const tal_t *ctx, + struct wallet *w, + u64 channel_id) +{ + struct db_stmt *stmt; + struct channel_state_change **res = tal_arr(ctx, + struct channel_state_change *, 0); + stmt = db_prepare_v2( + w->db, SQL("SELECT" + " timestamp," + " old_state," + " new_state," + " cause," + " message " + "FROM channel_state_changes " + "WHERE channel_id = ? " + "ORDER BY timestamp ASC;")); + db_bind_int(stmt, channel_id); + db_query_prepared(stmt); + + while (db_step(stmt)) { + struct channel_state_change *c; + + c = new_channel_state_change(res, + db_col_timeabs(stmt, "timestamp"), + db_col_int(stmt, "old_state"), + db_col_int(stmt, "new_state"), + state_change_in_db(db_col_int(stmt, "cause")), + take(db_col_strdup(NULL, stmt, "message"))); + tal_arr_expand(&res, c); + } + tal_free(stmt); + return res; +} + /** * wallet_stmt2channel - Helper to populate a wallet_channel from a `db_stmt` */ @@ -1536,6 +1560,8 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm u16 lease_chan_max_ppt; bool ignore_fee_limits; struct peer_update *remote_update; + struct channel_stats stats; + struct channel_state_change **state_changes; peer_dbid = db_col_u64(stmt, "peer_id"); peer = find_peer_by_dbid(w->ld, peer_dbid); @@ -1718,6 +1744,29 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm db_col_ignore(stmt, "remote_htlc_maximum_msat"); } + stats.in_payments_offered + = db_col_int_or_default(stmt, "in_payments_offered", 0); + stats.in_payments_fulfilled + = db_col_int_or_default(stmt, "in_payments_fulfilled", 0); + db_col_amount_msat_or_default(stmt, "in_msatoshi_offered", + &stats.in_msatoshi_offered, + AMOUNT_MSAT(0)); + db_col_amount_msat_or_default(stmt, "in_msatoshi_fulfilled", + &stats.in_msatoshi_fulfilled, + AMOUNT_MSAT(0)); + stats.out_payments_offered + = db_col_int_or_default(stmt, "out_payments_offered", 0); + stats.out_payments_fulfilled + = db_col_int_or_default(stmt, "out_payments_fulfilled", 0); + db_col_amount_msat_or_default(stmt, "out_msatoshi_offered", + &stats.out_msatoshi_offered, + AMOUNT_MSAT(0)); + db_col_amount_msat_or_default(stmt, "out_msatoshi_fulfilled", + &stats.out_msatoshi_fulfilled, + AMOUNT_MSAT(0)); + + /* Stolen by new_channel */ + state_changes = wallet_state_change_get(NULL, w, db_col_u64(stmt, "id")); chan = new_channel(peer, db_col_u64(stmt, "id"), &wshachain, channel_state_in_db(db_col_int(stmt, "state")), @@ -1779,7 +1828,9 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm htlc_maximum_msat, ignore_fee_limits, remote_update, - db_col_u64(stmt, "last_stable_connection")); + db_col_u64(stmt, "last_stable_connection"), + &stats, + state_changes); if (!wallet_channel_load_inflights(w, chan)) { tal_free(chan); @@ -1973,6 +2024,14 @@ static bool wallet_channels_load_active(struct wallet *w) ", remote_htlc_minimum_msat" ", remote_htlc_maximum_msat" ", last_stable_connection" + ", in_payments_offered" + ", in_payments_fulfilled" + ", in_msatoshi_offered" + ", in_msatoshi_fulfilled" + ", out_payments_offered" + ", out_payments_fulfilled" + ", out_msatoshi_offered" + ", out_msatoshi_fulfilled" " FROM channels" " WHERE state != ?;")); //? 0 db_bind_int(stmt, CLOSED); @@ -1998,56 +2057,12 @@ bool wallet_init_channels(struct wallet *w) return wallet_channels_load_active(w); } -static enum channel_state_bucket get_state_channel_db(const char *dir, const char *typ) -{ - enum channel_state_bucket channel_state = IN_OFFERED; - if (streq(dir, "out")) - channel_state += 2; - if (streq(typ, "fulfilled")) - channel_state += 1; - return channel_state; -} - -static -void wallet_channel_stats_incr_x(struct wallet *w, - char const *dir, - char const *typ, - u64 cdbid, - struct amount_msat msat) +static void wallet_channel_stats_incr_x(struct wallet *w, + u64 cdbid, + struct amount_msat msat, + const char *query) { struct db_stmt *stmt; - const char *query = NULL; - - switch (get_state_channel_db(dir, typ)) { - case IN_OFFERED: - query = SQL("UPDATE channels" - " SET in_payments_offered = COALESCE(in_payments_offered, 0) + 1" - " , in_msatoshi_offered = COALESCE(in_msatoshi_offered, 0) + ?" - " WHERE id = ?;"); - break; - case IN_FULLFILLED: - query = SQL("UPDATE channels" - " SET in_payments_fulfilled = COALESCE(in_payments_fulfilled, 0) + 1" - " , in_msatoshi_fulfilled = COALESCE(in_msatoshi_fulfilled, 0) + ?" - " WHERE id = ?;"); - break; - case OUT_OFFERED: - query = SQL("UPDATE channels" - " SET out_payments_offered = COALESCE(out_payments_offered, 0) + 1" - " , out_msatoshi_offered = COALESCE(out_msatoshi_offered, 0) + ?" - " WHERE id = ?;"); - break; - case OUT_FULLFILLED: - query = SQL("UPDATE channels" - " SET out_payments_fulfilled = COALESCE(out_payments_fulfilled, 0) + 1" - " , out_msatoshi_fulfilled = COALESCE(out_msatoshi_fulfilled, 0) + ?" - " WHERE id = ?;"); - break; - } - - // Sanity check! - if (!query) - fatal("Unknown channel state key (direction %s, type %s)", dir, typ); stmt = db_prepare_v2(w->db, query); db_bind_amount_msat(stmt, &msat); @@ -2055,70 +2070,43 @@ void wallet_channel_stats_incr_x(struct wallet *w, db_exec_prepared_v2(take(stmt)); } + +/* I would use macros for these, but gettext needs string literals :( */ void wallet_channel_stats_incr_in_offered(struct wallet *w, u64 id, struct amount_msat m) { - wallet_channel_stats_incr_x(w, "in", "offered", id, m); + const char query[] = SQL("UPDATE channels" + " SET in_payments_offered = COALESCE(in_payments_offered, 0) + 1" + " , in_msatoshi_offered = COALESCE(in_msatoshi_offered, 0) + ?" + " WHERE id = ?;"); + wallet_channel_stats_incr_x(w, id, m, query); } void wallet_channel_stats_incr_in_fulfilled(struct wallet *w, u64 id, struct amount_msat m) { - wallet_channel_stats_incr_x(w, "in", "fulfilled", id, m); + const char query[] = SQL("UPDATE channels" + " SET in_payments_fulfilled = COALESCE(in_payments_fulfilled, 0) + 1" + " , in_msatoshi_fulfilled = COALESCE(in_msatoshi_fulfilled, 0) + ?" + " WHERE id = ?;"); + wallet_channel_stats_incr_x(w, id, m, query); } void wallet_channel_stats_incr_out_offered(struct wallet *w, u64 id, struct amount_msat m) { - wallet_channel_stats_incr_x(w, "out", "offered", id, m); + const char query[] = SQL("UPDATE channels" + " SET out_payments_offered = COALESCE(out_payments_offered, 0) + 1" + " , out_msatoshi_offered = COALESCE(out_msatoshi_offered, 0) + ?" + " WHERE id = ?;"); + wallet_channel_stats_incr_x(w, id, m, query); } void wallet_channel_stats_incr_out_fulfilled(struct wallet *w, u64 id, struct amount_msat m) { - wallet_channel_stats_incr_x(w, "out", "fulfilled", id, m); -} - -void wallet_channel_stats_load(struct wallet *w, - u64 id, - struct channel_stats *stats) -{ - struct db_stmt *stmt; - int res; - stmt = db_prepare_v2(w->db, SQL( - "SELECT" - " in_payments_offered, in_payments_fulfilled" - ", in_msatoshi_offered, in_msatoshi_fulfilled" - ", out_payments_offered, out_payments_fulfilled" - ", out_msatoshi_offered, out_msatoshi_fulfilled" - " FROM channels" - " WHERE id = ?")); - db_bind_u64(stmt, id); - db_query_prepared(stmt); - - res = db_step(stmt); - - /* This must succeed, since we know the channel exists */ - assert(res); - - stats->in_payments_offered - = db_col_int_or_default(stmt, "in_payments_offered", 0); - stats->in_payments_fulfilled - = db_col_int_or_default(stmt, "in_payments_fulfilled", 0); - db_col_amount_msat_or_default(stmt, "in_msatoshi_offered", - &stats->in_msatoshi_offered, - AMOUNT_MSAT(0)); - db_col_amount_msat_or_default(stmt, "in_msatoshi_fulfilled", - &stats->in_msatoshi_fulfilled, - AMOUNT_MSAT(0)); - stats->out_payments_offered - = db_col_int_or_default(stmt, "out_payments_offered", 0); - stats->out_payments_fulfilled - = db_col_int_or_default(stmt, "out_payments_fulfilled", 0); - db_col_amount_msat_or_default(stmt, "out_msatoshi_offered", - &stats->out_msatoshi_offered, - AMOUNT_MSAT(0)); - db_col_amount_msat_or_default(stmt, "out_msatoshi_fulfilled", - &stats->out_msatoshi_fulfilled, - AMOUNT_MSAT(0)); - tal_free(stmt); + const char query[] = SQL("UPDATE channels" + " SET out_payments_fulfilled = COALESCE(out_payments_fulfilled, 0) + 1" + " , out_msatoshi_fulfilled = COALESCE(out_msatoshi_fulfilled, 0) + ?" + " WHERE id = ?;"); + wallet_channel_stats_incr_x(w, id, m, query); } u32 wallet_blocks_maxheight(struct wallet *w) @@ -2515,39 +2503,6 @@ void wallet_state_change_add(struct wallet *w, db_exec_prepared_v2(take(stmt)); } -struct state_change_entry *wallet_state_change_get(const tal_t *ctx, - struct wallet *w, - u64 channel_id) -{ - struct db_stmt *stmt; - struct state_change_entry tmp; - struct state_change_entry *res = tal_arr(ctx, - struct state_change_entry, 0); - stmt = db_prepare_v2( - w->db, SQL("SELECT" - " timestamp," - " old_state," - " new_state," - " cause," - " message " - "FROM channel_state_changes " - "WHERE channel_id = ? " - "ORDER BY timestamp ASC;")); - db_bind_int(stmt, channel_id); - db_query_prepared(stmt); - - while (db_step(stmt)) { - tmp.timestamp = db_col_timeabs(stmt, "timestamp"); - tmp.old_state = db_col_int(stmt, "old_state"); - tmp.new_state = db_col_int(stmt, "new_state"); - tmp.cause = state_change_in_db(db_col_int(stmt, "cause")); - tmp.message = db_col_strdup(res, stmt, "message"); - tal_arr_expand(&res, tmp); - } - tal_free(stmt); - return res; -} - static void wallet_peer_save(struct wallet *w, struct peer *peer) { const char *addr = diff --git a/wallet/wallet.h b/wallet/wallet.h index b26de521bc77..9329e5a37750 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -366,14 +366,6 @@ struct outpoint { u32 spendheight; }; -/* Statistics for a channel */ -struct channel_stats { - u64 in_payments_offered, in_payments_fulfilled; - struct amount_msat in_msatoshi_offered, in_msatoshi_fulfilled; - u64 out_payments_offered, out_payments_fulfilled; - struct amount_msat out_msatoshi_offered, out_msatoshi_fulfilled; -}; - struct channeltx { u32 channel_id; int type; @@ -670,13 +662,6 @@ void wallet_state_change_add(struct wallet *w, enum state_change cause, const char *message); -/** - * Gets all state change history entries for a channel from the database - */ -struct state_change_entry *wallet_state_change_get(const tal_t *ctx, - struct wallet *w, - u64 channel_id); - /** * wallet_delete_peer_if_unused -- After no more channels in peer, forget about it */ @@ -715,15 +700,6 @@ void wallet_channel_stats_incr_in_fulfilled(struct wallet *w, u64 cdbid, struct void wallet_channel_stats_incr_out_offered(struct wallet *w, u64 cdbid, struct amount_msat msatoshi); void wallet_channel_stats_incr_out_fulfilled(struct wallet *w, u64 cdbid, struct amount_msat msatoshi); -/** - * wallet_channel_stats_load - Load channel statistics - * - * @w: wallet containing the channel - * @cdbid: channel database id - * @stats: location to load statistics to - */ -void wallet_channel_stats_load(struct wallet *w, u64 cdbid, struct channel_stats *stats); - /** * Retrieve the blockheight of the last block processed by lightningd. *