diff --git a/Cargo.lock b/Cargo.lock index bbf6e718..7dc27305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "ahash" version = "0.7.8" @@ -57,9 +67,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arrayvec" @@ -91,7 +101,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "synstructure", ] @@ -103,7 +113,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -135,18 +145,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -175,12 +185,12 @@ dependencies = [ "async-trait", "axum-core", "bitflags 1.3.2", - "bytes 1.9.0", + "bytes 1.10.1", "futures-util", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "itoa 1.0.14", + "itoa 1.0.15", "matchit", "memchr", "mime", @@ -201,7 +211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes 1.9.0", + "bytes 1.10.1", "futures-util", "http 0.2.12", "http-body 0.4.6", @@ -222,7 +232,7 @@ dependencies = [ "instant", "pin-project-lite 0.2.16", "rand 0.8.5", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -280,6 +290,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitcoin" version = "0.32.5" @@ -366,9 +385,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" @@ -425,15 +444,15 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ "shlex", ] @@ -457,21 +476,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" dependencies = [ "cfg-if 1.0.0", - "cipher", + "cipher 0.3.0", "cpufeatures 0.1.5", "zeroize", ] +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", + "aead 0.4.3", + "chacha20 0.7.1", + "cipher 0.3.0", + "poly1305 0.7.2", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.2", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305 0.8.0", "zeroize", ] @@ -490,6 +533,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "2.34.0" @@ -512,14 +566,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55eefc811f7d5280586dec7342824a84ab81f1d7e0cdb4cd579c1470e3e236cc" dependencies = [ "anyhow", - "bytes 1.9.0", + "bytes 1.10.1", "futures", "log", "serde", "serde_json", - "tokio 1.43.0", + "tokio 1.44.1", "tokio-stream", - "tokio-util 0.7.13", + "tokio-util 0.7.14", "tracing", "tracing-subscriber", ] @@ -581,6 +635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -609,9 +664,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" [[package]] name = "der-parser" @@ -629,24 +684,24 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" dependencies = [ "powerfmt", ] [[package]] name = "derive_more" -version = "0.99.18" +version = "0.99.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -676,14 +731,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "dnssec-prover" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96487aad690d45a83f2b9876828ba856c5430bbb143cb5730d8a5d04a4805179" +checksum = "48f9e1163868b86c37d43c586af9d917e699c87f1266ebfdf356ad1003458118" [[package]] name = "ed25519" @@ -710,9 +765,9 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" @@ -725,9 +780,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -871,7 +926,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -938,14 +993,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -956,9 +1011,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "globset" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" dependencies = [ "aho-corasick", "bstr", @@ -993,16 +1048,16 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.1", + "indexmap 2.8.0", "slab", - "tokio 1.43.0", - "tokio-util 0.7.13", + "tokio 1.44.1", + "tokio-util 0.7.14", "tracing", ] @@ -1049,7 +1104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", - "bytes 1.9.0", + "bytes 1.10.1", "headers-core", "http 0.2.12", "httpdate 1.0.3", @@ -1145,20 +1200,20 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "fnv", - "itoa 1.0.14", + "itoa 1.0.15", ] [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "fnv", - "itoa 1.0.14", + "itoa 1.0.15", ] [[package]] @@ -1177,16 +1232,16 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "http 0.2.12", "pin-project-lite 0.2.16", ] [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1230,7 +1285,7 @@ version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "futures-channel", "futures-core", "futures-util", @@ -1239,10 +1294,10 @@ dependencies = [ "http-body 0.4.6", "httparse", "httpdate 1.0.3", - "itoa 1.0.14", + "itoa 1.0.15", "pin-project-lite 0.2.16", - "socket2 0.5.8", - "tokio 1.43.0", + "socket2 0.5.9", + "tokio 1.44.1", "tower-service", "tracing", "want", @@ -1256,7 +1311,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper 0.14.32", "pin-project-lite 0.2.16", - "tokio 1.43.0", + "tokio 1.44.1", "tokio-io-timeout", ] @@ -1266,10 +1321,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "hyper 0.14.32", "native-tls", - "tokio 1.43.0", + "tokio 1.44.1", "tokio-native-tls", ] @@ -1314,9 +1369,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -1338,9 +1393,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -1359,9 +1414,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -1388,7 +1443,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -1424,14 +1479,23 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.2", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1473,9 +1537,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" @@ -1574,9 +1638,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libm" @@ -1642,7 +1706,7 @@ checksum = "cb6a6c93b1e592f1d46bb24233cac4a33b4015c99488ee229927a81d16226e45" dependencies = [ "bitcoin", "lightning", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -1656,15 +1720,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "litemap" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -1678,9 +1742,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matchers" @@ -1727,18 +1791,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] [[package]] name = "minreq" -version = "2.13.2" +version = "2.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0c420feb01b9fb5061f8c8f452534361dd783756dcf38ec45191ce55e7a161" +checksum = "567496f13503d6cae8c9f961f34536850275f396307d7a6b981eef1464032f53" dependencies = [ "log", "serde", @@ -1804,7 +1868,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "similar", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -1813,7 +1877,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "encoding_rs", "futures-util", "http 0.2.12", @@ -1833,9 +1897,9 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "native-tls" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -1952,9 +2016,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -1964,11 +2028,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.69" +version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -1985,7 +2049,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -1996,9 +2060,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" dependencies = [ "cc", "libc", @@ -2055,16 +2119,16 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.5.8", + "redox_syscall 0.5.10", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "pem" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ "base64 0.22.1", "serde", @@ -2083,27 +2147,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.7.1", + "indexmap 2.8.0", ] [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2126,9 +2190,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "poly1305" @@ -2138,7 +2202,18 @@ checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.4.0", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", ] [[package]] @@ -2158,21 +2233,21 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "prettyplease" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" dependencies = [ "proc-macro2", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2201,9 +2276,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -2214,7 +2289,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "prost-derive", ] @@ -2224,7 +2299,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "heck 0.5.0", "itertools", "log", @@ -2235,7 +2310,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.96", + "syn 2.0.100", "tempfile", ] @@ -2249,7 +2324,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2263,13 +2338,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.4.6" @@ -2307,6 +2388,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2327,6 +2419,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -2360,6 +2462,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -2403,11 +2514,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -2470,7 +2581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.9.0", + "bytes 1.10.1", "encoding_rs", "futures-core", "futures-util", @@ -2493,7 +2604,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "system-configuration", - "tokio 1.43.0", + "tokio 1.44.1", "tokio-native-tls", "tokio-socks", "tower-service", @@ -2506,15 +2617,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if 1.0.0", "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -2560,11 +2670,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", @@ -2622,15 +2732,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" @@ -2680,7 +2790,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", @@ -2699,38 +2809,38 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.7.1", - "itoa 1.0.14", + "indexmap 2.8.0", + "itoa 1.0.15", "memchr", "ryu", "serde", @@ -2743,7 +2853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.14", + "itoa 1.0.15", "ryu", "serde", ] @@ -2844,9 +2954,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "socket2" @@ -2861,9 +2971,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2930,9 +3040,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -2953,7 +3063,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2989,13 +3099,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.16.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if 1.0.0", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3025,7 +3134,7 @@ dependencies = [ "structopt", "tempdir", "teos-common", - "tokio 1.43.0", + "tokio 1.44.1", "tokio-stream", "toml", "tonic", @@ -3040,7 +3149,7 @@ name = "teos-common" version = "0.2.0" dependencies = [ "bitcoin", - "chacha20poly1305", + "chacha20poly1305 0.8.0", "hex", "lightning", "prost", @@ -3052,6 +3161,30 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "teos-ldk-client" +version = "0.1.0" +dependencies = [ + "backoff", + "bincode", + "bitcoin", + "chacha20poly1305 0.10.1", + "hex", + "home", + "lightning", + "log", + "mockito", + "rand 0.9.0", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "tempdir", + "teos-common", + "tokio 1.44.1", + "tonic", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -3078,7 +3211,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -3093,12 +3226,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", - "itoa 1.0.14", + "itoa 1.0.15", "libc", "num-conv", "num_threads", @@ -3110,15 +3243,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -3154,18 +3287,18 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", - "bytes 1.9.0", + "bytes 1.10.1", "libc", "mio 1.0.3", "parking_lot 0.12.3", "pin-project-lite 0.2.16", "signal-hook-registry", - "socket2 0.5.8", + "socket2 0.5.9", "tokio-macros", "windows-sys 0.52.0", ] @@ -3177,7 +3310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite 0.2.16", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -3188,7 +3321,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -3198,7 +3331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -3209,7 +3342,7 @@ checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", "rustls-pki-types", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -3221,7 +3354,7 @@ dependencies = [ "either", "futures-util", "thiserror", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -3232,7 +3365,7 @@ checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite 0.2.16", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -3243,7 +3376,7 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "tokio 1.43.0", + "tokio 1.44.1", "tungstenite", ] @@ -3263,15 +3396,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "futures-core", "futures-sink", "pin-project-lite 0.2.16", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -3293,7 +3426,7 @@ dependencies = [ "async-trait", "axum", "base64 0.21.7", - "bytes 1.9.0", + "bytes 1.10.1", "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", @@ -3304,7 +3437,7 @@ dependencies = [ "prost", "rustls-pemfile 2.2.0", "rustls-pki-types", - "tokio 1.43.0", + "tokio 1.44.1", "tokio-rustls", "tokio-stream", "tower", @@ -3323,7 +3456,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -3343,7 +3476,7 @@ dependencies = [ "serde_derive", "sha2", "sha3", - "tokio 1.43.0", + "tokio 1.44.1", ] [[package]] @@ -3359,8 +3492,8 @@ dependencies = [ "pin-project-lite 0.2.16", "rand 0.8.5", "slab", - "tokio 1.43.0", - "tokio-util 0.7.13", + "tokio 1.44.1", + "tokio-util 0.7.14", "tower-layer", "tower-service", "tracing", @@ -3398,7 +3531,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -3469,9 +3602,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", - "bytes 1.9.0", + "bytes 1.10.1", "data-encoding", - "http 1.2.0", + "http 1.3.1", "httparse", "log", "rand 0.8.5", @@ -3483,9 +3616,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicase" @@ -3495,9 +3628,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -3521,6 +3654,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3595,7 +3738,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.1", "futures-channel", "futures-util", "headers", @@ -3611,9 +3754,9 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio 1.43.0", + "tokio 1.44.1", "tokio-tungstenite", - "tokio-util 0.7.13", + "tokio-util 0.7.14", "tower-service", "tracing", ] @@ -3632,9 +3775,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -3661,7 +3804,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -3696,7 +3839,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3727,7 +3870,7 @@ dependencies = [ "serde_json", "tempdir", "teos-common", - "tokio 1.43.0", + "tokio 1.44.1", "tonic", ] @@ -3935,11 +4078,11 @@ dependencies = [ [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -4011,49 +4154,48 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "synstructure", ] @@ -4074,7 +4216,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -4096,5 +4238,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] diff --git a/Cargo.toml b/Cargo.toml index a070916c..a8888e38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ resolver = "2" members = [ "teos", "teos-common", + "teos-ldk-client", "watchtower-plugin" ] diff --git a/teos-common/src/receipts.rs b/teos-common/src/receipts.rs index 5dc0e00c..fb93200d 100644 --- a/teos-common/src/receipts.rs +++ b/teos-common/src/receipts.rs @@ -1,6 +1,6 @@ //! Receipts issued by towers and handed to users as commitment proof. -use serde::Serialize; +use serde::{Deserialize, Serialize}; use bitcoin::secp256k1::SecretKey; @@ -19,7 +19,7 @@ use crate::{cryptography, UserId}; /// as long as the user info is still known. That is, if a user has a subscription with range (S, E) and the user renews the subscription /// before the tower wipes their data, then the tower can create a new receipt with (S, E') for E' > E instead of a second receipt (E, E'). // Notice this only applies as long as there is no gap between the two subscriptions. -#[derive(Serialize, Debug, Eq, PartialEq, Clone)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct RegistrationReceipt { user_id: UserId, available_slots: u32, @@ -107,7 +107,7 @@ impl RegistrationReceipt { /// Proof that a certain state was backed up with the tower. /// /// Appointment receipts can be used alongside a registration receipt that covers it, and on chain data (a breach not being reacted with a penalty), to prove a tower has not reacted to a channel breach. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Serialize)] pub struct AppointmentReceipt { user_signature: String, start_block: u32, diff --git a/teos-ldk-client/Cargo.toml b/teos-ldk-client/Cargo.toml new file mode 100644 index 00000000..fbbe804c --- /dev/null +++ b/teos-ldk-client/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "teos-ldk-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +# General +backoff = { version = "0.4.0", features = ["tokio"] } +hex = { version = "0.4.3", features = [ "serde" ] } +home = "0.5.3" +reqwest = { version = "0.11", features = [ "blocking", "json", "socks" ] } +log = "0.4.16" +rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } +serde = "1.0.130" +serde_json = { version = "1.0", features = [ "preserve_order" ] } +tonic = { version = "0.11", features = [ "tls", "transport" ] } +tokio = { version = "1.5", features = [ "rt-multi-thread", "fs" ] } +bincode = "1.3.3" + +# Bitcoin and Lightning +bitcoin = "0.32.0" +lightning = "0.1.0" + +# Local +teos-common = { path = "../teos-common" } +chacha20poly1305 = "0.10.1" +rand = "0.9.0" + +[dev-dependencies] +mockito = "0.32.4" +tempdir = "0.3.7" diff --git a/teos-ldk-client/README.md b/teos-ldk-client/README.md new file mode 100644 index 00000000..1fb7d1ab --- /dev/null +++ b/teos-ldk-client/README.md @@ -0,0 +1,238 @@ +# Watchtower LDK client + +This is a watchtower client crate to interact with an [Eye of Satoshi tower](https://github.com/talaia-labs/rust-teos), and eventually with any [BOLT13](https://github.com/sr-gi/bolt13/blob/master/13-watchtowers.md) compliant watchtower. It is designed to be integrated into [LDK-Node](https://github.com/lightningdevkit/ldk-node). + +The crate manages all the client-side logic to send appointment to a number of registered towers every time a new commitment transaction is generated. It also keeps a summary of the messages sent to the towers and their responses. + +The client instance has the following methods: + +- `register_tower `: registers the user id (compressed public key) with a given tower. +- `get_tower_info `: gets all the locally stored data about a given tower. +- `retry_tower `: tries to send pending appointment to a (previously) unreachable tower. +- `abandon_tower `: deletes all data associated with a given tower. +- `ping_tower `: Polls the tower to check if it is online. +- `list_towers`: lists all registered towers. +- `get_appointment `: queries a given tower about an appointment. +- `get_subscription_info `: gets the subscription information by querying the tower. +- `get_appointment_receipt `: pulls a given appointment receipt from the local database. +- `get_registration_receipt `: pulls the latest registration receipt from the local database. + +The general usage idea +- `on_commitment_revocation `: sends appointments to the registered towers for every new commitment transaction. + +# Configuration + +## User identification and encryptions + +The constructor accepts a key pair that will be used as the user identifier. All requests from the user are signed using the secret key, so the tower can authenticate the user after the registration process (`register_tower`). + +All the appointments generated by the tower, as well as all the registered towers' data, are encrytped using aforementioned key are stored in a [KVStore](https://docs.rs/lightning/latest/lightning/util/persist/trait.KVStore.html) persisted storage like [VSS](https://github.com/lightningdevkit/vss-server) or [lightning_persister](https://docs.rs/lightning-persister/latest/lightning_persister/index.html) + +## Network configuration + +Network configuration options currently available options are: + +- `max_retry_count`: how many times a retry strategy will try to reach a temporary unreachable tower before giving up. +- `retry_delay`: how long (in seconds) the client will wait before auto-retrying a failed tower. + +# Getting started as a developer + +## Creating a client instance +``` +let teos_client = TeosClient::new(key_pair, storage) + .set_max_retry_count(10) + .set_retry_delay(1) + .build(); +``` + + +## Registering with a tower + +Once the crate is instantiated, the first step is to register your node with an active tower. You can do so by calling: + +``` +async teos_client.register_tower(tower_connector: TowerConnector)) -> Result; +``` + +Where `tower_connector` represents the target tower public key. As a convenience, `tower_connector` may be of the form `tower_public_key@host` or `tower_public_key@host:port`. Port defaults to `9814`. + +### Example + +``` +teos_client = register_tower(TowerConnector::new("02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4@127.0.0.1:9814")).await? +``` + +If the tower is online, you should get back a response similar to this: + +``` +{ + "user_id": "032fd79e4052531955cf3782b09b495a75919317573ba2fb4dca199652595ced2a", + "available_slots": 10000, + "subscription_expiry": 4712 +} +``` + +Where `available_slots` is the amount of free slots the user has available in the tower, `user_id` is the user's public key and `subscription_expiry` is the block height when the subscription expires. Generally speaking, a slot fits an appointment, so in this example the user can send **10000** appointments in roughly **one month**. + +Notice that, ideally, the client and the tower have to agree on the **subscription details** (`available_slots` and `subscription_expiry`). Currently, those depend only on the tower, since it is offering the service for free. However, in the current state, hitting `register_tower` again will add another `10000` slots and reset the time to `current_height + roughtly_one_mont_in_blocks`. + +Note that this data will be stored internally inside of client's state and it does not require developer to do anything with it. + +## Sending data to the tower +Once node is registered with at least one tower it can start sending appointments to the tower for every commitment transaction update on any of your channels using `on_commitment_revocation(tx: CommitmentRevocation) -> Result<(), Error>`. Where `CommitmentRevocation` has the following structure: +``` +{ + channel_id: String, + commit_num: u32, + commitment_txid: Txid, + penalty_tx: Transaction, +} +``` +In the current version, everything is sent to every registered tower (**full replication**). For the end user there is nothing to be done here, under normal conditions, the crate takes care of it. + +## Checking the state of the towers + +To find out more information about registered towers, you can use `list_towers` and `get_tower_info`: + +``` +list_towers() -> Vec +``` +``` +{ + "public_key": "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4", + "net_addr": "http://localhost:9814", + "available_slots": 9996, + "subscription_expiry": 4712, + "status": "reachable", + "pending_appointments": [], + "invalid_appointments": [] +} +``` + +The overview contains the `id` and network address of the tower (`netaddr`), as well as the current `status` and two list of appointments: **pending** and **invalid**. + +The tower has 5 different statuses: + +- `reachable`: the tower is reachable at the given network address. +- `temporarily unreachable`: the tower is temporarily unreachable, meaning that one of the last requests sent to it has failed. +- `unreachable`: the tower has been unreachable for a while. +- `misbehaving`: the tower has sent us incorrect data. +- `subscription error`: the subscription with the tower has expired or run out of slots. + +The main difference between `temporarily unreachable` and `unreachable` is the amount of time that has passed since we last received a response. If a tower is temporarily unreachable, a backoff strategy is triggered and all the appointments that cannot be delivered are stored under `pending_appointments`. If the tower comes back online within the retry strategy, every pending appointment is sent through and the tower is flagged back as `reachable`. However, if the backoff strategy ends up giving up, the tower is flagged as `unreachable`. + +If the client receives data from a tower that is not properly signed, the tower is flagged as `misbehaving` and it is abandoned, meaning that no more appointments are sent to it. This state should never be reached by honest towers. + +A `subscription error` means that the subscription needs to be renewed (hit `registertower` again). + +Regarding `pending_appointments` and `invalid_appointments` they store the data that is pending to be sent to the tower (for unreachable towers) and the appointments that have been rejected by the tower for being invalid, respectively. The latter should never get populated for honest clients. + +`gettowerinfo` provides more detailed information about the tower: + +**Usage** + +``` +get_tower_info tower_id +``` + +**Call** + +``` +get_tower_info 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 +``` + +**Return** + +``` +{ + "net_addr": "http://localhost:9814", + "available_slots": 9996, + "subscription_expiry": 4712, + "status": "reachable", + "appointments": { + "b851b8ec05f5809b9a710f7d9d24db6c": "rbxrs8ncqgzyrxkw5h95a64tbeyhmx6wopdtqndktkko3mq8q3tkczjyk19epd713it8warbpnxgk8py6utq87dt16f3qk6ehkjw5c7q", + "10c6f7787fc33d6298fa89fc41f6a0eb": "dhrtt91bbswmmu41nu4quszt7bsxzpfyx84ycfc1yjt73rs8eqpqg3fwqq8q9tff8aqorohueo3bcgqrww1ocef38hdfuhna44ikjife", + "52dc9bd565bdfe227111927e3964d70b": "d9495n3giiof4rq5aqzh4a6fezftnhofwdi1gb7q5mciyq9besdh4xixczitpgo5dxzdnyzzdy4b9i7hd1zcojdgw833975dn8azfc7x", + "e2824d355f711806d38671c19b91110d": "rbxhpeztw74dspxsr3tk7jdekw7cbkt88kfmda4guf5xkmh1tcmeauqf3s15168y8eo438nbpath58qrxsh9usskzmxk8suf1h19meae" + }, + "pending_appointments": { + "062dc0f28ce5b31e6902c87ff1de15ee": { + "encrypted_blob": "e91bf1a1ab097f71976f240fb2d0c036f5b2188f14089dd1960e041b0a4d31a2bcbf9d6bec064a1d81471bdacf1f4d3b7c8d5df280d86a44504a5ee2ebf309adadc4976cc48cef7b94c9a8f17a16f0dcddfd6d0d105621bc519c0f20b46a8335a3a091bf6bfcc813bd4e34e644822bddda81b2a829d8a3b522b4c9b3f4465a6e416ae9ca8c808637cbc51e8d73dfe80cad3a6cc8c5ca018dd8a4cf2edbc02fd5f6cee0aef5ed5411731ef89061272712180c04150652f5bbb1b540ccc72547fe4ca5e92819c3bbb2feeccd7ce8f7b6568dd7f725fefdd64684f63d59e5d719b24a11272c64818b6319c19a261ee9c8a1674eb2e7c7367797893ac8", + "to_self_delay": 42 + }, + "ad229060698d4bc2b910a30933b1b50a": { + "encrypted_blob": "5881dce52efc18b698adc4f93b4ba275eb73271645b471227680ddb889ab60870972c4d44278dc55da4502021d9af67fd4e40803a2c9a6b4d2fe1d1f89b93373407302b67d12bb6c90b6e72b073f1c6bb3c69d57e635bfd5ff2b9648812364821b30bd95b3e8b3b2a888da8225e3d4d5cd2cd1cf2705d022b908b6b2d71c155ea50c38e2b3fb45a615c7bc1d61607a9240999c1bf174d6153b4ca7d086586614c99a45d7195c589fda8101ee8801e28b7ccad7c2b5fbda38cfe7b5e8ec13c23b8fa3cc3e6791ea9675f312cd59278ea0434538d15600b7fc905cf5a8371fc93d1e834e16d5c6399127b71c8f5c9ebfc11c5c8d3f72ce92e278f163", + "to_self_delay": 42 + } + }, + "invalid_appointments": {} +} +``` + +Notice that there are is a new field in this report: `appointments`. + +`appointments` contains a collection of `locator:tower_signature` pairs of all the appointments sent and accepted by the tower. + +The report may also contain a `misbehaving_proof` field if the tower has misbehaved (this is not the case for this example). The proof would look as follows: + +``` + "misbehaving_proof": { + "locator": "3ebd6c5a4d5ec18c815ad9fcda9aac75", + "appointment_receipt": { + "user_signature": "d7efykp63dy69jrtc3r65pssbdhp4335etq3jap1zqk135qmrtyhr8ghbdhw8y8f7nsjgmm9eoyhsfj6yugzq1bu657frmwwrudr9gpt", + "start_block": 391, + "signature": "rd41nsmhtjsawhc9pta1p5na7kmsyk48xttjy4bt3tbkbajboyzfq6mpamkjixs1w7qotocwjg3sxnbzg6uduec4cnahhkmctgddjn8w" + }, + "recovered_id": "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4" + } +``` + +Finally, notice how `pending_appointments` now contains all the data about the pending appointments (**the full appointment**). The same applies to `invalid_appointments`. + +## Manually retrying a tower (YAGNI ?) +If a tower has been flagged as **unreachable** (after the default backoff has failed) or there has been a **subscription error**, the tower won't be tried again until the user manually requests so. This can be managed with the `retry_tower` command: + +**Usage** + +``` +retry_tower tower_id +``` +**Call** + +``` +retry_tower 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 +``` +**Return** + +``` +"Retrying 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4" +``` + +Notice that this only works if the tower is **unreachable**. A tower cannot be retried if it is already being retried (**temporarily unreachable**). + +## Query data from a tower +Data can be queried from a tower to check, for instance, that the tower is keeping it or that it is correct. This can be done using the `get_appointment` command: + +**Usage** + +``` +get_appointment +``` +**Call** + +``` +get_appointment 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 b851b8ec05f5809b9a710f7d9d24db6c +``` + +**Return** + +``` +{ + "appointment": { + "locator": "b851b8ec05f5809b9a710f7d9d24db6c", + "encrypted_blob": "017044dd0686e89bd3cf69777f1fdcb63d13eafa35e1946a0ac1324247ed793f11e27b3ee599bb1676cc98862c1f07d8e5bd29ed51c94c4ea2721a2b6f205f11cbdb1478da413ced585fe5069c6f438e977d325499bdedb985c055eaff00466209007587f20d09d153b537b0b1b6f5b8151384a1ad9f94dfffd5d5f6c2d484bad7d007976fdcaff173b18dbc4e1e24ca2ae29f8ab7e6933468c179f3857c813441e303b2e9e9b7625b19d8460d368f66cf5a7a2f54139ae0a0c9f0ef0c56183734e5dd51289ecb4f046d97e02895373c97e242c71f910c3ed1fc1b32eda4a3c28c73ad7e5fef624094fadb0753c03f8c9a4189a427e721f3ddfc0a", + "to_self_delay": 42 + }, + "status": "being_watched" +} +``` diff --git a/teos-ldk-client/src/convert.rs b/teos-ldk-client/src/convert.rs new file mode 100644 index 00000000..77ca95ab --- /dev/null +++ b/teos-ldk-client/src/convert.rs @@ -0,0 +1,562 @@ +use std::fmt; +use std::{convert::TryFrom, str::FromStr}; + +use hex::FromHex; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use bitcoin::{Transaction, Txid}; + +use teos_common::appointment::Locator; +use teos_common::TowerId; + +/// Errors related to the `registertower` command. +#[derive(Debug)] +pub enum RegisterError { + InvalidId(String), + InvalidHost(String), + InvalidPort(String), + InvalidFormat(String), +} + +impl std::fmt::Display for RegisterError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RegisterError::InvalidId(x) => write!(f, "{x}"), + RegisterError::InvalidHost(x) => write!(f, "{x}"), + RegisterError::InvalidPort(x) => write!(f, "{x}"), + RegisterError::InvalidFormat(x) => write!(f, "{x}"), + } + } +} + +/// Parameters related to the `registertower` command. +#[derive(Debug, Serialize)] +pub struct RegisterParams { + pub tower_id: TowerId, + pub host: Option, + pub port: Option, +} + +impl RegisterParams { + fn new(tower_id: &str, host: Option<&str>, port: Option) -> Result { + let mut params = RegisterParams::from_id(tower_id)?; + + if host.is_some() { + params = params.with_host(host.unwrap())? + } + + if port.is_some() { + params = params.with_port(port.unwrap())? + } + + Ok(params) + } + + fn from_id(tower_id: &str) -> Result { + Ok(Self { + tower_id: TowerId::from_str(tower_id) + .map_err(|_| RegisterError::InvalidId("Invalid tower id".to_owned()))?, + host: None, + port: None, + }) + } + + fn with_host(self, host: &str) -> Result { + if host.is_empty() { + Err(RegisterError::InvalidHost("hostname is empty".to_owned())) + } else if host.contains(' ') { + Err(RegisterError::InvalidHost( + "hostname contains white spaces".to_owned(), + )) + } else { + Ok(Self { + host: Some(String::from(host)), + ..self + }) + } + } + + fn with_port(self, port: u64) -> Result { + if port > u16::MAX as u64 { + Err(RegisterError::InvalidPort(format!( + "port must be a 16-byte integer. Received: {port}" + ))) + } else { + Ok(Self { + port: Some(port as u16), + ..self + }) + } + } +} + +impl TryFrom for RegisterParams { + type Error = RegisterError; + + // clippy-fix: We are getting more than just the first item, so this clippy check does not make sense here + #[allow(clippy::get_first)] + fn try_from(value: serde_json::Value) -> Result { + match value { + serde_json::Value::String(s) => { + let s = s.trim(); + let mut v = s.split('@'); + let tower_id = v.next().unwrap(); + + match v.next() { + Some(x) => { + let mut v = x.split(':'); + let host = v.next(); + let port = if let Some(p) = v.next() { + p.parse() + .map(Some) + .map_err(|_| RegisterError::InvalidPort(format!("Port is not a number: {p}")))? + } else { + None + }; + + RegisterParams::new(tower_id, host, port) + } + None => RegisterParams::from_id(tower_id), + } + }, + serde_json::Value::Array(mut a) => { + let param_count = a.len(); + + match param_count { + 1 => RegisterParams::try_from(a.pop().unwrap()), + 2 | 3 => { + let tower_id = a.get(0).unwrap().as_str().ok_or_else(|| RegisterError::InvalidId("tower_id must be a string".to_string()))?; + let host = Some(a.get(1).unwrap().as_str().ok_or_else(|| RegisterError::InvalidHost("host must be a string".to_string()))?); + let port = if let Some(p) = a.get(2) { + Some(p.as_u64().ok_or_else(|| RegisterError::InvalidPort(format!("port must be a number. Received: {p}")))?) + } else { + None + }; + + RegisterParams::new(tower_id, host, port) + } + _ => Err(RegisterError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {param_count}"))), + } + }, + serde_json::Value::Object(mut m) => { + let allowed_keys = ["tower_id", "host", "port"]; + let param_count = m.len(); + + if m.is_empty() || param_count > allowed_keys.len() { + Err(RegisterError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {param_count}"))) + } else if !m.contains_key(allowed_keys[0]){ + Err(RegisterError::InvalidId(format!("{} is mandatory", allowed_keys[0]))) + } else if !m.iter().all(|(k, _)| allowed_keys.contains(&k.as_str())) { + Err(RegisterError::InvalidFormat("Invalid named parameter found in request".to_owned())) + } else { + let mut params = Vec::with_capacity(allowed_keys.len()); + for k in allowed_keys { + if let Some(v) = m.remove(k) { + params.push(v); + } + } + + RegisterParams::try_from(json!(params)) + } + }, + _ => Err(RegisterError::InvalidFormat( + format!("Unexpected request format. Expected: 'tower_id[@host][:port]' or 'tower_id [host] [port]'. Received: '{value}'"), + )), + } + } +} + +/// Errors related to the `getappointment` command. +#[derive(Debug)] +pub enum GetAppointmentError { + InvalidId(String), + InvalidLocator(String), + InvalidFormat(String), +} + +impl std::fmt::Display for GetAppointmentError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GetAppointmentError::InvalidId(x) => write!(f, "{x}"), + GetAppointmentError::InvalidLocator(x) => write!(f, "{x}"), + GetAppointmentError::InvalidFormat(x) => write!(f, "{x}"), + } + } +} + +/// Parameters related to the `getappointment` command. +#[derive(Debug)] +pub struct GetAppointmentParams { + pub tower_id: TowerId, + pub locator: Locator, +} + +impl TryFrom for GetAppointmentParams { + type Error = GetAppointmentError; + + // clippy-fix: We are getting more than just the first item, so this clippy check does not make sense here + #[allow(clippy::get_first)] + fn try_from(value: serde_json::Value) -> Result { + match value { + serde_json::Value::Array(a) => { + let param_count = a.len(); + if param_count != 2 { + Err(GetAppointmentError::InvalidFormat(format!( + "Unexpected request format. The request needs 2 parameter. Received: {param_count}" + ))) + } else { + let tower_id = if let Some(s) = a.get(0).unwrap().as_str() { + TowerId::from_str(s).map_err(|_| { + GetAppointmentError::InvalidId("Invalid tower id".to_owned()) + }) + } else { + Err(GetAppointmentError::InvalidId( + "tower_id must be a hex encoded string".to_owned(), + )) + }?; + + let locator = if let Some(s) = a.get(1).unwrap().as_str() { + Locator::from_hex(s).map_err(|_| { + GetAppointmentError::InvalidLocator("Invalid locator".to_owned()) + }) + } else { + Err(GetAppointmentError::InvalidLocator( + "locator must be a hex encoded string".to_owned(), + )) + }?; + + Ok(Self { tower_id, locator }) + } + } + serde_json::Value::Object(mut m) => { + let allowed_keys = ["tower_id", "locator"]; + + if m.len() > allowed_keys.len() { + return Err(GetAppointmentError::InvalidFormat( + "Invalid named argument found in request".to_owned(), + )); + } + + // DISCUSS: There may be a more idiomatic way of doing this + for k in allowed_keys.iter() { + if !m.contains_key(*k) { + return Err(GetAppointmentError::InvalidFormat(format!( + "{k} is mandatory" + ))); + } + } + + let mut params = Vec::with_capacity(allowed_keys.len()); + for k in allowed_keys { + if let Some(v) = m.remove(k) { + params.push(v); + } + } + GetAppointmentParams::try_from(json!(params)) + } + _ => Err(GetAppointmentError::InvalidFormat(format!( + "Unexpected request format. Expected: tower_id locator. Received: '{value}'" + ))), + } + } +} + +/// Data associated with a commitment revocation. Represents the data sent by CoreLN through the `commitment_revocation` hook. +#[derive(Debug, Serialize, Deserialize)] +pub struct CommitmentRevocation { + pub channel_id: String, + #[serde(rename(deserialize = "commitnum"))] + pub commit_num: u32, + pub commitment_txid: Txid, + #[serde(deserialize_with = "crate::ser::deserialize_tx")] + pub penalty_tx: Transaction, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::collections::HashMap; + + const VALID_ID: &str = "020dea894c967319407265764aba31bdef75d463f96800f34dd6df61380d82dfc0"; + + mod register_command { + use super::*; + + #[test] + fn test_from_id() { + // The tower id should be a valid id, otherwise the params construction will fail + let params = RegisterParams::from_id(VALID_ID).unwrap(); + assert!(params.host.is_none()); + assert!(params.port.is_none()); + + // Any incorrectly formatted id will make it fail + assert!(matches!( + RegisterParams::from_id(""), + Err(RegisterError::InvalidId(..)) + )); + } + + #[test] + fn test_with_host() { + // Any properly formatted host should work + let params = RegisterParams::from_id(VALID_ID).unwrap(); + let host = "myhost"; + assert_eq!(params.with_host(host).unwrap().host, Some(host.to_owned())); + + // Host must not be empty not have spaces + assert!(matches!( + RegisterParams::from_id(VALID_ID).unwrap().with_host(""), + Err(RegisterError::InvalidHost(..)) + )); + assert!(matches!( + RegisterParams::from_id(VALID_ID) + .unwrap() + .with_host("myhost "), + Err(RegisterError::InvalidHost(..)) + )); + } + + #[test] + fn test_with_port() { + let mut params = RegisterParams::from_id(VALID_ID).unwrap(); + + // Any 16-bytes value will do for the port + let port = 6677; + params = params.with_port(port).unwrap(); + assert_eq!(params.port, Some(port as u16)); + + // Going over u16::MAX will make this fail + let port = u16::MAX as u64 + 1; + assert!(matches!( + params.with_port(port), + Err(RegisterError::InvalidPort(..)) + )); + } + + #[test] + fn test_try_from_json_string() { + let ok = [ + format!("{VALID_ID}@host:80"), + format!("{VALID_ID}@host"), + VALID_ID.to_string(), + ]; + let wrong_id = ["", "id@host:80", "@host:80", "@:80"]; + let wrong_host = [ + format!("{VALID_ID}@"), + format!("{VALID_ID}@ "), + format!("{VALID_ID}@ host"), + format!("{VALID_ID}@:80"), + ]; + let wrong_port = [format!("{VALID_ID}@host:"), format!("{VALID_ID}@host:port")]; + + for s in ok { + let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); + let p = RegisterParams::try_from(v); + assert!(matches!(p, Ok(..))); + } + + for s in wrong_id { + let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); + let p = RegisterParams::try_from(v); + assert!(matches!(p, Err(RegisterError::InvalidId(..)))); + } + + for s in wrong_host { + let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); + let p = RegisterParams::try_from(v); + assert!(matches!(p, Err(RegisterError::InvalidHost(..)))); + } + + for s in wrong_port { + let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); + let p = RegisterParams::try_from(v); + assert!(matches!(p, Err(RegisterError::InvalidPort(..)))); + } + } + + #[test] + fn test_try_from_json_array() { + let id = json!(VALID_ID); + let number_id = json!(0); + + let host = json!("host"); + let number_host = json!(1); + + let port = json!(80); + let string_port = json!("80"); + + for v in [vec![&id, &host, &port], vec![&id, &host], vec![&id]] { + let p = RegisterParams::try_from(json!(v)); + assert!(matches!(p, Ok(..))); + } + + // Wrong id + let p = RegisterParams::try_from(json!(vec![&number_id, &host, &port])); + assert!(matches!(p, Err(RegisterError::InvalidId(..)))); + + // Wrong host + let p = RegisterParams::try_from(json!(vec![&id, &number_host, &port])); + assert!(matches!(p, Err(RegisterError::InvalidHost(..)))); + + // Wrong port + let p = RegisterParams::try_from(json!(vec![&id, &host, &string_port])); + assert!(matches!(p, Err(RegisterError::InvalidPort(..)))); + + // Wrong param count (params should be 1-3) + let p = RegisterParams::try_from(json!(vec![&id, &host, &port, &id])); + assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); + } + + #[test] + fn test_try_from_json_dict() { + let id = json!(VALID_ID); + let host = json!("host"); + let port = json!(80); + + for v in [ + HashMap::from([("tower_id", &id), ("host", &host), ("port", &port)]), + HashMap::from([("tower_id", &id), ("host", &host)]), + HashMap::from([("tower_id", &id)]), + ] { + let p = RegisterParams::try_from(json!(v)); + assert!(matches!(p, Ok(..))); + } + + // Id key missing + let p = + RegisterParams::try_from(json!(HashMap::from([("host", &host), ("port", &port)]))); + assert!(matches!(p, Err(RegisterError::InvalidId(..)))); + + // Wrong id key + let p = RegisterParams::try_from(json!(HashMap::from([ + ("wrong_tower_id", &id), + ("tower_id", &id), + ("host", &host), + ("port", &port) + ]))); + assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); + + // Wrong host key + let p = RegisterParams::try_from(json!(HashMap::from([ + ("tower_id", &id), + ("wrong_host", &host), + ("port", &port) + ]))); + assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); + + // Wrong port key + let p = RegisterParams::try_from(json!(HashMap::from([ + ("tower_id", &id), + ("host", &host), + ("wrong_port", &port) + ]))); + assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); + + // Wrong param count (params should be 1-3) + let p = RegisterParams::try_from(json!(HashMap::from([ + ("tower_id", &id), + ("host", &host), + ("port", &port), + ("another_param", &json!(0)) + ]))); + assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); + } + + #[test] + fn test_try_from_other_json() { + // Unexpected json object (it must be either String or Array) + let p = RegisterParams::try_from(json!(true)); + assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); + } + } + + mod get_appointment_command { + use super::*; + + #[test] + fn test_try_from_array() { + let id = json!(VALID_ID); + let wrong_id = + json!("050dea894c967319407265764aba31bdef75d463f96800f34dd6df61380d82dfc0"); + let number_id = json!(0); + + let locator = json!("c69517f00d9482e6b1c41639f9bdfd5c"); + let wrong_locator = + json!("c69517f00d9482e6b1c41639f9bdfd5cc69517f00d9482e6b1c41639f9bdfd5c"); + let number_locator = json!(1); + + // Valid params + let p = GetAppointmentParams::try_from(json!(vec![&id, &locator])); + assert!(matches!(p, Ok(..))); + + // Wrong params + // Id is a hex string but the format is wrong (wrong prefix) + let p = GetAppointmentParams::try_from(json!(vec![&wrong_id, &locator])); + assert!(matches!(p, Err(GetAppointmentError::InvalidId(..)))); + // Ud is not a hex string + let p = GetAppointmentParams::try_from(json!(vec![&number_id, &wrong_locator])); + assert!(matches!(p, Err(GetAppointmentError::InvalidId(..)))); + + // Locator is a hex string but not properly formatted (wrong length) + let p = GetAppointmentParams::try_from(json!(vec![&id, &wrong_locator])); + assert!(matches!(p, Err(GetAppointmentError::InvalidLocator(..)))); + // Locator is not a hex string + let p = GetAppointmentParams::try_from(json!(vec![&id, &number_locator])); + assert!(matches!(p, Err(GetAppointmentError::InvalidLocator(..)))); + } + + #[test] + fn test_try_from_dict() { + let id = json!(VALID_ID); + let locator = json!("c69517f00d9482e6b1c41639f9bdfd5c"); + + // Valid params + let p = GetAppointmentParams::try_from(json!(HashMap::from([ + ("tower_id", &id), + ("locator", &locator) + ]))); + assert!(matches!(p, Ok(..))); + + // Wrong keys + let p = GetAppointmentParams::try_from(json!(HashMap::from([ + ("wrong_tower_id", &id), + ("locator", &locator) + ]))); + assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..)))); + + let p = GetAppointmentParams::try_from(json!(HashMap::from([ + ("tower_id", &id), + ("wrong_locator", &locator) + ]))); + assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..)))); + + // Too many parameters + let p = GetAppointmentParams::try_from(json!(HashMap::from([ + ("tower_id", &id), + ("locator", &locator), + ("another_param", &json!(0)) + ]))); + assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..)))); + } + + #[test] + fn test_try_from_other_json() { + // Unexpected json object (it must be either String or Array) + let p = RegisterParams::try_from(json!(true)); + assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); + } + + #[test] + fn test_wrong_param_count() { + // The param count for get_appointment must be 2. + let params_vec = [vec![], vec![1], vec![1, 2, 3]]; + + for params in params_vec { + let p = GetAppointmentParams::try_from(json!(params)); + assert!(matches!(p, Err(..))); + } + } + } +} diff --git a/teos-ldk-client/src/lib.rs b/teos-ldk-client/src/lib.rs new file mode 100644 index 00000000..f6f0e896 --- /dev/null +++ b/teos-ldk-client/src/lib.rs @@ -0,0 +1,676 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::sync::MutexGuard; +use std::sync::{Arc, Mutex}; + +use serde::{Deserialize, Serialize}; + +use teos_common::appointment::{Appointment, Locator}; +use teos_common::net::NetAddr; +use teos_common::receipts::AppointmentReceipt; +use teos_common::TowerId; +use teos_common::{cryptography, errors}; + +use crate::convert::CommitmentRevocation; +use crate::http::AddAppointmentError; +use crate::net::http; +use crate::wt_client::{RevocationData, WTClient}; + +pub mod convert; +pub mod net; +pub mod retrier; +mod ser; +pub mod storage; +pub mod wt_client; + +#[cfg(test)] +mod test_utils; + +/// The status the tower can be found at. +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Copy, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TowerStatus { + Reachable, + TemporaryUnreachable, + Unreachable, + SubscriptionError, + Misbehaving, +} + +/// The status an appointment can be at. +pub enum AppointmentStatus { + Accepted, + Pending, + Invalid, +} + +/// Errors related to updating a subscription +#[derive(Debug, PartialEq, Eq)] +pub enum SubscriptionError { + Expiry, + Slots, +} + +impl SubscriptionError { + /// Whether the error is related to the expiry time or not. + pub fn is_expiry(&self) -> bool { + *self == SubscriptionError::Expiry + } +} + +impl fmt::Display for TowerStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + TowerStatus::Reachable => "reachable", + TowerStatus::TemporaryUnreachable => "temporary unreachable", + TowerStatus::Unreachable => "unreachable", + TowerStatus::SubscriptionError => "subscription error", + TowerStatus::Misbehaving => "misbehaving", + } + ) + } +} + +impl TowerStatus { + /// Whether the tower is reachable or not. + pub fn is_reachable(&self) -> bool { + *self == TowerStatus::Reachable + } + + /// Whether the tower is unreachable or not. + pub fn is_temporary_unreachable(&self) -> bool { + *self == TowerStatus::TemporaryUnreachable + } + + /// Whether the tower is unreachable or not. + pub fn is_unreachable(&self) -> bool { + *self == TowerStatus::Unreachable + } + + /// Whether the tower is misbehaving or not. + pub fn is_misbehaving(&self) -> bool { + *self == TowerStatus::Misbehaving + } + + /// Whether there is a subscription issue with the tower. + pub fn is_subscription_error(&self) -> bool { + *self == TowerStatus::SubscriptionError + } + + /// Whether the tower can be manually retried + pub fn is_retryable(&self) -> bool { + self.is_unreachable() || self.is_subscription_error() + } +} + +/// Summarized data associated with a given tower. +#[derive(Clone, Serialize, Debug, PartialEq, Eq)] +pub struct TowerSummary { + #[serde(flatten)] + pub net_addr: NetAddr, + pub available_slots: u32, + subscription_start: u32, + pub subscription_expiry: u32, + pub status: TowerStatus, + #[serde(serialize_with = "teos_common::ser::serialize_locators")] + pub pending_appointments: HashSet, + #[serde(serialize_with = "teos_common::ser::serialize_locators")] + pub invalid_appointments: HashSet, +} + +impl TowerSummary { + /// Creates a new [TowerSummary] instance. + pub fn new( + net_addr: String, + available_slots: u32, + subscription_start: u32, + subscription_expiry: u32, + ) -> Self { + Self { + net_addr: NetAddr::new(net_addr), + available_slots, + subscription_start, + subscription_expiry, + status: TowerStatus::Reachable, + pending_appointments: HashSet::new(), + invalid_appointments: HashSet::new(), + } + } + + /// Creates a new instance with some associated appointment data. + pub fn with_appointments( + net_addr: String, + available_slots: u32, + subscription_start: u32, + subscription_expiry: u32, + pending_appointments: HashSet, + invalid_appointments: HashSet, + ) -> Self { + Self { + net_addr: NetAddr::new(net_addr), + available_slots, + subscription_start, + subscription_expiry, + status: TowerStatus::Reachable, + pending_appointments, + invalid_appointments, + } + } + + /// Creates a new instance using the existing info but updating the status. + pub fn with_status(mut self, status: TowerStatus) -> Self { + self.status = status; + self + } + + /// Updates the main information about the summary while preserving the appointment maps. + pub fn udpate( + &mut self, + net_addr: String, + available_slots: u32, + subscription_start: u32, + subscription_expiry: u32, + ) { + self.net_addr = NetAddr::new(net_addr); + self.available_slots = available_slots; + self.subscription_start = subscription_start; + self.subscription_expiry = subscription_expiry; + } +} + +impl From for TowerSummary { + fn from(info: TowerInfo) -> Self { + TowerSummary::with_appointments( + info.net_addr, + info.available_slots, + info.subscription_start, + info.subscription_expiry, + info.pending_appointments + .iter() + .map(|a| a.locator) + .collect(), + info.invalid_appointments + .iter() + .map(|a| a.locator) + .collect(), + ) + .with_status(info.status) + } +} + +/// Summarized data associated with a given tower. +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +#[serde(crate = "serde")] +pub struct TowerInfo { + pub net_addr: String, + pub available_slots: u32, + pub subscription_start: u32, + pub subscription_expiry: u32, + pub status: TowerStatus, + #[serde(default)] + pub appointments: HashMap, + #[serde(default)] + pub pending_appointments: Vec, + #[serde(default)] + pub invalid_appointments: Vec, + #[serde(default)] + pub misbehaving_proof: Option, +} + +impl TowerInfo { + /// Creates a new [TowerInfo] instance. + pub fn new( + net_addr: String, + available_slots: u32, + subscription_start: u32, + subscription_expiry: u32, + appointments: HashMap, + pending_appointments: Vec, + invalid_appointments: Vec, + ) -> Self { + Self { + net_addr, + available_slots, + subscription_start, + subscription_expiry, + status: TowerStatus::Reachable, + appointments, + pending_appointments, + invalid_appointments, + misbehaving_proof: None, + } + } + + /// Creates a new instance using the existing info but updating the status. + pub fn with_status(mut self, status: TowerStatus) -> Self { + self.status = status; + self + } + + /// Sets the misbehaving proof of a tower. + pub fn set_misbehaving_proof(&mut self, proof: MisbehaviorProof) { + self.misbehaving_proof = Some(proof); + } + + pub fn to_vec(&self) -> Result, bincode::Error> { + bincode::serialize(self) + } + + pub fn from_slice(slice: &[u8]) -> Result { + bincode::deserialize(slice) + } +} + +/// A misbehaving proof. Contains proof of a tower replying with a public key different from the advertised one. +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +pub struct MisbehaviorProof { + #[serde(with = "hex::serde")] + pub locator: Locator, + pub appointment_receipt: AppointmentReceipt, + pub recovered_id: TowerId, +} + +impl MisbehaviorProof { + /// Creates a new [MisbehavingProof] instance. + pub fn new( + locator: Locator, + appointment_receipt: AppointmentReceipt, + recovered_id: TowerId, + ) -> Self { + Self { + locator, + appointment_receipt, + recovered_id, + } + } +} + +/// Sends an appointment to all registered towers for every new commitment transaction. +/// +/// The appointment is built using the data provided by the backend (dispute txid and penalty transaction). +pub async fn on_commitment_revocation( + wt_client: Arc>, + commitment_revocation: CommitmentRevocation, +) -> Result<(), Box> { + log::debug!( + "New commitment revocation received for channel {}. Commit number {}", + commitment_revocation.channel_id, + commitment_revocation.commit_num + ); + + // TODO: For now, to_self_delay is hardcoded to 42. Revisit and define it better / remove it when / if needed + let locator = Locator::new(commitment_revocation.commitment_txid); + let appointment = Appointment::new( + locator, + cryptography::encrypt( + &commitment_revocation.penalty_tx, + &commitment_revocation.commitment_txid, + ) + .unwrap(), + 42, + ); + let signature = cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk); + + // Looks like we cannot iterate through towers given a locked state is not Send (due to the async call), + // so we need to clone the bare minimum. + let towers = wt_client + .lock() + .unwrap() + .towers + .iter() + .map(|(id, info)| (*id, info.net_addr.clone(), info.status)) + .collect::>(); + + for (tower_id, net_addr, status) in towers { + if status.is_reachable() { + match http::add_appointment(tower_id, &net_addr, &appointment, &signature).await { + Ok((slots, receipt)) => { + wt_client + .lock() + .unwrap() + .add_appointment_receipt(tower_id, locator, slots, &receipt); + log::debug!("Response verified and data stored in the database"); + } + Err(e) => match e { + AddAppointmentError::RequestError(e) => { + if e.is_connection() { + log::warn!( + "{tower_id} cannot be reached. Adding {} to pending appointments", + appointment.locator + ); + let mut state = wt_client.lock().unwrap(); + state.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable); + state.add_pending_appointment(tower_id, &appointment); + send_to_retrier(&state, tower_id, appointment.locator); + } + } + AddAppointmentError::ApiError(e) => match e.error_code { + errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => { + log::warn!( + "There is a subscription issue with {tower_id}. Adding {} to pending", + appointment.locator + ); + let mut state = wt_client.lock().unwrap(); + state.set_tower_status(tower_id, TowerStatus::SubscriptionError); + state.add_pending_appointment(tower_id, &appointment); + send_to_retrier(&state, tower_id, appointment.locator); + } + + _ => { + log::warn!( + "{tower_id} rejected the appointment. Error: {}, error_code: {}", + e.error, + e.error_code + ); + wt_client + .lock() + .unwrap() + .add_invalid_appointment(tower_id, &appointment); + } + }, + AddAppointmentError::SignatureError(proof) => { + log::warn!("Cannot recover known tower_id from the appointment receipt. Flagging tower as misbehaving"); + wt_client + .lock() + .unwrap() + .flag_misbehaving_tower(tower_id, proof) + } + }, + }; + } else if status.is_misbehaving() { + log::warn!("{tower_id} is misbehaving. Not sending any further appointments",); + } else { + if status.is_subscription_error() { + log::warn!( + "There is a subscription issue with {tower_id}. Adding {} to pending", + appointment.locator + ); + } else { + log::warn!( + "{tower_id} is {status}. Adding {} to pending", + appointment.locator, + ); + } + + let mut state = wt_client.lock().unwrap(); + state.add_pending_appointment(tower_id, &appointment); + + if !status.is_unreachable() { + send_to_retrier(&state, tower_id, appointment.locator); + } + } + } + + Ok(()) +} + +/// Sends fresh data to a retrier as long as is does not exist, or it does and its running. +fn send_to_retrier(state: &MutexGuard, tower_id: TowerId, locator: Locator) { + if if let Some(status) = state.get_retrier_status(&tower_id) { + // A retrier in the retriers map can only be running or idle + status.is_running() + } else { + true + } { + state + .unreachable_towers + .send((tower_id, RevocationData::Fresh(locator))) + .unwrap(); + } else { + log::debug!("Not sending data to idle retrier ({tower_id}, {locator})") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const STATUSES: [TowerStatus; 5] = [ + TowerStatus::Reachable, + TowerStatus::TemporaryUnreachable, + TowerStatus::Unreachable, + TowerStatus::SubscriptionError, + TowerStatus::Misbehaving, + ]; + + const AVAILABLE_SLOTS: u32 = 21; + const SUBSCRIPTION_START: u32 = 100; + const SUBSCRIPTION_EXPIRY: u32 = SUBSCRIPTION_START + 42; + + mod tower_status { + use super::*; + use TowerStatus::*; + + #[test] + fn test_is_reachable() { + for status in STATUSES { + if status == Reachable { + assert!(status.is_reachable()) + } else { + assert!(!status.is_reachable()); + } + } + } + + #[test] + fn test_is_temporary_reachable() { + for status in STATUSES { + if status == TemporaryUnreachable { + assert!(status.is_temporary_unreachable()) + } else { + assert!(!status.is_temporary_unreachable()); + } + } + } + + #[test] + fn test_is_unreachable() { + for status in STATUSES { + if status == Unreachable { + assert!(status.is_unreachable()) + } else { + assert!(!status.is_unreachable()); + } + } + } + + #[test] + fn test_is_misbehaving() { + for status in STATUSES { + if status == Misbehaving { + assert!(status.is_misbehaving()) + } else { + assert!(!status.is_misbehaving()); + } + } + } + + #[test] + fn test_is_subscription_error() { + for status in STATUSES { + if status == SubscriptionError { + assert!(status.is_subscription_error()) + } else { + assert!(!status.is_subscription_error()); + } + } + } + + #[test] + fn test_is_retryable() { + for status in STATUSES { + if status == Unreachable || status == SubscriptionError { + assert!(status.is_retryable()) + } else { + assert!(!status.is_retryable()); + } + } + } + } + + mod tower_summary { + use super::*; + + use std::iter::FromIterator; + + use teos_common::test_utils::generate_random_appointment; + + impl TowerSummary { + pub fn set_net_addr(&mut self, net_addr: String) { + self.net_addr = NetAddr::new(net_addr); + } + } + + #[test] + fn test_new() { + let net_addr: String = "addr".to_owned(); + + let tower_summary = TowerSummary::new( + net_addr.clone(), + AVAILABLE_SLOTS, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + ); + assert_eq!( + tower_summary, + TowerSummary { + net_addr: NetAddr::new(net_addr), + available_slots: AVAILABLE_SLOTS, + subscription_start: SUBSCRIPTION_START, + subscription_expiry: SUBSCRIPTION_EXPIRY, + status: TowerStatus::Reachable, + pending_appointments: HashSet::new(), + invalid_appointments: HashSet::new(), + }, + ); + } + + #[test] + fn test_with_appointments() { + let net_addr: String = "addr".to_owned(); + + let pending_appointments = + HashSet::from_iter([generate_random_appointment(None).locator]); + let invalid_appointments = + HashSet::from_iter([generate_random_appointment(None).locator]); + + let tower_summary = TowerSummary::with_appointments( + net_addr.clone(), + AVAILABLE_SLOTS, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + pending_appointments.clone(), + invalid_appointments.clone(), + ); + assert_eq!( + tower_summary, + TowerSummary { + net_addr: NetAddr::new(net_addr), + available_slots: AVAILABLE_SLOTS, + subscription_start: SUBSCRIPTION_START, + subscription_expiry: SUBSCRIPTION_EXPIRY, + status: TowerStatus::Reachable, + pending_appointments, + invalid_appointments, + }, + ); + } + + #[test] + fn test_with_status() { + let mut tower_summary = TowerSummary::new( + "addr".to_owned(), + AVAILABLE_SLOTS, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + ); + + let unreachable_tower = tower_summary.clone().with_status(TowerStatus::Unreachable); + tower_summary.status = TowerStatus::Unreachable; + assert_eq!(unreachable_tower, tower_summary); + } + } + + mod tower_info { + use super::*; + + use teos_common::test_utils::{generate_random_appointment, get_random_user_id}; + + impl TowerInfo { + pub fn empty( + net_addr: String, + available_slots: u32, + subscription_start: u32, + subscription_expiry: u32, + ) -> Self { + TowerInfo::new( + net_addr, + available_slots, + subscription_start, + subscription_expiry, + HashMap::new(), + Vec::new(), + Vec::new(), + ) + } + } + + #[test] + fn test_new() { + let tower_info = TowerInfo::new( + "addr".to_owned(), + AVAILABLE_SLOTS, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + HashMap::new(), + Vec::new(), + Vec::new(), + ); + + assert!(tower_info.status.is_reachable()); + assert!(tower_info.misbehaving_proof.is_none()); + } + + #[test] + fn test_with_status() { + let mut tower_info = TowerInfo::empty( + "addr".to_owned(), + AVAILABLE_SLOTS, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + ); + + let unreachable_tower = tower_info.clone().with_status(TowerStatus::Unreachable); + tower_info.status = TowerStatus::Unreachable; + assert_eq!(unreachable_tower, tower_info); + } + + #[test] + fn test_set_misbehaving_proof() { + let mut tower_info = TowerInfo::empty( + "addr".to_owned(), + AVAILABLE_SLOTS, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + ); + assert_eq!(tower_info.misbehaving_proof, None); + + let appointment_receipt = AppointmentReceipt::with_signature( + "user_signature".to_owned(), + SUBSCRIPTION_START + 1, + "tower_signature".to_owned(), + ); + let proof = MisbehaviorProof::new( + generate_random_appointment(None).locator, + appointment_receipt, + get_random_user_id(), + ); + + tower_info.set_misbehaving_proof(proof.clone()); + assert_eq!(tower_info.misbehaving_proof, Some(proof)); + } + } +} diff --git a/teos-ldk-client/src/net/http.rs b/teos-ldk-client/src/net/http.rs new file mode 100644 index 00000000..d8e8307e --- /dev/null +++ b/teos-ldk-client/src/net/http.rs @@ -0,0 +1,638 @@ +use reqwest::{Method, Response}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use teos_common::appointment::Appointment; +use teos_common::cryptography; +use teos_common::net::http::Endpoint; +use teos_common::net::NetAddr; +use teos_common::protos as common_msgs; +use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; +use teos_common::{TowerId, UserId}; + +use crate::MisbehaviorProof; + +/// Represents a generic api response. +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +pub enum ApiResponse { + Response(T), + Error(ApiError), +} + +/// API errors that can be received when interacting with the tower. Error codes match `teos_common::errors`. +#[derive(Serialize, Deserialize, Debug)] +pub struct ApiError { + pub error: String, + pub error_code: u8, +} + +/// Errors related to requests sent to the tower. +#[derive(Debug, PartialEq, Eq)] +pub enum RequestError { + ConnectionError(String), + DeserializeError(String), + Unexpected(String), +} + +impl RequestError { + pub fn is_connection(&self) -> bool { + matches!(self, RequestError::ConnectionError(_)) + } +} + +/// Errors related to the `add_appointment` requests to the tower. +#[derive(Debug)] +pub enum AddAppointmentError { + RequestError(RequestError), + ApiError(ApiError), + SignatureError(MisbehaviorProof), +} + +impl From for AddAppointmentError { + fn from(r: RequestError) -> Self { + AddAppointmentError::RequestError(r) + } +} + +/// Handles the logic of interacting with the `register` endpoint of the tower. +pub async fn register( + tower_id: TowerId, + user_id: UserId, + tower_net_addr: &NetAddr, +) -> Result { + log::info!("Registering in the Eye of Satoshi (tower_id={tower_id})"); + process_post_response( + post_request( + tower_net_addr, + Endpoint::Register, + &common_msgs::RegisterRequest { + user_id: user_id.to_vec(), + }, + ) + .await, + ) + .await + .map(|r: common_msgs::RegisterResponse| { + RegistrationReceipt::with_signature( + user_id, + r.available_slots, + r.subscription_start, + r.subscription_expiry, + r.subscription_signature, + ) + }) +} + +/// Encapsulates the logging and response parsing of sending and appointment to the tower. +pub async fn add_appointment( + tower_id: TowerId, + tower_net_addr: &NetAddr, + appointment: &Appointment, + signature: &str, +) -> Result<(u32, AppointmentReceipt), AddAppointmentError> { + log::debug!( + "Sending appointment {} to tower {tower_id}", + appointment.locator + ); + let (response, receipt) = + send_appointment(tower_id, tower_net_addr, appointment, signature).await?; + log::debug!("Appointment accepted and signed by {tower_id}"); + log::debug!("Remaining slots: {}", response.available_slots); + log::debug!("Start block: {}", response.start_block); + + Ok((response.available_slots, receipt)) +} + +/// Handles the logic of interacting with the `add_appointment` endpoint of the tower. +pub async fn send_appointment( + tower_id: TowerId, + tower_net_addr: &NetAddr, + appointment: &Appointment, + signature: &str, +) -> Result<(common_msgs::AddAppointmentResponse, AppointmentReceipt), AddAppointmentError> { + let request_data = common_msgs::AddAppointmentRequest { + appointment: Some(appointment.clone().into()), + signature: signature.to_owned(), + }; + + match process_post_response( + post_request(tower_net_addr, Endpoint::AddAppointment, &request_data).await, + ) + .await? + { + ApiResponse::Response::(r) => { + let receipt = AppointmentReceipt::with_signature( + signature.to_owned(), + r.start_block, + r.signature.clone(), + ); + let recovered_id = TowerId( + cryptography::recover_pk(&receipt.to_vec(), &receipt.signature().unwrap()).unwrap(), + ); + if recovered_id == tower_id { + Ok((r, receipt)) + } else { + Err(AddAppointmentError::SignatureError(MisbehaviorProof::new( + appointment.locator, + receipt, + recovered_id, + ))) + } + } + ApiResponse::Error(e) => Err(AddAppointmentError::ApiError(e)), + } +} + +/// A generic function to send a request to a tower. +async fn request( + tower_net_addr: &NetAddr, + endpoint: Endpoint, + method: Method, + data: Option, +) -> Result { + // If there is no proxy we only build the client as long as the address is not onion + if tower_net_addr.is_onion() { + return Err(RequestError::ConnectionError( + "Cannot connect to an onion address without a proxy".to_owned(), + )); + } + let client = reqwest::Client::new(); + + let mut request_builder = client.request( + method, + format!("{}{}", tower_net_addr.net_addr(), endpoint.path()), + ); + + if let Some(data) = data { + request_builder = request_builder.json(&data); + } + + request_builder.send().await.map_err(|e| { + log::debug!("An error ocurred when sending data to the tower: {e}"); + if e.is_connect() | e.is_timeout() { + RequestError::ConnectionError( + "Cannot connect to the tower. Connection refused".to_owned(), + ) + } else { + RequestError::Unexpected("Unexpected error ocurred (see logs for more info)".to_owned()) + } + }) +} + +pub async fn post_request( + tower_net_addr: &NetAddr, + endpoint: Endpoint, + data: S, +) -> Result { + request(tower_net_addr, endpoint, Method::POST, Some(data)).await +} + +pub async fn get_request( + tower_net_addr: &NetAddr, + endpoint: Endpoint, +) -> Result { + request::<()>(tower_net_addr, endpoint, Method::GET, None).await +} + +/// Generic function to process the response of a given post request. +pub async fn process_post_response( + post_request: Result, +) -> Result { + // TODO: Check if this can be switched for a map. Not sure how to handle async with maps + match post_request { + Ok(r) => r.json().await.map_err(|e| { + RequestError::DeserializeError(format!("Unexpected response body. Error: {e}")) + }), + Err(e) => Err(e), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + use crate::test_utils::get_dummy_add_appointment_response; + use teos_common::test_utils::{ + generate_random_appointment, get_random_appointment_receipt, + get_random_registration_receipt, get_random_user_id, + }; + + mod request_error { + use super::*; + + #[test] + fn test_is_connection() { + let error_message = "error_msg"; + for error in [ + RequestError::ConnectionError(error_message.to_owned()), + RequestError::DeserializeError(error_message.to_owned()), + RequestError::Unexpected(error_message.to_owned()), + ] { + if error == RequestError::ConnectionError(error_message.to_owned()) { + assert!(error.is_connection()) + } else { + assert!(!error.is_connection()) + } + } + } + } + + #[tokio::test] + async fn test_register() { + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let mut registration_receipt = get_random_registration_receipt(); + registration_receipt.sign(&tower_sk); + + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(registration_receipt).to_string()) + .create_async() + .await; + + let receipt = register( + TowerId(tower_pk), + registration_receipt.user_id(), + &NetAddr::new(server.url()), + ) + .await + .unwrap(); + + api_mock.assert_async().await; + assert_eq!(receipt, registration_receipt); + } + + #[tokio::test] + async fn test_register_connection_error() { + let error = register( + get_random_user_id(), + get_random_user_id(), + &NetAddr::new("http://server_addr".to_owned()), + ) + .await + .unwrap_err(); + + assert!(matches!(error, RequestError::ConnectionError { .. })) + } + + #[tokio::test] + async fn test_register_deserialize_error() { + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!([]).to_string()) + .create_async() + .await; + + let error = register( + get_random_user_id(), + get_random_user_id(), + &NetAddr::new(server.url()), + ) + .await + .unwrap_err(); + + api_mock.assert_async().await; + assert!(matches!(error, RequestError::DeserializeError { .. })) + } + + #[tokio::test] + async fn test_add_appointment() { + // `add_appointment` is basically a pass trough function for `send_appointment` with some logging and a parse of the outputs + // in case there are no errors. All the error cases will be tested in `send_appointment`. + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let appointment = generate_random_appointment(None); + + let appointment_receipt = get_random_appointment_receipt(tower_sk); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); + + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; + + let (response, receipt) = add_appointment( + TowerId(tower_pk), + &NetAddr::new(server.url()), + &appointment, + appointment_receipt.user_signature(), + ) + .await + .unwrap(); + + api_mock.assert_async().await; + assert_eq!(response, add_appointment_response.available_slots); + assert_eq!(receipt, appointment_receipt); + } + + #[tokio::test] + async fn test_send_appointment() { + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let appointment = generate_random_appointment(None); + + let appointment_receipt = get_random_appointment_receipt(tower_sk); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); + + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; + + let (response, receipt) = send_appointment( + TowerId(tower_pk), + &NetAddr::new(server.url()), + &appointment, + appointment_receipt.user_signature(), + ) + .await + .unwrap(); + + api_mock.assert_async().await; + assert_eq!(response, add_appointment_response); + assert_eq!(receipt, appointment_receipt); + } + + #[tokio::test] + async fn test_send_appointment_misbehaving() { + let (sybil_tower_sk, sibyl_tower_pk) = cryptography::get_random_keypair(); + let appointment = generate_random_appointment(None); + + let appointment_receipt = get_random_appointment_receipt(sybil_tower_sk); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); + + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; + + let tower_id = get_random_user_id(); + let error = send_appointment( + tower_id, + &NetAddr::new(server.url()), + &appointment, + appointment_receipt.user_signature(), + ) + .await + .unwrap_err(); + + api_mock.assert_async().await; + if let AddAppointmentError::SignatureError(proof) = error { + assert_eq!( + MisbehaviorProof::new( + appointment.locator, + appointment_receipt, + TowerId(sibyl_tower_pk) + ), + proof + ) + } else { + panic!("SignatureError was expected") + } + } + + #[tokio::test] + async fn test_send_appointment_connection_error() { + let error = send_appointment( + get_random_user_id(), + &NetAddr::new("http://server_addr".to_owned()), + &generate_random_appointment(None), + "user_sig", + ) + .await + .unwrap_err(); + + if let AddAppointmentError::RequestError(e) = error { + assert!(matches!(e, RequestError::ConnectionError { .. })) + } else { + panic!("ConnectionError was expected") + } + } + + #[tokio::test] + async fn test_send_appointment_deserialize_error() { + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!([]).to_string()) + .create_async() + .await; + + let error = send_appointment( + get_random_user_id(), + &NetAddr::new(server.url()), + &generate_random_appointment(None), + "user_sig", + ) + .await + .unwrap_err(); + + api_mock.assert_async().await; + if let AddAppointmentError::RequestError(e) = error { + assert!(matches!(e, RequestError::DeserializeError { .. })) + } else { + panic!("DeserializeError was expected") + } + } + + #[tokio::test] + async fn test_send_appointment_api_error() { + let api_error = ApiError { + error: "error_msg".to_owned(), + error_code: 1, + }; + + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body(json!(api_error).to_string()) + .create_async() + .await; + + let error = send_appointment( + get_random_user_id(), + &NetAddr::new(server.url()), + &generate_random_appointment(None), + "user_sig", + ) + .await + .unwrap_err(); + + api_mock.assert_async().await; + assert!(matches!(error, AddAppointmentError::ApiError { .. })); + } + + #[tokio::test] + async fn test_request() { + let mut server = mockito::Server::new_async().await; + + // Test with POST + let api_mock_post = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + + let response_post = request( + &NetAddr::new(server.url()), + Endpoint::Register, + Method::POST, + Some(json!("")), + ) + .await; + + api_mock_post.assert_async().await; + assert!(matches!(response_post, Ok(Response { .. }))); + + // Test with GET + let api_mock_get = server + .mock("GET", Endpoint::Ping.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + + let response_get = request::<()>( + &NetAddr::new(server.url()), + Endpoint::Ping, + Method::GET, + None, + ) + .await; + + api_mock_get.assert_async().await; + assert!(matches!(response_get, Ok(Response { .. }))); + } + + #[tokio::test] + async fn test_request_connection_error() { + assert!(request( + &NetAddr::new("http://unreachable_url".to_owned()), + Endpoint::Register, + Method::POST, + Some(json!("")), + ) + .await + .unwrap_err() + .is_connection()); + + assert!(request( + &NetAddr::new("http://unreachable_url".to_owned()), + Endpoint::Ping, + Method::GET, + None::<&str>, + ) + .await + .unwrap_err() + .is_connection()); + } + + #[tokio::test] + async fn test_get_request() { + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("GET", Endpoint::Ping.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + let response = get_request(&NetAddr::new(server.url()), Endpoint::Ping).await; + + api_mock.assert_async().await; + + assert!(matches!(response, Ok(Response { .. }))); + } + + #[tokio::test] + async fn test_get_request_connection_error() { + assert!(get_request( + &NetAddr::new("http://unreachable_url".to_owned()), + Endpoint::Ping, + ) + .await + .unwrap_err() + .is_connection()); + } + + #[tokio::test] + async fn test_post_request() { + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + + let response = + post_request(&NetAddr::new(server.url()), Endpoint::Register, json!("")).await; + + api_mock.assert_async().await; + assert!(matches!(response, Ok(Response { .. }))); + } + + #[tokio::test] + async fn test_post_request_connection_error() { + assert!(post_request( + &NetAddr::new("http://unreachable_url".to_owned()), + Endpoint::Register, + json!(""), + ) + .await + .unwrap_err() + .is_connection()); + } + + #[tokio::test] + async fn test_process_post_response_json_error() { + // `process_post_response` is a pass-trough function that maps json deserialization errors from `post_request`. + // So just testing that specific case should be enough. + + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::GetAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + + // Any expected response work here as long as it cannot be properly deserialized + let error = process_post_response::>( + post_request( + &NetAddr::new(server.url()), + Endpoint::GetAppointment, + json!(""), + ) + .await, + ) + .await + .unwrap_err(); + + api_mock.assert_async().await; + assert!(matches!(error, RequestError::DeserializeError { .. })); + } +} diff --git a/teos-ldk-client/src/net/mod.rs b/teos-ldk-client/src/net/mod.rs new file mode 100644 index 00000000..3883215f --- /dev/null +++ b/teos-ldk-client/src/net/mod.rs @@ -0,0 +1 @@ +pub mod http; diff --git a/teos-ldk-client/src/retrier.rs b/teos-ldk-client/src/retrier.rs new file mode 100644 index 00000000..3ef05a7c --- /dev/null +++ b/teos-ldk-client/src/retrier.rs @@ -0,0 +1,1712 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use tokio::sync::mpsc::{error::TryRecvError, UnboundedReceiver}; + +use backoff::future::retry_notify; +use backoff::{Error, ExponentialBackoff}; + +use teos_common::appointment::Locator; +use teos_common::cryptography; +use teos_common::errors; +use teos_common::TowerId; + +use crate::net::http::{self, AddAppointmentError}; +use crate::wt_client::{RevocationData, WTClient}; +use crate::{MisbehaviorProof, TowerStatus}; + +const POLLING_TIME: u64 = 1; + +#[derive(Eq, PartialEq, Debug)] +enum RetryError { + // bool marks whether the Subscription error is permanent or not + Subscription(String, bool), + Unreachable, + Misbehaving(MisbehaviorProof), + Abandoned, +} + +impl Display for RetryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RetryError::Subscription(r, _) => write!(f, "{r}"), + RetryError::Unreachable => write!(f, "Tower cannot be reached"), + RetryError::Misbehaving(_) => write!(f, "Tower misbehaved"), + RetryError::Abandoned => write!(f, "Tower was abandoned. Skipping retry"), + } + } +} + +impl RetryError { + fn is_permanent(&self) -> bool { + matches!( + self, + RetryError::Subscription(_, true) | RetryError::Misbehaving(_) | RetryError::Abandoned + ) + } +} + +pub struct RetryManager { + wt_client: Arc>, + unreachable_towers: UnboundedReceiver<(TowerId, RevocationData)>, + max_elapsed_time_secs: u16, + auto_retry_delay: u32, + max_interval_time_secs: u16, + retriers: HashMap>, +} + +impl RetryManager { + pub fn new( + wt_client: Arc>, + unreachable_towers: UnboundedReceiver<(TowerId, RevocationData)>, + max_elapsed_time_secs: u16, + auto_retry_delay: u32, + max_interval_time_secs: u16, + ) -> Self { + RetryManager { + wt_client, + unreachable_towers, + max_elapsed_time_secs, + auto_retry_delay, + max_interval_time_secs, + retriers: HashMap::new(), + } + } + + /// Starts the retry manager's main logic loop. + /// This method will keep running until the `unreachable_towers` sender disconnects. + /// + /// It will receive a `(tower_id, revocation_data)` pair and try to send all the appointments contained + /// in `revocation_data` (identified by `locator`) to the tower with `tower_id`. This is done by spawning + /// a tokio thread for each `tower_id` that tries to send all the pending appointments. + /// + /// The content of [RevocationData] will depend on who called `unreachable_towers.send`: + /// - If it was called by `on_commitment_revocation`, the data will be fresh and contain a single locator + /// - If it was called by the [WTClient] constructor, or by manually retrying, then the data will the stale + /// and contain a `HashSet` with, potentially, many locators. + pub async fn manage_retry(&mut self) { + log::info!("Starting retry manager"); + + loop { + match self.unreachable_towers.try_recv() { + Ok((tower_id, data)) => { + // Not start a retry if the tower is flagged to be abandoned + if !self + .wt_client + .lock() + .unwrap() + .towers + .contains_key(&tower_id) + { + log::info!("Skipping retrying abandoned tower {tower_id}"); + } else if let Some(retrier) = self.retriers.get(&tower_id) { + if retrier.is_idle() { + if !data.is_none() { + log::error!("Data was send to an idle retrier. This should have never happened. Please report! ({data:?})"); + continue; + } + log::info!( + "Manually finished idling. Flagging {} for retry", + retrier.tower_id + ); + // While a retrier is idle data is not kept in memory. + // Load the pending appointments from the DB and feed them to the retrier + retrier.set_status(RetrierStatus::Stopped); + retrier.pending_appointments.lock().unwrap().extend( + self.wt_client + .lock() + .unwrap() + .storage + .load_appointment_locators( + retrier.tower_id, + crate::AppointmentStatus::Pending, + ), + ); + } else { + self.add_pending_appointments(tower_id, data.into()); + } + } else { + self.add_pending_appointments(tower_id, data.into()); + } + } + Err(TryRecvError::Empty) => { + // Keep only running retriers and retriers ready to be started/re-started. + // This will remove failed ones and ones finished successfully and have no pending appointments. + // + // Note that a failed retrier could have received some new appointments to retry. In this case, we don't try to send + // them because we know that that tower is unreachable. We most likely received these new appointments while the tower + // was still flagged as temporarily unreachable when cleaning up after giving up retrying. + self.retriers.retain(|_, retrier| { + retrier.remove_if_failed(); + retrier.should_start() || retrier.is_running() || retrier.is_idle() + }); + // Start all the ready retriers. + for retrier in self.retriers.values() { + if retrier.should_start() { + self.start_retrying(retrier.clone()); + // Effectively this is the same as `if retrier.is_idle` plus returning for how long is true. + } else if let Some(t) = retrier.get_elapsed_time() { + if t > self.auto_retry_delay as u64 { + log::info!( + "Finished idling. Flagging {} for retry", + retrier.tower_id + ); + // While a retrier is idle data is not kept in memory. + // Load the pending appointments from the DB and feed them to the retrier + retrier.set_status(RetrierStatus::Stopped); + retrier.pending_appointments.lock().unwrap().extend( + self.wt_client + .lock() + .unwrap() + .storage + .load_appointment_locators( + retrier.tower_id, + crate::AppointmentStatus::Pending, + ), + ); + } + } + } + // Sleep to not waste a lot of CPU cycles. + tokio::time::sleep(Duration::from_secs(POLLING_TIME)).await; + } + Err(TryRecvError::Disconnected) => break, + } + } + } + + /// Adds an appointment to pending for a given tower. + /// + /// If the tower is not currently being retried, a new entry for it is created, otherwise, the data is appended to the existing entry. + fn add_pending_appointments(&mut self, tower_id: TowerId, locators: HashSet) { + if let std::collections::hash_map::Entry::Vacant(e) = self.retriers.entry(tower_id) { + log::debug!("Creating a new entry for tower {tower_id}"); + e.insert(Arc::new(Retrier::new( + self.wt_client.clone(), + tower_id, + locators, + ))); + } else { + let mut pending_appointments = self + .retriers + .get(&tower_id) + .unwrap() + .pending_appointments + .lock() + .unwrap(); + for locator in locators { + log::debug!("Adding pending appointment {locator} to existing tower {tower_id}",); + pending_appointments.insert(locator); + } + } + } + + fn start_retrying(&self, retrier: Arc) { + log::info!("Retrying tower {}", retrier.tower_id); + retrier.start(self.max_elapsed_time_secs, self.max_interval_time_secs); + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum RetrierStatus { + /// Retrier is stopped. This could happen if the retrier was never started or it started and + /// finished successfully. If a retrier is stopped and has some pending appointments, it should be + /// started/re-started, otherwise, it can be deleted safely. + Stopped, + /// Retrier is currently retrying the tower. If the retrier receives new appointments, it will + /// **try** to send them along (but it might not send them). + /// + /// If a retrier status is `Running`, then its associated tower is either temporary unreachable or subscription error. + Running, + /// Retrier failed retrying the tower. Should not be re-started. + /// + /// If a retrier status is `Failed`, then its associated tower is neither reachable nor temporary unreachable. + Failed, + /// Retrier is currently idle waiting for a signal to start working again. An Idle retrier can be forced to start + /// working again by the user by manually calling `retrytower`. + /// + /// If a retrier status is `Idle`, then its associated tower is unreachable. + Idle(Instant), +} + +impl RetrierStatus { + /// Check whether the status is [Running](RetrierStatus::Stopped). + pub fn is_stopped(&self) -> bool { + *self == RetrierStatus::Stopped + } + + /// Check whether the status is [Running](RetrierStatus::Running). + pub fn is_running(&self) -> bool { + *self == RetrierStatus::Running + } + + /// Check whether the status is [Idle](RetrierStatus::Idle). + pub fn is_idle(&self) -> bool { + matches!(self, RetrierStatus::Idle { .. }) + } + + /// Check whether the status is [Failed](RetrierStatus::Failed). + pub fn failed(&self) -> bool { + *self == RetrierStatus::Failed + } + + /// Gets the elapsed time of an [Idle](RetrierStatus::Idle) status, [None] otherwise. + pub fn get_elapsed_time(&self) -> Option { + if let RetrierStatus::Idle(x) = *self { + Some(x.elapsed().as_secs()) + } else { + None + } + } +} + +pub struct Retrier { + wt_client: Arc>, + tower_id: TowerId, + pending_appointments: Mutex>, + status: Mutex, +} + +impl Retrier { + pub fn new( + wt_client: Arc>, + tower_id: TowerId, + locators: HashSet, + ) -> Self { + Self { + wt_client, + tower_id, + pending_appointments: Mutex::new(locators), + status: Mutex::new(RetrierStatus::Stopped), + } + } + + fn has_pending_appointments(&self) -> bool { + !self.pending_appointments.lock().unwrap().is_empty() + } + + fn set_status(&self, status: RetrierStatus) { + *self.status.lock().unwrap() = status.clone(); + + // Add or remove retriers from WTClient based on the RetrierStatus + if self.is_running() || self.is_idle() { + log::debug!("Adding {} to active retriers", self.tower_id); + self.wt_client + .lock() + .unwrap() + .retriers + .insert(self.tower_id, status); + } else if self.is_stopped() { + // We are not removing failed retriers here to prevent a manual retry until the retrier is removed from + // the manager + log::debug!("Removing retrier {} from active retriers", self.tower_id); + self.wt_client + .lock() + .unwrap() + .retriers + .remove(&self.tower_id); + } + } + + /// Maps [RetrierStatus::is_stopped] + pub fn is_stopped(&self) -> bool { + self.status.lock().unwrap().is_stopped() + } + + /// Maps [RetrierStatus::is_running] + pub fn is_running(&self) -> bool { + self.status.lock().unwrap().is_running() + } + + /// Maps [RetrierStatus::is_idle] + pub fn is_idle(&self) -> bool { + self.status.lock().unwrap().is_idle() + } + + /// Maps [RetrierStatus::failed] + pub fn failed(&self) -> bool { + self.status.lock().unwrap().failed() + } + + /// Maps [RetrierStatus::get_elapsed_time] + pub fn get_elapsed_time(&self) -> Option { + self.status.lock().unwrap().get_elapsed_time() + } + + pub fn should_start(&self) -> bool { + // A retrier can be started/re-started if it is stopped (i.e. not running and not failed) + // and has some pending appointments. + self.is_stopped() && self.has_pending_appointments() + } + + pub fn start(self: Arc, max_elapsed_time_secs: u16, max_interval_time_secs: u16) { + // We shouldn't be retrying failed and running retriers. + debug_assert_eq!(*self.status.lock().unwrap(), RetrierStatus::Stopped); + + // When manually retrying the tower may be in either SubscriptionError or Unreachable state. + // Flag this as TemporaryUnreachable only if there is no SubscriptionError. + // Rationale: if there is a subscription error that needs to be handled first, otherwise we'll + // waste a retry cycle with a request that will always fail. + { + let mut state = self.wt_client.lock().unwrap(); + if !state + .get_tower_status(&self.tower_id) + .unwrap() + .is_subscription_error() + { + state.set_tower_status(self.tower_id, TowerStatus::TemporaryUnreachable); + } + } + self.set_status(RetrierStatus::Running); + + tokio::spawn(async move { + let r = retry_notify( + ExponentialBackoff { + max_elapsed_time: Some(Duration::from_secs(max_elapsed_time_secs as u64)), + max_interval: Duration::from_secs(max_interval_time_secs as u64), + ..ExponentialBackoff::default() + }, + || async { self.run().await }, + |err, _| { + log::warn!("Retry error happened with {}. {err}", self.tower_id); + }, + ) + .await; + + match r { + Ok(_) => { + log::info!("Retry strategy succeeded for {}", self.tower_id); + // Set the tower status now so new appointment doesn't go to the retry manager. + self.wt_client + .lock() + .unwrap() + .set_tower_status(self.tower_id, TowerStatus::Reachable); + // Retrier succeeded and can be re-used by re-starting it. + self.set_status(RetrierStatus::Stopped); + } + Err(e) => { + // Notice we'll end up here after a permanent error. That is, either after finishing the backoff strategy + // unsuccessfully or by manually raising such an error (like when facing a tower misbehavior). + log::warn!("Retry strategy gave up for {}. {e}", self.tower_id); + if e.is_permanent() { + self.set_status(RetrierStatus::Failed); + } + + match e { + RetryError::Subscription(_, true) => { + log::info!("Setting {} status as subscription error", self.tower_id); + self.wt_client + .lock() + .unwrap() + .set_tower_status(self.tower_id, TowerStatus::SubscriptionError) + } + RetryError::Misbehaving(p) => { + log::warn!("Cannot recover known tower_id from the appointment receipt. Flagging tower as misbehaving"); + self.wt_client + .lock() + .unwrap() + .flag_misbehaving_tower(self.tower_id, p); + } + RetryError::Abandoned => { + log::info!("Skipping retrying abandoned tower {}", self.tower_id) + } + // This covers `RetryError::Unreachable` and `RetryError::Subscription(_, false)` + _ => { + log::debug!("Starting to idle"); + self.set_status(RetrierStatus::Idle(Instant::now())); + // Clear all pending appointments so they do not waste any memory while idling + self.pending_appointments.lock().unwrap().clear(); + self.wt_client + .lock() + .unwrap() + .set_tower_status(self.tower_id, TowerStatus::Unreachable); + } + } + } + } + }); + } + + async fn run(&self) -> Result<(), Error> { + // Create a new scope so we can get all the data only locking the WTClient once. + let (tower_id, status, net_addr, user_id, user_sk) = { + let wt_client = self.wt_client.lock().unwrap(); + if !wt_client.towers.contains_key(&self.tower_id) { + return Err(Error::permanent(RetryError::Abandoned)); + } + + let tower = wt_client.towers.get(&self.tower_id).unwrap(); + ( + self.tower_id, + tower.status, + tower.net_addr.clone(), + wt_client.user_id, + wt_client.user_sk, + ) + }; + + // If the tower state is subscription_error we need to re-register first. If we cannot, then the retry is aborted. + if status.is_subscription_error() { + let receipt = http::register(tower_id, user_id, &net_addr) + .await + .map_err(|e| { + log::debug!("Cannot renew registration with tower. Error: {e:?}"); + Error::transient(RetryError::Subscription( + "Cannot renew registration with tower".to_owned(), + false, + )) + })?; + if !receipt.verify(&tower_id) { + return Err(Error::permanent(RetryError::Subscription("Registration receipt contains bad signature. Are you using the right tower_id?".to_owned(), true))); + } + self.wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, net_addr.net_addr(), &receipt) + .map_err(|e| { + let reason = if e.is_expiry() { + "Registration receipt contains a subscription expiry that is not higher than the one we are currently registered for" + } else { + "Registration receipt does not contain more slots than the ones we are currently registered for" + }; + Error::permanent(RetryError::Subscription(reason.to_owned(), true)) + })?; + } + + while self.has_pending_appointments() { + let locators = self.pending_appointments.lock().unwrap().clone(); + for locator in locators.into_iter() { + let appointment = self + .wt_client + .lock() + .unwrap() + .storage + .load_appointment(locator) + .unwrap(); + + match http::add_appointment( + tower_id, + &net_addr, + &appointment, + &cryptography::sign(&appointment.to_vec(), &user_sk), + ) + .await + { + Ok((slots, receipt)) => { + self.pending_appointments.lock().unwrap().remove(&locator); + let mut wt_client = self.wt_client.lock().unwrap(); + wt_client.add_appointment_receipt( + tower_id, + appointment.locator, + slots, + &receipt, + ); + wt_client.remove_pending_appointment(tower_id, appointment.locator); + log::debug!("Response verified and data stored in the database"); + } + Err(e) => { + match e { + AddAppointmentError::RequestError(e) => { + if e.is_connection() { + log::warn!( + "{tower_id} cannot be reached. Tower will be retried later" + ); + return Err(Error::transient(RetryError::Unreachable)); + } + } + AddAppointmentError::ApiError(e) => match e.error_code { + errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => { + log::warn!("There is a subscription issue with {tower_id}"); + self.wt_client + .lock() + .unwrap() + .set_tower_status(tower_id, TowerStatus::SubscriptionError); + return Err(Error::transient(RetryError::Subscription( + "Subscription error".to_owned(), + false, + ))); + } + _ => { + log::warn!( + "{tower_id} rejected the appointment. Error: {}, error_code: {}", + e.error, + e.error_code + ); + // We need to move the appointment from pending to invalid + // Add it first to invalid and remove it from pending later so a cascade delete is not triggered + self.pending_appointments.lock().unwrap().remove(&locator); + let mut wt_client = self.wt_client.lock().unwrap(); + wt_client.add_invalid_appointment(tower_id, &appointment); + wt_client + .remove_pending_appointment(tower_id, appointment.locator); + } + }, + AddAppointmentError::SignatureError(proof) => { + return Err(Error::permanent(RetryError::Misbehaving(proof))); + } + } + } + } + } + } + + Ok(()) + } + + /// Removed our retrier identifier from the WTClient if the retrier has failed + pub fn remove_if_failed(&self) { + if self.failed() { + log::debug!( + "Removing failed retrier {} from active retriers", + self.tower_id + ); + self.wt_client + .lock() + .unwrap() + .retriers + .remove(&self.tower_id); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde_json::json; + + use tokio::sync::mpsc::unbounded_channel; + + use teos_common::errors; + use teos_common::net::http::Endpoint; + use teos_common::protos::AddAppointmentRequest; + use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; + use teos_common::test_utils::{ + generate_random_appointment, get_random_registration_receipt, + get_registration_receipt_from_previous, + }; + + use crate::net::http::ApiError; + use crate::storage::kv::KVStorage; + use crate::storage::mock_kv::MemoryStore; + use crate::storage::persister::Persister; + use crate::test_utils::get_dummy_add_appointment_response; + + const LONG_AUTO_RETRY_DELAY: u32 = 60; + const SHORT_AUTO_RETRY_DELAY: u32 = 3; + const API_DELAY: f64 = 0.5; + const HALF_API_DELAY: f64 = API_DELAY / 2.0; + const MAX_ELAPSED_TIME: u16 = 3; + const MAX_INTERVAL_TIME: u16 = 1; + const MAX_RUN_TIME: f64 = 1.0; + + macro_rules! wait_until { + () => {}; + ($cond:expr $(,)?) => { + loop { + if $cond { + break; + } + tokio::time::sleep(Duration::from_secs_f64(0.1)).await; + } + }; + } + + impl Retrier { + fn empty(wt_client: Arc>, tower_id: TowerId) -> Self { + Self { + wt_client, + tower_id, + pending_appointments: Mutex::new(HashSet::new()), + status: Mutex::new(RetrierStatus::Stopped), + } + } + } + + #[tokio::test] + async fn test_manage_retry_reachable() { + let (tx, rx) = unbounded_channel(); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + let mut server = mockito::Server::new_async().await; + + // Add a tower with pending appointments + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + // Add appointment to pending + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Prepare the mock response + let mut add_appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + add_appointment_receipt.sign(&tower_sk); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); + + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |_| { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(add_appointment_response).to_string().into() + }) + .create_async() + .await; + + // Start the task and send the tower to the channel for retry + tx.send((tower_id, RevocationData::Fresh(appointment.locator))) + .unwrap(); + + let wt_client_clone = wt_client.clone(); + let task = tokio::spawn(async move { + RetryManager::new( + wt_client_clone, + rx, + MAX_ELAPSED_TIME, + LONG_AUTO_RETRY_DELAY, + MAX_INTERVAL_TIME, + ) + .manage_retry() + .await + }); + + // Wait for a fraction of the API delay and check how the tower status changed + tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; + assert!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_running()); + + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .is_none()); + + { + let state = wt_client.lock().unwrap(); + assert!(state.get_tower_status(&tower_id).unwrap().is_reachable()); + assert!(!state + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + } + api_mock.assert_async().await; + + task.abort(); + } + + #[tokio::test] + async fn test_manage_retry_unreachable() { + let (tx, rx) = unbounded_channel(); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + + // Add a tower with pending appointments + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, "http://unreachable.tower", &receipt) + .unwrap(); + + // Add appointment to pending + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Start the task and send the tower to the channel for retry + tx.send((tower_id, RevocationData::Fresh(appointment.locator))) + .unwrap(); + + let wt_client_clone = wt_client.clone(); + let task = tokio::spawn(async move { + RetryManager::new( + wt_client_clone, + rx, + MAX_ELAPSED_TIME, + SHORT_AUTO_RETRY_DELAY, + MAX_INTERVAL_TIME, + ) + .manage_retry() + .await + }); + + // Wait for one retry round and check to tower status + tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME)).await; + assert!(wt_client + .lock() + .unwrap() + .get_tower_status(&tower_id) + .unwrap() + .is_temporary_unreachable()); + assert!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_running()); + + // Wait until the task gives up and check again (this gives up due to accumulation of transient errors, so the retriers will be idle). + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_idle()); + + assert!(wt_client + .lock() + .unwrap() + .get_tower_status(&tower_id) + .unwrap() + .is_unreachable()); + + // Add a proper server and check that the auto-retry works + // Prepare the mock response + let mut server = mockito::Server::new_async().await; + let mut add_appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + add_appointment_receipt.sign(&tower_sk); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; + + // Update the tower details + wt_client + .lock() + .unwrap() + .add_update_tower( + tower_id, + &server.url(), + &get_registration_receipt_from_previous(&receipt), + ) + .unwrap(); + + // Wait and check. We wait twice the short retry delay because it can be the case that the first auto retry + // is performed while we are patching the mock. + tokio::time::sleep(Duration::from_secs((SHORT_AUTO_RETRY_DELAY * 2) as u64)).await; + assert_eq!( + wt_client + .lock() + .unwrap() + .get_tower_status(&tower_id) + .unwrap(), + TowerStatus::Reachable + ); + assert!(!wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); + api_mock.assert_async().await; + + task.abort(); + } + + #[tokio::test] + async fn test_manage_retry_rejected() { + let (tx, rx) = unbounded_channel(); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + let mut server = mockito::Server::new_async().await; + + // Add a tower with pending appointments + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + // Add appointment to pending + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Prepare the mock response + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body_from_request(|_| { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(ApiError { + error: "error_msg".to_owned(), + error_code: 1, + }) + .to_string() + .into() + }) + .create_async() + .await; + + // Start the task and send the tower to the channel for retry + tx.send((tower_id, RevocationData::Fresh(appointment.locator))) + .unwrap(); + + let wt_client_clone = wt_client.clone(); + let task = tokio::spawn(async move { + RetryManager::new( + wt_client_clone, + rx, + MAX_ELAPSED_TIME, + LONG_AUTO_RETRY_DELAY, + MAX_INTERVAL_TIME, + ) + .manage_retry() + .await + }); + + // Wait for a fraction of the API delay and check how the tower status changed + tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; + assert!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_running()); + + // Wait for the remaining time and re-check + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .is_none()); + + assert!(wt_client + .lock() + .unwrap() + .get_tower_status(&tower_id) + .unwrap() + .is_reachable()); + assert!(!wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + assert!(wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .invalid_appointments + .contains(&appointment.locator)); + api_mock.assert_async().await; + + task.abort(); + } + + #[tokio::test] + async fn test_manage_retry_misbehaving() { + let (tx, rx) = unbounded_channel(); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + let mut server = mockito::Server::new_async().await; + + // Add a tower with pending appointments + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + // Add appointment to pending + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Prepare the mock response + let mut add_appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + // Sign with a random key so it counts as misbehaving + add_appointment_receipt.sign(&cryptography::get_random_keypair().0); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |_| { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(add_appointment_response).to_string().into() + }) + .create_async() + .await; + + // Start the task and send the tower to the channel for retry + tx.send((tower_id, RevocationData::Fresh(appointment.locator))) + .unwrap(); + + let wt_client_clone = wt_client.clone(); + let task = tokio::spawn(async move { + RetryManager::new( + wt_client_clone, + rx, + MAX_ELAPSED_TIME, + LONG_AUTO_RETRY_DELAY, + MAX_INTERVAL_TIME, + ) + .manage_retry() + .await + }); + + // Wait for a fraction of the API delay and check how the tower status changed + tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; + assert!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_running()); + + // Wait until the tower is no longer being retried. + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .is_none()); + + // The tower should have a misbehaving status. + assert!(wt_client + .lock() + .unwrap() + .get_tower_status(&tower_id) + .unwrap() + .is_misbehaving()); + api_mock.assert_async().await; + + task.abort(); + } + + #[tokio::test] + async fn test_manage_retry_abandoned() { + let keypair = cryptography::get_random_keypair(); + let (tx, rx) = unbounded_channel(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + let server = mockito::Server::new_async().await; + + // Add a tower with pending appointments + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + // Remove the tower (to simulate it has been abandoned) + wt_client.lock().unwrap().remove_tower(tower_id).unwrap(); + + // Start the task and send the tower to the channel for retry + tx.send((tower_id, RevocationData::None)).unwrap(); + + let wt_client_clone = wt_client.clone(); + let task = tokio::spawn(async move { + RetryManager::new( + wt_client_clone, + rx, + MAX_ELAPSED_TIME, + LONG_AUTO_RETRY_DELAY, + MAX_INTERVAL_TIME, + ) + .manage_retry() + .await + }); + assert!(!wt_client.lock().unwrap().towers.contains_key(&tower_id)); + + task.abort(); + } + + #[tokio::test] + async fn test_manage_retry_subscription_error() { + let (tx, rx) = unbounded_channel(); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + let mut server = mockito::Server::new_async().await; + + // Add a tower with pending appointments + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let mut registration_receipt = + RegistrationReceipt::new(wt_client.lock().unwrap().user_id, 21, 42, 420); + registration_receipt.sign(&tower_sk); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), ®istration_receipt) + .unwrap(); + + // Add appointment to pending + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Mock the registration and add_appointment response (this is right, so after the re-registration the appointments are accepted) + let mut re_registration_receipt = + get_registration_receipt_from_previous(®istration_receipt); + re_registration_receipt.sign(&tower_sk); + + let mut add_appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + add_appointment_receipt.sign(&tower_sk); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); + + let api_mock = server + .mock("POST", mockito::Matcher::Any) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |request| { + let response = if request.path() == Endpoint::Register.path().as_str() { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(re_registration_receipt).to_string() + } else if request.path() == Endpoint::AddAppointment.path().as_str() { + json!(add_appointment_response).to_string() + } else { + panic!("Wrong endpoint hit") + }; + response.into() + }) + .create_async() + .await + .expect(2); + + // Set the status as SubscriptionError so we simulate the retrier faced this in a previous round + wt_client + .lock() + .unwrap() + .set_tower_status(tower_id, TowerStatus::SubscriptionError); + + // Start the task and send the tower to the channel for retry + tx.send((tower_id, RevocationData::Fresh(appointment.locator))) + .unwrap(); + + let wt_client_clone = wt_client.clone(); + let task = tokio::spawn(async move { + RetryManager::new( + wt_client_clone, + rx, + MAX_ELAPSED_TIME, + LONG_AUTO_RETRY_DELAY, + MAX_INTERVAL_TIME, + ) + .manage_retry() + .await + }); + + // Wait for a fraction of the API delay and check how the tower status changed + tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; + assert!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_running()); + + // Wait for the remaining time and re-check + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .is_none()); + + { + let state = wt_client.lock().unwrap(); + let tower = state.towers.get(&tower_id).unwrap(); + assert!(tower.status.is_reachable()); + assert!(tower.pending_appointments.is_empty()); + } + api_mock.assert_async().await; + + task.abort(); + } + + #[tokio::test] + async fn test_manage_retry_while_idle() { + // Let's try adding a tower, setting it to idle and send revocation data in all its forms + // This replicates the three types of data the retrier can receive: + // - Initialization (from db) with stale data + // - Regular (fresh) data from `on_commitment_revocation` + // - A wake up call with no data + + let keypair = cryptography::get_random_keypair(); + let (tx, rx) = unbounded_channel(); + + // Stale data is sent on WTClient initialization if found in the database. We'll force that to happen by populating the DB before initializing the WTClient + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + + let store = MemoryStore::new().into_dyn_store(); + let mut storage = KVStorage::new(store.clone(), keypair.0.secret_bytes().to_vec()).unwrap(); + + let appointment = generate_random_appointment(None); + storage + .store_pending_appointment(tower_id, &appointment) + .unwrap(); + + let receipt = get_random_registration_receipt(); + storage + .store_tower_record(tower_id, "http://unreachable.tower", &receipt) + .unwrap(); + + // Now we can create the WTClient and check that the data is pending + let wt_client = Arc::new(Mutex::new( + WTClient::new(store, keypair.0, tx.clone()).await, + )); + + // Also create the retrier thread so retries can be managed + let wt_client_clone = wt_client.clone(); + + let task = tokio::spawn(async move { + RetryManager::new( + wt_client_clone, + rx, + MAX_ELAPSED_TIME, + LONG_AUTO_RETRY_DELAY, + MAX_INTERVAL_TIME, + ) + .manage_retry() + .await + }); + + { + // After the retriers gives up, it should go idling and flag the tower as unreachable + tokio::time::sleep(Duration::from_secs_f64( + MAX_ELAPSED_TIME as f64 + MAX_RUN_TIME, + )) + .await; + + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_idle()); + + let state = wt_client.lock().unwrap(); + let tower = state.towers.get(&tower_id).unwrap(); + assert!(tower.pending_appointments.contains(&appointment.locator)); + assert_eq!(tower.status, TowerStatus::Unreachable); + } + + // With the retrier idling all fresh data sent to it will be stored but it won't trigger a retry. + // (we can check the data was stored later on) + let appointment2 = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment2); + tx.send((tower_id, RevocationData::Fresh(appointment2.locator))) + .unwrap(); + + { + tokio::time::sleep(Duration::from_secs_f64(POLLING_TIME as f64 + MAX_RUN_TIME)).await; + let state = wt_client.lock().unwrap(); + assert!(state.get_retrier_status(&tower_id).unwrap().is_idle()); + let tower = state.towers.get(&tower_id).unwrap(); + assert_eq!(tower.status, TowerStatus::Unreachable); + } + + // Create the receipts, the responses and set the mocks + let mut appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + let mut appointment2_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment2.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + appointment_receipt.sign(&tower_sk); + appointment2_receipt.sign(&tower_sk); + + // Mock a proper response + let mut server = mockito::Server::new_async().await; + + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |request| { + let body = serde_json::from_slice::(request.body().unwrap()) + .unwrap(); + + let response = if body.appointment.unwrap().locator == appointment.locator.to_vec() + { + get_dummy_add_appointment_response(appointment.locator, &appointment_receipt) + } else { + get_dummy_add_appointment_response(appointment2.locator, &appointment2_receipt) + }; + json!(response).to_string().into() + }) + .expect(2) + .create_async() + .await; + + // Patch the tower address + wt_client + .lock() + .unwrap() + .towers + .get_mut(&tower_id) + .unwrap() + .set_net_addr(server.url()); + + // Check pending data is still there now, and is it not once the retrier succeeds + assert_eq!( + wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .len(), + 2, + ); + + // Send a retry flag to the retrier to force a retry. + tx.send((tower_id, RevocationData::None)).unwrap(); + + // After retrying the pending pool has been emptied, meaning that both appointments went trough + tokio::time::sleep(Duration::from_secs_f64(POLLING_TIME as f64 + MAX_RUN_TIME)).await; + assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); + assert!(wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .is_empty()); + api_mock.assert_async().await; + + task.abort(); + } + + #[tokio::test] + async fn test_retry_tower() { + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + + let mut server = mockito::Server::new_async().await; + + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + // Add appointment to pending + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Prepare the mock response + let mut add_appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + add_appointment_receipt.sign(&tower_sk); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; + + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, HashSet::from([appointment.locator])); + let r = retrier.run().await; + assert_eq!(r, Ok(())); + api_mock.assert_async().await; + } + + #[tokio::test] + async fn test_retry_tower_no_pending() { + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + let server = mockito::Server::new_async().await; + + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + // If there are no pending appointments the method will simply return + let r = Retrier::empty(wt_client, tower_id).run().await; + assert_eq!(r, Ok(())); + } + + #[tokio::test] + async fn test_retry_tower_misbehaving() { + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + let mut server = mockito::Server::new_async().await; + + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + // Add appointment to pending + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Prepare the mock response + let mut add_appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + add_appointment_receipt.sign(&cryptography::get_random_keypair().0); + let add_appointment_response = + get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; + + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, HashSet::from([appointment.locator])); + let r = retrier.run().await; + assert!(matches!( + r, + Err(Error::Permanent(RetryError::Misbehaving { .. },)) + )); + api_mock.assert_async().await; + } + + #[tokio::test] + async fn test_retry_tower_unreachable() { + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, "http://unreachable.tower", &receipt) + .unwrap(); + + // Add some pending appointments and try again (with an unreachable tower). + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, HashSet::from([appointment.locator])); + let r = retrier.run().await; + + assert_eq!(r, Err(Error::transient(RetryError::Unreachable))); + } + + #[tokio::test] + async fn test_retry_tower_subscription_error() { + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + + let mut server = mockito::Server::new_async().await; + + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body( + json!(ApiError { + error: "error_msg".to_owned(), + error_code: errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR, + }) + .to_string(), + ) + .create_async() + .await; + + // Add some pending appointments and try again (with an unreachable tower). + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, HashSet::from([appointment.locator])); + let r = retrier.run().await; + + assert!(matches!( + r, + Err(Error::Transient { + err: RetryError::Subscription { .. }, + .. + }) + )); + api_mock.assert_async().await; + } + + #[tokio::test] + async fn test_retry_tower_rejected() { + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + + let mut server = mockito::Server::new_async().await; + + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, &server.url(), &receipt) + .unwrap(); + + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body( + json!(ApiError { + error: "error_msg".to_owned(), + error_code: 1, + }) + .to_string(), + ) + .create_async() + .await; + + // Add some pending appointments and try again (with an unreachable tower). + let appointment = generate_random_appointment(None); + wt_client + .lock() + .unwrap() + .add_pending_appointment(tower_id, &appointment); + + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new( + wt_client.clone(), + tower_id, + HashSet::from([appointment.locator]), + ); + let r = retrier.run().await; + + assert!(wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .invalid_appointments + .contains(&appointment.locator)); + assert!(r.is_ok()); + api_mock.assert_async().await; + } + + #[tokio::test] + async fn test_retry_tower_abandoned() { + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let keypair = cryptography::get_random_keypair(); + let wt_client = Arc::new(Mutex::new( + WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await, + )); + + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, "http://tower.adrress", &receipt) + .unwrap(); + + // Remove the tower (to simulate it has been abandoned) + wt_client.lock().unwrap().remove_tower(tower_id).unwrap(); + + // If there are no pending appointments the method will simply return + let r = Retrier::empty(wt_client, tower_id).run().await; + + assert_eq!(r, Err(Error::permanent(RetryError::Abandoned))); + } +} diff --git a/teos-ldk-client/src/ser.rs b/teos-ldk-client/src/ser.rs new file mode 100644 index 00000000..50a9ef8f --- /dev/null +++ b/teos-ldk-client/src/ser.rs @@ -0,0 +1,33 @@ +use bitcoin::consensus::encode; +use bitcoin::Transaction; + +use hex::FromHex; +use serde::{de, Deserializer}; + +pub fn deserialize_tx<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct TransactionVisitor; + + impl<'de> de::Visitor<'de> for TransactionVisitor { + type Value = Transaction; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a hex string containing the transaction") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + let tx = encode::deserialize( + &Vec::from_hex(v).map_err(|_| E::custom("transaction is not hex encoded"))?, + ) + .map_err(|_| E::custom("transaction cannot be deserialized"))?; + Ok(tx) + } + } + + deserializer.deserialize_any(TransactionVisitor) +} diff --git a/teos-ldk-client/src/storage/encryptor.rs b/teos-ldk-client/src/storage/encryptor.rs new file mode 100644 index 00000000..13115d7e --- /dev/null +++ b/teos-ldk-client/src/storage/encryptor.rs @@ -0,0 +1,68 @@ +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + ChaCha20Poly1305, Key, Nonce, +}; + +use bitcoin::hashes::{sha256, Hash}; + +/// A struct handling encryption and decryption using ChaCha20Poly1305 +pub(crate) struct Encryptor { + cipher: ChaCha20Poly1305, + nonce: Nonce, +} + +impl Encryptor { + /// Creates a new Encryptor instance with the given secret + /// + /// # Arguments + /// * `secret` - The secret used to derive the encryption key + pub fn new(secret: &[u8]) -> Self { + let key_hash = sha256::Hash::hash(secret); + let key = Key::from_slice(key_hash.as_byte_array()); + + Self { + cipher: ChaCha20Poly1305::new(key), + nonce: Nonce::default(), // [0; 12] + } + } + + /// Encrypts a given message using the initialized cipher + /// + /// # Arguments + /// * `message` - The message to encrypt (expected to be a penalty transaction) + /// + /// # Returns + /// The encrypted message or an encryption error + pub fn encrypt(&self, message: &[u8]) -> Result, chacha20poly1305::aead::Error> { + self.cipher.encrypt(&self.nonce, message) + } + + /// Decrypts an encrypted blob using the initialized cipher + /// + /// # Arguments + /// * `encrypted_blob` - The encrypted data to decrypt + /// + /// # Returns + /// The decrypted message (expected to be a penalty transaction) or a decryption error + pub fn decrypt(&self, encrypted_blob: &[u8]) -> Result, chacha20poly1305::aead::Error> { + self.cipher.decrypt(&self.nonce, encrypted_blob) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encryption_decryption() { + let secret = b"test_secret"; + let message = b"Hello, World!"; + + let encryptor = Encryptor::new(secret); + + let encrypted = encryptor.encrypt(message).unwrap(); + let decrypted = encryptor.decrypt(&encrypted).unwrap(); + + assert_eq!(message.to_vec(), decrypted); + } +} diff --git a/teos-ldk-client/src/storage/kv.rs b/teos-ldk-client/src/storage/kv.rs new file mode 100644 index 00000000..7dd810f9 --- /dev/null +++ b/teos-ldk-client/src/storage/kv.rs @@ -0,0 +1,1300 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use crate::TowerStatus; + +use crate::storage::persister::{Persister, PersisterError}; +use lightning::io::Error as DBError; + +use lightning::util::persist::KVStore; +use teos_common::appointment::{Appointment, Locator}; +use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; +use teos_common::{TowerId, UserId}; + +use crate::storage::encryptor::Encryptor; +use crate::storage::namespace::{get_appointment_namespace, KeySpace, NameSpace}; + +impl From for PersisterError { + fn from(error: DBError) -> Self { + PersisterError::Other(error.to_string()) + } +} + +use crate::{AppointmentStatus, MisbehaviorProof, TowerInfo, TowerSummary}; +// XXX: this is taken from LDK and should be imported after it is made public there +pub type DynStore = dyn KVStore + Sync + Send; + +pub struct KVStorage { + store: Arc, + encryptor: Box, +} + +impl KVStorage { + pub fn new(store: Arc, sk: Vec) -> Result { + Ok(KVStorage { + store, + encryptor: Box::new(Encryptor::new(&sk[..])), + }) + } + + fn store_item( + &mut self, + key_space: KeySpace, + value: &T, + encrypted: bool, + ) -> Result<(), PersisterError> { + let value = bincode::serialize(value).unwrap(); + let value = if encrypted { + self.encryptor.encrypt(&value).unwrap() + } else { + value + }; + + self.store + .write( + key_space.namespace().primary(), + key_space.namespace().secondary(), + key_space.key(), + &value, + ) + .map_err(|e| PersisterError::StoreError(e.to_string())) + } + + fn load_item( + &self, + key_space: &KeySpace, + encrypted: bool, + ) -> Option { + match self.store.read( + key_space.namespace().primary(), + key_space.namespace().secondary(), + key_space.key(), + ) { + Ok(value) => { + let value = if encrypted { + self.encryptor.decrypt(&value).unwrap() + } else { + value + }; + + Some(bincode::deserialize(&value).unwrap()) + } + Err(_) => None, + } + } + + fn list_keys(&self, name_space: NameSpace) -> Vec { + self.store + .list(name_space.primary(), name_space.secondary()) + .unwrap() + } + + fn remove_item(&self, key_space: KeySpace) -> Result<(), PersisterError> { + self.store + .remove( + key_space.namespace().primary(), + key_space.namespace().secondary(), + key_space.key(), + false, + ) + .map_err(|_| PersisterError::StoreError(format!("removing: {}", key_space.key()))) + } + + fn store_appointment(&mut self, appointment: &Appointment) -> Result<(), PersisterError> { + self.store_item( + KeySpace::appointment(appointment.locator), + appointment, + true, + ) + } + + fn load_misbehaving_proof(&self, tower_id: TowerId) -> Option { + self.load_item(&KeySpace::misbehaving_proof(tower_id), true) + } + + fn remove_pending_appointments(&self, tower_id: TowerId) -> Result<(), PersisterError> { + let pending_keys = self + .list_keys(NameSpace::pending_appointments()) + .iter() + .filter(|l| l.starts_with(&tower_id.to_string())) + .map(|key| { + let parts: Vec<&str> = key.split(':').collect(); + let locator = Locator::from_slice(&hex::decode(parts[1]).unwrap()).unwrap(); + KeySpace::pending_appointment(tower_id, locator) + }) + .collect::>(); + for key_space in pending_keys { + self.remove_item(key_space)?; + } + + Ok(()) + } + + fn remove_invalid_appointments(&self, tower_id: TowerId) -> Result<(), PersisterError> { + let invalid_keys = self + .list_keys(NameSpace::invalid_appointments()) + .iter() + .filter(|l| l.starts_with(&tower_id.to_string())) + .map(|key| { + let parts: Vec<&str> = key.split(':').collect(); + let locator = Locator::from_slice(&hex::decode(parts[1]).unwrap()).unwrap(); + KeySpace::invalid_appointment(tower_id, locator) + }) + .collect::>(); + for key_space in invalid_keys { + self.remove_item(key_space)?; + } + + Ok(()) + } + + fn remove_registration_receipts(&self, tower_id: TowerId) -> Result<(), PersisterError> { + let registration_keys = self + .list_keys(NameSpace::registration_receipts(tower_id)) + .iter() + .map(|key| { + let expiry = key.parse::().unwrap(); + KeySpace::registration_receipt(tower_id, expiry) + }) + .collect::>(); + for key_space in registration_keys { + self.remove_item(key_space)?; + } + + Ok(()) + } + + fn remove_appointment_receipts(&self, tower_id: TowerId) -> Result<(), PersisterError> { + let receipt_keys = self + .list_keys(NameSpace::appointment_receipts()) + .iter() + .filter(|l| l.starts_with(&tower_id.to_string())) + .map(|key| { + let parts: Vec<&str> = key.split(':').collect(); + let locator = Locator::from_slice(&hex::decode(parts[1]).unwrap()).unwrap(); + KeySpace::appointment_receipt(tower_id, locator) + }) + .collect::>(); + for key_space in receipt_keys { + self.remove_item(key_space)?; + } + + Ok(()) + } + + fn remove_misbehaving_proofs(&self, tower_id: TowerId) -> Result<(), PersisterError> { + if self.load_misbehaving_proof(tower_id).is_some() { + self.remove_item(KeySpace::misbehaving_proof(tower_id))?; + } + + Ok(()) + } +} + +impl Persister for KVStorage { + /// Stores a tower record into the database alongside the corresponding registration receipt. + /// + /// This function MUST be guarded against inserting duplicate (tower_id, subscription_expiry) pairs. + /// This is currently done in WTClient::add_update_tower. + fn store_tower_record( + &mut self, + tower_id: TowerId, + net_addr: &str, + receipt: &RegistrationReceipt, + ) -> Result<(), PersisterError> { + // Create tower info + let tower_info = TowerInfo::new( + net_addr.to_string(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + HashMap::new(), + Vec::new(), + Vec::new(), + ); + + // Store the tower record + self.store_item(KeySpace::tower(tower_id), &tower_info, true)?; + + // Store the registration receipt + self.store_item( + KeySpace::registration_receipt(tower_id, receipt.subscription_expiry()), + receipt, + true, + )?; + + // Store the available slots + self.store_item( + KeySpace::available_slots(tower_id), + &receipt.available_slots(), + true, + ) + } + + /// Loads a tower record from the database. + /// + /// Tower records are composed from the tower information and the appointment data. The latter is split in: + /// accepted appointments (represented by appointment receipts), pending appointments and invalid appointments. + /// In the case that the tower has misbehaved, then a misbehaving proof is also attached to the record. + fn load_tower_record(&self, tower_id: TowerId) -> Option { + // Load base tower info + let mut tower_info: TowerInfo = self.load_item(&KeySpace::tower(tower_id), true)?; + + // Load all appointments data + tower_info.appointments = self.load_appointment_receipts(tower_id); + tower_info.pending_appointments = + self.load_appointments(tower_id, AppointmentStatus::Pending); + tower_info.invalid_appointments = + self.load_appointments(tower_id, AppointmentStatus::Invalid); + + // Load and update subscription info from latest registration receipt + let latest_expiry = self + .list_keys(NameSpace::registration_receipts(tower_id)) + .iter() + .filter_map(|s_e| s_e.parse::().ok()) + .max()?; + + let registration_receipt: RegistrationReceipt = self.load_item( + &KeySpace::registration_receipt(tower_id, latest_expiry), + true, + )?; + + tower_info.subscription_start = registration_receipt.subscription_start(); + tower_info.subscription_expiry = registration_receipt.subscription_expiry(); + + // Update tower status based on misbehavior and pending appointments + if let Some(proof) = self.load_misbehaving_proof(tower_id) { + tower_info.status = TowerStatus::Misbehaving; + tower_info.set_misbehaving_proof(proof); + } else if !tower_info.pending_appointments.is_empty() { + tower_info.status = TowerStatus::TemporaryUnreachable; + } + + // Load available slots + tower_info.available_slots = self.load_item(&KeySpace::available_slots(tower_id), true)?; + + Some(tower_info) + } + + /// Removes a tower record from the database. + /// + /// This triggers a cascade deletion of all related data, such as appointments, appointment receipts, etc. As long as there is a single + /// reference to them. + fn remove_tower_record(&self, tower_id: TowerId) -> Result<(), PersisterError> { + self.remove_pending_appointments(tower_id)?; + + self.remove_invalid_appointments(tower_id)?; + + self.remove_registration_receipts(tower_id)?; + + self.remove_appointment_receipts(tower_id)?; + + self.remove_misbehaving_proofs(tower_id)?; + + self.remove_item(KeySpace::tower(tower_id)) + } + + /// Loads all tower records from the database. + /// + /// Returns a key value pair with the tower id as key and the tower summary as value. + fn load_towers(&self) -> HashMap { + self.list_keys(NameSpace::tower_records()) + .iter() + .filter_map(|key| { + let tower_id = key.parse().unwrap(); + self.load_tower_record(tower_id) + .map(|info| (tower_id, TowerSummary::from(info))) + }) + .collect() + } + + /// Loads the latest registration receipt for a given tower. + /// + /// Latests is determined by the one with the `subscription_expiry` further into the future. + fn load_registration_receipt( + &self, + tower_id: TowerId, + user_id: UserId, + ) -> Option { + // Find the latest subscription expiry + let latest_expiry = self + .list_keys(NameSpace::registration_receipts(tower_id)) + .iter() + .map(|s_e| s_e.parse::().unwrap()) + .max()?; + + // Load the registration receipt using KeySpace + let receipt: RegistrationReceipt = self.load_item( + &KeySpace::registration_receipt(tower_id, latest_expiry), + true, + )?; + + // Create new receipt with the provided user_id + Some(RegistrationReceipt::with_signature( + user_id, + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + receipt.signature().unwrap(), + )) + } + + /// Stores an appointments receipt into the database representing an appointment accepted by a given tower. + fn store_appointment_receipt( + &mut self, + tower_id: TowerId, + locator: Locator, + available_slots: u32, + receipt: &AppointmentReceipt, + ) -> Result<(), PersisterError> { + // Store appointment receipt + self.store_item( + KeySpace::appointment_receipt(tower_id, locator), + receipt, + true, + )?; + + // Update the tower's available slots + self.store_item(KeySpace::available_slots(tower_id), &available_slots, true)?; + + // Load and update tower info + let tower: TowerInfo = self.load_item(&KeySpace::tower(tower_id), true).unwrap(); + + let tower_info = TowerInfo::new( + tower.net_addr, + available_slots, + tower.subscription_start, + tower.subscription_expiry, + HashMap::new(), + Vec::new(), + Vec::new(), + ); + + // Store updated tower info + self.store_item(KeySpace::tower(tower_id), &tower_info, true)?; + + Ok(()) + } + + /// Loads a given appointment receipt of a given tower from the database. + fn load_appointment_receipt( + &self, + tower_id: TowerId, + locator: Locator, + ) -> Option { + self.load_item(&KeySpace::appointment_receipt(tower_id, locator), true) + } + + /// Loads the appointment receipts associated to a given tower. + /// + /// TODO: Currently this is only loading a summary of the receipt, if we need to really load all the information + /// for any reason this method may need to be renamed. + fn load_appointment_receipts(&self, tower_id: TowerId) -> HashMap { + self.list_keys(NameSpace::appointment_receipts()) + .iter() + .filter(|key| key.starts_with(&tower_id.to_string())) + .filter_map(|key| { + // Extract locator from key + let locator_hex = key.split(':').nth(1)?; + let locator = Locator::from_slice(&hex::decode(locator_hex).ok()?).ok()?; + + // Load and get signature from receipt + self.load_appointment_receipt(tower_id, locator) + .and_then(|receipt| receipt.signature()) + .map(|signature| (locator, signature)) + }) + .collect() + } + + /// Loads a collection of locators from the database entry associated to a given tower. + /// + /// The loaded locators can be loaded either from appointment_receipts, pending_appointments or invalid_appointments + /// depending on `status`. + fn load_appointment_locators( + &self, + tower_id: TowerId, + status: AppointmentStatus, + ) -> HashSet { + self.list_keys(get_appointment_namespace(status)) + .iter() + .filter(|key| key.starts_with(&tower_id.to_string())) + .filter_map(|key| { + key.split(':') + .nth(1) + .and_then(|hex_str| hex::decode(hex_str).ok()) + .and_then(|bytes| Locator::from_slice(&bytes).ok()) + }) + .collect() + } + + /// Loads an appointment from the database. + fn load_appointment(&self, locator: Locator) -> Option { + self.load_item(&KeySpace::appointment(locator), true) + } + + /// Stores a pending appointment into the database. + /// + /// A pending appointment is an appointment that was sent to a tower when it was unreachable. + /// This data is stored so it can be resent once the tower comes back online. + /// Internally calls [Self::store_appointment]. + fn store_pending_appointment( + &mut self, + tower_id: TowerId, + appointment: &Appointment, + ) -> Result<(), PersisterError> { + // Check if pending appointment already exists + let key_space = KeySpace::pending_appointment(tower_id, appointment.locator); + if self.load_item::(&key_space, true).is_some() { + return Err(PersisterError::Other(format!( + "{}:{}", + tower_id, appointment.locator + ))); + } + + let key_space = KeySpace::pending_appointment(tower_id, appointment.locator); + // Store the pending appointment + self.store_item(key_space, appointment, true)?; + + // Store the appointment itself + self.store_appointment(appointment)?; + + Ok(()) + } + + /// Removes a pending appointment from the database. + /// + /// If the pending appointment is the only instance of the appointment, the appointment will also be deleted form the appointments table. + fn delete_pending_appointment( + &mut self, + tower_id: TowerId, + locator: Locator, + ) -> Result<(), PersisterError> { + // Count total references to this appointment + let total_references = { + // Count invalid appointments + let invalid_count = self + .list_keys(NameSpace::invalid_appointments()) + .iter() + .filter(|l| l.ends_with(&locator.to_string())) + .count(); + + // Count pending appointments + let pending_count = self + .list_keys(NameSpace::pending_appointments()) + .iter() + .filter(|l| l.ends_with(&locator.to_string())) + .count(); + + invalid_count + pending_count + }; + + // If this is the last reference, remove the appointment itself + if total_references == 1 { + self.remove_item(KeySpace::appointment(locator))?; + } + + // Remove the pending appointment reference + self.remove_item(KeySpace::pending_appointment(tower_id, locator)) + } + + /// Stores an invalid appointment into the database. + /// + /// An invalid appointment is an appointment that was rejected by the tower. + /// Storing this data may allow us to see what was the issue and send the data later on. + /// Internally calls [Self::store_appointment]. + fn store_invalid_appointment( + &mut self, + tower_id: TowerId, + appointment: &Appointment, + ) -> Result<(), PersisterError> { + let key_space = KeySpace::invalid_appointment(tower_id, appointment.locator); + + // Check if invalid appointment already exists using load_item + if self.load_item::(&key_space, true).is_some() { + return Err(PersisterError::Other(format!( + "{}:{}", + tower_id, appointment.locator + ))); + } + + // Store the invalid appointment + self.store_item(key_space, appointment, true)?; + + // Store the appointment itself + self.store_appointment(appointment)?; + + Ok(()) + } + + /// Loads non finalized appointments from the database for a given tower based on a status flag. + /// + /// This is meant to be used only for pending and invalid appointments, if the method is called for + /// accepted appointment, an empty collection will be returned. + fn load_appointments(&self, tower_id: TowerId, status: AppointmentStatus) -> Vec { + // Return early for accepted appointments + if matches!(status, AppointmentStatus::Accepted) { + return Vec::new(); + } + + self.list_keys(get_appointment_namespace(status)) + .iter() + .filter(|key| key.starts_with(&tower_id.to_string())) + .filter_map(|key| { + // Extract and parse locator from key + key.split(':') + .nth(1) + .and_then(|hex_str| hex::decode(hex_str).ok()) + .and_then(|bytes| Locator::from_slice(&bytes).ok()) + .and_then(|locator| self.load_appointment(locator)) + }) + .collect() + } + + /// Stores a misbehaving proof into the database. + /// + /// A misbehaving proof is proof that the tower has signed an appointment using a key different + /// than the one advertised to the user when they registered. + fn store_misbehaving_proof( + &mut self, + tower_id: TowerId, + proof: &MisbehaviorProof, + ) -> Result<(), PersisterError> { + // Store the appointment receipt + self.store_item( + KeySpace::appointment_receipt(tower_id, proof.locator), + &proof.appointment_receipt, + true, + )?; + + // Store the misbehavior proof + self.store_item(KeySpace::misbehaving_proof(tower_id), proof, true) + } + + fn appointment_exists(&self, locator: Locator) -> bool { + let key_space = KeySpace::appointment(locator); + + self.load_item::(&key_space, true).is_some() + } + + fn appointment_receipt_exists(&self, locator: Locator, tower_id: TowerId) -> bool { + let key_space = KeySpace::appointment_receipt(tower_id, locator); + + self.load_item::(&key_space, true) + .is_some() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::mock_kv::MemoryStore; + + use teos_common::test_utils::{ + generate_random_appointment, get_random_registration_receipt, get_random_user_id, + get_registration_receipt_from_previous, + }; + + impl KVStorage { + fn exists_misbehaving_proof(&self, tower_id: TowerId) -> bool { + self.load_item::(&KeySpace::misbehaving_proof(tower_id), true) + .is_some() + } + } + + fn create_test_kv_storage() -> KVStorage { + let store = MemoryStore::new().into_dyn_store(); + let sk = vec![0u8; 32]; // Test secret key + KVStorage::new(store, sk).unwrap() + } + + #[test] + fn test_store_load_tower_record() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + let receipt = get_random_registration_receipt(); + + let tower_info = TowerInfo::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + HashMap::new(), + Vec::new(), + Vec::new(), + ); + + // Check the loaded data matches the in memory data + let serialized = bincode::serialize(&tower_info).unwrap(); + + let deserialized: TowerInfo = bincode::deserialize(&serialized).unwrap(); + assert_eq!(tower_info, deserialized); + + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + let loaded = storage.load_tower_record(tower_id).unwrap(); + assert_eq!(loaded, tower_info); + + let loaded_receipt = storage + .load_registration_receipt(tower_id, receipt.user_id()) + .unwrap(); + assert_eq!(loaded_receipt, receipt); + } + + #[test] + fn test_load_registration_receipt() { + let mut storage = create_test_kv_storage(); + + // Registration receipts are stored alongside tower records when the register command is called + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + let receipt = get_random_registration_receipt(); + + // Check the receipt was stored + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + assert_eq!( + storage + .load_registration_receipt(tower_id, receipt.user_id()) + .unwrap(), + receipt + ); + + // Add another receipt for the same tower with a higher expiry and check this last one is loaded + let middle_receipt = get_registration_receipt_from_previous(&receipt); + let latest_receipt = get_registration_receipt_from_previous(&middle_receipt); + + storage + .store_tower_record(tower_id, net_addr, &latest_receipt) + .unwrap(); + assert_eq!( + storage + .load_registration_receipt(tower_id, latest_receipt.user_id()) + .unwrap(), + latest_receipt + ); + + // Add a final one with a lower expiry and check the last is still loaded + storage + .store_tower_record(tower_id, net_addr, &middle_receipt) + .unwrap(); + assert_eq!( + storage + .load_registration_receipt(tower_id, latest_receipt.user_id()) + .unwrap(), + latest_receipt + ); + } + + #[test] + fn test_load_same_registration_receipt() { + let mut storage = create_test_kv_storage(); + + // Registration receipts are stored alongside tower records when the register command is called + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + let receipt = get_random_registration_receipt(); + + // Store it once + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + assert_eq!( + storage + .load_registration_receipt(tower_id, receipt.user_id()) + .unwrap(), + receipt + ); + + // // Store the same again, this should fail due to UNIQUE PK constrains. + // // Notice store_tower_record is guarded against this by WTClient::add_update_tower though. + // let err = storage.store_tower_record(tower_id, net_addr, &receipt).unwrap_err(); + // assert_eq!( + // err, + // PersisterError::StoreError(format!("tower_id: {tower_id} already exists")) + // ); + } + + #[test] + fn test_load_nonexistent_tower_record() { + let storage = create_test_kv_storage(); + + // If the tower does not exists, `load_tower` will fail. + let tower_id = get_random_user_id(); + assert!(storage.load_tower_record(tower_id).is_none()); + } + + #[test] + fn test_store_load_towers() { + let mut storage = create_test_kv_storage(); + let mut towers = HashMap::new(); + + // In order to add a tower record we need to associated registration receipt. + for _ in 0..10 { + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + let mut receipt = get_random_registration_receipt(); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + // Add not only one registration receipt to test if the tower retrieves the one with furthest expiry date. + for _ in 0..10 { + receipt = get_registration_receipt_from_previous(&receipt); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + } + + towers.insert( + tower_id, + TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ), + ); + } + + assert_eq!(storage.load_towers(), towers); + } + + #[test] + fn test_load_towers_empty() { + // If there are no towers in the database, `load_towers` should return an empty map. + let storage = create_test_kv_storage(); + assert_eq!(storage.load_towers(), HashMap::new()); + } + + #[test] + fn test_remove_tower_record() { + let mut storage = create_test_kv_storage(); + + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + let receipt = get_random_registration_receipt(); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + assert!(matches!(storage.remove_tower_record(tower_id), Ok(()))); + assert_eq!(storage.load_towers(), HashMap::new()); + } + + #[test] + fn test_remove_tower_record_inexistent() { + let storage = create_test_kv_storage(); + let tower_id = get_random_user_id(); + let err = storage.remove_tower_record(tower_id).unwrap_err(); + assert_eq!( + err, + PersisterError::StoreError(format!("removing: {}", tower_id)) + ); + } + + #[test] + fn test_store_load_appointment_receipts() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + let mut tower_summary = TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + // Add some appointment receipts and check they match + let mut receipts = HashMap::new(); + for _ in 0..5 { + let appointment = generate_random_appointment(None); + let user_signature = "user_signature"; + let appointment_receipt = AppointmentReceipt::with_signature( + user_signature.to_owned(), + 42, + "tower_signature".to_owned(), + ); + + tower_summary.available_slots -= 1; + + storage + .store_appointment_receipt( + tower_id, + appointment.locator, + tower_summary.available_slots, + &appointment_receipt, + ) + .unwrap(); + receipts.insert( + appointment.locator, + appointment_receipt.signature().unwrap(), + ); + } + + assert_eq!(storage.load_appointment_receipts(tower_id), receipts); + } + + #[test] + fn test_load_appointment_receipt() { + let mut storage = create_test_kv_storage(); + let tower_id = get_random_user_id(); + let appointment = generate_random_appointment(None); + + // If there is no appointment receipt for the given (locator, tower_id) pair, Error::NotFound is returned + // Try first with both being unknown + assert!(storage + .load_appointment_receipt(tower_id, appointment.locator) + .is_none()); + + // Add the tower but not the appointment and try again + let net_addr = "talaia.watch"; + let receipt = get_random_registration_receipt(); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + assert!(storage + .load_appointment_receipt(tower_id, appointment.locator) + .is_none()); + + // Add both + let tower_summary = TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + let appointment_receipt = AppointmentReceipt::with_signature( + "user_signature".to_owned(), + 42, + "tower_signature".to_owned(), + ); + storage + .store_appointment_receipt( + tower_id, + appointment.locator, + tower_summary.available_slots, + &appointment_receipt, + ) + .unwrap(); + + assert_eq!( + storage + .load_appointment_receipt(tower_id, appointment.locator) + .unwrap(), + appointment_receipt + ); + } + + #[test] + fn test_load_appointment_locators() { + // `load_appointment_locators` is used to load locators from either `appointment_receipts`, `pending_appointments` or `invalid_appointments` + let mut storage = create_test_kv_storage(); + + // We first need to add a tower record to the database so we can add some associated data. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + let tower_summary = TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + // Create all types of appointments and store them in the db. + let user_signature = "user_signature"; + let mut receipts = HashSet::new(); + let mut pending_appointments = HashSet::new(); + let mut invalid_appointments = HashSet::new(); + for _ in 0..5 { + let appointment = generate_random_appointment(None); + let appointment_receipt = AppointmentReceipt::with_signature( + user_signature.to_owned(), + 42, + "tower_signature".to_owned(), + ); + let pending_appointment = generate_random_appointment(None); + let invalid_appointment = generate_random_appointment(None); + + storage + .store_appointment_receipt( + tower_id, + appointment.locator, + tower_summary.available_slots, + &appointment_receipt, + ) + .unwrap(); + storage + .store_pending_appointment(tower_id, &pending_appointment) + .unwrap(); + storage + .store_invalid_appointment(tower_id, &invalid_appointment) + .unwrap(); + + receipts.insert(appointment.locator); + pending_appointments.insert(pending_appointment.locator); + invalid_appointments.insert(invalid_appointment.locator); + } + + // Pull data from the db and check it matches the expected data + assert_eq!( + storage.load_appointment_locators(tower_id, AppointmentStatus::Accepted), + receipts + ); + assert_eq!( + storage.load_appointment_locators(tower_id, AppointmentStatus::Pending), + pending_appointments + ); + assert_eq!( + storage.load_appointment_locators(tower_id, AppointmentStatus::Invalid), + invalid_appointments + ); + } + + #[test] + fn test_store_load_appointment() { + let mut storage = create_test_kv_storage(); + + let appointment = generate_random_appointment(None); + storage.store_appointment(&appointment).unwrap(); + + let loaded_appointment = storage.load_appointment(appointment.locator); + assert_eq!(appointment, loaded_appointment.unwrap()); + } + + #[test] + fn test_store_load_appointment_inexistent() { + let storage = create_test_kv_storage(); + + let locator = generate_random_appointment(None).locator; + let loaded_appointment = storage.load_appointment(locator); + assert!(loaded_appointment.is_none()); + } + + #[test] + fn test_store_pending_appointment() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + let mut tower_summary = TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ) + .with_status(TowerStatus::TemporaryUnreachable); + + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + // Add some pending appointments and check they match + for _ in 0..5 { + let appointment = generate_random_appointment(None); + + tower_summary + .pending_appointments + .insert(appointment.locator); + + storage + .store_pending_appointment(tower_id, &appointment) + .unwrap(); + assert_eq!( + TowerSummary::from(storage.load_tower_record(tower_id).unwrap()), + tower_summary + ); + } + } + + #[test] + fn test_store_pending_appointment_twice() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id_1 = get_random_user_id(); + let tower_id_2 = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + storage + .store_tower_record(tower_id_1, net_addr, &receipt) + .unwrap(); + storage + .store_tower_record(tower_id_2, net_addr, &receipt) + .unwrap(); + + // If the same appointment is stored twice (by different towers) it should go through + // Since the appointment data will be stored only once and this will create two references + let appointment = generate_random_appointment(None); + storage + .store_pending_appointment(tower_id_1, &appointment) + .unwrap(); + storage + .store_pending_appointment(tower_id_2, &appointment) + .unwrap(); + + // If this is called twice with for the same tower it will fail, since two identical references + // can not exist. This is intended behavior and should not happen + assert!(storage + .store_pending_appointment(tower_id_2, &appointment) + .is_err()); + } + + #[test] + fn test_delete_pending_appointment() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + // Add a single one, remove it later + let appointment = generate_random_appointment(None); + storage + .store_pending_appointment(tower_id, &appointment) + .unwrap(); + assert!(storage + .delete_pending_appointment(tower_id, appointment.locator) + .is_ok()); + + // The appointment should be completely gone + assert!(!storage + .load_appointment_locators(tower_id, AppointmentStatus::Pending) + .contains(&appointment.locator)); + + assert!(!storage.appointment_exists(appointment.locator)); + + // Try again with more than one reference + let another_tower_id = get_random_user_id(); + storage + .store_tower_record(another_tower_id, net_addr, &receipt) + .unwrap(); + + // Add two + storage + .store_pending_appointment(tower_id, &appointment) + .unwrap(); + storage + .store_pending_appointment(another_tower_id, &appointment) + .unwrap(); + // Delete one + assert!(storage + .delete_pending_appointment(tower_id, appointment.locator) + .is_ok()); + // Check + assert!(!storage + .load_appointment_locators(tower_id, AppointmentStatus::Pending) + .contains(&appointment.locator)); + assert!(storage + .load_appointment_locators(another_tower_id, AppointmentStatus::Pending) + .contains(&appointment.locator)); + assert!(storage.appointment_exists(appointment.locator)); + + // Add an invalid reference and check again + storage + .store_invalid_appointment(tower_id, &appointment) + .unwrap(); + assert!(storage + .delete_pending_appointment(another_tower_id, appointment.locator) + .is_ok()); + assert!(!storage + .load_appointment_locators(another_tower_id, AppointmentStatus::Pending) + .contains(&appointment.locator)); + assert!(storage + .load_appointment_locators(tower_id, AppointmentStatus::Invalid) + .contains(&appointment.locator)); + assert!(storage.appointment_exists(appointment.locator)); + } + + #[test] + fn test_store_invalid_appointment() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + let mut tower_summary = TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + // Add some invalid appointments and check they match + for _ in 0..5 { + let appointment = generate_random_appointment(None); + + tower_summary + .invalid_appointments + .insert(appointment.locator); + + storage + .store_invalid_appointment(tower_id, &appointment) + .unwrap(); + assert_eq!( + TowerSummary::from(storage.load_tower_record(tower_id).unwrap()), + tower_summary + ); + } + } + + #[test] + fn test_store_invalid_appointment_twice() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id_1 = get_random_user_id(); + let tower_id_2 = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + storage + .store_tower_record(tower_id_1, net_addr, &receipt) + .unwrap(); + storage + .store_tower_record(tower_id_2, net_addr, &receipt) + .unwrap(); + + // Same as with pending appointments. Two references from different towers is allowed + let appointment = generate_random_appointment(None); + storage + .store_invalid_appointment(tower_id_1, &appointment) + .unwrap(); + storage + .store_invalid_appointment(tower_id_2, &appointment) + .unwrap(); + + // Two references from the same tower is not. + assert!(storage + .store_invalid_appointment(tower_id_2, &appointment) + .is_err()); + } + + #[test] + fn test_store_load_misbehaving_proof() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + let tower_summary = TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + assert_eq!( + TowerSummary::from(storage.load_tower_record(tower_id).unwrap()), + tower_summary + ); + + // Store a misbehaving proof and load it back + let appointment = generate_random_appointment(None); + let appointment_receipt = AppointmentReceipt::with_signature( + "user_signature".to_owned(), + 42, + "tower_signature".to_owned(), + ); + + let proof = MisbehaviorProof::new( + appointment.locator, + appointment_receipt, + get_random_user_id(), + ); + + storage.store_misbehaving_proof(tower_id, &proof).unwrap(); + assert_eq!(storage.load_misbehaving_proof(tower_id).unwrap(), proof); + } + + #[test] + fn test_store_load_non_existing_misbehaving_proof() { + let storage = create_test_kv_storage(); + assert!(storage + .load_misbehaving_proof(get_random_user_id()) + .is_none()); + } + + #[test] + fn test_store_exists_misbehaving_proof() { + let mut storage = create_test_kv_storage(); + + // In order to add a tower record we need to associated registration receipt. + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + + let receipt = get_random_registration_receipt(); + let tower_summary = TowerSummary::new( + net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + storage + .store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + assert_eq!( + TowerSummary::from(storage.load_tower_record(tower_id).unwrap()), + tower_summary + ); + + // // Store a misbehaving proof check + let appointment = generate_random_appointment(None); + let appointment_receipt = AppointmentReceipt::with_signature( + "user_signature".to_owned(), + 42, + "tower_signature".to_owned(), + ); + + let proof = MisbehaviorProof::new( + appointment.locator, + appointment_receipt, + get_random_user_id(), + ); + + storage.store_misbehaving_proof(tower_id, &proof).unwrap(); + assert!(storage.exists_misbehaving_proof(tower_id)); + } + + #[test] + fn test_exists_misbehaving_proof_false() { + let storage = create_test_kv_storage(); + assert!(!storage.exists_misbehaving_proof(get_random_user_id())); + } +} diff --git a/teos-ldk-client/src/storage/mock_kv.rs b/teos-ldk-client/src/storage/mock_kv.rs new file mode 100644 index 00000000..123a85c7 --- /dev/null +++ b/teos-ldk-client/src/storage/mock_kv.rs @@ -0,0 +1,192 @@ +use lightning::io::Error as DBError; +use lightning::util::persist::KVStore; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +pub(crate) type DynStore = dyn KVStore + Sync + Send; + +/// Type alias for the namespace to key-value mapping +type NamespaceMap = HashMap>; +/// Type alias for the storage data structure +type StorageMap = HashMap; +/// Type alias for thread-safe storage +type ThreadSafeStorage = Arc>; + +/// In-memory key-value store implementation for testing +#[derive(Clone, Debug)] +pub struct MemoryStore { + data: ThreadSafeStorage, +} + +impl MemoryStore { + /// Creates a new empty MemoryStore + pub fn new() -> Self { + MemoryStore { + data: Arc::new(Mutex::new(HashMap::new())), + } + } + + /// Creates the composite key used for storage + fn make_key(namespace: &str, key: &str) -> String { + format!("{}:{}", namespace, key) + } + + pub fn into_dyn_store(self) -> Arc { + Arc::new(self) + } +} + +impl Default for MemoryStore { + fn default() -> Self { + Self::new() + } +} + +impl KVStore for MemoryStore { + fn read( + &self, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + ) -> Result, DBError> { + let data = self + .data + .lock() + .map_err(|_| DBError::new(bitcoin::io::ErrorKind::AddrInUse, "Lock poisoned"))?; + + let namespace = Self::make_key(primary_namespace, secondary_namespace); + data.get(&namespace) + .and_then(|ns| ns.get(key)) + .cloned() + .ok_or_else(|| DBError::new(bitcoin::io::ErrorKind::NotFound, "Key not found")) + } + + fn write( + &self, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + value: &[u8], + ) -> Result<(), DBError> { + let mut data = self + .data + .lock() + .map_err(|_| DBError::new(bitcoin::io::ErrorKind::AddrInUse, "Lock poisoned"))?; + + let namespace = Self::make_key(primary_namespace, secondary_namespace); + let ns_map = data.entry(namespace).or_default(); + ns_map.insert(key.to_string(), value.to_vec()); + + Ok(()) + } + + fn remove( + &self, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + _lazy: bool, + ) -> Result<(), DBError> { + let mut data = self + .data + .lock() + .map_err(|_| DBError::new(bitcoin::io::ErrorKind::AddrInUse, "Lock poisoned"))?; + + let namespace = Self::make_key(primary_namespace, secondary_namespace); + if let Some(ns_map) = data.get_mut(&namespace) { + ns_map.remove(key); + Ok(()) + } else { + Err(DBError::new( + bitcoin::io::ErrorKind::NotFound, + "Key not found", + )) + } + } + + fn list( + &self, + primary_namespace: &str, + secondary_namespace: &str, + ) -> Result, DBError> { + let data = self + .data + .lock() + .map_err(|_| DBError::new(bitcoin::io::ErrorKind::AddrInUse, "Lock poisoned"))?; + + let namespace = Self::make_key(primary_namespace, secondary_namespace); + let res = data + .get(&namespace) + .map(|ns_map| ns_map.keys().cloned().collect()); + + match res { + None => Ok(Vec::new()), + Some(res) => Ok(res), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_operations() { + let store = MemoryStore::new(); + + // Test write and read + store + .write("primary", "secondary", "key1", b"value1") + .unwrap(); + assert_eq!( + store.read("primary", "secondary", "key1").unwrap(), + b"value1" + ); + + // Test remove + store.remove("primary", "secondary", "key1", false).unwrap(); + assert!(store.read("primary", "secondary", "key1").is_err()); + + // Test list + store + .write("primary", "secondary", "key1", b"value1") + .unwrap(); + store + .write("primary", "secondary", "key2", b"value2") + .unwrap(); + let keys = store.list("primary", "secondary").unwrap(); + assert_eq!(keys.len(), 2); + assert!(keys.contains(&"key1".to_string())); + assert!(keys.contains(&"key2".to_string())); + } + + #[test] + fn test_namespacing() { + let store = MemoryStore::new(); + + // Write same key to different namespaces + store + .write("primary1", "secondary1", "key", b"value1") + .unwrap(); + store + .write("primary1", "secondary2", "key", b"value2") + .unwrap(); + store + .write("primary2", "secondary1", "key", b"value3") + .unwrap(); + + // Verify they don't interfere + assert_eq!( + store.read("primary1", "secondary1", "key").unwrap(), + b"value1" + ); + assert_eq!( + store.read("primary1", "secondary2", "key").unwrap(), + b"value2" + ); + assert_eq!( + store.read("primary2", "secondary1", "key").unwrap(), + b"value3" + ); + } +} diff --git a/teos-ldk-client/src/storage/mod.rs b/teos-ldk-client/src/storage/mod.rs new file mode 100644 index 00000000..e3f47dbc --- /dev/null +++ b/teos-ldk-client/src/storage/mod.rs @@ -0,0 +1,33 @@ +pub mod persister; + +pub use crate::storage::persister::{Persister, PersisterError}; + +mod encryptor; +pub(crate) mod kv; +mod namespace; + +#[cfg(test)] +pub mod mock_kv; + +#[cfg(test)] +pub use mock_kv::MemoryStore; + +#[cfg(test)] +pub fn create_storage( + kv_store: Arc, + sk: Vec, +) -> Result, PersisterError> { + match KVStorage::new(kv_store, sk) { + Ok(storage) => Ok(Box::new(storage)), + Err(e) => Err(PersisterError::Other(format!( + "Error creating storage: {}", + e + ))), + } +} + +#[cfg(test)] +use std::sync::Arc; + +#[cfg(test)] +use kv::{DynStore, KVStorage}; diff --git a/teos-ldk-client/src/storage/namespace.rs b/teos-ldk-client/src/storage/namespace.rs new file mode 100644 index 00000000..1bbaf08b --- /dev/null +++ b/teos-ldk-client/src/storage/namespace.rs @@ -0,0 +1,164 @@ +use teos_common::appointment::Locator; +use teos_common::TowerId; + +use crate::AppointmentStatus; + +/// Namespace constants for the storage system +pub mod constants { + /// Primary namespace for all watchtower-related data + pub const PRIMARY: &str = "watchtower"; + + /// Secondary namespace constants + pub mod secondary { + pub const TOWER_RECORDS: &str = "tower_records"; + pub const REGISTRATION_RECEIPTS: &str = "registration_receipts"; + pub const APPOINTMENT_RECEIPTS: &str = "appointment_receipts"; + pub const APPOINTMENTS: &str = "appointments"; + pub const PENDING_APPOINTMENTS: &str = "appointments_pending"; + pub const INVALID_APPOINTMENTS: &str = "appointments_invalid"; + pub const MISBEHAVIOR_PROOFS: &str = "misbehavior_proofs"; + pub const AVAILABLE_SLOTS: &str = "available_slots"; + } +} + +use constants::secondary::*; +use constants::*; + +/// Gets the appropriate namespace based on appointment status +pub(crate) fn get_appointment_namespace(status: AppointmentStatus) -> NameSpace { + match status { + AppointmentStatus::Accepted => NameSpace::appointment_receipts(), + AppointmentStatus::Pending => NameSpace::pending_appointments(), + AppointmentStatus::Invalid => NameSpace::invalid_appointments(), + } +} + +/// Represents a namespace in the storage system +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct NameSpace { + primary_namespace: String, + secondary_namespace: String, +} + +impl NameSpace { + /// Returns the primary namespace + pub fn primary(&self) -> &str { + &self.primary_namespace + } + + /// Returns the secondary namespace + pub fn secondary(&self) -> &str { + &self.secondary_namespace + } +} + +impl NameSpace { + /// Creates a KeySpace from this namespace + pub fn with_key(&self, key: impl Into) -> KeySpace { + KeySpace::new(self.clone(), key) + } + + /// Creates a new NameSpace instance + fn new(secondary_namespace: impl Into) -> Self { + Self { + primary_namespace: PRIMARY.to_string(), + secondary_namespace: secondary_namespace.into(), + } + } + + /// Creates a new NameSpace instance with a formatted secondary namespace + fn new_formatted( + secondary_namespace: impl std::fmt::Display, + id: impl std::fmt::Display, + ) -> Self { + Self::new(format!("{}:{}", secondary_namespace, id)) + } + + pub fn registration_receipts(tower_id: TowerId) -> Self { + Self::new_formatted(REGISTRATION_RECEIPTS, tower_id) + } + + pub fn pending_appointments() -> Self { + Self::new(PENDING_APPOINTMENTS) + } + + pub fn invalid_appointments() -> Self { + Self::new(INVALID_APPOINTMENTS) + } + + pub fn appointment_receipts() -> Self { + Self::new(APPOINTMENT_RECEIPTS) + } + + pub fn tower_records() -> Self { + Self::new(TOWER_RECORDS) + } +} + +/// Represents a complete key space in the storage system +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct KeySpace { + namespace: NameSpace, + key: String, +} + +impl KeySpace { + /// Returns a reference to the namespace + pub fn namespace(&self) -> &NameSpace { + &self.namespace + } + + /// Returns a reference to the key + pub fn key(&self) -> &str { + &self.key + } + + /// Creates a new KeySpace instance + fn new(namespace: NameSpace, key: impl Into) -> Self { + Self { + namespace, + key: key.into(), + } + } + + /// Creates a new KeySpace instance with a formatted key + fn new_with_formatted_key( + secondary_namespace: impl Into, + id1: impl std::fmt::Display, + id2: impl std::fmt::Display, + ) -> Self { + NameSpace::new(secondary_namespace).with_key(format!("{}:{}", id1, id2)) + } + + pub fn tower(tower_id: TowerId) -> Self { + Self::new(NameSpace::tower_records(), tower_id.to_string()) + } + + pub fn appointment(locator: Locator) -> Self { + Self::new(NameSpace::new(APPOINTMENTS), locator.to_string()) + } + + pub fn misbehaving_proof(tower_id: TowerId) -> Self { + NameSpace::new(MISBEHAVIOR_PROOFS).with_key(tower_id.to_string()) + } + + pub fn registration_receipt(tower_id: TowerId, subscription_expiry: u32) -> Self { + NameSpace::registration_receipts(tower_id).with_key(subscription_expiry.to_string()) + } + + pub fn appointment_receipt(tower_id: TowerId, locator: Locator) -> Self { + Self::new_with_formatted_key(APPOINTMENT_RECEIPTS, tower_id, locator) + } + + pub fn pending_appointment(tower_id: TowerId, locator: Locator) -> Self { + Self::new_with_formatted_key(PENDING_APPOINTMENTS, tower_id, locator) + } + + pub fn invalid_appointment(tower_id: TowerId, locator: Locator) -> Self { + Self::new_with_formatted_key(INVALID_APPOINTMENTS, tower_id, locator) + } + + pub fn available_slots(tower_id: TowerId) -> Self { + NameSpace::new(AVAILABLE_SLOTS).with_key(tower_id.to_string()) + } +} diff --git a/teos-ldk-client/src/storage/persister.rs b/teos-ldk-client/src/storage/persister.rs new file mode 100644 index 00000000..6bc09040 --- /dev/null +++ b/teos-ldk-client/src/storage/persister.rs @@ -0,0 +1,162 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt; + +use teos_common::appointment::{Appointment, Locator}; +use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; +use teos_common::{TowerId, UserId}; + +use crate::{AppointmentStatus, MisbehaviorProof, TowerInfo, TowerSummary}; + +/// A general storage error type that can be used across different storage implementations +#[derive(Debug, PartialEq, Eq)] +pub enum PersisterError { + /// Error when storing data + StoreError(String), + /// Error when retrieving data + RetrievalError(String), + /// Error when data is not found + NotFound(String), + /// Any other storage-related error + Other(String), +} + +impl fmt::Display for PersisterError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PersisterError::StoreError(msg) => write!(f, "Storage store error: {}", msg), + PersisterError::RetrievalError(msg) => write!(f, "Storage retrieval error: {}", msg), + PersisterError::NotFound(msg) => write!(f, "Data not found: {}", msg), + PersisterError::Other(msg) => write!(f, "Storage error: {}", msg), + } + } +} + +impl std::error::Error for PersisterError {} + +/// Trait defining the interface for database operations +pub trait Persister: Send { + /// Stores a tower record into the database alongside the corresponding registration receipt. + /// + /// This function MUST be guarded against inserting duplicate (tower_id, subscription_expiry) pairs. + /// This is currently done in WTClient::add_update_tower. + fn store_tower_record( + &mut self, + tower_id: TowerId, + net_addr: &str, + receipt: &RegistrationReceipt, + ) -> Result<(), PersisterError>; + + /// Loads a tower record from the database. + /// + /// Tower records are composed from the tower information and the appointment data. The latter is split in: + /// accepted appointments (represented by appointment receipts), pending appointments and invalid appointments. + /// In the case that the tower has misbehaved, then a misbehaving proof is also attached to the record. + fn load_tower_record(&self, tower_id: TowerId) -> Option; + + /// Loads the latest registration receipt for a given tower. + /// + /// Latests is determined by the one with the `subscription_expiry` further into the future. + fn load_registration_receipt( + &self, + tower_id: TowerId, + user_id: UserId, + ) -> Option; + + /// Removes a tower record from the database. + /// + /// This triggers a cascade deletion of all related data, such as appointments, appointment receipts, etc. As long as there is a single + /// reference to them. + fn remove_tower_record(&self, tower_id: TowerId) -> Result<(), PersisterError>; + + /// Loads all tower records from the database. + fn load_towers(&self) -> HashMap; + + /// Stores an appointments receipt into the database representing an appointment accepted by a given tower. + fn store_appointment_receipt( + &mut self, + tower_id: TowerId, + locator: Locator, + available_slots: u32, + receipt: &AppointmentReceipt, + ) -> Result<(), PersisterError>; + + /// Loads a given appointment receipt of a given tower from the database. + fn load_appointment_receipt( + &self, + tower_id: TowerId, + locator: Locator, + ) -> Option; + + /// Loads the appointment receipts associated to a given tower. + /// + /// TODO: Currently this is only loading a summary of the receipt, if we need to really load all the information + /// for any reason this method may need to be renamed. + fn load_appointment_receipts(&self, tower_id: TowerId) -> HashMap; + + /// Loads a collection of locators from the database entry associated to a given tower. + /// + /// The loaded locators can be loaded either from appointment_receipts, pending_appointments or invalid_appointments + /// depending on `status`. + fn load_appointment_locators( + &self, + tower_id: TowerId, + status: AppointmentStatus, + ) -> HashSet; + + /// Loads an appointment from the database. + fn load_appointment(&self, locator: Locator) -> Option; + + /// Stores a pending appointment into the database. + /// + /// A pending appointment is an appointment that was sent to a tower when it was unreachable. + /// This data is stored so it can be resent once the tower comes back online. + /// Internally calls [Self::store_appointment]. + fn store_pending_appointment( + &mut self, + tower_id: TowerId, + appointment: &Appointment, + ) -> Result<(), PersisterError>; + + /// Removes a pending appointment from the database. + /// + /// If the pending appointment is the only instance of the appointment, the appointment will also be deleted form the appointments table. + fn delete_pending_appointment( + &mut self, + tower_id: TowerId, + locator: Locator, + ) -> Result<(), PersisterError>; + + /// Stores an invalid appointment into the database. + /// + /// An invalid appointment is an appointment that was rejected by the tower. + /// Storing this data may allow us to see what was the issue and send the data later on. + /// Internally calls [Self::store_appointment]. + fn store_invalid_appointment( + &mut self, + tower_id: TowerId, + appointment: &Appointment, + ) -> Result<(), PersisterError>; + + /// Loads non finalized appointments from the database for a given tower based on a status flag. + /// + /// This is meant to be used only for pending and invalid appointments, if the method is called for + /// accepted appointment, an empty collection will be returned. + fn load_appointments(&self, tower_id: TowerId, status: AppointmentStatus) -> Vec; + + /// Stores a misbehaving proof into the database. + /// + /// A misbehaving proof is proof that the tower has signed an appointment using a key different + /// than the one advertised to the user when they registered. + fn store_misbehaving_proof( + &mut self, + tower_id: TowerId, + proof: &MisbehaviorProof, + ) -> Result<(), PersisterError>; + + /// Checks if appointment for a given locator exists in a local sotrage + /// + fn appointment_exists(&self, locator: Locator) -> bool; + + /// Checks if appointment receipt from a give tower for a given locator exists in a local sotrage + fn appointment_receipt_exists(&self, locator: Locator, tower_id: TowerId) -> bool; +} diff --git a/teos-ldk-client/src/test_utils.rs b/teos-ldk-client/src/test_utils.rs new file mode 100644 index 00000000..be89b61a --- /dev/null +++ b/teos-ldk-client/src/test_utils.rs @@ -0,0 +1,16 @@ +use teos_common::appointment::Locator; +use teos_common::protos as common_msgs; +use teos_common::receipts::AppointmentReceipt; + +pub fn get_dummy_add_appointment_response( + locator: Locator, + receipt: &AppointmentReceipt, +) -> common_msgs::AddAppointmentResponse { + common_msgs::AddAppointmentResponse { + locator: locator.to_vec(), + start_block: receipt.start_block(), + signature: receipt.signature().unwrap(), + available_slots: 21, + subscription_expiry: 1000, + } +} diff --git a/teos-ldk-client/src/wt_client.rs b/teos-ldk-client/src/wt_client.rs new file mode 100644 index 00000000..d16fde76 --- /dev/null +++ b/teos-ldk-client/src/wt_client.rs @@ -0,0 +1,941 @@ +use crate::storage::persister::{Persister, PersisterError}; +use std::collections::{HashMap, HashSet}; +use std::iter::FromIterator; +use std::sync::Arc; + +use tokio::sync::mpsc::UnboundedSender; + +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + +use teos_common::appointment::{Appointment, Locator}; +use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; +use teos_common::{TowerId, UserId}; + +use crate::retrier::RetrierStatus; +use crate::{MisbehaviorProof, SubscriptionError, TowerInfo, TowerStatus, TowerSummary}; + +use crate::storage::kv::{DynStore, KVStorage}; + +#[derive(Eq, PartialEq)] +pub enum RevocationData { + Fresh(Locator), + Stale(HashSet), + None, +} + +impl RevocationData { + pub fn is_none(&self) -> bool { + *self == RevocationData::None + } +} + +impl From for HashSet { + fn from(r: RevocationData) -> Self { + match r { + RevocationData::Fresh(l) => HashSet::from_iter(vec![l]), + RevocationData::Stale(hs) => hs, + RevocationData::None => HashSet::new(), + } + } +} + +impl std::fmt::Debug for RevocationData { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + RevocationData::Fresh(l) => format!("Fresh: {l}"), + RevocationData::Stale(hs) => format!( + "Stale: {:?}", + hs.iter().map(|l| l.to_string()).collect::>() + ), + RevocationData::None => "None".to_owned(), + } + ) + } +} + +pub struct WTClient { + /// A database manager instance implementing the DatabaseManager trait + pub storage: Box, + /// A collection of towers the client is registered to. + pub towers: HashMap, + /// Queue of unreachable towers. + pub unreachable_towers: UnboundedSender<(TowerId, RevocationData)>, + // Map of existing retriers and its state. + pub retriers: HashMap, + /// The user secret key. + pub user_sk: SecretKey, + /// The user identifier. + pub user_id: UserId, +} + +impl WTClient { + pub async fn new( + store: Arc, + user_sk: SecretKey, + unreachable_towers: UnboundedSender<(TowerId, RevocationData)>, + ) -> Self { + let storage = Box::new(KVStorage::new(store, user_sk.secret_bytes().to_vec()).unwrap()); + let user_id = UserId(PublicKey::from_secret_key(&Secp256k1::new(), &user_sk)); + + let towers = storage.load_towers(); + for (tower_id, tower) in towers.iter() { + if tower.status.is_temporary_unreachable() { + unreachable_towers + .send(( + *tower_id, + RevocationData::Stale(tower.pending_appointments.iter().cloned().collect()), + )) + .unwrap(); + } + } + + log::info!("Plugin watchtower client initialized. User id = {user_id}"); + + WTClient { + towers, + unreachable_towers, + retriers: HashMap::new(), + storage, + user_sk, + user_id, + } + } + + /// Adds or updates a tower entry. + pub fn add_update_tower( + &mut self, + tower_id: TowerId, + tower_net_addr: &str, + receipt: &RegistrationReceipt, + ) -> Result<(), SubscriptionError> { + if let Some(tower) = self.towers.get(&tower_id) { + // TODO: For now we're forcing updates to increase both slots and expiry. This is not mandatory and may + // be changed in the future, but the tower is currently set to do this anyway so let's keep it simple. + if receipt.subscription_expiry() <= tower.subscription_expiry { + return Err(SubscriptionError::Expiry); + } else { + let tower_info = self.storage.load_tower_record(tower_id).unwrap(); + if receipt.available_slots() <= tower_info.available_slots { + return Err(SubscriptionError::Slots); + } + } + } + + self.storage + .store_tower_record(tower_id, tower_net_addr, receipt) + .unwrap(); + + if let Some(summary) = self.towers.get_mut(&tower_id) { + summary.udpate( + tower_net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + } else { + self.towers.insert( + tower_id, + TowerSummary::new( + tower_net_addr.to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ), + ); + }; + + Ok(()) + } + + /// Gets the latest registration receipt of a given tower. + pub fn get_registration_receipt(&self, tower_id: TowerId) -> Option { + self.storage + .load_registration_receipt(tower_id, self.user_id) + } + + /// Loads a tower record from the database. + pub fn load_tower_info(&self, tower_id: TowerId) -> Option { + self.storage.load_tower_record(tower_id) + } + + /// Gets the given tower status (identified by tower_id), if found. + pub fn get_tower_status(&self, tower_id: &TowerId) -> Option { + Some(self.towers.get(tower_id)?.status) + } + + /// Sets the tower status to any of the `TowerStatus` variants. + pub fn set_tower_status(&mut self, tower_id: TowerId, status: TowerStatus) { + if let Some(tower) = self.towers.get_mut(&tower_id) { + if tower.status != status { + tower.status = status + } else { + log::debug!("{tower_id} status is already {status}") + } + } else { + log::error!("Cannot change tower status to {status}. Unknown tower_id: {tower_id}"); + } + } + + /// Gets the given tower status (identified by tower_id), if found. + pub fn get_retrier_status(&self, tower_id: &TowerId) -> Option<&RetrierStatus> { + self.retriers.get(tower_id) + } + + /// Adds an appointment receipt to the tower record. + pub fn add_appointment_receipt( + &mut self, + tower_id: TowerId, + locator: Locator, + available_slots: u32, + receipt: &AppointmentReceipt, + ) { + if let Some(tower) = self.towers.get_mut(&tower_id) { + // DISCUSS: It may be nice to independently compute the slots and compare + tower.available_slots = available_slots; + + self.storage + .store_appointment_receipt(tower_id, locator, available_slots, receipt) + .unwrap(); + } else { + log::error!("Cannot add appointment receipt to tower. Unknown tower_id: {tower_id}"); + } + } + + /// Gets an appointment receipt from the database (if found). + pub fn get_appointment_receipt( + &self, + tower_id: TowerId, + locator: Locator, + ) -> Option { + self.storage.load_appointment_receipt(tower_id, locator) + } + + /// Adds a pending appointment to the tower record. + pub fn add_pending_appointment(&mut self, tower_id: TowerId, appointment: &Appointment) { + if let Some(tower) = self.towers.get_mut(&tower_id) { + tower.pending_appointments.insert(appointment.locator); + + self.storage + .store_pending_appointment(tower_id, appointment) + .unwrap(); + } else { + log::error!("Cannot add pending appointment to tower. Unknown tower_id: {tower_id}"); + } + } + + /// Removes a pending appointment from the tower record. + pub fn remove_pending_appointment(&mut self, tower_id: TowerId, locator: Locator) { + if let Some(tower) = self.towers.get_mut(&tower_id) { + tower.pending_appointments.remove(&locator); + + self.storage + .delete_pending_appointment(tower_id, locator) + .unwrap(); + } else { + log::error!("Cannot remove pending appointment to tower. Unknown tower_id: {tower_id}"); + } + } + + /// Adds an invalid appointment to the tower record. + pub fn add_invalid_appointment(&mut self, tower_id: TowerId, appointment: &Appointment) { + if let Some(tower) = self.towers.get_mut(&tower_id) { + tower.invalid_appointments.insert(appointment.locator); + + self.storage + .store_invalid_appointment(tower_id, appointment) + .unwrap(); + } else { + log::error!("Cannot add invalid appointment to tower. Unknown tower_id: {tower_id}"); + } + } + + /// Flags a given tower as misbehaving, storing the misbehaving proof in the database. + pub fn flag_misbehaving_tower(&mut self, tower_id: TowerId, proof: MisbehaviorProof) { + if let Some(tower) = self.towers.get_mut(&tower_id) { + self.storage + .store_misbehaving_proof(tower_id, &proof) + .unwrap(); + tower.status = TowerStatus::Misbehaving; + } else { + log::error!("Cannot flag tower. Unknown tower_id: {tower_id}"); + } + } + + /// Removes a tower from the client (both memory and database). + /// + /// Any data associated to the tower will be deleted (i.e. links to appointments) + pub fn remove_tower(&mut self, tower_id: TowerId) -> Result<(), PersisterError> { + if self.towers.contains_key(&tower_id) { + self.towers.remove(&tower_id); + self.storage.remove_tower_record(tower_id) + } else { + Err(PersisterError::NotFound(format!("tower_id: {tower_id}"))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::mock_kv::MemoryStore; + use teos_common::cryptography; + use tokio::sync::mpsc::unbounded_channel; + + use teos_common::test_utils::{ + generate_random_appointment, get_random_appointment_receipt, + get_random_registration_receipt, get_random_user_id, + get_registration_receipt_from_previous, + }; + + #[tokio::test] + async fn test_add_update_load_tower() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + // Adding a new tower will add a summary to towers and the full data to the + let mut receipt = get_random_registration_receipt(); + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let tower_info = TowerInfo::empty( + "talaia.watch".to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + + wt_client + .add_update_tower(tower_id, &tower_info.net_addr, &receipt) + .unwrap(); + assert_eq!( + wt_client.towers.get(&tower_id), + Some(&TowerSummary::from(tower_info.clone())) + ); + assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); + + // Calling the method again with updated information should also updated the records in memory and the database + receipt = get_registration_receipt_from_previous(&receipt); + + let updated_tower_info = TowerInfo::empty( + "talaia.watch".to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + wt_client + .add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt) + .unwrap(); + + assert_eq!( + wt_client.towers.get(&tower_id), + Some(&TowerSummary::from(updated_tower_info.clone())) + ); + assert_eq!( + wt_client.load_tower_info(tower_id).unwrap(), + updated_tower_info + ); + + // If we try to update without increasing both the end_time and the slots, this will fail + let mut receipt_same_slots = RegistrationReceipt::new( + receipt.user_id(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry() + 1, + ); + receipt_same_slots.sign(&tower_sk); + let mut receipt_same_expiry = RegistrationReceipt::new( + receipt.user_id(), + receipt.available_slots() + 1, + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + receipt_same_expiry.sign(&tower_sk); + + assert!(matches!( + wt_client.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt), + Err(SubscriptionError::Expiry) + )); + assert!(matches!( + wt_client.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt_same_slots), + Err(SubscriptionError::Slots) + )); + assert!(matches!( + wt_client.add_update_tower( + tower_id, + &updated_tower_info.net_addr, + &receipt_same_expiry + ), + Err(SubscriptionError::Expiry) + )); + + // Decrease the slots count (simulate exhaustion) and update with more than the current count it should work + let locator = generate_random_appointment(None).locator; + wt_client.add_appointment_receipt( + tower_id, + locator, + 0, + &get_random_appointment_receipt(tower_sk), + ); + wt_client + .add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt_same_slots) + .unwrap(); + } + + #[tokio::test] + async fn test_get_tower_status() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + // If the tower is unknown, get_tower_status returns None + let tower_id = get_random_user_id(); + assert!(wt_client.get_tower_status(&tower_id).is_none()); + + // Add a tower + let receipt = get_random_registration_receipt(); + wt_client + .add_update_tower(tower_id, "talaia.watch", &receipt) + .unwrap(); + + // If the tower is known, get_tower_status matches getting the same data from the towers collection + assert_eq!( + wt_client.towers.get(&tower_id).unwrap().status, + wt_client.get_tower_status(&tower_id).unwrap() + ) + } + + #[tokio::test] + async fn test_set_tower_status() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + // If the tower is unknown nothing will happen + let unknown_tower = get_random_user_id(); + wt_client.set_tower_status(unknown_tower, TowerStatus::Reachable); + assert!(!wt_client.towers.contains_key(&unknown_tower)); + + // If the tower is known, the status will be updated. + let receipt = get_random_registration_receipt(); + let tower_id = get_random_user_id(); + wt_client + .add_update_tower(tower_id, "talaia.watch", &receipt) + .unwrap(); + + for status in [ + TowerStatus::Reachable, + TowerStatus::TemporaryUnreachable, + TowerStatus::Unreachable, + TowerStatus::SubscriptionError, + TowerStatus::Misbehaving, + ] { + wt_client.set_tower_status(tower_id, status); + assert_eq!(status, wt_client.get_tower_status(&tower_id).unwrap()); + } + } + + #[tokio::test] + async fn test_add_appointment_receipt() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + + let locator = generate_random_appointment(None).locator; + let registration_receipt = get_random_registration_receipt(); + let appointment_receipt = get_random_appointment_receipt(tower_sk); + + // If we call this on an unknown tower it will simply do nothing + wt_client.add_appointment_receipt( + tower_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt, + ); + assert!(!wt_client.towers.contains_key(&tower_id)); + + // Add the tower to the state and try again + let tower_info = TowerInfo::new( + "talaia.watch".to_owned(), + registration_receipt.available_slots(), + registration_receipt.subscription_start(), + registration_receipt.subscription_expiry(), + HashMap::from([(locator, appointment_receipt.signature().unwrap())]), + Vec::new(), + Vec::new(), + ); + wt_client + .add_update_tower(tower_id, &tower_info.net_addr, ®istration_receipt) + .unwrap(); + wt_client.add_appointment_receipt( + tower_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt, + ); + + assert!(wt_client.towers.contains_key(&tower_id)); + assert_eq!( + wt_client.towers.get(&tower_id).unwrap(), + &TowerSummary::from(tower_info.clone()) + ); + assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); + } + + #[tokio::test] + async fn test_add_pending_appointment() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let tower_id = get_random_user_id(); + + let registration_receipt = get_random_registration_receipt(); + let appointment = generate_random_appointment(None); + + // If we call this on an unknown tower it will simply do nothing + wt_client.add_pending_appointment(tower_id, &appointment); + assert!(!wt_client.towers.contains_key(&tower_id)); + + // Add the tower to the state and try again + let tower_info = TowerInfo::new( + "talaia.watch".to_owned(), + registration_receipt.available_slots(), + registration_receipt.subscription_start(), + registration_receipt.subscription_expiry(), + HashMap::new(), + vec![appointment.clone()], + Vec::new(), + ); + + wt_client + .add_update_tower(tower_id, &tower_info.net_addr, ®istration_receipt) + .unwrap(); + wt_client.add_pending_appointment(tower_id, &appointment); + + assert!(wt_client.towers.contains_key(&tower_id)); + assert_eq!( + wt_client.towers.get(&tower_id).unwrap(), + &TowerSummary::from(tower_info.clone()) + ); + // When towers data is loaded from the database, it is assumed to be reachable. + assert_eq!( + wt_client.load_tower_info(tower_id).unwrap(), + tower_info.with_status(TowerStatus::TemporaryUnreachable) + ); + } + + #[tokio::test] + async fn test_remove_pending_appointment() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let tower_id = get_random_user_id(); + + let registration_receipt = get_random_registration_receipt(); + let appointment = generate_random_appointment(None); + + // If we call this on an unknown tower it will simply do nothing + wt_client.remove_pending_appointment(tower_id, appointment.locator); + + // Add the tower to the state and try again + wt_client + .add_update_tower(tower_id, "talaia.watch", ®istration_receipt) + .unwrap(); + wt_client.add_pending_appointment(tower_id, &appointment); + + wt_client.remove_pending_appointment(tower_id, appointment.locator); + assert!(!wt_client + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + // This bit is tested exhaustively in the Storage. + assert!(!wt_client.storage.appointment_exists(appointment.locator)); + } + + #[tokio::test] + async fn test_add_invalid_appointment() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let tower_id = get_random_user_id(); + + let registration_receipt = get_random_registration_receipt(); + let appointment = generate_random_appointment(None); + + // If we call this on an unknown tower it will simply do nothing + wt_client.add_invalid_appointment(tower_id, &appointment); + assert!(!wt_client.towers.contains_key(&tower_id)); + + // Add the tower to the state and try again + let tower_info = TowerInfo::new( + "talaia.watch".to_owned(), + registration_receipt.available_slots(), + registration_receipt.subscription_start(), + registration_receipt.subscription_expiry(), + HashMap::new(), + Vec::new(), + vec![appointment.clone()], + ); + + wt_client + .add_update_tower(tower_id, &tower_info.net_addr, ®istration_receipt) + .unwrap(); + wt_client.add_invalid_appointment(tower_id, &appointment); + + assert!(wt_client.towers.contains_key(&tower_id)); + assert_eq!( + wt_client.towers.get(&tower_id).unwrap(), + &TowerSummary::from(tower_info.clone()) + ); + assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); + } + + #[tokio::test] + async fn test_move_pending_appointment_to_invalid() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let tower_id = get_random_user_id(); + + let registration_receipt = get_random_registration_receipt(); + let appointment = generate_random_appointment(None); + + wt_client + .add_update_tower(tower_id, "talaia.watch", ®istration_receipt) + .unwrap(); + wt_client.add_pending_appointment(tower_id, &appointment); + + // Check that the appointment can be moved from pending to invalid + wt_client.add_invalid_appointment(tower_id, &appointment); + wt_client.remove_pending_appointment(tower_id, appointment.locator); + + assert!(!wt_client + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + assert!(wt_client + .towers + .get(&tower_id) + .unwrap() + .invalid_appointments + .contains(&appointment.locator)); + assert!(!wt_client + .storage + .load_appointment_locators(tower_id, crate::AppointmentStatus::Pending) + .contains(&appointment.locator)); + assert!(wt_client + .storage + .load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid) + .contains(&appointment.locator)); + assert!(wt_client.storage.appointment_exists(appointment.locator)); + } + + #[tokio::test] + async fn test_move_pending_appointment_to_invalid_multiple_towers() { + // Check that moving an appointment from pending to invalid can be done even if multiple towers have a reference to it + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let tower_id = get_random_user_id(); + let another_tower_id = get_random_user_id(); + let tower_net_addr = "talaia.watch"; + + let registration_receipt = get_random_registration_receipt(); + let appointment = generate_random_appointment(None); + + wt_client + .add_update_tower(tower_id, tower_net_addr, ®istration_receipt) + .unwrap(); + wt_client + .add_update_tower(another_tower_id, tower_net_addr, ®istration_receipt) + .unwrap(); + wt_client.add_pending_appointment(tower_id, &appointment); + wt_client.add_pending_appointment(another_tower_id, &appointment); + + // Check that the appointment can be moved from pending to invalid + wt_client.add_invalid_appointment(tower_id, &appointment); + wt_client.remove_pending_appointment(tower_id, appointment.locator); + + // TOWER_ID CHECKS + assert!(!wt_client + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + assert!(wt_client + .towers + .get(&tower_id) + .unwrap() + .invalid_appointments + .contains(&appointment.locator)); + assert!(!wt_client + .storage + .load_appointment_locators(tower_id, crate::AppointmentStatus::Pending) + .contains(&appointment.locator)); + assert!(wt_client + .storage + .load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid) + .contains(&appointment.locator)); + + // ANOTHER_TOWER_ID CHECKS + assert!(wt_client + .towers + .get(&another_tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + assert!(!wt_client + .towers + .get(&another_tower_id) + .unwrap() + .invalid_appointments + .contains(&appointment.locator)); + assert!(wt_client + .storage + .load_appointment_locators(another_tower_id, crate::AppointmentStatus::Pending) + .contains(&appointment.locator)); + assert!(!wt_client + .storage + .load_appointment_locators(another_tower_id, crate::AppointmentStatus::Invalid) + .contains(&appointment.locator)); + + // GENERAL + assert!(wt_client.storage.appointment_exists(appointment.locator)); + } + + #[tokio::test] + async fn test_flag_misbehaving_tower() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + + // If we call this on an unknown tower it will simply do nothing + let appointment = generate_random_appointment(None); + let receipt = get_random_appointment_receipt(tower_sk); + let proof = MisbehaviorProof::new(appointment.locator, receipt, get_random_user_id()); + wt_client.flag_misbehaving_tower(tower_id, proof.clone()); + assert!(!wt_client.towers.contains_key(&tower_id)); + + // Add the tower to the state and try again + let registration_receipt = get_random_registration_receipt(); + wt_client + .add_update_tower(tower_id, "talaia.watch", ®istration_receipt) + .unwrap(); + wt_client.flag_misbehaving_tower(tower_id, proof.clone()); + + // Check data in memory + let tower_summary = wt_client.towers.get(&tower_id); + assert!(tower_summary.is_some()); + assert!(tower_summary.unwrap().status.is_misbehaving()); + + // Check data in DB + let loaded_info = wt_client.load_tower_info(tower_id).unwrap(); + assert!(loaded_info.status.is_misbehaving()); + assert_eq!(loaded_info.misbehaving_proof, Some(proof)); + assert!(loaded_info.appointments.contains_key(&appointment.locator)); + } + + #[tokio::test] + async fn test_remove_tower() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let receipt = get_random_registration_receipt(); + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let tower_info = TowerInfo::empty( + "talaia.watch".to_owned(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + + // Add the tower and check it is there + wt_client + .add_update_tower(tower_id, &tower_info.net_addr, &receipt) + .unwrap(); + assert_eq!( + wt_client.towers.get(&tower_id), + Some(&TowerSummary::from(tower_info.clone())) + ); + assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); + + // Remove the tower and check it is not there anymore + wt_client.remove_tower(tower_id).unwrap(); + assert!(wt_client.load_tower_info(tower_id).is_none()); + assert!(!wt_client.towers.contains_key(&tower_id)); + + // Try again but this time with an associated appointment to check that it also gets removed + wt_client + .add_update_tower(tower_id, &tower_info.net_addr, &receipt) + .unwrap(); + + let locator = generate_random_appointment(None).locator; + let registration_receipt = get_random_registration_receipt(); + let appointment_receipt = get_random_appointment_receipt(tower_sk); + + // If we call this on an unknown tower it will simply do nothing + wt_client.add_appointment_receipt( + tower_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt, + ); + assert!(wt_client + .storage + .appointment_receipt_exists(locator, tower_id)); + + // Remove and check both the tower and the appointment + wt_client.remove_tower(tower_id).unwrap(); + assert!(wt_client.load_tower_info(tower_id).is_none()); + assert!(!wt_client.towers.contains_key(&tower_id)); + assert!(!wt_client + .storage + .appointment_receipt_exists(locator, tower_id)); + } + + #[tokio::test] + async fn test_remove_tower_shared_appointment() { + // Lets test removing a tower that has associated data shared with another tower. + // For instance, having an appointment that was sent to two towers, and then deleting one of them + // should only remove the link between the tower and the appointment, but not delete the data. + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let receipt = get_random_registration_receipt(); + let (tower1_sk, tower1_pk) = cryptography::get_random_keypair(); + let tower1_id = TowerId(tower1_pk); + let (tower2_sk, tower2_pk) = cryptography::get_random_keypair(); + let tower2_id = TowerId(tower2_pk); + + wt_client + .add_update_tower(tower1_id, "talaia.watch", &receipt) + .unwrap(); + wt_client + .add_update_tower(tower2_id, "talaia.watch", &receipt) + .unwrap(); + + let locator = generate_random_appointment(None).locator; + let registration_receipt = get_random_registration_receipt(); + let appointment_receipt_1 = get_random_appointment_receipt(tower1_sk); + let appointment_receipt_2 = get_random_appointment_receipt(tower2_sk); + + wt_client.add_appointment_receipt( + tower1_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt_1, + ); + wt_client.add_appointment_receipt( + tower2_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt_2, + ); + + // Check that the data exists in both towers + assert!(wt_client + .storage + .appointment_receipt_exists(locator, tower1_id)); + assert!(wt_client + .storage + .appointment_receipt_exists(locator, tower2_id)); + + // Remove tower1 and check that the appointment receipt can still be found for tower2 + wt_client.remove_tower(tower1_id).unwrap(); + assert!(wt_client.load_tower_info(tower1_id).is_none()); + + assert!(!wt_client + .storage + .appointment_receipt_exists(locator, tower1_id)); + assert!(wt_client + .storage + .appointment_receipt_exists(locator, tower2_id)); + } + + #[tokio::test] + async fn test_remove_inexistent_tower() { + let keypair = cryptography::get_random_keypair(); + let mut wt_client = WTClient::new( + MemoryStore::new().into_dyn_store(), + keypair.0, + unbounded_channel().0, + ) + .await; + + let tower_id = get_random_user_id(); + let err = wt_client.remove_tower(tower_id).unwrap_err(); + assert_eq!( + err, + PersisterError::NotFound(format!("tower_id: {tower_id}")) + ); + } +} diff --git a/watchtower-plugin/src/lib.rs b/watchtower-plugin/src/lib.rs index 8f416586..145f2a13 100755 --- a/watchtower-plugin/src/lib.rs +++ b/watchtower-plugin/src/lib.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use teos_common::appointment::{Appointment, Locator}; use teos_common::net::NetAddr; @@ -20,7 +20,7 @@ pub mod wt_client; mod test_utils; /// The status the tower can be found at. -#[derive(Clone, Serialize, PartialEq, Eq, Copy, Debug)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Copy, Debug)] #[serde(rename_all = "snake_case")] pub enum TowerStatus { Reachable, @@ -195,7 +195,7 @@ impl From for TowerSummary { } /// Summarized data associated with a given tower. -#[derive(Clone, Serialize, Debug, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct TowerInfo { pub net_addr: String, pub available_slots: u32, @@ -249,7 +249,7 @@ impl TowerInfo { } /// A misbehaving proof. Contains proof of a tower replying with a public key different from the advertised one. -#[derive(Clone, Serialize, Debug, PartialEq, Eq)] +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] pub struct MisbehaviorProof { #[serde(with = "hex::serde")] pub locator: Locator,