commit 9fa97ebb5c3f23e8d2cad1778a79ca125a533d0b Author: Goblin Date: Thu Jul 2 08:22:18 2026 -0400 floonet-rs: hardened nostr-rs-relay for the Grin community nostr-rs-relay + a default-deny admission pipeline (kinds 0,3,5,13,1059, 10002,10050,27235 only), NIP-42 auth, neutral NIP-11, a built-in name authority (paid names via GoblinPay), and a config-toggled co-located mixnet exit supervisor. Single binary + installer + hardened systemd, or Docker Compose. Relay core untouched (additive admission + authority). diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..bff29e6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..aba3ad6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: Test and build + +on: + push: + branches: + - master + +jobs: + test_floonet-rs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Update local toolchain + run: | + sudo apt-get install -y protobuf-compiler + rustup update + rustup component add clippy + rustup install nightly + + - name: Toolchain info + run: | + cargo --version --verbose + rustc --version + cargo clippy --version + + # - name: Lint + # run: | + # cargo fmt -- --check + # cargo clippy -- -D warnings + + - name: Test + run: | + cargo check + cargo test --all + + - name: Build + run: | + cargo build --release --locked diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4c3c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +**/target/ +nostr.db +nostr.db-* +justfile +result +**/.idea/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5925d8c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/doublify/pre-commit-rust + rev: v1.0 + hooks: +# - id: fmt + - id: cargo-check + - id: clippy diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0b1d1ee --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4354 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg 1.5.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.3", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.5.0", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bitcoin" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3" +dependencies = [ + "bech32", + "bitcoin_hashes 0.11.0", + "secp256k1 0.24.3", + "serde", +] + +[[package]] +name = "bitcoin" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" +dependencies = [ + "bech32", + "bitcoin-private", + "bitcoin_hashes 0.12.0", + "hex_lit", + "secp256k1 0.27.0", + "serde", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "cln-rpc" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974dac6f40275b7b828087f4f9973c39658f9b4a46cc589c083a2c6c27cf67cb" +dependencies = [ + "anyhow", + "bitcoin 0.30.2", + "bytes", + "futures-util", + "hex", + "log", + "serde", + "serde_json", + "tokio", + "tokio-util", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ad70579325f1a38ea4c13412b82241c5900700a69785d73e2736bd65a33f86" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "console-api" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" +dependencies = [ + "prost", + "prost-types", + "tonic 0.9.2", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic 0.9.2", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dlv-list" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "floonet-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "async-trait", + "base64 0.21.7", + "bech32", + "bitcoin_hashes 0.10.0", + "chrono", + "clap", + "cln-rpc", + "config", + "console-subscriber", + "const_format", + "futures", + "futures-util", + "governor", + "hex", + "http", + "hyper", + "hyper-rustls", + "indicatif", + "itertools 0.14.0", + "lazy_static", + "log", + "nonzero_ext", + "nostr", + "parse_duration", + "prometheus", + "prost", + "qrcode", + "r2d2", + "r2d2_sqlite", + "rand 0.8.5", + "regex", + "rusqlite", + "secp256k1 0.21.3", + "serde", + "serde_json", + "sqlx", + "thiserror 1.0.69", + "tikv-jemallocator", + "tokio", + "tokio-tungstenite", + "tonic 0.8.3", + "tonic-build", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tungstenite", + "url", + "uuid", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "governor" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19775995ee20209163239355bc3ad2f33f83da35d9ef72dea26e5af753552c87" +dependencies = [ + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.5", + "quanta", + "rand 0.8.5", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.8", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "log", + "rustls 0.21.12", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg 1.5.0", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "nostr" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0446103768cddfb2bc1b87a52e98c35227b82711c2b3ce7098f8d85d9b0ee" +dependencies = [ + "aes", + "base64 0.21.7", + "bitcoin 0.29.2", + "cbc", + "getrandom 0.2.17", + "instant", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.5.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.5.0", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg 1.5.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.5.0", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg 1.5.0", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "ordered-multimap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +dependencies = [ + "dlv-list", + "hashbrown 0.9.1", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse_duration" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d" +dependencies = [ + "lazy_static", + "num", + "regex", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.13.0", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.12.5", + "protobuf", + "thiserror 1.0.69", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools 0.10.5", + "lazy_static", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 1.0.109", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", +] + +[[package]] +name = "quanta" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" +dependencies = [ + "crossbeam-utils", + "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.2+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.5", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ca3c9468a76fc2ad724c486a59682fc362efeac7b18d1c012958bc19f34800" +dependencies = [ + "r2d2", + "rusqlite", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "rusqlite" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" +dependencies = [ + "bitflags 1.3.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.7.0", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rust-ini" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.5", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "secp256k1" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c42e6f1735c5f00f51e43e28d6634141f2bcad10931b2609ddd74a86d751260" +dependencies = [ + "bitcoin_hashes 0.10.0", + "rand 0.6.5", + "secp256k1-sys 0.4.2", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" +dependencies = [ + "bitcoin_hashes 0.11.0", + "rand 0.8.5", + "secp256k1-sys 0.6.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes 0.12.0", + "secp256k1-sys 0.8.2", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +dependencies = [ + "ahash 0.7.8", + "atoi", + "base64 0.13.1", + "bitflags 1.3.2", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dirs", + "dotenvy", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-util", + "hashlink 0.8.4", + "hex", + "hkdf", + "hmac", + "indexmap 1.9.3", + "itoa", + "libc", + "log", + "md-5", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rand 0.8.5", + "rustls 0.20.9", + "rustls-pemfile", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror 1.0.69", + "tokio-stream", + "url", + "webpki-roots", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "once_cell", + "proc-macro2", + "quote", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn 1.0.109", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot 0.12.5", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tonic" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.13.1", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum", + "base64 0.21.7", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "thiserror 2.0.17", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha-1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d7ea943 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "floonet-rs" +version = "0.1.0" +edition = "2021" +authors = ["The Floonet Developers", "Greg Heartsfield "] +description = "A hardened Floonet relay for the Grin community Nostr network, forked from nostr-rs-relay" +readme = "README.md" +homepage = "https://floonet.dev" +license = "MIT" +keywords = ["nostr", "server", "grin", "floonet"] +categories = ["network-programming", "web-programming"] +default-run = "floonet-rs" + +[dependencies] +clap = { version = "4.0.32", features = ["env", "default", "derive"]} +tracing = "0.1.37" +tracing-appender = "0.2.2" +tracing-subscriber = "0.3.16" +tokio = { version = "1", features = ["full", "tracing", "signal"] } +prost = "0.11" +tonic = "0.8.3" +console-subscriber = "0.1.8" +futures = "0.3" +futures-util = "0.3" +tokio-tungstenite = "0.17" +tungstenite = "0.17" +thiserror = "1" +uuid = { version = "1.1.2", features = ["v4"] } +config = { version = "0.12", features = ["toml"] } +bitcoin_hashes = { version = "0.10", features = ["serde"] } +secp256k1 = {version = "0.21", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = {version = "1.0", features = ["preserve_order"]} +hex = "0.4" +rusqlite = { version = "0.26", features = ["limits","bundled","modern_sqlite", "trace"]} +r2d2 = "0.8" +r2d2_sqlite = "0.19" +lazy_static = "1.4" +governor = "0.4" +nonzero_ext = "0.3" +hyper = { version="0.14", features=["client", "server","http1","http2","tcp"] } +hyper-rustls = { version = "0.24" } +http = { version = "0.2" } +parse_duration = "2" +rand = "0.8" +const_format = "0.2.28" +regex = "1" +async-trait = "0.1.60" +async-std = "1.12.0" +sqlx = { version ="0.6.2", features=["runtime-tokio-rustls", "postgres", "chrono"]} +chrono = "0.4.23" +prometheus = "0.13.3" +indicatif = "0.17.3" +bech32 = "0.9.1" +url = "2.3.1" +qrcode = { version = "0.12.0", default-features = false, features = ["svg"] } +nostr = { version = "0.18.0", default-features = false, features = ["base", "nip04", "nip19"] } +log = "0.4" +cln-rpc = "0.1.9" +itertools = "0.14.0" +base64 = "0.21" + +[target.'cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))'.dependencies] +tikv-jemallocator = "0.5" + +[dev-dependencies] +anyhow = "1" + +[build-dependencies] +tonic-build = { version="0.8.3", features = ["prost"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3894d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +FROM docker.io/library/rust:1-bookworm as builder +ARG CARGO_LOG +RUN apt-get update \ + && apt-get install -y cmake protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* +RUN USER=root cargo install cargo-auditable +RUN USER=root cargo new --bin floonet-rs +WORKDIR ./floonet-rs +COPY ./Cargo.toml ./Cargo.toml +COPY ./Cargo.lock ./Cargo.lock +# build dependencies only (caching) +RUN cargo auditable build --release --locked +# get rid of starter project code +RUN rm src/*.rs + +# copy project source code +COPY ./src ./src +COPY ./proto ./proto +COPY ./assets ./assets +COPY ./build.rs ./build.rs + +# build auditable release using locked deps +RUN rm ./target/release/deps/floonet* +RUN cargo auditable build --release --locked + +FROM docker.io/library/debian:bookworm-slim + +ARG APP=/usr/src/app +ARG APP_DATA=/usr/src/app/db +RUN apt-get update \ + && apt-get install -y ca-certificates tzdata sqlite3 libc6 \ + && rm -rf /var/lib/apt/lists/* + +EXPOSE 8080 + +ENV TZ=Etc/UTC \ + APP_USER=appuser + +RUN groupadd $APP_USER \ + && useradd -g $APP_USER $APP_USER \ + && mkdir -p ${APP} \ + && mkdir -p ${APP_DATA} + +COPY --from=builder /floonet-rs/target/release/floonet-rs ${APP}/floonet-rs + +RUN chown -R $APP_USER:$APP_USER ${APP} + +USER $APP_USER +WORKDIR ${APP} + +ENV RUST_LOG=info,floonet_rs=info +ENV APP_DATA=${APP_DATA} + +CMD ./floonet-rs --db ${APP_DATA} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ef47c1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Greg Heartsfield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..515f43a --- /dev/null +++ b/README.md @@ -0,0 +1,274 @@ +# floonet-rs + +A hardened [Floonet](https://floonet.dev) relay for the Grin community +Nostr network, forked from +[nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay). + +Floonet is a network of Nostr relays for the Grin community: anyone can +run one, and anyone can run a name authority on it so people can claim +(and optionally pay for) a `name@domain` identity. floonet-rs keeps the +upstream relay core intact and adds four configurable, modular features: + +* An **event kind whitelist** (the keystone): default-deny admission. + The relay accepts ONLY the kinds it is configured to allow and rejects + everything else. The shipped set is + `0, 3, 5, 13, 1059, 10002, 10050, 27235`. +* **Authentication**: NIP-42, with optional require-auth-to-write and an + author whitelist. +* A **built-in name authority**: `name@domain` NIP-05 identities with + NIP-98 authenticated self-service registration, served in-process. + Optionally paid in GRIN through GoblinPay. +* A **co-located mixnet exit** (config toggle): wallets can reach this + relay over the mixnet, with no public DNS on the payment path. + +The public relay metadata stays neutral on purpose: the NIP-11 document +and landing page never mention payments. The relay only ever sees opaque +gift-wrapped ciphertext, so payment wording would be both inaccurate and +an operational liability. + +## Deploy + +Pick your comfort level. All three paths end with the same relay. + +### 1. Docker Compose (recommended) + +Brings up the relay plus a Caddy TLS proxy in one command: + +```sh +cp config.toml my-config.toml +# edit my-config.toml: info.relay_url, and [network] address = "0.0.0.0" +echo 'FLOONET_DOMAIN=relay.example.com' > .env +docker compose up -d +``` + +The relay container is non-root with a read-only root filesystem; Caddy +obtains certificates automatically and forwards the real client IP. + +### 2. Binary + installer + systemd + +From an unpacked release archive (or a source checkout after building), +the installer drops the binary, a default config, and a hardened +systemd unit; no toolchain needed at install time: + +```sh +sudo sh deploy/install.sh +sudo $EDITOR /etc/floonet-rs/config.toml # set info.relay_url +sudo systemctl start floonet-rs +``` + +Put a TLS proxy in front (see `deploy/Caddyfile`). The unit runs as a +dynamic unprivileged user with a read-only system view +(`ProtectSystem=strict`, `NoNewPrivileges`, `MemoryDenyWriteExecute`, +syscall filtering); only `/var/lib/floonet-rs` is writable. + +### 3. Source build + +```sh +cargo build --release +./target/release/floonet-rs --config config.toml --db . +``` + +Requires a protobuf compiler (`protoc`) for the gRPC extension point. + +## The whitelist (keystone) + +```toml +[limits] +event_kind_allowlist = [0, 3, 5, 13, 1059, 10002, 10050, 27235] +``` + +Fail-closed semantics, enforced in the write path before anything is +queued for persistence: + +* The listed kinds are accepted; **everything else is rejected** with an + `OK false` / `blocked:` message. +* Removing the line keeps the built-in Floonet set. There is no + allow-all: an empty list denies everything. +* To add a kind, add it to the list and restart. Never narrow the list + below what your users' wallets already depend on. + +## Authentication (NIP-42) + +```toml +[authorization] +nip42_auth = true # send AUTH challenges +require_auth_to_write = true # refuse writes until the client AUTHs +nip42_dms = true # gift wraps only to their recipients +#pubkey_whitelist = [""] # restrict authors entirely +``` + +Unauthenticated writes are refused with an `auth-required:` prefixed OK +message, so compliant clients authenticate and resend. + +## Name authority + +Enable the built-in authority to serve `name@yourdomain` identities: + +```toml +[name_authority] +enabled = true +domain = "example.com" +base_url = "https://example.com" # must match what clients reach +``` + +Endpoints, all on the relay's own listener: + +| Endpoint | Purpose | +| --- | --- | +| `GET /.well-known/nostr.json?name=` | NIP-05 resolution | +| `POST /api/v1/register` | claim a name (NIP-98 auth) | +| `DELETE /api/v1/register/{name}` | release a name (NIP-98 auth) | +| `GET /api/v1/name/{name}` | availability | +| `GET /api/v1/profile/{name}` | name to pubkey | +| `GET /api/v1/by-pubkey/{pubkey}` | reverse lookup | +| `GET /api/v1/health` | liveness | + +Rules carried over from goblin-nip05d: lowercase `[a-z0-9._-]` names +(3 to 20 characters, alphanumeric at both ends), a built-in reserved +list plus your own domain labels with look-alike folding (`g0blin` +cannot impersonate `goblin`), one active name per key enforced by the +database, NIP-98 verification with a bounded replay window, per-IP rate +limits, and a release-armed rename cooldown. Claims live in the relay's +own SQLite database (`name_claims` table). + +## Charge GRIN for your relay + +Paid use is one switch plus a price. Point the relay at your GoblinPay +server and pick a mode: + +```toml +[goblinpay] +pay_mode = "name" # or "write", or "off" +url = "https://pay.example.com" +api_token = "" +name_price_grin = 1.0 +``` + +Or keep secrets out of the file entirely and use the environment: +`FLOONET_PAY_MODE`, `FLOONET_GOBLINPAY_URL`, `FLOONET_GOBLINPAY_TOKEN`, +`FLOONET_NAME_PRICE_GRIN`. + +* **`pay_mode = "name"`**: claiming a name answers + `402 {"error":"payment_required","pay_url":...}` with a hosted + GoblinPay page (GoblinPay, manual slatepack, or a `grin1` address if + the operator enabled that method). Once the payment confirms on chain, + the same register call succeeds. Clients have everything they need to + send the user straight to the pay page and retry. +* **`pay_mode = "write"`**: publishing requires a paid admission; the + relay reuses its pay-to-relay account model with GoblinPay as the + payment processor. +* A GoblinPay webhook may POST `{"invoice_id": ...}` to `/goblinpay` to + speed things up; the relay always re-verifies the invoice with the + GoblinPay server before admitting anything, so a forged webhook cannot + fake a payment. + +Payments admit the pubkey, not the request: after one confirmed payment +a key can claim, release, and re-claim its single name without paying +again (the rename cooldown still applies). + +Prices are plain config values; edit and restart to change them. The +public relay metadata stays payment-free regardless of mode. + +## Mixnet exit + +Flip one toggle and this relay also runs a co-located mixnet exit, so +wallets can reach it over the mixnet: + +```toml +[exit] +enabled = true +binary = "/usr/local/bin/floonet-mixexit" +data_dir = "/var/lib/floonet-rs/mixexit" +upstream = "relay.example.com:443" # your public TLS endpoint +``` + +The exit (bundled in `mixexit/`) is an ordinary unbonded mixnet client: +no node registration, no tokens, no directory listing. It forwards +every accepted stream to the ONE configured upstream, never a +caller-chosen target, so it is structurally not an open proxy and you +carry no exit liability. Wallets run hostname-validated TLS end to end +through the pipe; the exit only ever sees ciphertext. + +The exit's mixnet address is stable across restarts (the identity +persists in `data_dir`; back it up). It is printed at startup and +written to `/nym_address.txt`; publish it, for example in the +Floonet relay pool `exit` field, so wallets prefer your exit and fall +back to the public mixnet route when it is down. + +Build the exit binary separately (it pulls the mixnet SDK tree): + +```sh +cargo build --release --manifest-path mixexit/Cargo.toml +``` + +The path dependency expects the Goblin `nym` checkout (branch `goblin`) +two directories up; adjust `mixexit/Cargo.toml` for your layout. Verify +with `floonet-mixexit --selftest`, which joins the mixnet, prints the +stable address, and exits. + +## Extending: policies and paid resources + +Admission is a small ordered pipeline in `src/admission.rs`. Each check +implements one trait: + +```rust +pub trait AdmissionPolicy: Send + Sync { + fn check(&self, event: &Event, authed_pubkey: Option<&str>) -> Decision; +} +``` + +To add a policy (a paid gate, a spam filter, a tag rule), implement the +trait and append it in `Admission::from_settings`; the first denial +wins. To add a kind, edit the config; no code change needed. The gRPC +`event_admission_server` extension point from upstream also remains +available for out-of-process policies. + +Paid uses follow the same pattern as names: quote a price, hand the +client a GoblinPay pay page, verify the confirmed invoice, then grant +the resource. Names are the first paid resource; **paid media storage +for GRIN** (NIP-96 HTTP file storage or Blossom content-addressed +blobs, advertised with a kind 10063 server list, priced per upload or +per MB) is the designed-for next example: the same +`402 pay_url -> confirm -> grant` gate applied to an upload endpoint. + +## Operational notes + +* **Reverse proxy**: terminate TLS at Caddy or nginx and forward + `X-Real-IP` (`remote_ip_header` in the config). All per-IP rate + limiting keys off it. +* **Event size**: keep `max_event_bytes` at its default (256 KB) or + larger; gift-wrapped payloads can be big. +* **Database**: SQLite by default; the schema migrates automatically at + startup (this fork adds `name_claims` at version 19). The name + authority requires the sqlite engine; postgres remains available for + the plain relay. +* **Secrets**: nothing in this repository; the GoblinPay token comes + from the config file (0600) or the environment. +* **Multiple identities, one wallet**: a Goblin wallet can hold several + Nostr identities. If you pay for a name and want to keep it, load the + same wallet and switch to (or add) that npub; different identities + share one wallet. + +Upstream documentation for the inherited features lives in `docs/` +(database maintenance, gRPC extensions, reverse proxies, and more) and +in `docs/upstream/README.md`. + +## Development + +```sh +cargo build --release # build the relay +cargo test # unit + integration tests +``` + +The integration tests stand up real relays on loopback and cover the +whitelist end to end (allowed kind accepted, disallowed kind rejected), +the name authority round trip (register, resolve, reverse lookup, +conflicts, reserved names, release, cooldown), the paid-name flow +against a stub GoblinPay server, and the payment-free NIP-11 rule. + +## License + +MIT, same as upstream. The upstream relay is by Greg Heartsfield and +contributors; the Floonet additions are by the Floonet developers. + +🤖 Built with AI pair-programming assistance (Claude) diff --git a/assets/floonet-logo.svg b/assets/floonet-logo.svg new file mode 100644 index 0000000..c5a0add --- /dev/null +++ b/assets/floonet-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2dc88f9 --- /dev/null +++ b/build.rs @@ -0,0 +1,7 @@ +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(false) + .protoc_arg("--experimental_allow_proto3_optional") + .compile(&["proto/nauthz.proto"], &["proto"])?; + Ok(()) +} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..f0ee483 --- /dev/null +++ b/config.toml @@ -0,0 +1,221 @@ +# floonet-rs relay configuration. +# +# Every setting shown commented-out is the built-in default. The shipped +# defaults give you a hardened Floonet relay: a default-deny event kind +# whitelist, neutral public metadata, and everything paid switched off. + +[info] +# The advertised URL for the Nostr websocket. Set this to your public +# wss:// address; NIP-42 auth validates against it. +relay_url = "wss://relay.example.com/" + +# Relay information for clients (NIP-11). Keep these neutral: the public +# relay metadata says nothing about payments, by design. +name = "floonet-rs-relay" +description = "A Floonet relay for the Grin community Nostr network." + +# Administrative contact pubkey (32-byte hex, not npub) +#pubkey = "0c2d168a4ae8ca58c9f1ab237b5df682599c6c7ab74307ea8b05684b60405d41" + +# Administrative contact URI +#contact = "mailto:contact@example.com" + +# Favicon location, relative to the current directory (ICO format). +#favicon = "favicon.ico" + +# URL of the relay's icon. +#relay_icon = "https://example.com/img.png" + +# Path to a custom relay html landing page. When unset, the relay serves +# a neutral Floonet page with the Floonet logo. +#relay_page = "index.html" + +[database] +# Database engine (sqlite/postgres). Defaults to sqlite. The built-in +# name authority requires sqlite. +#engine = "sqlite" + +# Directory for SQLite files. +data_directory = "/var/lib/floonet-rs" + +# Database connection pool settings for subscribers: +#min_conn = 0 +#max_conn = 8 + +[logging] +# Directory to store log files. Log files roll over daily. +#folder_path = "./log" +#file_prefix = "floonet-rs" + +[grpc] +# gRPC extension point for externalized event admission (see +# proto/nauthz.proto). Optional; the built-in admission layer already +# enforces the kind whitelist and auth policies. +#event_admission_server = "http://[::1]:50051" +#restricts_write = true + +[network] +# Bind to this network address. Keep loopback and put a reverse proxy +# (Caddy/nginx) in front for TLS; see deploy/Caddyfile. +address = "127.0.0.1" + +# Listen on this port +port = 8080 + +# Read the real client IP from this header. LOAD-BEARING behind a +# reverse proxy: per-IP rate limits key off it. +remote_ip_header = "x-real-ip" + +[options] +# Reject events with timestamps too far in the future, in seconds. +reject_future_seconds = 1800 + +[limits] +# Limit events created per second (server-wide, averaged over a minute). +messages_per_sec = 5 + +# Limit client subscriptions created per minute. +subscriptions_per_min = 30 + +# Maximum size of an EVENT message in bytes. Keep this large enough for +# gift-wrapped payloads (the default 256 KB is safe). +#max_event_bytes = 262144 + +# THE KEYSTONE: default-deny event kind whitelist. The relay accepts +# ONLY these kinds and rejects everything else. Removing the line +# entirely keeps this exact built-in set (never allow-all); an empty +# list denies everything. +# +# 0 profile metadata +# 3 contacts +# 5 delete (NIP-09) +# 13 seal +# 1059 gift wrap (NIP-59) +# 10002 relay list (NIP-65) +# 10050 DM relays (NIP-17) +# 27235 HTTP auth (NIP-98, used by the name authority) +event_kind_allowlist = [0, 3, 5, 13, 1059, 10002, 10050, 27235] + +# Rejects imprecise requests (kind-only or author-only scrapes). +limit_scrapers = false + +[authorization] +# Restrict event publishing to these authors (32-byte hex pubkeys). +#pubkey_whitelist = [ +# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f", +#] + +# Enable NIP-42 authentication (the relay sends an AUTH challenge). +#nip42_auth = false + +# Send gift wraps and DMs only to their authenticated recipients. +#nip42_dms = false + +# With nip42_auth on, refuse writes from clients that have not +# completed AUTH (they receive an `auth-required:` OK message). +#require_auth_to_write = false + +[goblinpay] +# Charge GRIN for relay uses via a GoblinPay server. Modes: +# "off" everything is free (default) +# "name" claiming a name at the built-in name authority requires a +# confirmed Grin payment +# "write" publishing events requires a paid admission +# The same keys are readable from the environment instead: +# FLOONET_PAY_MODE, FLOONET_GOBLINPAY_URL, FLOONET_GOBLINPAY_TOKEN, +# FLOONET_NAME_PRICE_GRIN. +#pay_mode = "off" + +# Your GoblinPay server and its API token (GP_API_TOKEN). Keep this file +# unreadable to other users (chmod 0600) when a token is set, or pass +# the token via FLOONET_GOBLINPAY_TOKEN. +#url = "https://pay.example.com" +#api_token = "" + +# Prices in GRIN, editable any time. +#name_price_grin = 1.0 +#admission_price_grin = 1.0 + +[name_authority] +# The built-in name authority: name@domain NIP-05 identities with +# NIP-98 authenticated self-service registration, served on this relay's +# own listener (/.well-known/nostr.json and /api/v1/*). +#enabled = false + +# The bare host names live under (the `@domain` part) and the public +# base URL clients reach. base_url is LOAD-BEARING: NIP-98 auth events +# are verified against it, so it must be https:// and match what +# clients actually use. +#domain = "example.com" +#base_url = "https://example.com" + +# Relays advertised in /.well-known/nostr.json. Defaults to this +# relay's own relay_url. +#relays = ["wss://relay.example.com"] + +# Name policy. +#name_min = 3 +#name_max = 20 +#name_change_cooldown_secs = 600 + +# NIP-98 freshness bound in seconds (with one-time-use replay guard). +#auth_max_age_secs = 60 + +# Per-IP rate limits (requests per window, window in seconds). +#read_rate_max = 120 +#read_rate_window_secs = 60 +#write_rate_max = 10 +#write_rate_window_secs = 3600 + +# Optional file of extra reserved names (one per line, # comments). +# The built-in generic list and your own domain labels are always +# reserved, including digit/separator look-alikes. +#reserved_file = "/etc/floonet-rs/reserved" + +[exit] +# Co-located mixnet exit. When enabled the relay runs the bundled +# floonet-mixexit binary next to itself: an ordinary unbonded mixnet +# client that forwards every accepted stream to ONE fixed upstream (your +# relay), never a caller-chosen target, so it is structurally not an +# open proxy. Wallets can then reach this relay over the mixnet with no +# public DNS on the payment path; they fall back to the public mixnet +# route when the exit is down. +#enabled = false + +# Path to the bundled floonet-mixexit binary. +#binary = "/usr/local/bin/floonet-mixexit" + +# Data dir for the persistent mixnet identity. The exit's STABLE mixnet +# address is printed at startup and written to /nym_address.txt; +# publish it (for example in the Floonet relay pool `exit` field) so +# wallets can prefer this exit. Back the directory up: losing it rotates +# the address. +#data_dir = "/var/lib/floonet-rs/mixexit" + +# Upstream the exit pipes every stream to. Point it at your PUBLIC TLS +# endpoint so wallets get your real certificate through the mixnet. +# Empty means this relay's local listener (no TLS). +#upstream = "relay.example.com:443" + +[verified_users] +# NIP-05 verification of users (upstream feature; the built-in name +# authority is separate). "enabled" enforces, "passive" observes, +# "disabled" does nothing. +#mode = "disabled" + +[pay_to_relay] +# Upstream pay-to-relay admission. You normally do NOT edit this +# section: setting goblinpay.pay_mode = "write" configures it for +# GoblinPay automatically. It remains available for operators who want +# the upstream Lightning processors instead. +#enabled = false +#processor = "GoblinPay" +#admission_cost = 1000000000 +#cost_per_event = 0 +#node_url = "" +#api_secret = "" +#sign_ups = false +#direct_message = false +#terms_message = """ +#Use this relay lawfully and without abuse. +#""" diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..4766143 --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,19 @@ +# Caddy reverse proxy for floonet-rs, with automatic HTTPS. +# +# floonet-rs serves everything on one port: the websocket relay, the +# NIP-11 document, the landing page, and (when enabled) the name +# authority endpoints. Point your domain at this host and Caddy obtains +# certificates automatically. +# +# SECURITY-CRITICAL: X-Real-IP must be set from the real client address. +# The relay and the name authority key ALL of their per-IP rate limiting +# off this header (config.toml `remote_ip_header = "x-real-ip"`); if the +# proxy does not set it, every request looks like one client and the +# limiter is defeated. Caddy's {remote_host} is the connecting peer, not +# a forwardable client header. + +relay.example.com { + reverse_proxy 127.0.0.1:8080 { + header_up X-Real-IP {remote_host} + } +} diff --git a/deploy/Caddyfile.compose b/deploy/Caddyfile.compose new file mode 100644 index 0000000..fe12a47 --- /dev/null +++ b/deploy/Caddyfile.compose @@ -0,0 +1,11 @@ +# Caddyfile used by docker-compose.yml. The upstream is the compose +# service name `relay`; FLOONET_DOMAIN is injected from .env. +# +# SECURITY-CRITICAL: X-Real-IP is set from the real client address; the +# relay and name authority key their per-IP rate limiting off it. + +{$FLOONET_DOMAIN} { + reverse_proxy relay:8080 { + header_up X-Real-IP {remote_host} + } +} diff --git a/deploy/floonet-rs.service b/deploy/floonet-rs.service new file mode 100644 index 0000000..713579e --- /dev/null +++ b/deploy/floonet-rs.service @@ -0,0 +1,65 @@ +# Hardened systemd unit for floonet-rs on bare metal. +# +# Install (or just run deploy/install.sh): +# sudo install -m0755 floonet-rs /usr/local/bin/ +# sudo install -m0755 floonet-mixexit /usr/local/bin/ # optional, mixnet exit +# sudo install -d -m0755 /etc/floonet-rs +# sudo install -m0600 config.toml /etc/floonet-rs/config.toml +# sudo install -m0644 deploy/floonet-rs.service /etc/systemd/system/ +# sudo systemctl daemon-reload && sudo systemctl enable --now floonet-rs +# +# The service is locked down: dynamic unprivileged user, read-only +# system, no new privileges; only its state directory is writable. + +[Unit] +Description=floonet-rs relay (Floonet relay for the Grin community Nostr network) +After=network-online.target +Wants=network-online.target + +[Service] +Type=exec +# DynamicUser allocates a throwaway unprivileged user at runtime. If you +# need a stable owner for the data dir, comment this out and set +# `User=floonet` (create the user first). +DynamicUser=yes + +# Managed state at /var/lib/floonet-rs (created and chowned by systemd). +# config.toml's data_directory and exit.data_dir point inside it. +StateDirectory=floonet-rs +StateDirectoryMode=0750 + +# Optional environment overrides (paid mode without editing config.toml): +# FLOONET_PAY_MODE, FLOONET_GOBLINPAY_URL, FLOONET_GOBLINPAY_TOKEN, +# FLOONET_NAME_PRICE_GRIN. Keep the file 0600 when it holds a token. +#EnvironmentFile=-/etc/floonet-rs/env + +ExecStart=/usr/local/bin/floonet-rs --config /etc/floonet-rs/config.toml +Restart=on-failure +RestartSec=2 + +# --- hardening --- +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +ProtectClock=yes +ProtectHostname=yes +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallFilter=~@privileged @resources +# Only the state directory is writable. +ReadWritePaths=/var/lib/floonet-rs +# No raw sockets; only IP (and unix for the database). +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX + +[Install] +WantedBy=multi-user.target diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100755 index 0000000..a16e9c8 --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# floonet-rs installer: drops the binary, config, and hardened systemd +# unit. No toolchain needed when run from an unpacked release archive. +# +# Usage: +# sudo sh deploy/install.sh +# +# Looks for the binaries next to this script's parent directory in this +# order: ./floonet-rs (release archive layout), then +# target/release/floonet-rs (source build layout). The optional +# floonet-mixexit binary is installed the same way when present. +# +# Idempotent: re-running upgrades the binaries and unit but never +# overwrites an existing /etc/floonet-rs/config.toml. + +set -eu + +if [ "$(id -u)" -ne 0 ]; then + echo "error: run as root (sudo sh deploy/install.sh)" >&2 + exit 1 +fi + +here="$(cd "$(dirname "$0")/.." && pwd)" + +find_binary() { + name="$1" + for candidate in "$here/$name" "$here/target/release/$name"; do + if [ -x "$candidate" ]; then + echo "$candidate" + return 0 + fi + done + return 1 +} + +relay_bin="$(find_binary floonet-rs)" || { + echo "error: floonet-rs binary not found; build it first (cargo build --release)" >&2 + exit 1 +} + +echo "installing $relay_bin -> /usr/local/bin/floonet-rs" +install -m0755 "$relay_bin" /usr/local/bin/floonet-rs + +if exit_bin="$(find_binary floonet-mixexit)"; then + echo "installing $exit_bin -> /usr/local/bin/floonet-mixexit" + install -m0755 "$exit_bin" /usr/local/bin/floonet-mixexit +else + echo "note: floonet-mixexit binary not found; skipping (the mixnet exit" + echo " toggle needs it; see mixexit/README section in README.md)" +fi + +install -d -m0755 /etc/floonet-rs +if [ ! -f /etc/floonet-rs/config.toml ]; then + echo "installing default config -> /etc/floonet-rs/config.toml" + install -m0600 "$here/config.toml" /etc/floonet-rs/config.toml + echo ">>> EDIT /etc/floonet-rs/config.toml: set info.relay_url at minimum." +else + echo "keeping existing /etc/floonet-rs/config.toml" +fi + +echo "installing systemd unit -> /etc/systemd/system/floonet-rs.service" +install -m0644 "$here/deploy/floonet-rs.service" /etc/systemd/system/floonet-rs.service +systemctl daemon-reload +systemctl enable floonet-rs + +echo +echo "done. next steps:" +echo " 1. edit /etc/floonet-rs/config.toml (relay_url, and optionally" +echo " the name authority, paid mode, and mixnet exit sections)" +echo " 2. put a TLS proxy in front (see deploy/Caddyfile)" +echo " 3. systemctl start floonet-rs && journalctl -fu floonet-rs" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f99fc49 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +# One-command deploy: the relay plus a Caddy TLS proxy. +# +# 1. cp config.toml my-config.toml +# edit: info.relay_url, and set [network] address = "0.0.0.0" +# (Caddy reaches the relay over the compose network) +# 2. echo 'FLOONET_DOMAIN=relay.example.com' > .env +# 3. docker compose up -d +# +# The relay container runs as a non-root user with a read-only root +# filesystem; only the data volume is writable. Caddy terminates TLS and +# forwards the real client IP in X-Real-IP (load-bearing for the per-IP +# rate limits). + +services: + relay: + build: . + restart: unless-stopped + read_only: true + volumes: + - relay-data:/usr/src/app/db + - ./my-config.toml:/usr/src/app/config.toml:ro + environment: + RUST_LOG: warn,floonet_rs=info + # Paid mode without baking secrets into the config file: + # FLOONET_PAY_MODE: "name" + # FLOONET_GOBLINPAY_URL: "https://pay.example.com" + # FLOONET_GOBLINPAY_TOKEN: "..." + # FLOONET_NAME_PRICE_GRIN: "1.0" + expose: + - "8080" + + caddy: + image: caddy:2-alpine + restart: unless-stopped + environment: + FLOONET_DOMAIN: ${FLOONET_DOMAIN:?set FLOONET_DOMAIN in .env} + ports: + - "80:80" + - "443:443" + volumes: + - ./deploy/Caddyfile.compose:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + +volumes: + relay-data: + caddy-data: + caddy-config: diff --git a/docs/database-maintenance.md b/docs/database-maintenance.md new file mode 100644 index 0000000..f68cc90 --- /dev/null +++ b/docs/database-maintenance.md @@ -0,0 +1,129 @@ +# Database Maintenance + +`nostr-rs-relay` uses the SQLite embedded database to minimize +dependencies and overall footprint of running a relay. If traffic is +light, the relay should just run with very little need for +intervention. For heavily trafficked relays, there are a number of +steps that the operator may need to take to maintain performance and +limit disk usage. + +This maintenance guide is current as of version `0.8.2`. Future +versions may incorporate and automate some of these steps. + +## Backing Up the Database + +To prevent data loss, the database should be backed up regularly. The +recommended method is to use the `sqlite3` command to perform an +"Online Backup". This can be done while the relay is running, queries +can still run and events will be persisted during the backup. + +The following commands will perform a backup of the database to a +dated file, and then compress to minimize size: + +```console +BACKUP_FILE=/var/backups/nostr/`date +%Y%m%d_%H%M`.db +sqlite3 -readonly /apps/nostr-relay/nostr.db ".backup $BACKUP_FILE" +sqlite3 $BACKUP_FILE "vacuum;" +bzip2 -9 $BACKUP_FILE +``` + +Nostr events are very compressible. Expect a compression ratio on the +order of 4:1, resulting in a 75% space saving. + +## Vacuuming the Database + +As the database is updated, it can become fragmented. Performing a +full `vacuum` will rebuild the entire database file, and can reduce +space. Running this may reduce the size of the database file, +especially if a large amount of data was updated or deleted. + +```console +vacuum; +``` + +## Clearing Hidden Events + +When events are deleted, the event is not actually removed from the +database. Instead, a flag `HIDDEN` is set to true for the event, +which excludes it from search results. High volume replacements from +profile or other replaceable events are deleted, not hidden, in the +current version of the relay. + +In the current version, removing hidden events should not result in +significant space savings, but it can still be used if there is no +desire to hold on to events that can never be re-broadcast. + +```console +PRAGMA foreign_keys = ON; +delete from event where HIDDEN=true; +``` + +## Manually Removing Events + +For a variety of reasons, an operator may wish to remove some events +from the database. The only way of achieving this today is with +manually run SQL commands. + +It is recommended to have a good backup prior to manually running SQL +commands! + +In all cases, it is mandatory to enable foreign keys, and this must be +done for every connection. Otherwise, you will likely orphan rows in +the `tag` table. + +### Deleting Specific Event + +```console +PRAGMA foreign_keys = ON; +delete from event where event_hash=x'00000000000c1271675dc86e3e1dd1336827bccabb90dc4c9d3b4465efefe00e'; +``` + +### Querying and Deleting All Events for Pubkey + +```console +PRAGMA foreign_keys = ON; + +select lower(hex(author)) as author, count(*) as c from event group by author order by c asc; + +delete from event where author=x'000000000002c7831d9c5a99f183afc2813a6f69a16edda7f6fc0ed8110566e6'; +``` + +### Querying and Deleting All Events of a Kind + + +```console +PRAGMA foreign_keys = ON; + +select printf('%7d', kind), count(*) as c from event group by kind order by c; + +delete from event where kind=70202; +``` + +### Deleting Old Events + +In this scenario, we wish to delete any event that has been stored by +our relay for more than 1 month. Crucially, this is based on when the +event was stored, not when the event says it was created. If an event +has a `created` field of 2 years ago, but was first sent to our relay +yesterday, it would not be deleted in this scenario. Keep in mind, we +do not track anything for re-broadcast events that we already have, so +this is not a very effective way of implementing a "least recently +seen" policy. + +```console +PRAGMA foreign_keys = ON; + +DELETE FROM event WHERE first_seen < CAST(strftime('%s', date('now', '-30 day')) AS INT); +``` + +### Delete Profile Events with No Recent Events + +Many users create profiles, post a "hello world" event, and then never +appear again (likely using an ephemeral keypair that was lost in the +browser cache). We can find these accounts and remove them after some +time. + +```console +PRAGMA foreign_keys = ON; +TODO! +``` diff --git a/docs/grpc-extensions.md b/docs/grpc-extensions.md new file mode 100644 index 0000000..1f44feb --- /dev/null +++ b/docs/grpc-extensions.md @@ -0,0 +1,79 @@ +# gRPC Extensions Design Document + +The relay will be extensible through gRPC endpoints, definable in the +main configuration file. These will allow external programs to host +logic for deciding things such as, should this event be persisted, +should this connection be allowed, and should this subscription +request be registered. The primary goal is allow for relay operator +specific functionality that allows them to serve smaller communities +and reduce spam and abuse. + +This will likely evolve substantially, the first goal is to get a +basic one-way service that lets an externalized program decide on +event persistence. This does not represent the final state of gRPC +extensibility in `nostr-rs-relay`. + +## Considerations + +Write event latency must not be significantly affected. However, the +primary reason we are implementing this is spam/abuse protection, so +we are willing to tolerate some increase in latency if that protects +us against outages! + +The interface should provide enough information to make simple +decisions, without burdening the relay to do extra queries. The +decision endpoint will be mostly responsible for maintaining state and +gathering additional details. + +## Design Overview + +A gRPC server may be defined in the `config.toml` file. If it exists, +the relay will attempt to connect to it and send a message for each +`EVENT` command submitted by clients. If a successful response is +returned indicating the event is permitted, the relay continues +processing the event as normal. All existing whitelist, blacklist, +and `NIP-05` validation checks are still performed and MAY still +result in the event being rejected. If a successful response is +returned indicated the decision is anything other than permit, then +the relay MUST reject the event, and return a command result to the +user (using `NIP-20`) indicating the event was blocked (optionally +providing a message). + +In the event there is an error in the gRPC interface, event processing +proceeds as if gRPC was disabled (fail open). This allows gRPC +servers to be deployed with minimal chance of causing a full relay +outage. + +## Design Details + +Currently one procedure call is supported, `EventAdmit`, in the +`Authorization` service. It accepts the following data in order to +support authorization decisions: + +- The event itself +- The client IP that submitted the event +- The client's HTTP origin header, if one exists +- The client's HTTP user agent header, if one exists +- The public key of the client, if `NIP-42` authentication was + performed (not supported in the relay yet!) +- The `NIP-05` associated with the event's public key, if it is known + to the relay + +A server providing authorization decisions will return the following: + +- A decision to permit or deny the event +- An optional message that explains why the event was denied, to be + transmitted to the client + +## Security Issues + +There is little attempt to secure this interface, since it is intended +for use processes running on the same host. It is recommended to +ensure that the gRPC server providing the API is not exposed to the +public Internet. Authorization server implementations should have +their own security reviews performed. + +A slow gRPC server could cause availability issues for event +processing, since this is performed on a single thread. Avoid any +expensive or long-running processes that could result from submitted +events, since any client can initiate a gRPC call to the service. diff --git a/docs/pay-to-relay.md b/docs/pay-to-relay.md new file mode 100644 index 0000000..12723dc --- /dev/null +++ b/docs/pay-to-relay.md @@ -0,0 +1,84 @@ +# Pay to Relay Design Document + +The relay with use payment as a form of spam prevention. In order to post to the relay a user must pay a set rate. There is also the option to require a payment for each note posted to the relay. There is no cost to read from the relay. + +## Configuration + +Currently, [LNBits](https://github.com/lnbits/lnbits) is implemented as the payment processor. LNBits exposes a simple API for creating invoices, to use this API create a wallet and on the right side find "API info" you will need to add the invoice/read key to this relays config file. + +The below configuration will need to be added to config.toml +``` +[pay_to_relay] +# Enable pay to relay +enabled = true +# The cost to be admitted to relay +admission_cost = 1000 +# The cost in sats per post +cost_per_event = 0 +# Url of lnbits api +node_url = "https://:5001/api/v1/payments" +# LNBits api secret +api_secret = "" +# Terms of service +terms_message = """This service .... +""" +# Whether or not new sign ups should be allowed +sign_ups = true +secret_key = "" +``` + +The LNBits instance must have a signed HTTPS a self signed certificate will not work. + +## Design Overview + +### Concepts + +All authors are initially not admitted to write to the relay. There are two ways to gain access write to the relay. The first is by attempting to post the the relay, upon receiving an event from an author that is not admitted, the relay will send a direct message including the terms of service of the relay and a lighting invoice for the admission cost. Once this invoice is paid the author can write to the relay. For this method to work the author must be reading from the relay. An author can also pay and accept the terms of service via a webpage `https:///join`. + +## Design Details + +Authors are stored in a dedicated table. This tracks: + +* `pubkey` +* `is_admitted` whether on no the admission invoice has been paid, accepting the terms of service. +* `balance` the current balance in sats of the author, used if there is a cost per post +* `tos_accepted_at` the timestamp of when the author accepted the tos + +Invoice information is stored in a dedicated table. This tracks: +* `payment_hash` the payment hash of the lighting invoice +* `pubkey` of the author the invoice is issued to +* `invoice` bolt11 invoice +* `amount` in sats +* `status` (Paid/Unpaid/Expired) +* `description` +* `created_at` timestamp of creation +* `confirmed_at` timestamp of payment + +### Event Handling + +If "pay to relay" is enabled, all incoming events are evaluated to determine whether the author is on the relay's whitelist or if they have paid the admission fee and accepted the terms. If "pay per note" is enabled, there is an additional check to ensure that the author has enough balance, which is then reduced by the cost per note. If the author is on the whitelist, this balance check is not necessary. + +### Integration + +We have an existing database writer thread, which receives events and +attempts to persist them to disk. Once validated and persisted, these +events are broadcast to all subscribers. + +When "pay to relay" is enabled, the writer must check if the author is admitted to post. If the author is not admitted to post the event is forwarded to the payment module. Where an invoice is generated, persisted and broadcast as an direct message to the author. + +### Threat Scenarios + +Some of these mitigation's are fully implemented, others are documented +simply to demonstrate a mitigation is possible. + +### Sign up Spamming + +*Threat*: An attacker generates a large number of new pubkeys publishing to the relays. Causing a large number of new invoices to be created for each new pubkey. + +*Mitigation*: Rate limit number of new sign ups + +### Admitted Author Spamming + +*Threat*: An attacker gains write access by paying the admission fee, and then floods the relay with a large number of spam events. + +*Mitigation*: The attacker's admission can be revoked and their admission fee will not be refunded. Enabling "cost per event" and increasing the admission cost can also discourage this type of behavior. diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md new file mode 100644 index 0000000..27f77b0 --- /dev/null +++ b/docs/reverse-proxy.md @@ -0,0 +1,199 @@ +# Reverse Proxy Setup Guide + +It is recommended to run `nostr-rs-relay` behind a reverse proxy such +as `haproxy`, `nginx` or `traefik` to provide TLS termination. Simple examples +for `haproxy`, `nginx` and `traefik` configurations are documented here. + +## Minimal HAProxy Configuration + +Assumptions: + +* HAProxy version is `2.4.10` or greater (older versions not tested). +* Hostname for the relay is `relay.example.com`. +* Your relay should be available over wss://relay.example.com +* Your (NIP-11) relay info page should be available on https://relay.example.com +* SSL certificate is located in `/etc/certs/example.com.pem`. +* Relay is running on port 8080. +* Limit connections to 400 concurrent. +* HSTS (HTTP Strict Transport Security) is desired. +* Only TLS 1.2 or greater is allowed. + +``` +global + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + +frontend fe_prod + mode http + bind :443 ssl crt /etc/certs/example.com.pem alpn h2,http/1.1 + bind :80 + http-request set-header X-Forwarded-Proto https if { ssl_fc } + redirect scheme https code 301 if !{ ssl_fc } + acl host_relay hdr(host) -i -m beg relay.example.com + use_backend relay if host_relay + # HSTS (1 year) + http-response set-header Strict-Transport-Security max-age=31536000 + +backend relay + mode http + timeout connect 5s + timeout client 50s + timeout server 50s + timeout tunnel 1h + timeout client-fin 30s + option tcp-check + default-server maxconn 400 check inter 20s fastinter 1s + server relay 127.0.0.1:8080 +``` + +### HAProxy Notes + +You may experience WebSocket connection problems with Firefox if +HTTP/2 is enabled, for older versions of HAProxy (2.3.x). Either +disable HTTP/2 (`h2`), or upgrade HAProxy. + +## Bare-bones Nginx Configuration + +Assumptions: + +* `Nginx` version is `1.18.0` (other versions not tested). +* Hostname for the relay is `relay.example.com`. +* SSL certificate and key are located at `/etc/letsencrypt/live/relay.example.com/`. +* Relay is running on port `8080`. + +``` +http { + server { + listen 443 ssl; + server_name relay.example.com; + ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem; + ssl_protocols TLSv1.3 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_ecdh_curve secp521r1:secp384r1; + ssl_ciphers EECDH+AESGCM:EECDH+AES256; + + # Optional Diffie-Helmann parameters + # Generate with openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 + #ssl_dhparam /etc/ssl/certs/dhparam.pem; + + ssl_session_cache shared:TLS:2m; + ssl_buffer_size 4k; + + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001]; # Cloudflare + + # Set HSTS to 365 days + add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always; + keepalive_timeout 70; + + location / { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_read_timeout 1d; + proxy_send_timeout 1d; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + } +} +``` + +### Nginx Notes + +The above configuration was tested on `nginx` `1.18.0` on `Ubuntu` `20.04` and `22.04` + +For help installing `nginx` on `Ubuntu`, see [this guide](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04). + +For guidance on using `letsencrypt` to obtain a cert on `Ubuntu`, including an `nginx` plugin, see [this post](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04). + + +## Example Traefik Configuration + +Assumptions: + +* `Traefik` version is `2.9` (other versions not tested). +* `Traefik` is used for provisioning of Let's Encrypt certificates. +* `Traefik` is running in `Docker`, using `docker compose` and labels for the static configuration. An equivalent setup using a Traefik config file is possible too (but not covered here). +* Strict Transport Security is enabled. +* Hostname for the relay is `relay.example.com`, email address for ACME certificates provider is `name@example.com`. +* ipv6 is enabled, a viable private ipv6 subnet is specified in the example below. +* Relay is running on port `8080`. + +``` +version: '3' + +networks: + nostr: + enable_ipv6: true + ipam: + config: + - subnet: fd00:db8:a::/64 + gateway: fd00:db8:a::1 + +services: + traefik: + image: traefik:v2.9 + networks: + nostr: + command: + - "--log.level=ERROR" + # letsencrypt configuration + - "--certificatesResolvers.http.acme.email==name@example.com" + - "--certificatesResolvers.http.acme.storage=/certs/acme.json" + - "--certificatesResolvers.http.acme.httpChallenge.entryPoint=http" + # define entrypoints + - "--entryPoints.http.address=:80" + - "--entryPoints.http.http.redirections.entryPoint.to=https" + - "--entryPoints.http.http.redirections.entryPoint.scheme=https" + - "--entryPoints.https.address=:443" + - "--entryPoints.https.forwardedHeaders.insecure=true" + - "--entryPoints.https.proxyProtocol.insecure=true" + # docker provider (get configuration from container labels) + - "--providers.docker.endpoint=unix:///var/run/docker.sock" + - "--providers.docker.exposedByDefault=false" + - "--providers.file.directory=/config" + - "--providers.file.watch=true" + ports: + - "80:80" + - "443:443" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + - "$(pwd)/traefik/certs:/certs" + - "$(pwd)/traefik/config:/config" + logging: + driver: "local" + restart: always + + # example nostr config. only labels: section is relevant for Traefik config + nostr: + image: nostr-rs-relay:latest + container_name: nostr-relay + networks: + nostr: + restart: always + user: 100:100 + volumes: + - '$(pwd)/nostr/data:/usr/src/app/db:Z' + - '$(pwd)/nostr/config/config.toml:/usr/src/app/config.toml:ro,Z' + labels: + - "traefik.enable=true" + - "traefik.http.routers.nostr.entrypoints=https" + - "traefik.http.routers.nostr.rule=Host(`relay.example.com`)" + - "traefik.http.routers.nostr.tls.certresolver=http" + - "traefik.http.routers.nostr.service=nostr" + - "traefik.http.services.nostr.loadbalancer.server.port=8080" + - "traefik.http.services.nostr.loadbalancer.passHostHeader=true" + - "traefik.http.middlewares.nostr.headers.sslredirect=true" + - "traefik.http.middlewares.nostr.headers.stsincludesubdomains=true" + - "traefik.http.middlewares.nostr.headers.stspreload=true" + - "traefik.http.middlewares.nostr.headers.stsseconds=63072000" + - "traefik.http.routers.nostr.middlewares=nostr" +``` + +### Traefik Notes + +Traefik will take care of the provisioning and renewal of certificates. In case of an ipv4-only relay, simply detele the `enable_ipv6:` and `ipam:` entries in the `networks:` section of the docker-compose file. diff --git a/docs/run-as-linux-system-process.md b/docs/run-as-linux-system-process.md new file mode 100644 index 0000000..66c0fb5 --- /dev/null +++ b/docs/run-as-linux-system-process.md @@ -0,0 +1,40 @@ +# Run as a linux system process + +Docker makes it easy to spin up and down environments but it's also possible to run `nostr-rs-relay` as a systemd linux process. +This guide assumes you're on a Linux machine and that Rust is already installed. + +## Instructions + +### Build nostr-rs-relay from source +Start by building the application from source. Here is how to do that: +1. `git clone https://github.com/scsibug/nostr-rs-relay.git` +2. `cd nostr-rs-relay` +3. `cargo build --release` + +### Place the files where they belong +We want to place the nostr-rs-relay binary and the config.toml file where they belong. While still in the root level of the nostr-rs-relay folder you cloned in last step, run the following commands: +1. `sudo cp target/release/nostr-rs-relay /usr/local/bin/` +2. `sudo mkdir /etc/nostr-rs-relay` +2. `sudo cp config.toml /etc/nostr-rs-relay` + +### Create the Systemd service file +We need to create a new Systemd service file. These files are placed in the `/etc/systemd/system/` folder where you will find many other services running. + +1. `sudo vim /etc/systemd/system/nostr-rs-relay.service` +2. Paste in the contents of [this service file](../contrib/nostr-rs-relay.service). Remember to replace the `User` value with your own username. +3. Save the file and exit your text editor + + +### Run the service +To get the service running, we need to reload the systemd daemon and enable the service. + +1. `sudo systemctl daemon-reload` +2. `sudo systemctl start nostr-rs-relay.service` +3. `sudo systemctl enable nostr-rs-relay.service` +4. `sudo systemctl status nostr-rs-relay.service` + + +### Tips + +#### Logs +The application will write logs to the journal. To read it, execute `sudo journalctl -f -u nostr-rs-relay` diff --git a/docs/upstream/README.md b/docs/upstream/README.md new file mode 100644 index 0000000..5629945 --- /dev/null +++ b/docs/upstream/README.md @@ -0,0 +1,170 @@ +# [nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay) + +This is a [nostr](https://github.com/nostr-protocol/nostr) relay, +written in Rust. It currently supports the entire relay protocol, and +persists data with SQLite. There is experimental support for +Postgresql. + +The project master repository is available on +[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is +mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay). + +[![builds.sr.ht status](https://builds.sr.ht/~gheartsfield/nostr-rs-relay/commits/master.svg)](https://builds.sr.ht/~gheartsfield/nostr-rs-relay/commits/master?) + +![Github CI](https://github.com/scsibug/nostr-rs-relay/actions/workflows/ci.yml/badge.svg) + + +## Features + +[NIPs](https://github.com/nostr-protocol/nips) with a relay-specific implementation are listed here. + +- [x] NIP-01: [Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md) + * Core event model + * Hide old metadata events + * Id/Author prefix search +- [x] NIP-02: [Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md) +- [ ] NIP-03: [OpenTimestamps Attestations for Events](https://github.com/nostr-protocol/nips/blob/master/03.md) +- [x] NIP-05: [Mapping Nostr keys to DNS-based internet identifiers](https://github.com/nostr-protocol/nips/blob/master/05.md) +- [x] NIP-09: [Event Deletion](https://github.com/nostr-protocol/nips/blob/master/09.md) +- [x] NIP-11: [Relay Information Document](https://github.com/nostr-protocol/nips/blob/master/11.md) +- [x] NIP-12: [Generic Tag Queries](https://github.com/nostr-protocol/nips/blob/master/12.md) +- [x] NIP-15: [End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md) +- [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md) +- [x] NIP-20: [Command Results](https://github.com/nostr-protocol/nips/blob/master/20.md) +- [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_) +- [ ] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md) (_implemented, but currently disabled_) +- [x] NIP-28: [Public Chat](https://github.com/nostr-protocol/nips/blob/master/28.md) +- [x] NIP-33: [Parameterized Replaceable Events](https://github.com/nostr-protocol/nips/blob/master/33.md) +- [x] NIP-40: [Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md) +- [x] NIP-42: [Authentication of clients to relays](https://github.com/nostr-protocol/nips/blob/master/42.md) +- [x] NIP-91: [AND operator for filters](https://github.com/nostr-protocol/nips/pull/1365) + +## Quick Start + +The provided `Dockerfile` will compile and build the server +application. Use a bind mount to store the SQLite database outside of +the container image, and map the container's 8080 port to a host port +(7000 in the example below). + +The examples below start a rootless podman container, mapping a local +data directory and config file. + +```console +$ podman build --pull -t nostr-rs-relay . + +$ mkdir data + +$ podman unshare chown 100:100 data + +$ podman run -it --rm -p 7000:8080 \ + --user=100:100 \ + -v $(pwd)/data:/usr/src/app/db:Z \ + -v $(pwd)/config.toml:/usr/src/app/config.toml:ro,Z \ + --name nostr-relay nostr-rs-relay:latest + +Nov 19 15:31:15.013 INFO nostr_rs_relay: Starting up from main +Nov 19 15:31:15.017 INFO nostr_rs_relay::server: listening on: 0.0.0.0:8080 +Nov 19 15:31:15.019 INFO nostr_rs_relay::server: db writer created +Nov 19 15:31:15.019 INFO nostr_rs_relay::server: control message listener started +Nov 19 15:31:15.019 INFO nostr_rs_relay::db: Built a connection pool "event writer" (min=1, max=4) +Nov 19 15:31:15.019 INFO nostr_rs_relay::db: opened database "/usr/src/app/db/nostr.db" for writing +Nov 19 15:31:15.019 INFO nostr_rs_relay::schema: DB version = 0 +Nov 19 15:31:15.054 INFO nostr_rs_relay::schema: database pragma/schema initialized to v7, and ready +Nov 19 15:31:15.054 INFO nostr_rs_relay::schema: All migration scripts completed successfully. Welcome to v7. +Nov 19 15:31:15.521 INFO nostr_rs_relay::db: Built a connection pool "client query" (min=4, max=128) +``` + +Use a `nostr` client such as +[`noscl`](https://github.com/fiatjaf/noscl) to publish and query +events. + +```console +$ noscl publish "hello world" +Sent to 'ws://localhost:8090'. +Seen it on 'ws://localhost:8090'. +$ noscl home +Text Note [81cf...2652] from 296a...9b92 5 seconds ago + hello world +``` + +A pre-built container is also available on DockerHub: +https://hub.docker.com/r/scsibug/nostr-rs-relay + +## Build and Run (without Docker) + +Building `nostr-rs-relay` requires an installation of Cargo & Rust: https://www.rust-lang.org/tools/install + +The following OS packages will be helpful; on Debian/Ubuntu: +```console +$ sudo apt-get install build-essential cmake protobuf-compiler pkg-config libssl-dev +``` + +On OpenBSD: +```console +$ doas pkg_add rust protobuf +``` + +Clone this repository, and then build a release version of the relay: + +```console +$ git clone -q https://git.sr.ht/\~gheartsfield/nostr-rs-relay +$ cd nostr-rs-relay +$ cargo build -q -r +``` + +The relay executable is now located in +`target/release/nostr-rs-relay`. In order to run it with logging +enabled, execute it with the `RUST_LOG` variable set: + +```console +$ RUST_LOG=warn,nostr_rs_relay=info ./target/release/nostr-rs-relay +Dec 26 10:31:56.455 INFO nostr_rs_relay: Starting up from main +Dec 26 10:31:56.464 INFO nostr_rs_relay::server: listening on: 0.0.0.0:8080 +Dec 26 10:31:56.466 INFO nostr_rs_relay::server: db writer created +Dec 26 10:31:56.466 INFO nostr_rs_relay::db: Built a connection pool "event writer" (min=1, max=2) +Dec 26 10:31:56.466 INFO nostr_rs_relay::db: opened database "./nostr.db" for writing +Dec 26 10:31:56.466 INFO nostr_rs_relay::schema: DB version = 11 +Dec 26 10:31:56.467 INFO nostr_rs_relay::db: Built a connection pool "maintenance writer" (min=1, max=2) +Dec 26 10:31:56.467 INFO nostr_rs_relay::server: control message listener started +Dec 26 10:31:56.468 INFO nostr_rs_relay::db: Built a connection pool "client query" (min=4, max=8) +``` + +You now have a running relay, on port `8080`. Use a `nostr` client or +`websocat` to connect and send/query for events. + +## Configuration + +The sample [`config.toml`](config.toml) file demonstrates the +configuration available to the relay. This file is optional, but may +be mounted into a docker container like so: + +```console +$ docker run -it -p 7000:8080 \ + --mount src=$(pwd)/config.toml,target=/usr/src/app/config.toml,type=bind \ + --mount src=$(pwd)/data,target=/usr/src/app/db,type=bind \ + --mount src=$(pwd)/index.html,target=/usr/src/app/index.html,type=bind \ + nostr-rs-relay +``` + +Options include rate-limiting, event size limits, and network address +settings. + +## Reverse Proxy Configuration + +For examples of putting the relay behind a reverse proxy (for TLS +termination, load balancing, and other features), see [Reverse +Proxy](docs/reverse-proxy.md). + +## Dev Channel + +For development discussions, please feel free to use the [sourcehut +mailing list](https://lists.sr.ht/~gheartsfield/nostr-rs-relay-devel). + +License +--- +This project is MIT licensed. + +External Documentation and Links +--- + +* [BlockChainCaffe's Nostr Relay Setup Guide](https://github.com/BlockChainCaffe/Nostr-Relay-Setup-Guide) diff --git a/docs/user-verification-nip05.md b/docs/user-verification-nip05.md new file mode 100644 index 0000000..005e52a --- /dev/null +++ b/docs/user-verification-nip05.md @@ -0,0 +1,248 @@ +# Author Verification Design Document + +The relay will use NIP-05 DNS-based author verification to limit which +authors can publish events to a relay. This document describes how +this feature will operate. + +## Considerations + +DNS-based author verification is designed to be deployed in relays that +want to prevent spam, so there should be strong protections to prevent +unauthorized authors from persisting data. This includes data needed to +verify new authors. + +There should be protections in place to ensure the relay cannot be +used to spam or flood other webservers. Additionally, there should be +protections against server-side request forgery (SSRF). + +## Design Overview + +### Concepts + +All authors are initially "unverified". Unverified authors that submit +appropriate `NIP-05` metadata events become "candidates" for +verification. A candidate author becomes verified when the relay +inspects a kind `0` metadata event for the author with a `nip05` field, +and follows the procedure in `NIP-05` to successfully associate the +author with an internet identifier. + +The `NIP-05` procedure verifies an author for a fixed period of time, +configurable by the relay operator. If this "verification expiration +time" (`verify_expiration`) is exceeded without being refreshed, they +are once again unverified. + +Verified authors have their status regularly and automatically updated +through scheduled polling to their verified domain, this process is +"re-verification". It is performed based on the configuration setting +`verify_update_frequency`, which defines how long the relay waits +between verification attempts (whether the result was success or +failure). + +Authors may change their verification data (the internet identifier from +`NIP-05`) with a new metadata event, which then requires +re-verification. Their old verification remains valid until +expiration. + +Performing candidate author verification is a best-effort activity and +may be significantly rate-limited to prevent relays being used to +attack other hosts. Candidate verification (untrusted authors) should +never impact re-verification (trusted authors). + +## Operating Modes + +The relay may operate in one of three modes. "Disabled" performs no +validation activities, and will never permit or deny events based on +an author's NIP-05 metadata. "Passive" performs NIP-05 validation, +but does not permit or deny events based on the validity or presence +of NIP-05 metadata. "Enabled" will require current and valid NIP-05 +metadata for any events to be persisted. "Enabled" mode will +additionally consider domain whitelist/blacklist configuration data to +restrict which author's events are persisted. + +## Design Details + +### Data Storage + +Verification is stored in a dedicated table. This tracks: + +* `nip05` identifier +* most recent verification timestamp +* most recent verification failure timestamp +* reference to the metadata event (used for tracking `created_at` and + `pubkey`) + +### Event Handling + +All events are first validated to ensure the signature is valid. + +Incoming events of kind _other_ than metadata (kind `0`) submitted by +clients will be evaluated as follows. + +* If the event's author has a current verification, the event is + persisted as normal. +* If the event's author has either no verification, or the + verification is expired, the event is rejected. + +If the event is a metadata event, we handle it differently. + +We first determine the verification status of the event's pubkey. + +* If the event author is unverified, AND the event contains a `nip05` + key, we consider this a verification candidate. +* If the event author is unverified, AND the event does not contain a + `nip05` key, this is not a candidate, and the event is dropped. + +* If the event author is verified, AND the event contains a `nip05` + key that is identical to the currently stored value, no special + action is needed. +* If the event author is verified, AND the event contains a different + `nip05` than was previously verified, with a more recent timestamp, + we need to re-verify. +* If the event author is verified, AND the event is missing a `nip05` + key, and the event timestamp is more recent than what was verified, + we do nothing. The current verification will be allowed to expire. + +### Candidate Verification + +When a candidate verification is requested, a rate limit will be +utilized. If the rate limit is exceeded, new candidate verification +requests will be dropped. In practice, this is implemented by a +size-limited channel that drops events that exceed a threshold. + +Candidates are never persisted in the database. + +### Re-Verification + +Re-verification is straightforward when there has been no change to +the `nip05` key. A new request to the `nip05` domain is performed, +and if successful, the verification timestamp is updated to the +current time. If the request fails due to a timeout or server error, +the failure timestamp is updated instead. + +When the the `nip05` key has changed and this event is more recent, we +will create a new verification record, and delete all other records +for the same name. + +Regarding creating new records vs. updating: We never update the event +reference or `nip05` identifier in a verification record. Every update +either reset the last failure or last success timestamp. + +### Determining Verification Status + +In determining if an event is from a verified author, the following +procedure should be used: + +Join the verification table with the event table, to provide +verification data alongside the event `created_at` and `pubkey` +metadata. Find the most recent verification record for the author, +based on the `created_at` time. + +Reject the record if the success timestamp is not within our +configured expiration time. + +Reject records with disallowed domains, based on any whitelists or +blacklists in effect. + +If a result remains, the author is treated as verified. + +This does give a time window for authors transitioning their verified +status between domains. There may be a period of time in which there +are multiple valid rows in the verification table for a given author. + +### Cleaning Up Inactive Verifications + +After a author verification has expired, we will continue to check for +it to become valid again. After a configurable number of attempts, we +should simply forget it, and reclaim the space. + +### Addition of Domain Whitelist/Blacklist + +A set of whitelisted or blacklisted domains may be provided. If both +are provided, only the whitelist is used. In this context, domains +are either "allowed" (present on a whitelist and NOT present on a +blacklist), or "denied" (NOT present on a whitelist and present on a +blacklist). + +The processes outlined so far are modified in the presence of these +options: + +* Only authors with allowed domains can become candidates for + verification. +* Verification status queries additionally filter out any denied + domains. +* Re-verification processes only proceed with allowed domains. + +### Integration + +We have an existing database writer thread, which receives events and +attempts to persist them to disk. Once validated and persisted, these +events are broadcast to all subscribers. + +When verification is enabled, the writer must check to ensure a valid, +unexpired verification record exists for the author. All metadata +events (regardless of verification status) are forwarded to a verifier +module. If the verifier determines a new verification record is +needed, it is also responsible for persisting and broadcasting the +event, just as the database writer would have done. + +## Threat Scenarios + +Some of these mitigations are fully implemented, others are documented +simply to demonstrate a mitigation is possible. + +### Domain Spamming + +*Threat*: A author with a high-volume of events creates a metadata event +with a bogus domain, causing the relay to generate significant +unwanted traffic to a target. + +*Mitigation*: Rate limiting for all candidate verification will limit +external requests to a reasonable amount. Currently, this is a simple +delay that slows down the HTTP task. + +### Denial of Service for Legitimate Authors + +*Threat*: A author with a high-volume of events creates a metadata event +with a domain that is invalid for them, _but which is used by other +legitimate authors_. This triggers rate-limiting against the legitimate +domain, and blocks authors from updating their own metadata. + +*Mitigation*: Rate limiting should only apply to candidates, so any +existing verified authors have priority for re-verification. New +authors will be affected, as we can not distinguish between the threat +and a legitimate author. _(Unimplemented)_ + +### Denial of Service by Consuming Storage + +*Threat*: A author creates a high volume of random metadata events with +unique domains, in order to cause us to store large amounts of data +for to-be-verified authors. + +*Mitigation*: No data is stored for candidate authors. This makes it +harder for new authors to become verified, but is effective at +preventing this attack. + +### Metadata Replay for Verified Author + +*Threat*: Attacker replays out-of-date metadata event for a author, to +cause a verification to fail. + +*Mitigation*: New metadata events have their signed timestamp compared +against the signed timestamp of the event that has most recently +verified them. If the metadata event is older, it is discarded. + +### Server-Side Request Forgery via Metadata + +*Threat*: Attacker includes malicious data in the `nip05` event, which +is used to generate HTTP requests against potentially internal +resources. Either leaking data, or invoking webservices beyond their +own privileges. + +*Mitigation*: Consider detecting and dropping when the `nip05` field +is an IP address. Allow the relay operator to utilize the `blacklist` +or `whitelist` to constrain hosts that will be contacted. Most +importantly, the verification process is hardcoded to only make +requests to a known url path +(`.well-known/nostr.json?name=`). The `` +component is restricted to a basic ASCII subset (preventing additional +URL components). diff --git a/examples/nauthz/Cargo.lock b/examples/nauthz/Cargo.lock new file mode 100644 index 0000000..cc93a59 --- /dev/null +++ b/examples/nauthz/Cargo.lock @@ -0,0 +1,1010 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-util" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "nauthz-server" +version = "0.1.0" +dependencies = [ + "prost", + "tokio", + "tonic", + "tonic-build", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "petgraph" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dc42e00223fc37204bd4aa177e69420c604ca4a183209a8f9de30c6d934698" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f8ad728fb08fe212df3c05169e940fbb6d9d16a877ddde14644a983ba2012e" +dependencies = [ + "bytes", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda8c0881ea9f722eb9629376db3d0b903b462477c1aafcb0566610ac28ac5d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e0526209433e96d83d750dd81a99118edbc55739e7e61a46764fd2ad537788" +dependencies = [ + "bytes", + "prost", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic-build" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/examples/nauthz/Cargo.toml b/examples/nauthz/Cargo.toml new file mode 100644 index 0000000..70b6c32 --- /dev/null +++ b/examples/nauthz/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "nauthz-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Common dependencies +tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } +prost = "0.11" +tonic = "0.8.3" + +[build-dependencies] +tonic-build = { version="0.8.3", features = ["prost"] } diff --git a/examples/nauthz/build.rs b/examples/nauthz/build.rs new file mode 100644 index 0000000..ada482b --- /dev/null +++ b/examples/nauthz/build.rs @@ -0,0 +1,7 @@ +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(true) + .protoc_arg("--experimental_allow_proto3_optional") + .compile(&["../../proto/nauthz.proto"], &["../../proto"])?; + Ok(()) +} diff --git a/examples/nauthz/src/main.rs b/examples/nauthz/src/main.rs new file mode 100644 index 0000000..4778181 --- /dev/null +++ b/examples/nauthz/src/main.rs @@ -0,0 +1,60 @@ +use tonic::{transport::Server, Request, Response, Status}; + +use nauthz_grpc::authorization_server::{Authorization, AuthorizationServer}; +use nauthz_grpc::{Decision, EventReply, EventRequest}; + +pub mod nauthz_grpc { + tonic::include_proto!("nauthz"); +} + +#[derive(Default)] +pub struct EventAuthz { + allowed_kinds: Vec, +} + +#[tonic::async_trait] +impl Authorization for EventAuthz { + async fn event_admit( + &self, + request: Request, + ) -> Result, Status> { + let reply; + let req = request.into_inner(); + let event = req.event.unwrap(); + let content_prefix: String = event.content.chars().take(40).collect(); + println!("recvd event, [kind={}, origin={:?}, nip05_domain={:?}, tag_count={}, content_sample={:?}]", + event.kind, req.origin, req.nip05.map(|x| x.domain), event.tags.len(), content_prefix); + // Permit any event with a whitelisted kind + if self.allowed_kinds.contains(&event.kind) { + println!("This looks fine! (kind={})", event.kind); + reply = nauthz_grpc::EventReply { + decision: Decision::Permit as i32, + message: None, + }; + } else { + println!("Blocked! (kind={})", event.kind); + reply = nauthz_grpc::EventReply { + decision: Decision::Deny as i32, + message: Some(format!("kind {} not permitted", event.kind)), + }; + } + Ok(Response::new(reply)) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "[::1]:50051".parse().unwrap(); + + // A simple authorization engine that allows kinds 0-3 + let checker = EventAuthz { + allowed_kinds: vec![0, 1, 2, 3], + }; + println!("EventAuthz Server listening on {}", addr); + // Start serving + Server::builder() + .add_service(AuthorizationServer::new(checker)) + .serve(addr) + .await?; + Ok(()) +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1522201 --- /dev/null +++ b/flake.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "crane": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1719249093, + "narHash": "sha256-0q1haa3sw6GbmJ+WhogMnducZGjEaCa/iR6hF2vq80I=", + "owner": "ipetkov", + "repo": "crane", + "rev": "9791c77eb7e98b8d8ac5b0305d47282f994411ca", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1719254875, + "narHash": "sha256-ECni+IkwXjusHsm9Sexdtq8weAq/yUyt1TWIemXt3Ko=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2893f56de08021cffd9b6b6dfc70fd9ccd51eb60", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765334520, + "narHash": "sha256-jTof2+ir9UPmv4lWksYO6WbaXCC0nsDExrB9KZj7Dz4=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "db61f666aea93b28f644861fbddd37f235cc5983", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..93988d5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,72 @@ +{ + description = "Nostr Relay written in Rust"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + flake-utils.url = "github:numtide/flake-utils"; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.flake-utils.follows = "flake-utils"; + }; + + crane = { + url = "github:ipetkov/crane"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs@{ self, ... }: + inputs.flake-utils.lib.eachDefaultSystem (system: + let + # Import nixpkgs with rust-overlay + overlays = [ (import inputs.rust-overlay) ]; + pkgs = import inputs.nixpkgs { + inherit system overlays; + }; + + # Use Rust 1.81 or later (required by home@0.5.11) + # Using stable.latest should give us at least 1.81 + rustToolchain = pkgs.rust-bin.stable.latest.minimal; + + # Override pkgs to use the newer Rust toolchain + pkgsWithRust = pkgs.extend (final: prev: { + rustc = rustToolchain; + cargo = rustToolchain; + }); + + craneLib = inputs.crane.mkLib pkgsWithRust; + src = pkgs.lib.cleanSourceWith { + src = ./.; + filter = path: type: + (pkgs.lib.hasSuffix "\.proto" path) || + # Default filter from crane (allow .rs files) + (craneLib.filterCargoSources path type) + ; + }; + crate = craneLib.buildPackage { + name = "floonet-rs"; + inherit src; + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.protobuf + ]; + }; + in + { + checks = { + inherit crate; + }; + packages.default = crate; + formatter = pkgs.nixpkgs-fmt; + devShells.default = pkgs.mkShell { + buildInputs = [ + rustToolchain + pkgs.pkg-config + pkgs.protobuf + ]; + }; + }); +} diff --git a/mixexit/Cargo.lock b/mixexit/Cargo.lock new file mode 100644 index 0000000..423877e --- /dev/null +++ b/mixexit/Cargo.lock @@ -0,0 +1,8449 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array 0.14.7", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes 0.8.4", + "cipher 0.4.4", + "ctr", + "ghash", + "subtle 2.6.1", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes 0.8.4", + "cipher 0.4.4", + "ctr", + "polyval", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "aes-keywrap" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b6f24a1f796bc46415a1d0d18dc0a8203ccba088acf5def3291c4f61225522" +dependencies = [ + "aes 0.9.1", + "byteorder", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rayon", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.6", + "rayon", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + +[[package]] +name = "bip32" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db40d3dfbeab4e031d78c844642fa0caa0b0db11ce1607ac9d2986dff1405c69" +dependencies = [ + "bs58", + "hmac", + "k256", + "rand_core 0.6.4", + "ripemd", + "secp256k1", + "sha2 0.10.9", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", + "zeroize", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "bnum" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.9", + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "bytecodec" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "celes" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55028d5b1eebb35237512a3838ce5583211434a233c8bb179551a7197ffb7bd4" +dependencies = [ + "phf", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "coarsetime" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-models" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657f625ff361906f779745d08375ae3cc9fef87a35fba5f22874cf773010daf4" +dependencies = [ + "hax-lib", + "pastey", + "rand 0.9.2", +] + +[[package]] +name = "cosmos-sdk-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" +dependencies = [ + "prost", + "tendermint-proto", +] + +[[package]] +name = "cosmrs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e74fa7a22930fe0579bef560f2d64b78415d4c47b9dd976c0635136809471d" +dependencies = [ + "bip32", + "cosmos-sdk-proto", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature 2.2.0", + "subtle-encoding", + "tendermint", + "tendermint-rpc", + "thiserror 1.0.69", +] + +[[package]] +name = "cosmwasm-core" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9899c6499b006d10e5dc64052e642d365f239ba00339615e2714c50c6aa86389" + +[[package]] +name = "cosmwasm-crypto" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a3f5419d8f6ee9ae698db5a3d5d34ecd4ff82e8b5694bba7a620403c862717" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-serialize", + "cosmwasm-core", + "curve25519-dalek", + "digest 0.10.7", + "ecdsa", + "ed25519-zebra", + "k256", + "num-traits", + "p256", + "rand_core 0.6.4", + "rayon", + "sha2 0.10.9", + "thiserror 1.0.69", +] + +[[package]] +name = "cosmwasm-derive" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a625e259b6ab0cae1a758adf9a68a11ecddd023d1ab3d9c5d1785c144663c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "cosmwasm-schema" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6984ab21b47a096e17ae4c73cea2123a704d4b6686c39421247ad67020d76f95" +dependencies = [ + "cosmwasm-schema-derive", + "schemars", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "cosmwasm-schema-derive" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01c9214319017f6ebd8e299036e1f717fa9bb6724e758f7d6fb2477599d1a29" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "cosmwasm-std" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf82335c14bd94eeb4d3c461b7aa419ecd7ea13c2efe24b97cd972bdb8044e7d" +dependencies = [ + "base64 0.22.1", + "bech32", + "bnum", + "cosmwasm-core", + "cosmwasm-crypto", + "cosmwasm-derive", + "derive_more", + "hex", + "rand_core 0.6.4", + "rmp-serde", + "schemars", + "serde", + "serde-json-wasm", + "sha2 0.10.9", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + +[[package]] +name = "ct-codecs" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fb0c6640b4507ebd99ff67677009e381ba5eee1d14df78de4a3d16eb123c39" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "serde", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + +[[package]] +name = "cw-controllers" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c1804013d21060b994dea28a080f9eab78a3bcb6b617f05e7634b0600bf7b1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw-storage-plus" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07dfee7f12f802431a856984a32bce1cb7da1e6c006b5409e3981035ce562dec" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04852cd38f044c0751259d5f78255d07590d136b8a86d4e09efdd7666bd6d27" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "semver", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw20" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42212b6bf29bbdda693743697c621894723f35d3db0d5df930be22903d0e27c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e53c2057526c65d9c88be8b2a564729ebad7a3d87ee97b97665a71446f913a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw20", + "schemars", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw4" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d33f5c8a6b6cd1bd24e212d7f44967697bfa3c4f9cc3f9a8e1c58f5fe5db032d" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle 2.6.1", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect 0.2.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "serde", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-compact" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c24599140dc39d7a81e4476e7573d41bbc18e07c803900298e522a5fbcfbfb6" +dependencies = [ + "ct-codecs", + "getrandom 0.4.3", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "hashbrown 0.14.5", + "hex", + "rand_core 0.6.4", + "sha2 0.10.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.7", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "eyre", + "paste", +] + +[[package]] +name = "floonet-mixexit" +version = "0.1.0" +dependencies = [ + "nym-sdk", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "serde", + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug 0.3.1", + "polyval", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.4.2", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.2", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "handlebars" +version = "3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error", + "serde", + "serde_json", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "hax-lib" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" +dependencies = [ + "hax-lib-macros", + "num-bigint", + "num-traits", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "h2 0.4.15", + "hickory-proto", + "http 1.4.2", + "idna", + "ipnet", + "jni", + "rand 0.10.2", + "rustls 0.23.41", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls 0.26.4", + "tracing", + "url", + "webpki-roots 1.0.8", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.2", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.2", + "resolv-conf", + "rustls 0.23.41", + "smallvec", + "system-configuration 0.7.0", + "thiserror 2.0.18", + "tokio", + "tokio-rustls 0.26.4", + "tracing", + "webpki-roots 1.0.8", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha1-compact" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823" + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019ece39bbefc17f13f677a690328cb978dbf6790e141a3c24e66372cb38588b" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpcodec" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f49d64351430cabd543943b79d48aaf0bc95a41d9ccf5b8774c2cfd23422775" +dependencies = [ + "bytecodec", + "trackable 0.2.24", +] + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "ctutils", + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.2", + "hyper 1.10.1", + "hyper-util", + "rustls 0.23.41", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "hyper 1.10.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.4", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array 0.14.7", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.4", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jwt-simple" +version = "0.12.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a4207b9d1f423858853fb9ee8b34aebc2b661f3b446ca4efda46c4b3700a2f" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.6", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.9", + "signature 2.2.0", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libcrux-aesgcm" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f2a019dab4097585a7d4f5b9deebe46cd1e628b16a5bc4cb0ce35e1da334e6" +dependencies = [ + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-chacha20poly1305" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc08d044676af21343b32b988411fa98dbb5cf65a03c9df478ced221bbdfdb1b" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-curve25519" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1e5fd8476a6ed609d24ef42aee5ab6f99f7c65d054f92412da9f499e423299" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-ecdh" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f73ce79337c762eb38bbac91e4c9b9e60cf318e8501b812750c640814d45e" +dependencies = [ + "libcrux-curve25519", + "libcrux-p256", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-ed25519" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835919315b7042fe9e03b6458efe0db94bf2aa7b873934dbee5b5463a8124b43" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", + "rand_core 0.9.5", + "tls_codec", +] + +[[package]] +name = "libcrux-hacl-rs" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2637dc87d158e1f1b550fd9b226443e84153fded4de69028d897b534d16d22e6" +dependencies = [ + "libcrux-macros", +] + +[[package]] +name = "libcrux-hkdf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1a89ca0c89be3a268a921e47105fb7873badf7267f5e3ebf4ea46baedd73ef" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-hmac", + "libcrux-secrets", +] + +[[package]] +name = "libcrux-hmac" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7a242707d65960770bd7e14e4f18a92bdf0b967777dd404887db8d087a643b" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", +] + +[[package]] +name = "libcrux-intrinsics" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b5db005ff8001e026b73a6842ee81bbef8ec5ff0e1915a67ae65fd2a9fafa5" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-kem" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12631592f491d22fd1a176d32b2c6edfb673998fd3987e9d95f8fa79ad2a737b" +dependencies = [ + "libcrux-curve25519", + "libcrux-ecdh", + "libcrux-ml-kem", + "libcrux-p256", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "libcrux-ml-dsa" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a72929ed421cc3bf16a946b3e7d2a58d215b0b5c2a12be26b53629f081bf49b2" +dependencies = [ + "core-models", + "hax-lib", + "libcrux-intrinsics", + "libcrux-macros", + "libcrux-platform", + "libcrux-sha3", + "tls_codec", +] + +[[package]] +name = "libcrux-ml-kem" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14ab3e477de9df6ee1273a114018ff62c4996ca9220070c4e5cb1743f94a67d" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-p256" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4778ba25cb08bb8a96bd100e19ed9aecf78337198fd176036e21042b2dd99bc" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-sha2", + "libcrux-traits", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-poly1305" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02491808ee5b9db8cb65fad64ae0be812db64beef179d945c00c7787dc7dfcf9" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", +] + +[[package]] +name = "libcrux-psq" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ade7aa5e1b4b400c716b313cbf69070988dd005f92e961c2da4c3c42fbea4" +dependencies = [ + "libcrux-aesgcm", + "libcrux-chacha20poly1305", + "libcrux-ecdh", + "libcrux-ed25519", + "libcrux-hkdf", + "libcrux-hmac", + "libcrux-kem", + "libcrux-ml-dsa", + "libcrux-ml-kem", + "libcrux-sha2", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" +dependencies = [ + "hax-lib", +] + +[[package]] +name = "libcrux-sha2" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d253473f259fc74a280c43f29c464f7e374abdf28b4942234dc707f529d4b7" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-traits", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ae0b7d0e1cc4793a609fd0ff2ca3b3a3fabae523770c619a3d4bc86417b0d7" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e4fa89f3f5e34b47f928b22b1b78395a0d4ec23b1f583db635f128159d65f" +dependencies = [ + "libcrux-secrets", + "rand 0.9.2", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "bitflags 2.13.0", + "libc", + "plain", + "redox_syscall 0.9.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2 0.8.1", + "chacha", + "keystream", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ml-dsa" +version = "0.1.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163f15320f3fba11760c373af52d7f69d638482c2c350d877fb06513b1c3137c" +dependencies = [ + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8 0.11.0", + "sha3", + "signature 3.0.0", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "nym-api-requests" +version = "1.21.1" +dependencies = [ + "bs58", + "celes", + "cosmrs", + "cosmwasm-std", + "ecdsa", + "hex", + "humantime-serde", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-signer-check-types", + "nym-ecash-time", + "nym-kkt-ciphersuite", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-node-requests", + "nym-noise-keys", + "nym-serde-helpers", + "nym-ticketbooks-merkle", + "schemars", + "serde", + "serde_json", + "sha2 0.10.9", + "strum", + "strum_macros", + "tendermint", + "tendermint-rpc", + "thiserror 2.0.18", + "time", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-bandwidth-controller" +version = "1.21.1" +dependencies = [ + "async-trait", + "log", + "nym-credential-storage", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-task", + "nym-validator-client", + "rand 0.8.6", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-bin-common" +version = "1.21.1" +dependencies = [ + "const-str", + "log", + "schemars", + "serde", + "tracing", + "tracing-subscriber", + "utoipa", + "vergen", +] + +[[package]] +name = "nym-bls12_381-fork" +version = "0.8.0-forked" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce84633751030f960a2fd167b5270ec21da4c40d9b6400e1b56676a682fe6f3d" +dependencies = [ + "digest 0.10.7", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "serdect 0.3.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "nym-client-core" +version = "1.21.1" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bs58", + "cfg-if", + "futures", + "getrandom 0.3.3", + "gloo-timers", + "http-body-util", + "humantime", + "hyper 1.10.1", + "hyper-util", + "nym-bandwidth-controller", + "nym-client-core-config-types", + "nym-client-core-gateways-storage", + "nym-client-core-surb-storage", + "nym-credential-storage", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-gateway-client", + "nym-gateway-requests", + "nym-http-api-client", + "nym-id", + "nym-mixnet-client", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-nonexhaustive-delayqueue", + "nym-pemstore", + "nym-sphinx", + "nym-statistics-common", + "nym-task", + "nym-topology", + "nym-validator-client", + "nym-wasm-utils", + "rand 0.8.6", + "rand_chacha 0.3.1", + "serde", + "serde_json", + "sha2 0.10.9", + "si-scale", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio_with_wasm", + "tracing", + "tungstenite", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-client-core-config-types" +version = "1.21.1" +dependencies = [ + "humantime-serde", + "nym-config", + "nym-pemstore", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-statistics-common", + "serde", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "nym-client-core-gateways-storage" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "nym-crypto", + "nym-gateway-client", + "nym-gateway-requests", + "serde", + "sqlx", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "nym-client-core-surb-storage" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "dashmap", + "nym-crypto", + "nym-sphinx", + "nym-sqlx-pool-guard", + "nym-task", + "sqlx", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "nym-coconut-dkg-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw2", + "cw4", + "nym-contracts-common", + "nym-multisig-contract-common", +] + +[[package]] +name = "nym-common" +version = "1.21.1" +dependencies = [ + "tracing", + "tracing-test", +] + +[[package]] +name = "nym-compact-ecash" +version = "1.21.1" +dependencies = [ + "bincode", + "bs58", + "cfg-if", + "digest 0.10.7", + "ff", + "group", + "itertools 0.14.0", + "nym-bls12_381-fork", + "nym-network-defaults", + "nym-pemstore", + "rand 0.8.6", + "serde", + "sha2 0.10.9", + "subtle 2.6.1", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "nym-config" +version = "1.21.1" +dependencies = [ + "dirs", + "handlebars", + "log", + "nym-network-defaults", + "serde", + "thiserror 2.0.18", + "toml", + "url", +] + +[[package]] +name = "nym-contracts-common" +version = "1.21.1" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", + "thiserror 2.0.18", + "vergen", +] + +[[package]] +name = "nym-credential-storage" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "log", + "nym-compact-ecash", + "nym-credentials", + "nym-ecash-time", + "nym-sqlx-pool-guard", + "serde", + "sqlx", + "thiserror 2.0.18", + "time", + "tokio", + "zeroize", +] + +[[package]] +name = "nym-credential-utils" +version = "1.21.1" +dependencies = [ + "log", + "nym-bandwidth-controller", + "nym-client-core", + "nym-config", + "nym-credential-storage", + "nym-credentials", + "nym-credentials-interface", + "nym-ecash-time", + "nym-validator-client", + "thiserror 2.0.18", + "time", + "tokio", +] + +[[package]] +name = "nym-credentials" +version = "1.21.1" +dependencies = [ + "bincode", + "cosmrs", + "log", + "nym-api-requests", + "nym-bls12_381-fork", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", + "nym-http-api-client", + "nym-network-defaults", + "nym-serde-helpers", + "nym-validator-client", + "serde", + "thiserror 2.0.18", + "time", + "tokio", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-credentials-interface" +version = "1.21.1" +dependencies = [ + "nym-bls12_381-fork", + "nym-compact-ecash", + "nym-ecash-time", + "nym-network-defaults", + "nym-upgrade-mode-check", + "rand 0.8.6", + "serde", + "strum", + "strum_macros", + "thiserror 2.0.18", + "time", + "utoipa", +] + +[[package]] +name = "nym-crypto" +version = "1.21.1" +dependencies = [ + "aead", + "aes 0.8.4", + "aes-gcm-siv", + "base64 0.22.1", + "blake3", + "bs58", + "cipher 0.4.4", + "ctr", + "curve25519-dalek", + "digest 0.10.7", + "ed25519-dalek", + "generic-array 0.14.7", + "hkdf", + "hmac", + "jwt-simple", + "libcrux-curve25519", + "libcrux-psq", + "nym-pemstore", + "nym-sphinx-types", + "rand 0.8.6", + "rand 0.9.2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle-encoding", + "thiserror 2.0.18", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "1.21.1" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-ecash-signer-check-types" +version = "1.21.1" +dependencies = [ + "nym-coconut-dkg-common", + "nym-crypto", + "semver", + "serde", + "thiserror 2.0.18", + "time", + "tracing", + "url", + "utoipa", +] + +[[package]] +name = "nym-ecash-time" +version = "1.21.1" +dependencies = [ + "nym-compact-ecash", + "time", +] + +[[package]] +name = "nym-exit-policy" +version = "1.21.1" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-gateway-client" +version = "1.21.1" +dependencies = [ + "futures", + "getrandom 0.2.17", + "gloo-utils", + "nym-bandwidth-controller", + "nym-credential-storage", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-gateway-requests", + "nym-http-api-client", + "nym-network-defaults", + "nym-pemstore", + "nym-sphinx", + "nym-statistics-common", + "nym-task", + "nym-validator-client", + "nym-wasm-utils", + "rand 0.8.6", + "serde", + "si-scale", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "tungstenite", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-gateway-requests" +version = "1.21.1" +dependencies = [ + "bs58", + "futures", + "generic-array 0.14.7", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-pemstore", + "nym-serde-helpers", + "nym-sphinx", + "nym-statistics-common", + "nym-task", + "rand 0.8.6", + "serde", + "serde_json", + "strum", + "subtle 2.6.1", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", + "tungstenite", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-group-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cw-controllers", + "cw4", + "schemars", + "serde", +] + +[[package]] +name = "nym-http-api-client" +version = "1.21.1" +dependencies = [ + "async-trait", + "bincode", + "bytes", + "cfg-if", + "encoding_rs", + "fastrand", + "hickory-resolver", + "http 1.4.2", + "inventory", + "itertools 0.14.0", + "mime", + "nym-bin-common", + "nym-http-api-client-macro", + "nym-http-api-common", + "nym-network-defaults", + "once_cell", + "reqwest 0.13.4", + "rustls 0.23.41", + "serde", + "serde_json", + "serde_plain", + "serde_yaml", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtimer", + "webpki-roots 0.26.11", +] + +[[package]] +name = "nym-http-api-client-macro" +version = "1.21.1" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", + "uuid", +] + +[[package]] +name = "nym-http-api-common" +version = "1.21.1" +dependencies = [ + "bincode", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "nym-id" +version = "1.21.1" +dependencies = [ + "nym-credential-storage", + "nym-credentials", + "thiserror 2.0.18", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "nym-ip-packet-requests" +version = "1.21.1" +dependencies = [ + "bincode", + "bytes", + "nym-bin-common", + "nym-crypto", + "nym-service-provider-requests-common", + "nym-sphinx", + "rand 0.8.6", + "semver", + "serde", + "thiserror 2.0.18", + "time", + "tokio-util", + "tracing", +] + +[[package]] +name = "nym-kkt-ciphersuite" +version = "1.21.1" +dependencies = [ + "num_enum", + "semver", + "strum", + "strum_macros", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-lp-data" +version = "1.21.1" +dependencies = [ + "bytes", + "num_enum", + "nym-common", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "nym-metrics" +version = "1.21.1" +dependencies = [ + "dashmap", + "lazy_static", + "prometheus", + "tracing", +] + +[[package]] +name = "nym-mixnet-client" +version = "1.21.1" +dependencies = [ + "dashmap", + "futures", + "nym-metrics", + "nym-noise", + "nym-sphinx", + "nym-task", + "strum", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "nym-mixnet-contract-common" +version = "1.21.1" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "humantime-serde", + "nym-contracts-common", + "schemars", + "semver", + "serde", + "serde_repr", + "thiserror 2.0.18", + "time", + "utoipa", +] + +[[package]] +name = "nym-multisig-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw3", + "cw4", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-network-defaults" +version = "1.21.1" +dependencies = [ + "cargo_metadata 0.19.2", + "dotenvy", + "regex", + "schemars", + "serde", + "serde_json", + "tracing", + "url", + "utoipa", +] + +[[package]] +name = "nym-network-monitors-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-node-families-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-contracts-common", + "nym-mixnet-contract-common", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-node-requests" +version = "1.21.1" +dependencies = [ + "celes", + "humantime-serde", + "nym-bin-common", + "nym-crypto", + "nym-exit-policy", + "nym-kkt-ciphersuite", + "nym-noise-keys", + "nym-upgrade-mode-check", + "nym-wireguard-types", + "schemars", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror 2.0.18", + "time", + "url", + "utoipa", +] + +[[package]] +name = "nym-noise" +version = "1.21.1" +dependencies = [ + "arc-swap", + "bytes", + "futures", + "nym-crypto", + "nym-noise-keys", + "pin-project", + "sha2 0.10.9", + "snow", + "strum", + "strum_macros", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "nym-noise-keys" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "schemars", + "serde", + "utoipa", +] + +[[package]] +name = "nym-nonexhaustive-delayqueue" +version = "1.21.1" +dependencies = [ + "tokio", + "tokio-stream", + "tokio-util", + "wasmtimer", +] + +[[package]] +name = "nym-ordered-buffer" +version = "1.21.1" +dependencies = [ + "log", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-outfox" +version = "1.21.1" +dependencies = [ + "blake3", + "chacha20 0.9.1", + "chacha20poly1305", + "sphinx-packet", + "thiserror 2.0.18", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "nym-pemstore" +version = "1.21.1" +dependencies = [ + "pem", + "tracing", + "zeroize", +] + +[[package]] +name = "nym-performance-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sdk" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bip39", + "bytecodec", + "bytes", + "clap", + "dashmap", + "dirs", + "futures", + "http 1.4.2", + "httpcodec", + "log", + "nym-bandwidth-controller", + "nym-bin-common", + "nym-client-core", + "nym-credential-storage", + "nym-credential-utils", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-gateway-requests", + "nym-http-api-client", + "nym-ip-packet-requests", + "nym-lp-data", + "nym-network-defaults", + "nym-ordered-buffer", + "nym-service-providers-common", + "nym-socks5-client-core", + "nym-socks5-requests", + "nym-sphinx", + "nym-sphinx-addressing", + "nym-statistics-common", + "nym-task", + "nym-topology", + "nym-validator-client", + "rand 0.8.6", + "semver", + "serde", + "tap", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "toml", + "tracing", + "tracing-subscriber", + "url", + "uuid", + "zeroize", +] + +[[package]] +name = "nym-serde-helpers" +version = "1.21.1" +dependencies = [ + "base64 0.22.1", + "bs58", + "hex", + "serde", + "time", +] + +[[package]] +name = "nym-service-provider-requests-common" +version = "1.21.1" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-service-providers-common" +version = "1.21.1" +dependencies = [ + "async-trait", + "log", + "nym-bin-common", + "nym-sphinx-anonymous-replies", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-socks5-client-core" +version = "1.21.1" +dependencies = [ + "anyhow", + "dirs", + "futures", + "log", + "nym-bandwidth-controller", + "nym-client-core", + "nym-config", + "nym-contracts-common", + "nym-credential-storage", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-service-providers-common", + "nym-socks5-proxy-helpers", + "nym-socks5-requests", + "nym-sphinx", + "nym-task", + "nym-validator-client", + "pin-project", + "rand 0.8.6", + "reqwest 0.13.4", + "schemars", + "serde", + "tap", + "thiserror 2.0.18", + "tokio", + "url", +] + +[[package]] +name = "nym-socks5-proxy-helpers" +version = "1.21.1" +dependencies = [ + "bytes", + "futures", + "log", + "nym-ordered-buffer", + "nym-socks5-requests", + "nym-task", + "tokio", + "tokio-util", +] + +[[package]] +name = "nym-socks5-requests" +version = "1.21.1" +dependencies = [ + "bincode", + "log", + "nym-exit-policy", + "nym-service-providers-common", + "nym-sphinx-addressing", + "serde", + "serde_json", + "tap", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-metrics", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-anonymous-replies", + "nym-sphinx-chunking", + "nym-sphinx-cover", + "nym-sphinx-forwarding", + "nym-sphinx-framing", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "rand_chacha 0.3.1", + "rand_distr", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "nym-sphinx-acknowledgements" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-pemstore", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "nym-sphinx-addressing" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-sphinx-types", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-anonymous-replies" +version = "1.21.1" +dependencies = [ + "bs58", + "nym-crypto", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "thiserror 2.0.18", + "tracing", + "wasm-bindgen", +] + +[[package]] +name = "nym-sphinx-chunking" +version = "1.21.1" +dependencies = [ + "dashmap", + "log", + "nym-crypto", + "nym-metrics", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-sphinx-types", + "rand 0.8.6", + "serde", + "thiserror 2.0.18", + "utoipa", + "wasmtimer", +] + +[[package]] +name = "nym-sphinx-cover" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-chunking", + "nym-sphinx-forwarding", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-forwarding" +version = "1.21.1" +dependencies = [ + "nym-sphinx-addressing", + "nym-sphinx-anonymous-replies", + "nym-sphinx-params", + "nym-sphinx-types", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-framing" +version = "1.21.1" +dependencies = [ + "bytes", + "cfg-if", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-forwarding", + "nym-sphinx-params", + "nym-sphinx-types", + "thiserror 2.0.18", + "tokio-util", + "tracing", +] + +[[package]] +name = "nym-sphinx-params" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-sphinx-types", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-routing" +version = "1.21.1" +dependencies = [ + "nym-sphinx-addressing", + "nym-sphinx-types", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-types" +version = "1.21.1" +dependencies = [ + "nym-outfox", + "sphinx-packet", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sqlx-pool-guard" +version = "1.21.1" +dependencies = [ + "proc_pidinfo", + "sqlx", + "tokio", + "tracing", + "windows 0.61.3", +] + +[[package]] +name = "nym-statistics-common" +version = "1.21.1" +dependencies = [ + "futures", + "log", + "nym-credentials-interface", + "nym-crypto", + "nym-metrics", + "nym-sphinx", + "nym-task", + "serde", + "serde_json", + "sha2 0.10.9", + "si-scale", + "strum", + "strum_macros", + "sysinfo", + "thiserror 2.0.18", + "time", + "tokio", + "wasmtimer", +] + +[[package]] +name = "nym-task" +version = "1.21.1" +dependencies = [ + "cfg-if", + "futures", + "log", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmtimer", +] + +[[package]] +name = "nym-ticketbooks-merkle" +version = "1.21.1" +dependencies = [ + "nym-credentials-interface", + "nym-serde-helpers", + "rs_merkle", + "schemars", + "serde", + "sha2 0.10.9", + "time", + "utoipa", +] + +[[package]] +name = "nym-topology" +version = "1.21.1" +dependencies = [ + "async-trait", + "nym-api-requests", + "nym-crypto", + "nym-mixnet-contract-common", + "nym-sphinx-addressing", + "nym-sphinx-types", + "rand 0.8.6", + "reqwest 0.13.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "tracing", +] + +[[package]] +name = "nym-upgrade-mode-check" +version = "1.21.1" +dependencies = [ + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.13.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-validator-client" +version = "1.21.1" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bip32", + "bip39", + "colored", + "cosmrs", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "cw3", + "cw4", + "eyre", + "flate2", + "futures", + "itertools 0.14.0", + "nym-api-requests", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-http-api-client", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-network-defaults", + "nym-network-monitors-contract-common", + "nym-node-families-contract-common", + "nym-performance-contract-common", + "nym-serde-helpers", + "nym-vesting-contract-common", + "prost", + "reqwest 0.13.4", + "serde", + "serde_json", + "sha2 0.10.9", + "tendermint-rpc", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", + "url", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-vesting-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-contracts-common", + "nym-mixnet-contract-common", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-wasm-utils" +version = "1.21.1" +dependencies = [ + "futures", + "getrandom 0.2.17", + "gloo-net", + "gloo-utils", + "js-sys", + "tungstenite", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "nym-wireguard-types" +version = "1.21.1" +dependencies = [ + "base64 0.22.1", + "nym-crypto", + "serde", + "thiserror 2.0.18", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "peg" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" + +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.1", + "once_cell", + "regex", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[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 0.3.1", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug 0.3.1", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc_pidinfo" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29492a7b48a00ab80202528e235d2f80a04ccff3747540b4ec6881f2f2bc42d1" +dependencies = [ + "libc", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 2.0.18", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.41", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.41", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.4", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "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.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_syscall" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "hyper 1.10.1", + "hyper-rustls 0.27.9", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.41", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle 2.6.1", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rs_merkle" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb09b49230ba22e8c676e7b75dfe2887dea8121f18b530ae0ba519ce442d2b21" +dependencies = [ + "sha2 0.10.9", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature 2.2.0", + "spki 0.7.3", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.41", + "rustls-native-certs 0.8.4", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array 0.14.7", + "pkcs8 0.10.2", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05da0d153dd4595bdffd5099dc0e9ce425b205ee648eb93437ff7302af8c9a5" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug 0.3.1", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "si-scale" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72e7cd0744e007e382ba320435f1ed1ecd709409b4ebd5cfbc843d77b25a8aa" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2 0.10.6", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "rustc_version", + "sha2 0.10.9", + "subtle 2.6.1", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sphinx-packet" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e" +dependencies = [ + "aes 0.8.4", + "arrayref", + "blake2 0.8.1", + "bs58", + "byteorder", + "chacha", + "ctr", + "curve25519-dalek", + "digest 0.10.7", + "hkdf", + "hmac", + "lioness", + "rand 0.8.6", + "rand_distr", + "sha2 0.10.9", + "subtle 2.6.1", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls 0.23.41", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.118", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.118", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "time", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "superboring" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad752f6ecf1cde1cd92cff4400137c5d92d376f78bb901d2807a35dba5872a7" +dependencies = [ + "aes-gcm", + "aes-keywrap", + "getrandom 0.2.17", + "hmac-sha256", + "hmac-sha512", + "ml-dsa", + "rand 0.8.6", + "rsa", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.62.2", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendermint" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.9", + "signature 2.2.0", + "subtle 2.6.1", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-config" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069d1791f9b02a596abcd26eb72003b2e9906c6169a60fa82ffc080dd3a43fda" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint", + "toml", + "url", +] + +[[package]] +name = "tendermint-proto" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" +dependencies = [ + "bytes", + "flex-error", + "prost", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-rpc" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e0569a4b4cc42ff00df5a665be2858a39ff79df4790b176f1cd0e169bc0fc2" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom 0.2.17", + "peg", + "pin-project", + "rand 0.8.6", + "reqwest 0.11.27", + "semver", + "serde", + "serde_bytes", + "serde_json", + "subtle 2.6.1", + "subtle-encoding", + "tendermint", + "tendermint-config", + "tendermint-proto", + "thiserror 1.0.69", + "time", + "tokio", + "tracing", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.41", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", + "tungstenite", + "webpki-roots 0.25.4", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "tokio_with_wasm" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e40fbbbd95441133fe9483f522db15dbfd26dc636164ebd8f2dd28759a6aa6" +dependencies = [ + "js-sys", + "tokio", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01145a2c788d6aae4cd653afec1e8332534d7d783d01897cefcafe4428de992" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags 2.13.0", + "bytes", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.6", + "rustls 0.21.12", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", + "webpki-roots 0.24.0", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle 2.6.1", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +dependencies = [ + "anyhow", + "cargo_metadata 0.18.1", + "cfg-if", + "regex", + "rustc_version", + "rustversion", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasix" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae86f02046da16a333a9129d31451423e1657737ecdafed4193838a5f54c5cfe" +dependencies = [ + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" +dependencies = [ + "rustls-webpki 0.101.7", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/mixexit/Cargo.toml b/mixexit/Cargo.toml new file mode 100644 index 0000000..b60849f --- /dev/null +++ b/mixexit/Cargo.toml @@ -0,0 +1,27 @@ +# floonet-mixexit: the scoped mixnet exit bundled with a Floonet relay. +# +# Built separately from the relay (it pulls the whole nym-sdk tree): +# cargo build --release --manifest-path mixexit/Cargo.toml +# The nym-sdk path dependency expects the Goblin nym checkout (branch +# `goblin`) two directories up; adjust the path for your layout. + +[package] +name = "floonet-mixexit" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +description = "Scoped mixnet exit bundled with a Floonet relay: pipes accepted mixnet streams to ONE fixed upstream (never arbitrary targets)." + +[workspace] + +[dependencies] +## Path dep into the local nym checkout (branch goblin, pinned rev; the +## same checkout the Goblin wallet path-depends on, so both ends speak +## the same MixnetStream protocol). +nym-sdk = { path = "../../nym/sdk/rust/nym-sdk" } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal"] } +## Only to surface nym-sdk's tracing logs (RUST_LOG-style filtering). +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[profile.release] +strip = true diff --git a/mixexit/rustfmt.toml b/mixexit/rustfmt.toml new file mode 100644 index 0000000..7dbfd36 --- /dev/null +++ b/mixexit/rustfmt.toml @@ -0,0 +1,2 @@ +hard_tabs = true +edition = "2024" diff --git a/mixexit/src/main.rs b/mixexit/src/main.rs new file mode 100644 index 0000000..0c72025 --- /dev/null +++ b/mixexit/src/main.rs @@ -0,0 +1,184 @@ +// Copyright 2026 The Goblin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! floonet-mixexit: the SCOPED mixnet exit bundled with a Floonet relay. +//! +//! An ordinary UNBONDED mixnet client (no nym-node, no pledge, no directory +//! listing) that accepts incoming [`MixnetStream`]s and pipes each one to ONE +//! fixed upstream — the operator's own relay. No per-stream target or host +//! header is honored, so this is structurally NOT an open proxy: the only +//! thing it can ever reach is the configured relay, which is why operators +//! carry zero open-proxy liability and need no exit policy. +//! +//! The mixnet identity persists in `FLOONET_MIXEXIT_DIR`, so `nym_address()` +//! is STABLE across restarts — that address is what wallets pin (relay-pool +//! `exit` field / NIP-11 `nym_exit`). Wallets run hostname-validated TLS +//! (SNI = the relay host) end-to-end THROUGH the pipe, so this exit sees only +//! ciphertext. Design: ~/.claude/plans/floonet-nym-exit.md. + +use std::path::PathBuf; + +use nym_sdk::mixnet::{MixnetClientBuilder, MixnetStream, StoragePaths}; +use tokio::io::copy_bidirectional; +use tokio::net::TcpStream; + +const USAGE: &str = "\ +floonet-mixexit: scoped mixnet exit for a Floonet relay + +Accepts incoming mixnet streams and pipes each one to ONE fixed upstream +(the co-located relay). Per-stream targets are never honored, so this is +structurally not an open proxy. The mixnet identity persists in the data +dir, keeping the mixnet address stable across restarts. + +USAGE: + floonet-mixexit [--help | --selftest] + +MODES: + (none) serve: accept mixnet streams, pipe each to the upstream + --selftest connect to the mixnet, print the (stable) mixnet address and + exit — never touches the upstream + --help this text + +ENVIRONMENT: + FLOONET_MIXEXIT_DIR data dir for the persistent mixnet identity; + the mixnet address is also written to + /nym_address.txt [default: ./mixexit-data] + FLOONET_EXIT_UPSTREAM fixed host:port every stream is piped to + [default: relay.goblin.st:443] + RUST_LOG nym-sdk log filter [default: warn] +"; + +/// Data dir for the persistent mixnet identity (`FLOONET_MIXEXIT_DIR`). +fn data_dir() -> PathBuf { + std::env::var_os("FLOONET_MIXEXIT_DIR") + .map(Into::into) + .unwrap_or_else(|| PathBuf::from("./mixexit-data")) +} + +/// The ONE upstream every stream is piped to (`FLOONET_EXIT_UPSTREAM`). +fn upstream() -> String { + std::env::var("FLOONET_EXIT_UPSTREAM").unwrap_or_else(|_| "relay.goblin.st:443".to_string()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mode = std::env::args().nth(1); + match mode.as_deref() { + Some("--help" | "-h") => { + print!("{USAGE}"); + return Ok(()); + } + None | Some("--selftest") => {} + Some(other) => { + eprintln!("unknown argument: {other}\n\n{USAGE}"); + std::process::exit(2); + } + } + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "warn".into()), + ) + .init(); + + // Persistent identity: same data dir → same keystore (generated on first + // run) → the SAME nym address across restarts. That address is what + // wallets pin, so back this directory up — losing it rotates the address + // and strands wallet pins until the next pool/NIP-11 refresh. + let dir = data_dir(); + std::fs::create_dir_all(&dir)?; + let storage_paths = StoragePaths::new_from_dir(&dir)?; + let mut client = MixnetClientBuilder::new_with_default_storage(storage_paths) + .await? + .build()? + .connect_to_mixnet() + .await?; + + let address = *client.nym_address(); + let address_file = dir.join("nym_address.txt"); + std::fs::write(&address_file, format!("{address}\n"))?; + println!("============================================================="); + println!(" floonet-mixexit is on the mixnet. Mixnet address (STABLE, pin"); + println!(" this in the relay pool `exit` field / NIP-11 `nym_exit`):"); + println!(" {address}"); + println!(" also written to {}", address_file.display()); + println!("============================================================="); + + if mode.as_deref() == Some("--selftest") { + println!("selftest OK"); + client.disconnect().await; + return Ok(()); + } + + let upstream = upstream(); + println!("piping every accepted stream to fixed upstream {upstream}"); + + let mut listener = client.listener()?; + loop { + tokio::select! { + _ = shutdown_signal() => { + println!("shutdown signal received; stopping"); + break; + } + accepted = listener.accept() => match accepted { + Some(stream) => { + let upstream = upstream.clone(); + tokio::spawn(pipe(stream, upstream)); + } + None => { + eprintln!("mixnet stream router stopped; exiting"); + break; + } + } + } + } + + client.disconnect().await; + println!("floonet-mixexit stopped"); + Ok(()) +} + +/// One accepted stream: TCP to the FIXED upstream (never a caller-chosen +/// target), then bytes both ways until either side closes. Errors are logged +/// and drop only this stream — the accept loop keeps serving. +async fn pipe(mut mix: MixnetStream, upstream: String) { + let mut tcp = match TcpStream::connect(&upstream).await { + Ok(tcp) => tcp, + Err(e) => { + eprintln!("stream dropped: upstream {upstream} connect failed: {e}"); + return; + } + }; + match copy_bidirectional(&mut mix, &mut tcp).await { + Ok((up, down)) => println!("stream closed ({up} B in → relay, {down} B relay → out)"), + Err(e) => eprintln!("stream ended with error: {e}"), + } +} + +/// Resolves on SIGINT (Ctrl-C) or SIGTERM (systemd/docker stop). +async fn shutdown_signal() { + let ctrl_c = tokio::signal::ctrl_c(); + #[cfg(unix)] + { + let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("SIGTERM handler"); + tokio::select! { + _ = ctrl_c => {} + _ = term.recv() => {} + } + } + #[cfg(not(unix))] + { + let _ = ctrl_c.await; + } +} diff --git a/proto/nauthz.proto b/proto/nauthz.proto new file mode 100644 index 0000000..829b273 --- /dev/null +++ b/proto/nauthz.proto @@ -0,0 +1,60 @@ +syntax = "proto3"; + +// Nostr Authorization Services +package nauthz; + +// Authorization for actions against a relay +service Authorization { + // Determine if an event should be admitted to the relay + rpc EventAdmit(EventRequest) returns (EventReply) {} +} + +message Event { + bytes id = 1; // 32-byte SHA256 hash of serialized event + bytes pubkey = 2; // 32-byte public key of event creator + fixed64 created_at = 3; // UNIX timestamp provided by event creator + uint64 kind = 4; // event kind + string content = 5; // arbitrary event contents + repeated TagEntry tags = 6; // event tag array + bytes sig = 7; // 32-byte signature of the event id + // Individual values for a single tag + message TagEntry { + repeated string values = 1; + } +} + +// Event data and metadata for authorization decisions +message EventRequest { + Event event = + 1; // the event to be admitted for further relay processing + optional string ip_addr = + 2; // IP address of the client that submitted the event + optional string origin = + 3; // HTTP origin header from the client, if one exists + optional string user_agent = + 4; // HTTP user-agent header from the client, if one exists + optional bytes auth_pubkey = + 5; // the public key associated with a NIP-42 AUTH'd session, if + // authentication occurred + optional Nip05Name nip05 = + 6; // NIP-05 address associated with the event pubkey, if it is + // known and has been validated by the relay + // A NIP_05 verification record + message Nip05Name { + string local = 1; + string domain = 2; + } +} + +// A permit or deny decision +enum Decision { + DECISION_UNSPECIFIED = 0; + DECISION_PERMIT = 1; // Admit this event for further processing + DECISION_DENY = 2; // Deny persisting or propagating this event +} + +// Response to a event authorization request +message EventReply { + Decision decision = 1; // decision to enforce + optional string message = 2; // informative message for the client +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..01f4d94 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +edition = "2021" +#max_width = 140 +#chain_width = 100 +#fn_call_width = 100 diff --git a/src/admission.rs b/src/admission.rs new file mode 100644 index 0000000..a9db663 --- /dev/null +++ b/src/admission.rs @@ -0,0 +1,282 @@ +//! Event admission: composable write-side policies (Floonet addition). +//! +//! Every EVENT a client publishes passes through one `Admission::check` +//! call in the websocket write path, before the event is queued for the +//! database writer. The admission layer is a fixed, ordered list of small +//! policies; the first policy that denies wins. To add a new policy +//! (paid gate, name-authority check, spam filter), implement +//! [`AdmissionPolicy`] and append it in [`Admission::from_settings`]. +//! +//! The keystone policy is the default-deny kind whitelist: the relay +//! accepts ONLY the event kinds it was explicitly configured to allow and +//! rejects everything else. If no allowlist is configured at all, the +//! built-in Floonet set applies (fail closed, never fail open). + +use crate::config::Settings; +use crate::event::Event; + +/// The Floonet default kind whitelist, applied when the operator has not +/// configured `event_kind_allowlist` explicitly. Kinds: +/// 0 profile metadata, 3 contacts, 5 delete (NIP-09), 13 seal, +/// 1059 gift wrap (NIP-59), 10002 relay list (NIP-65), +/// 10050 DM relays (NIP-17), 27235 NIP-98 HTTP auth. +pub const DEFAULT_ALLOWED_KINDS: [u64; 8] = [0, 3, 5, 13, 1059, 10002, 10050, 27235]; + +/// Outcome of an admission check. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Decision { + /// Event may proceed to the write path. + Allow, + /// Event is rejected before persistence. + Deny { + /// Human-readable reason, sent to the client in the OK message. + reason: String, + /// True when the denial is because the client has not completed + /// NIP-42 AUTH; the client should be told with an + /// `auth-required:` prefixed OK message. + auth_required: bool, + }, +} + +impl Decision { + fn deny(reason: &str) -> Decision { + Decision::Deny { + reason: reason.to_string(), + auth_required: false, + } + } + + fn deny_auth(reason: &str) -> Decision { + Decision::Deny { + reason: reason.to_string(), + auth_required: true, + } + } +} + +/// One admission policy. `authed_pubkey` is the NIP-42 authenticated pubkey +/// for this connection, if any. +pub trait AdmissionPolicy: Send + Sync { + fn check(&self, event: &Event, authed_pubkey: Option<&str>) -> Decision; +} + +/// Default-deny event kind whitelist (the keystone). +pub struct KindWhitelist { + allowed: Vec, +} + +impl AdmissionPolicy for KindWhitelist { + fn check(&self, event: &Event, _authed_pubkey: Option<&str>) -> Decision { + if self.allowed.contains(&event.kind) { + Decision::Allow + } else { + Decision::deny(&format!( + "event kind {} not accepted by this relay", + event.kind + )) + } + } +} + +/// Require a completed NIP-42 AUTH before any event is accepted. +pub struct RequireAuth; + +impl AdmissionPolicy for RequireAuth { + fn check(&self, _event: &Event, authed_pubkey: Option<&str>) -> Decision { + if authed_pubkey.is_some() { + Decision::Allow + } else { + Decision::deny_auth("authentication required to publish events") + } + } +} + +/// Restrict publishing to a fixed set of author pubkeys. Matches the +/// upstream semantics: when pay-to-relay is enabled the whitelist means +/// "posts for free" and is handled by the payment layer instead, so this +/// policy is only installed when pay-to-relay is off. +pub struct PubkeyWhitelist { + allowed: Vec, +} + +impl AdmissionPolicy for PubkeyWhitelist { + fn check(&self, event: &Event, _authed_pubkey: Option<&str>) -> Decision { + if self.allowed.contains(&event.pubkey) { + Decision::Allow + } else { + Decision::deny("pubkey is not allowed to publish to this relay") + } + } +} + +/// The composed admission pipeline the server consults. +pub struct Admission { + policies: Vec>, +} + +impl Admission { + /// Build the policy pipeline from settings. Order matters: the kind + /// whitelist runs first (cheapest, and the keystone), then auth, then + /// author restrictions. + pub fn from_settings(settings: &Settings) -> Admission { + let mut policies: Vec> = Vec::new(); + // Keystone: default-deny kind whitelist. A missing allowlist gets + // the built-in Floonet set; an explicitly empty list denies all. + let allowed = settings + .limits + .event_kind_allowlist + .clone() + .unwrap_or_else(|| DEFAULT_ALLOWED_KINDS.to_vec()); + policies.push(Box::new(KindWhitelist { allowed })); + // Optional: require NIP-42 auth to write. + if settings.authorization.nip42_auth && settings.authorization.require_auth_to_write { + policies.push(Box::new(RequireAuth)); + } + // Optional: author whitelist (free relays only; paid relays treat + // the whitelist as a fee exemption in the payment layer). + if !settings.pay_to_relay.enabled { + if let Some(whitelist) = &settings.authorization.pubkey_whitelist { + policies.push(Box::new(PubkeyWhitelist { + allowed: whitelist.clone(), + })); + } + } + Admission { policies } + } + + /// Check an event against every policy in order; first denial wins. + pub fn check(&self, event: &Event, authed_pubkey: Option<&str>) -> Decision { + for policy in &self.policies { + match policy.check(event, authed_pubkey) { + Decision::Allow => continue, + deny => return deny, + } + } + Decision::Allow + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn event_of_kind(kind: u64) -> Event { + let mut e = Event::simple_event(); + e.kind = kind; + e + } + + fn floonet_settings() -> Settings { + // The shipped defaults already carry the Floonet whitelist. + Settings::default() + } + + #[test] + fn default_whitelist_accepts_allowed_kinds() { + let admission = Admission::from_settings(&floonet_settings()); + for kind in DEFAULT_ALLOWED_KINDS { + assert_eq!( + admission.check(&event_of_kind(kind), None), + Decision::Allow, + "kind {kind} should be allowed" + ); + } + } + + #[test] + fn default_whitelist_rejects_disallowed_kinds() { + let admission = Admission::from_settings(&floonet_settings()); + // kind 1 (short text note) and other common kinds are NOT accepted. + for kind in [1u64, 4, 6, 7, 42, 1984, 9735, 30023] { + match admission.check(&event_of_kind(kind), None) { + Decision::Deny { auth_required, .. } => { + assert!(!auth_required, "kind rejection is not an auth issue"); + } + Decision::Allow => panic!("kind {kind} must be rejected by default"), + } + } + } + + #[test] + fn missing_allowlist_falls_back_to_floonet_set_not_allow_all() { + let mut settings = floonet_settings(); + settings.limits.event_kind_allowlist = None; + let admission = Admission::from_settings(&settings); + assert_eq!(admission.check(&event_of_kind(1059), None), Decision::Allow); + assert_ne!(admission.check(&event_of_kind(1), None), Decision::Allow); + } + + #[test] + fn empty_allowlist_denies_everything() { + let mut settings = floonet_settings(); + settings.limits.event_kind_allowlist = Some(vec![]); + let admission = Admission::from_settings(&settings); + assert_ne!(admission.check(&event_of_kind(0), None), Decision::Allow); + assert_ne!(admission.check(&event_of_kind(1059), None), Decision::Allow); + } + + #[test] + fn custom_allowlist_is_respected() { + let mut settings = floonet_settings(); + settings.limits.event_kind_allowlist = Some(vec![1, 7]); + let admission = Admission::from_settings(&settings); + assert_eq!(admission.check(&event_of_kind(1), None), Decision::Allow); + assert_ne!(admission.check(&event_of_kind(0), None), Decision::Allow); + } + + #[test] + fn require_auth_denies_unauthed_writes_with_auth_required() { + let mut settings = floonet_settings(); + settings.authorization.nip42_auth = true; + settings.authorization.require_auth_to_write = true; + let admission = Admission::from_settings(&settings); + match admission.check(&event_of_kind(1059), None) { + Decision::Deny { auth_required, .. } => assert!(auth_required), + Decision::Allow => panic!("unauthenticated write must be denied"), + } + // After AUTH, the same event is accepted. + let pk = "aa".repeat(32); + assert_eq!( + admission.check(&event_of_kind(1059), Some(pk.as_str())), + Decision::Allow + ); + } + + #[test] + fn require_auth_without_nip42_is_inert() { + // require_auth_to_write only makes sense with nip42_auth on; the + // relay never sends a challenge otherwise, so the gate is skipped. + let mut settings = floonet_settings(); + settings.authorization.nip42_auth = false; + settings.authorization.require_auth_to_write = true; + let admission = Admission::from_settings(&settings); + assert_eq!(admission.check(&event_of_kind(1059), None), Decision::Allow); + } + + #[test] + fn pubkey_whitelist_enforced_when_free() { + let mut settings = floonet_settings(); + let good = "aa".repeat(32); + settings.authorization.pubkey_whitelist = Some(vec![good.clone()]); + let admission = Admission::from_settings(&settings); + let mut e = event_of_kind(0); + e.pubkey = good; + assert_eq!(admission.check(&e, None), Decision::Allow); + e.pubkey = "bb".repeat(32); + assert_ne!(admission.check(&e, None), Decision::Allow); + } + + #[test] + fn kind_check_runs_before_auth_check() { + let mut settings = floonet_settings(); + settings.authorization.nip42_auth = true; + settings.authorization.require_auth_to_write = true; + let admission = Admission::from_settings(&settings); + match admission.check(&event_of_kind(1), None) { + Decision::Deny { auth_required, .. } => { + assert!(!auth_required, "disallowed kind must not leak auth hints"); + } + Decision::Allow => panic!("must deny"), + } + } +} diff --git a/src/bin/bulkloader.rs b/src/bin/bulkloader.rs new file mode 100644 index 0000000..283d0a2 --- /dev/null +++ b/src/bin/bulkloader.rs @@ -0,0 +1,180 @@ +use floonet_rs::config; +use floonet_rs::error::{Error, Result}; +use floonet_rs::event::{single_char_tagname, Event}; +use floonet_rs::repo::sqlite::{build_pool, PooledConnection}; +use floonet_rs::repo::sqlite_migration::{curr_db_version, DB_VERSION}; +use floonet_rs::utils::is_lower_hex; +use rusqlite::params; +use rusqlite::{OpenFlags, Transaction}; +use std::io; +use std::path::Path; +use std::sync::mpsc; +use std::thread; +use tracing::info; + +/// Bulk load JSONL data from STDIN to the database specified in config.toml (or ./nostr.db as a default). +/// The database must already exist, this will not create a new one. +/// Tested against schema v13. +pub fn main() -> Result<()> { + let _trace_sub = tracing_subscriber::fmt::try_init(); + println!("Nostr-rs-relay Bulk Loader"); + // check for a database file, or create one. + let settings = config::Settings::new(&None)?; + if !Path::new(&settings.database.data_directory).is_dir() { + info!("Database directory does not exist"); + return Err(Error::DatabaseDirError); + } + // Get a database pool + let pool = build_pool( + "bulk-loader", + &settings, + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, + 1, + 4, + false, + ); + { + // check for database schema version + let mut conn: PooledConnection = pool.get()?; + let version = curr_db_version(&mut conn)?; + info!("current version is: {:?}", version); + // ensure the schema version is current. + if version != DB_VERSION { + info!("version is not current, exiting"); + panic!("cannot write to schema other than v{DB_VERSION}"); + } + } + // this channel will contain parsed events ready to be inserted + let (event_tx, event_rx) = mpsc::sync_channel(100_000); + // Thread for reading events + let _stdin_reader_handler = thread::spawn(move || { + let stdin = io::stdin(); + for readline in stdin.lines() { + if let Ok(line) = readline { + // try to parse a nostr event + let eres: Result = serde_json::from_str(&line); + if let Ok(mut e) = eres { + if let Ok(()) = e.validate() { + e.build_index(); + //debug!("Event: {:?}", e); + event_tx.send(Some(e)).ok(); + } else { + info!("could not validate event"); + } + } else { + info!("error reading event: {:?}", eres); + } + } else { + // error reading + info!("error reading: {:?}", readline); + } + } + info!("finished parsing events"); + event_tx.send(None).ok(); + let ok: Result<()> = Ok(()); + ok + }); + let mut conn: PooledConnection = pool.get()?; + let mut events_read = 0; + let event_batch_size = 50_000; + let mut new_events = 0; + let mut has_more_events = true; + while has_more_events { + // begin a transaction + let tx = conn.transaction()?; + // read in batch_size events and commit + for _ in 0..event_batch_size { + match event_rx.recv() { + Ok(Some(e)) => { + events_read += 1; + // ignore ephemeral events + if !(e.kind >= 20000 && e.kind < 30000) { + match write_event(&tx, e) { + Ok(c) => { + new_events += c; + } + Err(e) => { + info!("error inserting event: {:?}", e); + } + } + } + } + Ok(None) => { + // signal that the sender will never produce more + // events + has_more_events = false; + break; + } + Err(_) => { + info!("sender is closed"); + // sender is done + } + } + } + info!("committed {} events...", new_events); + tx.commit()?; + conn.execute_batch("pragma wal_checkpoint(truncate)")?; + } + info!("processed {} events", events_read); + info!("stored {} new events", new_events); + // get a connection for writing events + // read standard in. + info!("finished reading input"); + Ok(()) +} + +/// Write an event and update the tag table. +/// Assumes the event has its index built. +fn write_event(tx: &Transaction, e: Event) -> Result { + let id_blob = hex::decode(&e.id).ok(); + let pubkey_blob: Option> = hex::decode(&e.pubkey).ok(); + let delegator_blob: Option> = e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok()); + let event_str = serde_json::to_string(&e).ok(); + // ignore if the event hash is a duplicate. + let ins_count = tx.execute( + "INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);", + params![id_blob, e.created_at, e.kind, pubkey_blob, delegator_blob, event_str] + )?; + if ins_count == 0 { + return Ok(0); + } + // we want to capture the event_id that had the tag, the tag name, and the tag hex value. + let event_id = tx.last_insert_rowid(); + // look at each event, and each tag, creating new tag entries if appropriate. + for t in e.tags.iter().filter(|x| x.len() > 1) { + let tagname = t.first().unwrap(); + let tagnamechar_opt = single_char_tagname(tagname); + if tagnamechar_opt.is_none() { + continue; + } + // safe because len was > 1 + let tagval = t.get(1).unwrap(); + // insert as BLOB if we can restore it losslessly. + // this means it needs to be even length and lowercase. + if (tagval.len() % 2 == 0) && is_lower_hex(tagval) { + tx.execute( + "INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);", + params![event_id, tagname, hex::decode(tagval).ok()], + )?; + } else { + // otherwise, insert as text + tx.execute( + "INSERT INTO tag (event_id, name, value) VALUES (?1, ?2, ?3);", + params![event_id, tagname, &tagval], + )?; + } + } + if e.is_replaceable() { + //let query = "SELECT id FROM event WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1;"; + //let count: usize = tx.query_row(query, params![e.kind, pubkey_blob], |row| row.get(0))?; + //info!("found {} rows that /would/ be preserved", count); + match tx.execute( + "DELETE FROM event WHERE kind=? and author=? and id NOT IN (SELECT id FROM event WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1);", + params![e.kind, pubkey_blob, e.kind, pubkey_blob], + ) { + Ok(_) => {}, + Err(x) => {info!("error deleting replaceable event: {:?}",x);} + } + } + Ok(ins_count) +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..15a6f14 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,20 @@ +use clap::Parser; + +#[derive(Parser)] +#[command(about = "A nostr relay written in Rust", author = env!("CARGO_PKG_AUTHORS"), version = env!("CARGO_PKG_VERSION"))] +pub struct CLIArgs { + #[arg( + short, + long, + help = "Use the as the location of the database", + required = false + )] + pub db: Option, + #[arg( + short, + long, + help = "Use the as the location of the config file", + required = false + )] + pub config: Option, +} diff --git a/src/close.rs b/src/close.rs new file mode 100644 index 0000000..388c192 --- /dev/null +++ b/src/close.rs @@ -0,0 +1,32 @@ +//! Subscription close request parsing +//! +//! Representation and parsing of `CLOSE` messages sent from clients. +use crate::error::{Error, Result}; +use serde::{Deserialize, Serialize}; + +/// Close command in network format +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct CloseCmd { + /// Protocol command, expected to always be "CLOSE". + cmd: String, + /// The subscription identifier being closed. + id: String, +} + +/// Identifier of the subscription to be closed. +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Close { + /// The subscription identifier being closed. + pub id: String, +} + +impl From for Result { + fn from(cc: CloseCmd) -> Result { + // ensure command is correct + if cc.cmd == "CLOSE" { + Ok(Close { id: cc.id }) + } else { + Err(Error::CommandUnknownError) + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..2f4f785 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,561 @@ +//! Configuration file and settings management +use crate::payment::Processor; +use config::{Config, ConfigError, File}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[allow(unused)] +pub struct Info { + pub relay_url: Option, + pub name: Option, + pub description: Option, + pub pubkey: Option, + pub contact: Option, + pub favicon: Option, + pub relay_icon: Option, + pub relay_page: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Database { + pub data_directory: String, + pub engine: String, + pub in_memory: bool, + pub min_conn: u32, + pub max_conn: u32, + pub connection: String, + pub connection_write: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Grpc { + pub event_admission_server: Option, + pub restricts_write: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Network { + pub port: u16, + pub address: String, + pub remote_ip_header: Option, // retrieve client IP from this HTTP header if present + pub ping_interval_seconds: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Options { + pub reject_future_seconds: Option, // if defined, reject any events with a timestamp more than X seconds in the future +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Retention { + // TODO: implement + pub max_events: Option, // max events + pub max_bytes: Option, // max size + pub persist_days: Option, // oldest message + pub whitelist_addresses: Option>, // whitelisted addresses (never delete) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Limits { + pub messages_per_sec: Option, // Artificially slow down event writing to limit disk consumption (averaged over 1 minute) + pub subscriptions_per_min: Option, // Artificially slow down request (db query) creation to prevent abuse (averaged over 1 minute) + pub db_conns_per_client: Option, // How many concurrent database queries (not subscriptions) may a client have? + pub max_blocking_threads: usize, + pub max_event_bytes: Option, // Maximum size of an EVENT message + pub max_ws_message_bytes: Option, + pub max_ws_frame_bytes: Option, + pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory) + pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow) + pub event_kind_blacklist: Option>, + pub event_kind_allowlist: Option>, + pub limit_scrapers: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Authorization { + pub pubkey_whitelist: Option>, // If present, only allow these pubkeys to publish events + pub nip42_auth: bool, // if true enables NIP-42 authentication + pub nip42_dms: bool, // if true send DMs only to their authenticated recipients + pub require_auth_to_write: bool, // if true (with nip42_auth), only authenticated clients may publish +} + +/// GoblinPay: the Grin payment server used for paid names and paid writes +/// (Floonet addition). One place to configure it; both the built-in name +/// authority and the pay-to-relay admission use these values. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct GoblinPay { + /// Paid mode: "off" (everything free), "name" (claiming a name at the + /// built-in name authority requires a confirmed payment), or "write" + /// (publishing events requires a paid admission). + pub pay_mode: String, + /// Base URL of the GoblinPay server, e.g. `https://pay.example.com`. + pub url: String, + /// GoblinPay API token (`GP_API_TOKEN`); grants invoice create/read. + pub api_token: String, + /// Price of a name in GRIN when `pay_mode = "name"`. + pub name_price_grin: f64, + /// Price of relay admission in GRIN when `pay_mode = "write"`. + pub admission_price_grin: f64, +} + +impl GoblinPay { + #[must_use] + pub fn name_price_nanogrin(&self) -> u64 { + grin_to_nanogrin(self.name_price_grin) + } + + #[must_use] + pub fn admission_price_nanogrin(&self) -> u64 { + grin_to_nanogrin(self.admission_price_grin) + } +} + +/// 1 GRIN = 1_000_000_000 nanogrin. +#[must_use] +pub fn grin_to_nanogrin(grin: f64) -> u64 { + (grin * 1_000_000_000.0).round().max(0.0) as u64 +} + +/// Built-in name authority (the goblin-nip05d capability), served +/// in-process on the relay's own HTTP listener (Floonet addition). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct NameAuthority { + pub enabled: bool, + /// Bare host the names live under, e.g. `example.com` (the `@domain` + /// part of `name@domain`). + pub domain: String, + /// Public base URL clients reach, e.g. `https://example.com`. + /// LOAD-BEARING: NIP-98 auth events are verified against + /// ``, so this must be exactly what clients use. + pub base_url: String, + /// Relays advertised in `/.well-known/nostr.json`. Unset means + /// "advertise this relay" (`info.relay_url`). + pub relays: Option>, + /// Name length bounds in characters. + pub name_min: usize, + pub name_max: usize, + /// Seconds a key must wait to claim a new name after releasing one. + pub name_change_cooldown_secs: u64, + /// Max age (seconds) of an accepted NIP-98 auth event. + pub auth_max_age_secs: i64, + /// Read endpoints: requests per IP per window. + pub read_rate_max: usize, + pub read_rate_window_secs: u64, + /// Write endpoints (register/release): requests per IP per window. + pub write_rate_max: usize, + pub write_rate_window_secs: u64, + /// Optional file of extra reserved names (one per line, # comments). + pub reserved_file: Option, +} + +/// The co-located mixnet exit (Floonet addition): when enabled, the relay +/// supervises a bundled `floonet-mixexit` process so wallets can reach +/// this relay over the mixnet without public DNS on the payment path. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct MixnetExit { + pub enabled: bool, + /// Path to the bundled floonet-mixexit binary. + pub binary: String, + /// Data dir for the persistent mixnet identity. The exit's stable + /// mixnet address is written to `/nym_address.txt`. + pub data_dir: String, + /// Upstream host:port the exit pipes every stream to. Empty means this + /// relay's own listener (`127.0.0.1:`). Point it at your + /// public TLS endpoint (e.g. `relay.example.com:443`) so wallets see + /// the same certificate through the mixnet. + pub upstream: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct PayToRelay { + pub enabled: bool, + pub admission_cost: u64, // Cost to have pubkey whitelisted + pub cost_per_event: u64, // Cost author to pay per event + pub node_url: String, + pub api_secret: String, + pub terms_message: String, + pub sign_ups: bool, // allow new users to sign up to relay + pub direct_message: bool, // Send direct message to user with invoice and terms + pub secret_key: Option, + pub processor: Processor, + pub rune_path: Option, // To access clightning API +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Diagnostics { + pub tracing: bool, // enables tokio console-subscriber +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub enum VerifiedUsersMode { + Enabled, + Passive, + Disabled, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct VerifiedUsers { + pub mode: VerifiedUsersMode, // Mode of operation: "enabled" (enforce) or "passive" (check only). If none, this is simply disabled. + pub domain_whitelist: Option>, // If present, only allow verified users from these domains can publish events + pub domain_blacklist: Option>, // If present, allow all verified users from any domain except these + pub verify_expiration: Option, // how long a verification is cached for before no longer being used + pub verify_update_frequency: Option, // how often to attempt to update verification + pub verify_expiration_duration: Option, // internal result of parsing verify_expiration + pub verify_update_frequency_duration: Option, // internal result of parsing verify_update_frequency + pub max_consecutive_failures: usize, // maximum number of verification failures in a row, before ceasing future checks +} + +impl VerifiedUsers { + pub fn init(&mut self) { + self.verify_expiration_duration = self.verify_expiration_duration(); + self.verify_update_frequency_duration = self.verify_update_duration(); + } + + #[must_use] + pub fn is_enabled(&self) -> bool { + self.mode == VerifiedUsersMode::Enabled + } + + #[must_use] + pub fn is_active(&self) -> bool { + self.mode == VerifiedUsersMode::Enabled || self.mode == VerifiedUsersMode::Passive + } + + #[must_use] + pub fn is_passive(&self) -> bool { + self.mode == VerifiedUsersMode::Passive + } + + #[must_use] + pub fn verify_expiration_duration(&self) -> Option { + self.verify_expiration + .as_ref() + .and_then(|x| parse_duration::parse(x).ok()) + } + + #[must_use] + pub fn verify_update_duration(&self) -> Option { + self.verify_update_frequency + .as_ref() + .and_then(|x| parse_duration::parse(x).ok()) + } + + #[must_use] + pub fn is_valid(&self) -> bool { + self.verify_expiration_duration().is_some() && self.verify_update_duration().is_some() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Logging { + pub folder_path: Option, + pub file_prefix: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(unused)] +pub struct Settings { + pub info: Info, + pub diagnostics: Diagnostics, + pub database: Database, + pub grpc: Grpc, + pub network: Network, + pub limits: Limits, + pub authorization: Authorization, + pub pay_to_relay: PayToRelay, + pub goblinpay: GoblinPay, + pub name_authority: NameAuthority, + pub exit: MixnetExit, + pub verified_users: VerifiedUsers, + pub retention: Retention, + pub options: Options, + pub logging: Logging, +} + +impl Settings { + pub fn new(config_file_name: &Option) -> Result { + let default_settings = Self::default(); + // attempt to construct settings with file + let from_file = Self::new_from_default(&default_settings, config_file_name); + match from_file { + Err(e) => { + // pass up the parse error if the config file was specified, + // otherwise use the default config (with a warning). + if config_file_name.is_some() { + Err(e) + } else { + eprintln!("Error reading config file ({:?})", e); + eprintln!("WARNING: Default configuration settings will be used"); + Ok(default_settings) + } + } + ok => ok, + } + } + + fn new_from_default( + default: &Settings, + config_file_name: &Option, + ) -> Result { + let default_config_file_name = "config.toml".to_string(); + let config: &String = match config_file_name { + Some(value) => value, + None => &default_config_file_name, + }; + let builder = Config::builder(); + let config: Config = builder + // use defaults + .add_source(Config::try_from(default)?) + // override with file contents + .add_source(File::with_name(config)) + .build()?; + let mut settings: Settings = config.try_deserialize()?; + // Floonet env overrides, so paid mode can be flipped without + // editing the config file (secrets can stay out of it entirely). + if let Ok(v) = std::env::var("FLOONET_PAY_MODE") { + settings.goblinpay.pay_mode = v; + } + if let Ok(v) = std::env::var("FLOONET_GOBLINPAY_URL") { + settings.goblinpay.url = v; + } + if let Ok(v) = std::env::var("FLOONET_GOBLINPAY_TOKEN") { + settings.goblinpay.api_token = v; + } + if let Ok(v) = std::env::var("FLOONET_NAME_PRICE_GRIN") { + if let Ok(price) = v.parse::() { + settings.goblinpay.name_price_grin = price; + } + } + // Validate + apply the Floonet paid mode. + match settings.goblinpay.pay_mode.as_str() { + "off" => {} + "name" | "write" => { + assert!( + !settings.goblinpay.url.is_empty(), + "goblinpay.url must be set when goblinpay.pay_mode is enabled" + ); + assert!( + !settings.goblinpay.api_token.is_empty(), + "goblinpay.api_token must be set when goblinpay.pay_mode is enabled" + ); + if settings.goblinpay.pay_mode == "write" { + // "write" rides the upstream pay-to-relay admission, + // with GoblinPay as the payment processor. + settings.pay_to_relay.enabled = true; + settings.pay_to_relay.processor = Processor::GoblinPay; + settings.pay_to_relay.node_url = settings.goblinpay.url.clone(); + settings.pay_to_relay.api_secret = settings.goblinpay.api_token.clone(); + settings.pay_to_relay.admission_cost = + settings.goblinpay.admission_price_nanogrin(); + if settings.pay_to_relay.terms_message.is_empty() { + settings.pay_to_relay.terms_message = + "Use this relay lawfully and without abuse.".to_string(); + } + } + } + other => panic!("goblinpay.pay_mode must be off, name, or write (got `{other}`)"), + } + if settings.name_authority.enabled { + assert!( + !settings.name_authority.domain.is_empty(), + "name_authority.domain must be set" + ); + let base = &settings.name_authority.base_url; + assert!( + base.starts_with("https://") + || base.starts_with("http://127.0.0.1") + || base.starts_with("http://localhost"), + "name_authority.base_url must be https:// (http only for localhost testing)" + ); + assert!( + settings.name_authority.name_min > 0 + && settings.name_authority.name_min <= settings.name_authority.name_max, + "invalid name_authority name length bounds" + ); + assert!( + settings.database.engine == "sqlite", + "the built-in name authority requires the sqlite database engine" + ); + } + // ensure connection pool size is logical + assert!( + settings.database.min_conn <= settings.database.max_conn, + "Database min_conn setting ({}) cannot exceed max_conn ({})", + settings.database.min_conn, + settings.database.max_conn + ); + // ensure durations parse + assert!( + settings.verified_users.is_valid(), + "VerifiedUsers time settings could not be parsed" + ); + // initialize durations for verified users + settings.verified_users.init(); + + // Validate pay to relay settings + if settings.pay_to_relay.enabled { + if settings.pay_to_relay.processor == Processor::ClnRest { + assert!(settings + .pay_to_relay + .rune_path + .as_ref() + .is_some_and(|path| path != "")); + } else if settings.pay_to_relay.processor == Processor::LNBits { + assert_ne!(settings.pay_to_relay.api_secret, ""); + } + // Should check that url is valid + assert_ne!(settings.pay_to_relay.node_url, ""); + assert_ne!(settings.pay_to_relay.terms_message, ""); + + if settings.pay_to_relay.direct_message { + assert!(settings + .pay_to_relay + .secret_key + .as_ref() + .is_some_and(|key| key != "")); + } + } + + Ok(settings) + } +} + +impl Default for Settings { + fn default() -> Self { + Settings { + info: Info { + relay_url: None, + name: Some("floonet-rs-relay".to_owned()), + description: Some( + "A Floonet relay for the Grin community Nostr network.".to_owned(), + ), + pubkey: None, + contact: None, + favicon: None, + relay_icon: None, + relay_page: None, + }, + diagnostics: Diagnostics { tracing: false }, + database: Database { + data_directory: ".".to_owned(), + engine: "sqlite".to_owned(), + in_memory: false, + min_conn: 4, + max_conn: 8, + connection: "".to_owned(), + connection_write: None, + }, + grpc: Grpc { + event_admission_server: None, + restricts_write: false, + }, + network: Network { + port: 8080, + ping_interval_seconds: 300, + address: "0.0.0.0".to_owned(), + remote_ip_header: None, + }, + limits: Limits { + messages_per_sec: None, + subscriptions_per_min: None, + db_conns_per_client: None, + max_blocking_threads: 16, + max_event_bytes: Some(2 << 17), // 128K + max_ws_message_bytes: Some(2 << 17), // 128K + max_ws_frame_bytes: Some(2 << 17), // 128K + broadcast_buffer: 16384, + event_persist_buffer: 4096, + event_kind_blacklist: None, + // Floonet keystone: default-deny kind whitelist. + event_kind_allowlist: Some(crate::admission::DEFAULT_ALLOWED_KINDS.to_vec()), + limit_scrapers: false, + }, + authorization: Authorization { + pubkey_whitelist: None, // Allow any address to publish + nip42_auth: false, // Disable NIP-42 authentication + nip42_dms: false, // Send DMs to everybody + require_auth_to_write: false, + }, + pay_to_relay: PayToRelay { + enabled: false, + admission_cost: 4200, + cost_per_event: 0, + terms_message: "".to_string(), + node_url: "".to_string(), + api_secret: "".to_string(), + rune_path: None, + sign_ups: false, + direct_message: false, + secret_key: None, + processor: Processor::LNBits, + }, + goblinpay: GoblinPay { + pay_mode: "off".to_owned(), + url: String::new(), + api_token: String::new(), + name_price_grin: 1.0, + admission_price_grin: 1.0, + }, + name_authority: NameAuthority { + enabled: false, + domain: String::new(), + base_url: String::new(), + relays: None, + name_min: 3, + name_max: 20, + name_change_cooldown_secs: 600, + auth_max_age_secs: 60, + read_rate_max: 120, + read_rate_window_secs: 60, + write_rate_max: 10, + write_rate_window_secs: 3600, + reserved_file: None, + }, + exit: MixnetExit { + enabled: false, + binary: "/usr/local/bin/floonet-mixexit".to_owned(), + data_dir: "./mixexit-data".to_owned(), + upstream: String::new(), + }, + verified_users: VerifiedUsers { + mode: VerifiedUsersMode::Disabled, + domain_whitelist: None, + domain_blacklist: None, + verify_expiration: Some("1 week".to_owned()), + verify_update_frequency: Some("1 day".to_owned()), + verify_expiration_duration: None, + verify_update_frequency_duration: None, + max_consecutive_failures: 20, + }, + retention: Retention { + max_events: None, // max events + max_bytes: None, // max size + persist_days: None, // oldest message + whitelist_addresses: None, // whitelisted addresses (never delete) + }, + options: Options { + reject_future_seconds: None, // Reject events in the future if defined + }, + logging: Logging { + folder_path: None, + file_prefix: None, + }, + } + } +} diff --git a/src/conn.rs b/src/conn.rs new file mode 100644 index 0000000..107b9e0 --- /dev/null +++ b/src/conn.rs @@ -0,0 +1,229 @@ +//! Client connection state +use std::collections::HashMap; + +use tracing::{debug, trace}; +use uuid::Uuid; + +use crate::close::Close; +use crate::conn::Nip42AuthState::{AuthPubkey, Challenge, NoAuth}; +use crate::error::Error; +use crate::error::Result; +use crate::event::Event; +use crate::subscription::Subscription; +use crate::utils::{host_str, unix_time}; + +/// A subscription identifier has a maximum length +const MAX_SUBSCRIPTION_ID_LEN: usize = 256; + +/// NIP-42 authentication state +pub enum Nip42AuthState { + /// The client is not authenticated yet + NoAuth, + /// The AUTH challenge sent + Challenge(String), + /// The client is authenticated + AuthPubkey(String), +} + +/// State for a client connection +pub struct ClientConn { + /// Client IP (either from socket, or configured proxy header + client_ip_addr: String, + /// Unique client identifier generated at connection time + client_id: Uuid, + /// The current set of active client subscriptions + subscriptions: HashMap, + /// Per-connection maximum concurrent subscriptions + max_subs: usize, + /// NIP-42 AUTH + auth: Nip42AuthState, +} + +impl Default for ClientConn { + fn default() -> Self { + Self::new("unknown".to_owned()) + } +} + +impl ClientConn { + /// Create a new, empty connection state. + #[must_use] + pub fn new(client_ip_addr: String) -> Self { + let client_id = Uuid::new_v4(); + ClientConn { + client_ip_addr, + client_id, + subscriptions: HashMap::new(), + max_subs: 32, + auth: NoAuth, + } + } + + #[must_use] + pub fn subscriptions(&self) -> &HashMap { + &self.subscriptions + } + + /// Check if the given subscription already exists + #[must_use] + pub fn has_subscription(&self, sub: &Subscription) -> bool { + self.subscriptions.values().any(|x| x == sub) + } + + /// Get a short prefix of the client's unique identifier, suitable + /// for logging. + #[must_use] + pub fn get_client_prefix(&self) -> String { + self.client_id.to_string().chars().take(8).collect() + } + + #[must_use] + pub fn ip(&self) -> &str { + &self.client_ip_addr + } + + #[must_use] + pub fn auth_pubkey(&self) -> Option<&String> { + match &self.auth { + AuthPubkey(pubkey) => Some(pubkey), + _ => None, + } + } + + #[must_use] + pub fn auth_challenge(&self) -> Option<&String> { + match &self.auth { + Challenge(pubkey) => Some(pubkey), + _ => None, + } + } + + /// Add a new subscription for this connection. + /// # Errors + /// + /// Will return `Err` if the client has too many subscriptions, or + /// if the provided name is excessively long. + pub fn subscribe(&mut self, s: Subscription) -> Result<()> { + let k = s.get_id(); + let sub_id_len = k.len(); + // prevent arbitrarily long subscription identifiers from + // being used. + if sub_id_len > MAX_SUBSCRIPTION_ID_LEN { + debug!( + "ignoring sub request with excessive length: ({})", + sub_id_len + ); + return Err(Error::SubIdMaxLengthError); + } + // check if an existing subscription exists, and replace if so + if self.subscriptions.contains_key(&k) { + self.subscriptions.remove(&k); + self.subscriptions.insert(k, s.clone()); + trace!( + "replaced existing subscription (cid: {}, sub: {:?})", + self.get_client_prefix(), + s.get_id() + ); + return Ok(()); + } + + // check if there is room for another subscription. + if self.subscriptions.len() >= self.max_subs { + return Err(Error::SubMaxExceededError); + } + // add subscription + self.subscriptions.insert(k, s); + trace!( + "registered new subscription, currently have {} active subs (cid: {})", + self.subscriptions.len(), + self.get_client_prefix(), + ); + Ok(()) + } + + /// Remove the subscription for this connection. + pub fn unsubscribe(&mut self, c: &Close) { + // TODO: return notice if subscription did not exist. + self.subscriptions.remove(&c.id); + trace!( + "removed subscription, currently have {} active subs (cid: {})", + self.subscriptions.len(), + self.get_client_prefix(), + ); + } + + pub fn generate_auth_challenge(&mut self) { + self.auth = Challenge(Uuid::new_v4().to_string()); + } + + pub fn authenticate(&mut self, event: &Event, relay_url: &str) -> Result<()> { + match &self.auth { + Challenge(_) => (), + AuthPubkey(_) => { + // already authenticated + return Ok(()); + } + NoAuth => { + // unexpected AUTH request + return Err(Error::AuthFailure); + } + } + match event.validate() { + Ok(_) => { + if event.kind != 22242 { + return Err(Error::AuthFailure); + } + + let curr_time = unix_time(); + let past_cutoff = curr_time - 600; // 10 minutes + let future_cutoff = curr_time + 600; // 10 minutes + if event.created_at < past_cutoff || event.created_at > future_cutoff { + return Err(Error::AuthFailure); + } + + let mut challenge: Option<&str> = None; + let mut relay: Option<&str> = None; + + for tag in &event.tags { + if tag.len() == 2 && tag.first() == Some(&"challenge".into()) { + challenge = tag.get(1).map(|x| x.as_str()); + } + if tag.len() == 2 && tag.first() == Some(&"relay".into()) { + relay = tag.get(1).map(|x| x.as_str()); + } + } + + match (challenge, &self.auth) { + (Some(received_challenge), Challenge(sent_challenge)) => { + if received_challenge != sent_challenge { + return Err(Error::AuthFailure); + } + } + (_, _) => { + return Err(Error::AuthFailure); + } + } + + match (relay.and_then(host_str), host_str(relay_url)) { + (Some(received_relay), Some(our_relay)) => { + if received_relay != our_relay { + return Err(Error::AuthFailure); + } + } + (_, _) => { + return Err(Error::AuthFailure); + } + } + + self.auth = AuthPubkey(event.pubkey.clone()); + trace!( + "authenticated pubkey {} (cid: {})", + event.pubkey.chars().take(8).collect::(), + self.get_client_prefix() + ); + Ok(()) + } + Err(_) => Err(Error::AuthFailure), + } + } +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..05dadf9 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,481 @@ +//! Event persistence and querying +use crate::config::Settings; +use crate::error::{Error, Result}; +use crate::event::Event; +use crate::nauthz; +use crate::notice::Notice; +use crate::payment::PaymentMessage; +use crate::repo::postgres::{PostgresPool, PostgresRepo}; +use crate::repo::sqlite::SqliteRepo; +use crate::repo::NostrRepo; +use crate::server::NostrMetrics; +use governor::clock::Clock; +use governor::{Quota, RateLimiter}; +use log::LevelFilter; +use nostr::key::FromPkStr; +use nostr::key::Keys; +use r2d2; +use sqlx::pool::PoolOptions; +use sqlx::postgres::PgConnectOptions; +use sqlx::ConnectOptions; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; +use tracing::{debug, info, trace, warn}; + +pub type SqlitePool = r2d2::Pool; +pub type PooledConnection = r2d2::PooledConnection; + +/// Events submitted from a client, with a return channel for notices +pub struct SubmittedEvent { + pub event: Event, + pub notice_tx: tokio::sync::mpsc::Sender, + pub source_ip: String, + pub origin: Option, + pub user_agent: Option, + pub auth_pubkey: Option>, +} + +/// Database file +pub const DB_FILE: &str = "nostr.db"; + +/// Build repo +/// # Panics +/// +/// Will panic if the pool could not be created. +pub async fn build_repo(settings: &Settings, metrics: NostrMetrics) -> Arc { + match settings.database.engine.as_str() { + "sqlite" => Arc::new(build_sqlite_pool(settings, metrics).await), + "postgres" => Arc::new(build_postgres_pool(settings, metrics).await), + _ => panic!("Unknown database engine"), + } +} + +async fn build_sqlite_pool(settings: &Settings, metrics: NostrMetrics) -> SqliteRepo { + let repo = SqliteRepo::new(settings, metrics); + repo.start().await.ok(); + repo.migrate_up().await.ok(); + repo +} + +async fn build_postgres_pool(settings: &Settings, metrics: NostrMetrics) -> PostgresRepo { + let mut options: PgConnectOptions = settings.database.connection.as_str().parse().unwrap(); + options.log_statements(LevelFilter::Debug); + options.log_slow_statements(LevelFilter::Warn, Duration::from_secs(60)); + + let pool: PostgresPool = PoolOptions::new() + .max_connections(settings.database.max_conn) + .min_connections(settings.database.min_conn) + .idle_timeout(Duration::from_secs(60)) + .connect_with(options) + .await + .unwrap(); + + let write_pool: PostgresPool = match &settings.database.connection_write { + Some(cfg_write) => { + let mut options_write: PgConnectOptions = cfg_write.as_str().parse().unwrap(); + options_write.log_statements(LevelFilter::Debug); + options_write.log_slow_statements(LevelFilter::Warn, Duration::from_secs(60)); + + PoolOptions::new() + .max_connections(settings.database.max_conn) + .min_connections(settings.database.min_conn) + .idle_timeout(Duration::from_secs(60)) + .connect_with(options_write) + .await + .unwrap() + } + None => pool.clone(), + }; + + let repo = PostgresRepo::new(pool, write_pool, metrics); + + // Panic on migration failure + let version = repo.migrate_up().await.unwrap(); + info!("Postgres migration completed, at v{}", version); + // startup scheduled tasks + repo.start().await.ok(); + repo +} + +/// Spawn a database writer that persists events to the `SQLite` store. +pub async fn db_writer( + repo: Arc, + settings: Settings, + mut event_rx: tokio::sync::mpsc::Receiver, + bcast_tx: tokio::sync::broadcast::Sender, + metadata_tx: tokio::sync::broadcast::Sender, + payment_tx: tokio::sync::broadcast::Sender, + mut shutdown: tokio::sync::broadcast::Receiver<()>, +) -> Result<()> { + // are we performing NIP-05 checking? + let nip05_active = settings.verified_users.is_active(); + // are we requriing NIP-05 user verification? + let nip05_enabled = settings.verified_users.is_enabled(); + + let pay_to_relay_enabled = settings.pay_to_relay.enabled; + let cost_per_event = settings.pay_to_relay.cost_per_event; + debug!("Pay to relay: {}", pay_to_relay_enabled); + + //upgrade_db(&mut pool.get()?)?; + + // Make a copy of the whitelist + let whitelist = &settings.authorization.pubkey_whitelist.clone(); + + // get rate limit settings + let rps_setting = settings.limits.messages_per_sec; + let mut most_recent_rate_limit = Instant::now(); + let mut lim_opt = None; + let clock = governor::clock::QuantaClock::default(); + if let Some(rps) = rps_setting { + if rps > 0 { + info!("Enabling rate limits for event creation ({}/sec)", rps); + let quota = core::num::NonZeroU32::new(rps * 60).unwrap(); + lim_opt = Some(RateLimiter::direct(Quota::per_minute(quota))); + } + } + // create a client if GRPC is enabled. + // Check with externalized event admitter service, if one is defined. + let mut grpc_client = if let Some(svr) = settings.grpc.event_admission_server { + Some(nauthz::EventAuthzService::connect(&svr).await) + } else { + None + }; + + //let gprc_client = settings.grpc.event_admission_server.map(|s| { + // event_admitter_connect(&s); + // }); + + loop { + if shutdown.try_recv().is_ok() { + info!("shutting down database writer"); + break; + } + // call blocking read on channel + let next_event = event_rx.recv().await; + // if the channel has closed, we will never get work + if next_event.is_none() { + break; + } + // track if an event write occurred; this is used to + // update the rate limiter + let mut event_write = false; + let subm_event = next_event.unwrap(); + let event = subm_event.event; + let notice_tx = subm_event.notice_tx; + + // Check that event kind isn't blacklisted + let kinds_blacklist = &settings.limits.event_kind_blacklist.clone(); + if let Some(event_kind_blacklist) = kinds_blacklist { + if event_kind_blacklist.contains(&event.kind) { + debug!( + "rejecting event: {}, blacklisted kind: {}", + &event.get_event_id_prefix(), + &event.kind + ); + notice_tx + .try_send(Notice::blocked(event.id, "event kind is blocked by relay")) + .ok(); + continue; + } + } + + // Check that event kind isn't allowlisted + let kinds_allowlist = &settings.limits.event_kind_allowlist.clone(); + if let Some(event_kind_allowlist) = kinds_allowlist { + if !event_kind_allowlist.contains(&event.kind) { + debug!( + "rejecting event: {}, allowlist kind: {}", + &event.get_event_id_prefix(), + &event.kind + ); + notice_tx + .try_send(Notice::blocked(event.id, "event kind is blocked by relay")) + .ok(); + continue; + } + } + + // Set to none until balance is got from db + // Will stay none if user in whitelisted and does not have to pay to post + // When pay to relay is enabled the whitelist is not a list of who can post + // It is a list of who can post for free + let mut user_balance: Option = None; + if !pay_to_relay_enabled { + // check if this event is authorized. + if let Some(allowed_addrs) = whitelist { + // TODO: incorporate delegated pubkeys + // if the event address is not in allowed_addrs. + if !allowed_addrs.contains(&event.pubkey) { + debug!( + "rejecting event: {}, unauthorized author", + event.get_event_id_prefix() + ); + notice_tx + .try_send(Notice::blocked( + event.id, + "pubkey is not allowed to publish to this relay", + )) + .ok(); + continue; + } + } + } else { + // If the user is on whitelist there is no need to check if the user is admitted or has balance to post + if whitelist.is_none() + || (whitelist.is_some() && !whitelist.as_ref().unwrap().contains(&event.pubkey)) + { + let key = Keys::from_pk_str(&event.pubkey).unwrap(); + match repo.get_account_balance(&key).await { + Ok((user_admitted, balance)) => { + // Checks to make sure user is admitted + if !user_admitted { + debug!("user: {}, is not admitted", &event.pubkey); + + // If the user is in DB but not admitted + // Send meeage to payment thread to check if outstanding invoice has been paid + payment_tx + .send(PaymentMessage::CheckAccount(event.pubkey)) + .ok(); + notice_tx + .try_send(Notice::blocked(event.id, "User is not admitted")) + .ok(); + continue; + } + + // Checks that user has enough balance to post + // TODO: this should send an invoice to user to top up + if balance < cost_per_event { + debug!("user: {}, does not have a balance", &event.pubkey,); + notice_tx + .try_send(Notice::blocked(event.id, "Insufficient balance")) + .ok(); + continue; + } + user_balance = Some(balance); + debug!("User balance: {:?}", user_balance); + } + Err( + Error::SqlError(rusqlite::Error::QueryReturnedNoRows) + | Error::SqlxError(sqlx::Error::RowNotFound), + ) => { + // User does not exist + info!("Unregistered user"); + if settings.pay_to_relay.sign_ups && settings.pay_to_relay.direct_message { + payment_tx + .send(PaymentMessage::NewAccount(event.pubkey)) + .ok(); + } + let msg = "Pubkey not registered"; + notice_tx.try_send(Notice::error(event.id, msg)).ok(); + continue; + } + Err(err) => { + warn!("Error checking admission status: {:?}", err); + let msg = "relay experienced an error checking your admission status"; + notice_tx.try_send(Notice::error(event.id, msg)).ok(); + // Other error + continue; + } + } + } + } + + // get a validation result for use in verification and GPRC + let validation = if nip05_active { + Some(repo.get_latest_user_verification(&event.pubkey).await) + } else { + None + }; + + // check for NIP-05 verification + if nip05_enabled && validation.is_some() { + match validation.as_ref().unwrap() { + Ok(uv) => { + if uv.is_valid(&settings.verified_users) { + info!( + "new event from verified author ({:?},{:?})", + uv.name.to_string(), + event.get_author_prefix() + ); + } else { + info!( + "rejecting event, author ({:?} / {:?}) verification invalid (expired/wrong domain)", + uv.name.to_string(), + event.get_author_prefix() + ); + notice_tx + .try_send(Notice::blocked( + event.id, + "NIP-05 verification is no longer valid (expired/wrong domain)", + )) + .ok(); + continue; + } + } + Err( + Error::SqlError(rusqlite::Error::QueryReturnedNoRows) + | Error::SqlxError(sqlx::Error::RowNotFound), + ) => { + debug!( + "no verification records found for pubkey: {:?}", + event.get_author_prefix() + ); + notice_tx + .try_send(Notice::blocked( + event.id, + "NIP-05 verification needed to publish events", + )) + .ok(); + continue; + } + Err(e) => { + warn!("checking nip05 verification status failed: {:?}", e); + continue; + } + } + } + + // nip05 address + let nip05_address: Option = + validation.and_then(|x| x.ok().map(|y| y.name)); + + // GRPC check + if let Some(ref mut c) = grpc_client { + trace!("checking if grpc permits"); + let grpc_start = Instant::now(); + let decision_res = c + .admit_event( + &event, + &subm_event.source_ip, + subm_event.origin, + subm_event.user_agent, + nip05_address, + subm_event.auth_pubkey, + ) + .await; + match decision_res { + Ok(decision) => { + if !decision.permitted() { + // GPRC returned a decision to reject this event + info!( + "GRPC rejected event: {:?} (kind: {}) from: {:?} in: {:?} (IP: {:?})", + event.get_event_id_prefix(), + event.kind, + event.get_author_prefix(), + grpc_start.elapsed(), + subm_event.source_ip + ); + notice_tx + .try_send(Notice::blocked( + event.id, + &decision.message().unwrap_or_default(), + )) + .ok(); + continue; + } + } + Err(e) => { + warn!("GRPC server error: {:?}", e); + } + } + } + + // send any metadata events to the NIP-05 verifier + if nip05_active && event.is_kind_metadata() { + // we are sending this prior to even deciding if we + // persist it. this allows the nip05 module to + // inspect it, update if necessary, or persist a new + // event and broadcast it itself. + metadata_tx.send(event.clone()).ok(); + } + + // TODO: cache recent list of authors to remove a DB call. + let start = Instant::now(); + if event.is_ephemeral() { + bcast_tx.send(event.clone()).ok(); + debug!( + "published ephemeral event: {:?} from: {:?} in: {:?}", + event.get_event_id_prefix(), + event.get_author_prefix(), + start.elapsed() + ); + event_write = true; + + // send OK message + notice_tx.try_send(Notice::saved(event.id)).ok(); + } else { + match repo.write_event(&event).await { + Ok(updated) => { + if updated == 0 { + trace!("ignoring duplicate or deleted event"); + notice_tx.try_send(Notice::duplicate(event.id)).ok(); + } else { + info!( + "persisted event: {:?} (kind: {}) from: {:?} in: {:?} (IP: {:?})", + event.get_event_id_prefix(), + event.kind, + event.get_author_prefix(), + start.elapsed(), + subm_event.source_ip, + ); + event_write = true; + // send this out to all clients + bcast_tx.send(event.clone()).ok(); + notice_tx.try_send(Notice::saved(event.id)).ok(); + } + } + Err(err) => { + warn!("event insert failed: {:?}", err); + let msg = "relay experienced an error trying to publish the latest event"; + notice_tx.try_send(Notice::error(event.id, msg)).ok(); + } + } + } + + // use rate limit, if defined, and if an event was actually written. + if event_write { + // If pay to relay is diabaled or the cost per event is 0 + // No need to update user balance + if pay_to_relay_enabled && cost_per_event > 0 { + // If the user balance is some, user was not on whitelist + // Their balance should be reduced by the cost per event + if let Some(_balance) = user_balance { + let pubkey = Keys::from_pk_str(&event.pubkey)?; + repo.update_account_balance(&pubkey, false, cost_per_event) + .await?; + } + } + if let Some(ref lim) = lim_opt { + if let Err(n) = lim.check() { + let wait_for = n.wait_time_from(clock.now()); + // check if we have recently logged rate + // limits, but print out a message only once + // per second. + if most_recent_rate_limit.elapsed().as_secs() > 10 { + warn!( + "rate limit reached for event creation (sleep for {:?}) (suppressing future messages for 10 seconds)", + wait_for + ); + // reset last rate limit message + most_recent_rate_limit = Instant::now(); + } + // block event writes, allowing them to queue up + thread::sleep(wait_for); + continue; + } + } + } + } + info!("database connection closed"); + Ok(()) +} + +/// Serialized event associated with a specific subscription request. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct QueryResult { + /// Subscription identifier + pub sub_id: String, + /// Serialized event + pub event: String, +} diff --git a/src/delegation.rs b/src/delegation.rs new file mode 100644 index 0000000..cf7bd4e --- /dev/null +++ b/src/delegation.rs @@ -0,0 +1,406 @@ +//! Event parsing and validation +use crate::error::Error; +use crate::error::Result; +use crate::event::Event; +use bitcoin_hashes::{sha256, Hash}; +use lazy_static::lazy_static; +use regex::Regex; +use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tracing::{debug, info}; + +// This handles everything related to delegation, in particular the +// condition/rune parsing and logic. + +// Conditions are poorly specified, so we will implement the minimum +// necessary for now. + +// fields MUST be either "kind" or "created_at". +// operators supported are ">", "<", "=", "!". +// no operations on 'content' are supported. + +// this allows constraints for: +// valid date ranges (valid from X->Y dates). +// specific kinds (publish kind=1,5) +// kind ranges (publish ephemeral events, kind>19999&kind<30001) + +// for more complex scenarios (allow delegatee to publish ephemeral +// AND replacement events), it may be necessary to generate and use +// different condition strings, since we do not support grouping or +// "OR" logic. + +lazy_static! { + /// Secp256k1 verification instance. + pub static ref SECP: Secp256k1 = Secp256k1::verification_only(); +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub enum Field { + Kind, + CreatedAt, +} + +impl FromStr for Field { + type Err = Error; + fn from_str(value: &str) -> Result { + if value == "kind" { + Ok(Field::Kind) + } else if value == "created_at" { + Ok(Field::CreatedAt) + } else { + Err(Error::DelegationParseError) + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub enum Operator { + LessThan, + GreaterThan, + Equals, + NotEquals, +} +impl FromStr for Operator { + type Err = Error; + fn from_str(value: &str) -> Result { + if value == "<" { + Ok(Operator::LessThan) + } else if value == ">" { + Ok(Operator::GreaterThan) + } else if value == "=" { + Ok(Operator::Equals) + } else if value == "!" { + Ok(Operator::NotEquals) + } else { + Err(Error::DelegationParseError) + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct ConditionQuery { + pub conditions: Vec, +} + +impl ConditionQuery { + #[must_use] + pub fn allows_event(&self, event: &Event) -> bool { + // check each condition, to ensure that the event complies + // with the restriction. + for c in &self.conditions { + if !c.allows_event(event) { + // any failing conditions invalidates the delegation + // on this event + return false; + } + } + // delegation was permitted unconditionally, or all conditions + // were true + true + } +} + +// Verify that the delegator approved the delegation; return a ConditionQuery if so. +#[must_use] +pub fn validate_delegation( + delegator: &str, + delegatee: &str, + cond_query: &str, + sigstr: &str, +) -> Option { + // form the token + let tok = format!("nostr:delegation:{delegatee}:{cond_query}"); + // form SHA256 hash + let digest: sha256::Hash = sha256::Hash::hash(tok.as_bytes()); + let sig = schnorr::Signature::from_str(sigstr).unwrap(); + if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) { + if let Ok(pubkey) = XOnlyPublicKey::from_str(delegator) { + let verify = SECP.verify_schnorr(&sig, &msg, &pubkey); + if verify.is_ok() { + // return the parsed condition query + cond_query.parse::().ok() + } else { + debug!("client sent an delegation signature that did not validate"); + None + } + } else { + debug!("client sent malformed delegation pubkey"); + None + } + } else { + info!("error converting delegation digest to secp256k1 message"); + None + } +} + +/// Parsed delegation condition +/// see +/// An example complex condition would be: `kind=1,2,3&created_at<1665265999` +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Condition { + pub field: Field, + pub operator: Operator, + pub values: Vec, +} + +impl Condition { + /// Check if this condition allows the given event to be delegated + #[must_use] + pub fn allows_event(&self, event: &Event) -> bool { + // determine what the right-hand side of the operator is + let resolved_field = match &self.field { + Field::Kind => event.kind, + Field::CreatedAt => event.created_at, + }; + match &self.operator { + Operator::LessThan => { + // the less-than operator is only valid for single values. + if self.values.len() == 1 { + if let Some(v) = self.values.first() { + return resolved_field < *v; + } + } + } + Operator::GreaterThan => { + // the greater-than operator is only valid for single values. + if self.values.len() == 1 { + if let Some(v) = self.values.first() { + return resolved_field > *v; + } + } + } + Operator::Equals => { + // equals is interpreted as "must be equal to at least one provided value" + return self.values.iter().any(|&x| resolved_field == x); + } + Operator::NotEquals => { + // not-equals is interpreted as "must not be equal to any provided value" + // this is the one case where an empty list of values could be allowed; even though it is a pointless restriction. + return self.values.iter().all(|&x| resolved_field != x); + } + } + false + } +} + +fn str_to_condition(cs: &str) -> Option { + // a condition is a string (alphanum+underscore), an operator (<>=!), and values (num+comma) + lazy_static! { + static ref RE: Regex = Regex::new("([[:word:]]+)([<>=!]+)([,[[:digit:]]]*)").unwrap(); + } + // match against the regex + let caps = RE.captures(cs)?; + let field = caps.get(1)?.as_str().parse::().ok()?; + let operator = caps.get(2)?.as_str().parse::().ok()?; + // values are just comma separated numbers, but all must be parsed + let rawvals = caps.get(3)?.as_str(); + let values = rawvals + .split_terminator(',') + .map(|n| n.parse::().ok()) + .collect::>>()?; + // convert field string into Field + Some(Condition { + field, + operator, + values, + }) +} + +/// Parse a condition query from a string slice +impl FromStr for ConditionQuery { + type Err = Error; + fn from_str(value: &str) -> Result { + // split the string with '&' + let mut conditions = vec![]; + let condstrs = value.split_terminator('&'); + // parse each individual condition + for c in condstrs { + conditions.push(str_to_condition(c).ok_or(Error::DelegationParseError)?); + } + Ok(ConditionQuery { conditions }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // parse condition strings + #[test] + fn parse_empty() -> Result<()> { + // given an empty condition query, produce an empty vector + let empty_cq = ConditionQuery { conditions: vec![] }; + let parsed = "".parse::()?; + assert_eq!(parsed, empty_cq); + Ok(()) + } + + // parse field 'kind' + #[test] + fn test_kind_field_parse() -> Result<()> { + let field = "kind".parse::()?; + assert_eq!(field, Field::Kind); + Ok(()) + } + // parse field 'created_at' + #[test] + fn test_created_at_field_parse() -> Result<()> { + let field = "created_at".parse::()?; + assert_eq!(field, Field::CreatedAt); + Ok(()) + } + // parse unknown field + #[test] + fn unknown_field_parse() { + let field = "unk".parse::(); + assert!(field.is_err()); + } + + // parse a full conditional query with an empty array + #[test] + fn parse_kind_equals_empty() -> Result<()> { + // given an empty condition query, produce an empty vector + let kind_cq = ConditionQuery { + conditions: vec![Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![], + }], + }; + let parsed = "kind=".parse::()?; + assert_eq!(parsed, kind_cq); + Ok(()) + } + // parse a full conditional query with a single value + #[test] + fn parse_kind_equals_singleval() -> Result<()> { + // given an empty condition query, produce an empty vector + let kind_cq = ConditionQuery { + conditions: vec![Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![1], + }], + }; + let parsed = "kind=1".parse::()?; + assert_eq!(parsed, kind_cq); + Ok(()) + } + // parse a full conditional query with multiple values + #[test] + fn parse_kind_equals_multival() -> Result<()> { + // given an empty condition query, produce an empty vector + let kind_cq = ConditionQuery { + conditions: vec![Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![1, 2, 4], + }], + }; + let parsed = "kind=1,2,4".parse::()?; + assert_eq!(parsed, kind_cq); + Ok(()) + } + // parse multiple conditions + #[test] + fn parse_multi_conditions() -> Result<()> { + // given an empty condition query, produce an empty vector + let cq = ConditionQuery { + conditions: vec![ + Condition { + field: Field::Kind, + operator: Operator::GreaterThan, + values: vec![10000], + }, + Condition { + field: Field::Kind, + operator: Operator::LessThan, + values: vec![20000], + }, + Condition { + field: Field::Kind, + operator: Operator::NotEquals, + values: vec![10001], + }, + Condition { + field: Field::CreatedAt, + operator: Operator::LessThan, + values: vec![1_665_867_123], + }, + ], + }; + let parsed = + "kind>10000&kind<20000&kind!10001&created_at<1665867123".parse::()?; + assert_eq!(parsed, cq); + Ok(()) + } + // Check for condition logic on event w/ empty values + #[test] + fn condition_with_empty_values() { + let mut c = Condition { + field: Field::Kind, + operator: Operator::GreaterThan, + values: vec![], + }; + let e = Event::simple_event(); + assert!(!c.allows_event(&e)); + c.operator = Operator::LessThan; + assert!(!c.allows_event(&e)); + c.operator = Operator::Equals; + assert!(!c.allows_event(&e)); + // Not Equals applied to an empty list *is* allowed + // (pointless, but logically valid). + c.operator = Operator::NotEquals; + assert!(c.allows_event(&e)); + } + + // Check for condition logic on event w/ single value + #[test] + fn condition_kind_gt_event_single() { + let c = Condition { + field: Field::Kind, + operator: Operator::GreaterThan, + values: vec![10], + }; + let mut e = Event::simple_event(); + // kind is not greater than 10, not allowed + e.kind = 1; + assert!(!c.allows_event(&e)); + // kind is greater than 10, allowed + e.kind = 100; + assert!(c.allows_event(&e)); + // kind is 10, not allowed + e.kind = 10; + assert!(!c.allows_event(&e)); + } + // Check for condition logic on event w/ multi values + #[test] + fn condition_with_multi_values() { + let mut c = Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![0, 10, 20], + }; + let mut e = Event::simple_event(); + // Allow if event kind is in list for Equals + e.kind = 10; + assert!(c.allows_event(&e)); + // Deny if event kind is not in list for Equals + e.kind = 11; + assert!(!c.allows_event(&e)); + // Deny if event kind is in list for NotEquals + e.kind = 10; + c.operator = Operator::NotEquals; + assert!(!c.allows_event(&e)); + // Allow if event kind is not in list for NotEquals + e.kind = 99; + c.operator = Operator::NotEquals; + assert!(c.allows_event(&e)); + // Always deny if GreaterThan/LessThan for a list + c.operator = Operator::LessThan; + assert!(!c.allows_event(&e)); + c.operator = Operator::GreaterThan; + assert!(!c.allows_event(&e)); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..9cd6968 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,192 @@ +//! Error handling +use std::result; +use thiserror::Error; +use tungstenite::error::Error as WsError; + +/// Simple `Result` type for errors in this module +pub type Result = result::Result; + +/// Custom error type for Nostr +#[derive(Error, Debug)] +pub enum Error { + #[error("Protocol parse error")] + ProtoParseError, + #[error("Connection error")] + ConnError, + #[error("Client write error")] + ConnWriteError, + #[error("EVENT parse failed")] + EventParseFailed, + #[error("CLOSE message parse failed")] + CloseParseFailed, + #[error("Event invalid signature")] + EventInvalidSignature, + #[error("Event invalid id")] + EventInvalidId, + #[error("Event malformed pubkey")] + EventMalformedPubkey, + #[error("Event could not canonicalize")] + EventCouldNotCanonicalize, + #[error("Event too large")] + EventMaxLengthError(usize), + #[error("Subscription identifier max length exceeded")] + SubIdMaxLengthError, + #[error("Maximum concurrent subscription count reached")] + SubMaxExceededError, + // this should be used if the JSON is invalid + #[error("JSON parsing failed")] + JsonParseFailed(serde_json::Error), + #[error("WebSocket proto error")] + WebsocketError(WsError), + #[error("Command unknown")] + CommandUnknownError, + #[error("SQL error")] + SqlError(rusqlite::Error), + #[error("Config error : {0}")] + ConfigError(config::ConfigError), + #[error("Data directory does not exist")] + DatabaseDirError, + #[error("Database Connection Pool Error")] + DatabasePoolError(r2d2::Error), + #[error("SQL error")] + SqlxError(sqlx::Error), + #[error("Database Connection Pool Error")] + SqlxDatabasePoolError(sqlx::Error), + #[error("Custom Error : {0}")] + CustomError(String), + #[error("Task join error")] + JoinError, + #[error("Hyper Client error")] + HyperError(hyper::Error), + #[error("Hex encoding error")] + HexError(hex::FromHexError), + #[error("Delegation parse error")] + DelegationParseError, + #[error("Channel closed error")] + ChannelClosed, + #[error("Authz error")] + AuthzError, + #[error("Tonic GRPC error")] + TonicError(tonic::Status), + #[error("Invalid AUTH message")] + AuthFailure, + #[error("I/O Error")] + IoError(std::io::Error), + #[error("Event builder error")] + EventError(nostr::event::builder::Error), + #[error("Nostr key error")] + NostrKeyError(nostr::key::Error), + #[error("Payment hash mismatch")] + PaymentHash, + #[error("Error parsing url")] + URLParseError(url::ParseError), + #[error("HTTP error")] + HTTPError(http::Error), + #[error("Unknown/Undocumented")] + UnknownError, +} + +//impl From> for Error { +// fn from(e: Box) -> Self { +// Error::CustomError("error".to_owned()) +// } +//} + +impl From for Error { + fn from(h: hex::FromHexError) -> Self { + Error::HexError(h) + } +} + +impl From for Error { + fn from(h: hyper::Error) -> Self { + Error::HyperError(h) + } +} + +impl From for Error { + fn from(d: r2d2::Error) -> Self { + Error::DatabasePoolError(d) + } +} + +impl From for Error { + /// Wrap SQL error + fn from(_j: tokio::task::JoinError) -> Self { + Error::JoinError + } +} + +impl From for Error { + /// Wrap SQL error + fn from(r: rusqlite::Error) -> Self { + Error::SqlError(r) + } +} + +impl From for Error { + fn from(d: sqlx::Error) -> Self { + Error::SqlxDatabasePoolError(d) + } +} + +impl From for Error { + /// Wrap JSON error + fn from(r: serde_json::Error) -> Self { + Error::JsonParseFailed(r) + } +} + +impl From for Error { + /// Wrap Websocket error + fn from(r: WsError) -> Self { + Error::WebsocketError(r) + } +} + +impl From for Error { + /// Wrap Config error + fn from(r: config::ConfigError) -> Self { + Error::ConfigError(r) + } +} + +impl From for Error { + /// Wrap Config error + fn from(r: tonic::Status) -> Self { + Error::TonicError(r) + } +} + +impl From for Error { + fn from(r: std::io::Error) -> Self { + Error::IoError(r) + } +} +impl From for Error { + /// Wrap event builder error + fn from(r: nostr::event::builder::Error) -> Self { + Error::EventError(r) + } +} + +impl From for Error { + /// Wrap nostr key error + fn from(r: nostr::key::Error) -> Self { + Error::NostrKeyError(r) + } +} + +impl From for Error { + /// Wrap nostr key error + fn from(r: url::ParseError) -> Self { + Error::URLParseError(r) + } +} + +impl From for Error { + /// Wrap nostr key error + fn from(r: http::Error) -> Self { + Error::HTTPError(r) + } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..86fdd20 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,798 @@ +//! Event parsing and validation +use crate::delegation::validate_delegation; +use crate::error::Error::{ + CommandUnknownError, EventCouldNotCanonicalize, EventInvalidId, EventInvalidSignature, + EventMalformedPubkey, +}; +use crate::error::Result; +use crate::event::EventWrapper::WrappedAuth; +use crate::event::EventWrapper::WrappedEvent; +use crate::nip05; +use crate::utils::unix_time; +use bitcoin_hashes::{sha256, Hash}; +use lazy_static::lazy_static; +use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::value::Value; +use serde_json::Number; +use std::collections::HashMap; +use std::collections::HashSet; +use std::str::FromStr; +use tracing::{debug, info}; +use crate::subscription::TagOperand; + +lazy_static! { + /// Secp256k1 verification instance. + pub static ref SECP: Secp256k1 = Secp256k1::verification_only(); +} + +/// Event command in network format. +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct EventCmd { + // expecting static "EVENT" + cmd: String, + event: Event, +} + +impl EventCmd { + #[must_use] + pub fn event_id(&self) -> &str { + &self.event.id + } +} + +/// Parsed nostr event. +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Event { + pub id: String, + pub pubkey: String, + #[serde(skip)] + pub delegated_by: Option, + pub created_at: u64, + pub kind: u64, + #[serde(deserialize_with = "tag_from_string")] + // NOTE: array-of-arrays may need to be more general than a string container + pub tags: Vec>, + pub content: String, + pub sig: String, + // Optimization for tag search, built on demand. + #[serde(skip)] + pub tagidx: Option>>, +} + +/// Simple tag type for array of array of strings. +type Tag = Vec>; + +/// Deserializer that ensures we always have a [`Tag`]. +fn tag_from_string<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + +/// Attempt to form a single-char tag name. +#[must_use] +pub fn single_char_tagname(tagname: &str) -> Option { + // We return the tag character if and only if the tagname consists + // of a single char. + let mut tagnamechars = tagname.chars(); + let firstchar = tagnamechars.next(); + match firstchar { + Some(_) => { + // check second char + if tagnamechars.next().is_none() { + firstchar + } else { + None + } + } + None => None, + } +} + +pub enum EventWrapper { + WrappedEvent(Event), + WrappedAuth(Event), +} + +/// Convert network event to parsed/validated event. +impl From for Result { + fn from(ec: EventCmd) -> Result { + // ensure command is correct + if ec.cmd == "EVENT" { + ec.event.validate().map(|_| { + let mut e = ec.event; + e.build_index(); + e.update_delegation(); + WrappedEvent(e) + }) + } else if ec.cmd == "AUTH" { + // we don't want to validate the event here, because NIP-42 can be disabled + // it will be validated later during the authentication process + Ok(WrappedAuth(ec.event)) + } else { + Err(CommandUnknownError) + } + } +} + +impl Event { + #[cfg(test)] + #[must_use] + pub fn simple_event() -> Event { + Event { + id: "0".to_owned(), + pubkey: "0".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: vec![], + content: "".to_owned(), + sig: "0".to_owned(), + tagidx: None, + } + } + + #[must_use] + pub fn is_kind_metadata(&self) -> bool { + self.kind == 0 + } + + /// Should this event be persisted? + #[must_use] + pub fn is_ephemeral(&self) -> bool { + self.kind >= 20000 && self.kind < 30000 + } + + /// Is this event currently expired? + pub fn is_expired(&self) -> bool { + if let Some(exp) = self.expiration() { + exp <= unix_time() + } else { + false + } + } + + /// Determine the time at which this event should expire + pub fn expiration(&self) -> Option { + let default = "".to_string(); + let dvals: Vec<&String> = self + .tags + .iter() + .filter(|x| !x.is_empty()) + .filter(|x| x.first().unwrap() == "expiration") + .map(|x| x.get(1).unwrap_or(&default)) + .take(1) + .collect(); + let val_first = dvals.first(); + val_first.and_then(|t| t.parse::().ok()) + } + + /// Should this event be replaced with newer timestamps from same author? + #[must_use] + pub fn is_replaceable(&self) -> bool { + self.kind == 0 + || self.kind == 3 + || self.kind == 41 + || (self.kind >= 10000 && self.kind < 20000) + } + + /// Should this event be replaced with newer timestamps from same author, for distinct `d` tag values? + #[must_use] + pub fn is_param_replaceable(&self) -> bool { + self.kind >= 30000 && self.kind < 40000 + } + + /// Should this event be replaced with newer timestamps from same author, for distinct `d` tag values? + #[must_use] + pub fn distinct_param(&self) -> Option { + if self.is_param_replaceable() { + let default = "".to_string(); + let dvals: Vec<&String> = self + .tags + .iter() + .filter(|x| !x.is_empty()) + .filter(|x| x.first().unwrap() == "d") + .map(|x| x.get(1).unwrap_or(&default)) + .take(1) + .collect(); + let dval_first = dvals.first(); + match dval_first { + Some(_) => dval_first.map(|x| x.to_string()), + None => Some(default), + } + } else { + None + } + } + + /// Pull a NIP-05 Name out of the event, if one exists + #[must_use] + pub fn get_nip05_addr(&self) -> Option { + if self.is_kind_metadata() { + // very quick check if we should attempt to parse this json + if self.content.contains("\"nip05\"") { + // Parse into JSON + let md_parsed: Value = serde_json::from_str(&self.content).ok()?; + let md_map = md_parsed.as_object()?; + let nip05_str = md_map.get("nip05")?.as_str()?; + return nip05::Nip05Name::try_from(nip05_str).ok(); + } + } + None + } + + // is this event delegated (properly)? + // does the signature match, and are conditions valid? + // if so, return an alternate author for the event + #[must_use] + pub fn delegated_author(&self) -> Option { + // is there a delegation tag? + let delegation_tag: Vec = self + .tags + .iter() + .filter(|x| x.len() == 4) + .filter(|x| x.first().unwrap() == "delegation") + .take(1) + .next()? + .clone(); // get first tag + + //let delegation_tag = self.tag_values_by_name("delegation"); + // delegation tags should have exactly 3 elements after the name (pubkey, condition, sig) + // the event is signed by the delagatee + let delegatee = &self.pubkey; + // the delegation tag references the claimed delagator + let delegator: &str = delegation_tag.get(1)?; + let querystr: &str = delegation_tag.get(2)?; + let sig: &str = delegation_tag.get(3)?; + + // attempt to get a condition query; this requires the delegation to have a valid signature. + if let Some(cond_query) = validate_delegation(delegator, delegatee, querystr, sig) { + // The signature was valid, now we ensure the delegation + // condition is valid for this event: + if cond_query.allows_event(self) { + // since this is allowed, we will provide the delegatee + Some(delegator.into()) + } else { + debug!("an event failed to satisfy delegation conditions"); + None + } + } else { + debug!("event had had invalid delegation signature"); + None + } + } + + /// Update delegation status + pub fn update_delegation(&mut self) { + self.delegated_by = self.delegated_author(); + } + /// Build an event tag index + pub fn build_index(&mut self) { + // if there are no tags; just leave the index as None + if self.tags.is_empty() { + return; + } + // otherwise, build an index + let mut idx: HashMap> = HashMap::new(); + // iterate over tags that have at least 2 elements + for t in self.tags.iter().filter(|x| x.len() > 1) { + let tagname = t.first().unwrap(); + let tagnamechar_opt = single_char_tagname(tagname); + if tagnamechar_opt.is_none() { + continue; + } + let tagnamechar = tagnamechar_opt.unwrap(); + let tagval = t.get(1).unwrap(); + // ensure a vector exists for this tag + idx.entry(tagnamechar).or_default(); + // get the tag vec and insert entry + let idx_tag_vec = idx.get_mut(&tagnamechar).expect("could not get tag vector"); + idx_tag_vec.insert(tagval.clone()); + } + // save the tag structure + self.tagidx = Some(idx); + } + + /// Create a short event identifier, suitable for logging. + #[must_use] + pub fn get_event_id_prefix(&self) -> String { + self.id.chars().take(8).collect() + } + #[must_use] + pub fn get_author_prefix(&self) -> String { + self.pubkey.chars().take(8).collect() + } + + /// Retrieve tag initial values across all tags matching the name + #[must_use] + pub fn tag_values_by_name(&self, tag_name: &str) -> Vec { + self.tags + .iter() + .filter(|x| x.len() > 1) + .filter(|x| x.first().unwrap() == tag_name) + .map(|x| x.get(1).unwrap().clone()) + .collect() + } + + #[must_use] + pub fn is_valid_timestamp(&self, reject_future_seconds: Option) -> bool { + if let Some(allowable_future) = reject_future_seconds { + let curr_time = unix_time(); + // calculate difference, plus how far future we allow + if curr_time + (allowable_future as u64) < self.created_at { + let delta = self.created_at - curr_time; + debug!( + "event is too far in the future ({} seconds), rejecting", + delta + ); + return false; + } + } + true + } + + /// Check if this event has a valid signature. + pub fn validate(&self) -> Result<()> { + // TODO: return a Result with a reason for invalid events + // validation is performed by: + // * parsing JSON string into event fields + // * create an array: + // ** [0, pubkey-hex-string, created-at-num, kind-num, tags-array-of-arrays, content-string] + // * serialize with no spaces/newlines + let c_opt = self.to_canonical(); + if c_opt.is_none() { + debug!("could not canonicalize"); + return Err(EventCouldNotCanonicalize); + } + let c = c_opt.unwrap(); + // * compute the sha256sum. + let digest: sha256::Hash = sha256::Hash::hash(c.as_bytes()); + let hex_digest = format!("{digest:x}"); + // * ensure the id matches the computed sha256sum. + if self.id != hex_digest { + debug!("event id does not match digest"); + return Err(EventInvalidId); + } + // * validate the message digest (sig) using the pubkey & computed sha256 message hash. + let sig = schnorr::Signature::from_str(&self.sig).map_err(|_| EventInvalidSignature)?; + if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) { + if let Ok(pubkey) = XOnlyPublicKey::from_str(&self.pubkey) { + SECP.verify_schnorr(&sig, &msg, &pubkey) + .map_err(|_| EventInvalidSignature) + } else { + debug!("client sent malformed pubkey"); + Err(EventMalformedPubkey) + } + } else { + info!("error converting digest to secp256k1 message"); + Err(EventInvalidSignature) + } + } + + /// Convert event to canonical representation for signing. + pub fn to_canonical(&self) -> Option { + // create a JsonValue for each event element + let mut c: Vec = vec![]; + // id must be set to 0 + let id = Number::from(0_u64); + c.push(serde_json::Value::Number(id)); + // public key + c.push(Value::String(self.pubkey.clone())); + // creation time + let created_at = Number::from(self.created_at); + c.push(serde_json::Value::Number(created_at)); + // kind + let kind = Number::from(self.kind); + c.push(serde_json::Value::Number(kind)); + // tags + c.push(self.tags_to_canonical()); + // content + c.push(Value::String(self.content.clone())); + serde_json::to_string(&Value::Array(c)).ok() + } + + /// Convert tags to a canonical form for signing. + fn tags_to_canonical(&self) -> Value { + let mut tags = Vec::::new(); + // iterate over self tags, + for t in &self.tags { + // each tag is a vec of strings + let mut a = Vec::::new(); + for v in t.iter() { + a.push(serde_json::Value::String(v.clone())); + } + tags.push(serde_json::Value::Array(a)); + } + serde_json::Value::Array(tags) + } + + /// Determine if the given tag and value set intersect with tags in this event. + #[must_use] + pub fn generic_tag_val_intersect(&self, tagname: char, check: &TagOperand) -> bool { + match &self.tagidx { + // check if this is indexable tagname + Some(idx) => match idx.get(&tagname) { + Some(valset) => { + match &check { + TagOperand::And(v) => valset.intersection(v).count() == v.len(), + TagOperand::Or(v) => valset.intersection(v).count() > 0 + } + } + None => false, + }, + None => false, + } + } +} + +impl From for Event { + fn from(nostr_event: nostr::Event) -> Self { + Event { + id: nostr_event.id.to_hex(), + pubkey: nostr_event.pubkey.to_string(), + created_at: nostr_event.created_at.as_u64(), + kind: nostr_event.kind.as_u64(), + tags: nostr_event.tags.iter().map(|x| x.as_vec()).collect(), + content: nostr_event.content, + sig: nostr_event.sig.to_string(), + delegated_by: None, + tagidx: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn event_creation() { + // create an event + let event = Event::simple_event(); + assert_eq!(event.id, "0"); + } + + #[test] + fn event_serialize() -> Result<()> { + // serialize an event to JSON string + let event = Event::simple_event(); + let j = serde_json::to_string(&event)?; + assert_eq!(j, "{\"id\":\"0\",\"pubkey\":\"0\",\"created_at\":0,\"kind\":0,\"tags\":[],\"content\":\"\",\"sig\":\"0\"}"); + Ok(()) + } + + #[test] + fn empty_event_tag_match() { + let event = Event::simple_event(); + assert!(!event + .generic_tag_val_intersect('e', &TagOperand::Or(HashSet::from(["foo".to_owned(), "bar".to_owned()])))); + } + + #[test] + fn single_event_tag_match() { + let mut event = Event::simple_event(); + event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]]; + event.build_index(); + assert!( + event.generic_tag_val_intersect( + 'e', + &TagOperand::Or(HashSet::from(["foo".to_owned(), "bar".to_owned()])), + ) + ); + } + + #[test] + fn event_tags_serialize() -> Result<()> { + // serialize an event with tags to JSON string + let mut event = Event::simple_event(); + event.tags = vec![ + vec![ + "e".to_owned(), + "xxxx".to_owned(), + "wss://example.com".to_owned(), + ], + vec![ + "p".to_owned(), + "yyyyy".to_owned(), + "wss://example.com:3033".to_owned(), + ], + ]; + let j = serde_json::to_string(&event)?; + assert_eq!(j, "{\"id\":\"0\",\"pubkey\":\"0\",\"created_at\":0,\"kind\":0,\"tags\":[[\"e\",\"xxxx\",\"wss://example.com\"],[\"p\",\"yyyyy\",\"wss://example.com:3033\"]],\"content\":\"\",\"sig\":\"0\"}"); + Ok(()) + } + + #[test] + fn event_deserialize() -> Result<()> { + let raw_json = r#"{"id":"1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c","pubkey":"bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca","created_at":1612650459,"kind":1,"tags":null,"content":"hello world","sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}"#; + let e: Event = serde_json::from_str(raw_json)?; + assert_eq!(e.kind, 1); + assert_eq!(e.tags.len(), 0); + Ok(()) + } + + #[test] + fn event_canonical() { + let e = Event { + id: "999".to_owned(), + pubkey: "012345".to_owned(), + delegated_by: None, + created_at: 501_234, + kind: 1, + tags: vec![], + content: "this is a test".to_owned(), + sig: "abcde".to_owned(), + tagidx: None, + }; + let c = e.to_canonical(); + let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned()); + assert_eq!(c, expected); + } + + #[test] + fn event_tag_select() { + let e = Event { + id: "999".to_owned(), + pubkey: "012345".to_owned(), + delegated_by: None, + created_at: 501_234, + kind: 1, + tags: vec![ + vec!["j".to_owned(), "abc".to_owned()], + vec!["e".to_owned(), "foo".to_owned()], + vec!["e".to_owned(), "bar".to_owned()], + vec!["e".to_owned(), "baz".to_owned()], + vec![ + "p".to_owned(), + "aaaa".to_owned(), + "ws://example.com".to_owned(), + ], + ], + content: "this is a test".to_owned(), + sig: "abcde".to_owned(), + tagidx: None, + }; + let v = e.tag_values_by_name("e"); + assert_eq!(v, vec!["foo", "bar", "baz"]); + } + + #[test] + fn event_no_tag_select() { + let e = Event { + id: "999".to_owned(), + pubkey: "012345".to_owned(), + delegated_by: None, + created_at: 501_234, + kind: 1, + tags: vec![ + vec!["j".to_owned(), "abc".to_owned()], + vec!["e".to_owned(), "foo".to_owned()], + vec!["e".to_owned(), "baz".to_owned()], + vec![ + "p".to_owned(), + "aaaa".to_owned(), + "ws://example.com".to_owned(), + ], + ], + content: "this is a test".to_owned(), + sig: "abcde".to_owned(), + tagidx: None, + }; + let v = e.tag_values_by_name("x"); + // asking for tags that don't exist just returns zero-length vector + assert_eq!(v.len(), 0); + } + + #[test] + fn event_canonical_with_tags() { + let e = Event { + id: "999".to_owned(), + pubkey: "012345".to_owned(), + delegated_by: None, + created_at: 501_234, + kind: 1, + tags: vec![ + vec!["#e".to_owned(), "aoeu".to_owned()], + vec![ + "#p".to_owned(), + "aaaa".to_owned(), + "ws://example.com".to_owned(), + ], + ], + content: "this is a test".to_owned(), + sig: "abcde".to_owned(), + tagidx: None, + }; + let c = e.to_canonical(); + let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###; + let expected = Some(expected_json.to_owned()); + assert_eq!(c, expected); + } + + #[test] + fn ephemeral_event() { + let mut event = Event::simple_event(); + event.kind = 20000; + assert!(event.is_ephemeral()); + event.kind = 29999; + assert!(event.is_ephemeral()); + event.kind = 30000; + assert!(!event.is_ephemeral()); + event.kind = 19999; + assert!(!event.is_ephemeral()); + } + + #[test] + fn replaceable_event() { + let mut event = Event::simple_event(); + event.kind = 0; + assert!(event.is_replaceable()); + event.kind = 3; + assert!(event.is_replaceable()); + event.kind = 10000; + assert!(event.is_replaceable()); + event.kind = 19999; + assert!(event.is_replaceable()); + event.kind = 20000; + assert!(!event.is_replaceable()); + } + + #[test] + fn param_replaceable_event() { + let mut event = Event::simple_event(); + event.kind = 30000; + assert!(event.is_param_replaceable()); + event.kind = 39999; + assert!(event.is_param_replaceable()); + event.kind = 29999; + assert!(!event.is_param_replaceable()); + event.kind = 40000; + assert!(!event.is_param_replaceable()); + } + + #[test] + fn param_replaceable_value_case_1() { + // NIP case #1: "tags":[["d",""]] + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![vec!["d".to_owned(), "".to_owned()]]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_2() { + // NIP case #2: "tags":[]: implicit d tag with empty value + let mut event = Event::simple_event(); + event.kind = 30000; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_3() { + // NIP case #3: "tags":[["d"]]: implicit empty value "" + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![vec!["d".to_owned()]]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_4() { + // NIP case #4: "tags":[["d",""],["d","not empty"]]: only first d tag is considered + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned(), "".to_string()], + vec!["d".to_owned(), "not empty".to_string()], + ]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_4b() { + // Variation of #4 with + // NIP case #4: "tags":[["d","not empty"],["d",""]]: only first d tag is considered + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned(), "not empty".to_string()], + vec!["d".to_owned(), "".to_string()], + ]; + assert_eq!(event.distinct_param(), Some("not empty".to_string())); + } + + #[test] + fn param_replaceable_value_case_5() { + // NIP case #5: "tags":[["d"],["d","some value"]]: only first d tag is considered + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned()], + vec!["d".to_owned(), "second value".to_string()], + vec!["d".to_owned(), "third value".to_string()], + ]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_6() { + // NIP case #6: "tags":[["e"]]: same as no tags + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![vec!["e".to_owned()]]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn expiring_event_none() { + // regular events do not expire + let mut event = Event::simple_event(); + event.kind = 7; + event.tags = vec![vec!["test".to_string(), "foo".to_string()]]; + assert_eq!(event.expiration(), None); + } + + #[test] + fn expiring_event_empty() { + // regular events do not expire + let mut event = Event::simple_event(); + event.kind = 7; + event.tags = vec![vec!["expiration".to_string()]]; + assert_eq!(event.expiration(), None); + } + + #[test] + fn expiring_event_future() { + // a normal expiring event + let exp: u64 = 1676264138; + let mut event = Event::simple_event(); + event.kind = 1; + event.tags = vec![vec!["expiration".to_string(), exp.to_string()]]; + assert_eq!(event.expiration(), Some(exp)); + } + + #[test] + fn expiring_event_negative() { + // expiration set to a negative value (invalid) + let exp: i64 = -90; + let mut event = Event::simple_event(); + event.kind = 1; + event.tags = vec![vec!["expiration".to_string(), exp.to_string()]]; + assert_eq!(event.expiration(), None); + } + + #[test] + fn expiring_event_zero() { + // a normal expiring event set to zero + let exp: i64 = 0; + let mut event = Event::simple_event(); + event.kind = 1; + event.tags = vec![vec!["expiration".to_string(), exp.to_string()]]; + assert_eq!(event.expiration(), Some(0)); + } + + #[test] + fn expiring_event_fraction() { + // expiration is fractional (invalid) + let exp: f64 = 23.334; + let mut event = Event::simple_event(); + event.kind = 1; + event.tags = vec![vec!["expiration".to_string(), exp.to_string()]]; + assert_eq!(event.expiration(), None); + } + + #[test] + fn expiring_event_multiple() { + // multiple values, we just take the first + let mut event = Event::simple_event(); + event.kind = 1; + event.tags = vec![ + vec!["expiration".to_string(), (10).to_string()], + vec!["expiration".to_string(), (20).to_string()], + ]; + assert_eq!(event.expiration(), Some(10)); + } +} diff --git a/src/exit.rs b/src/exit.rs new file mode 100644 index 0000000..9acb414 --- /dev/null +++ b/src/exit.rs @@ -0,0 +1,86 @@ +//! Co-located mixnet exit supervisor (Floonet addition). +//! +//! When `[exit] enabled = true`, the relay runs the bundled +//! `floonet-mixexit` binary alongside itself and keeps it running. The +//! exit is a scoped pipe: it joins the mixnet as an ordinary unbonded +//! client and forwards every accepted stream to ONE fixed upstream (this +//! relay), never a caller-chosen target, so it is structurally not an +//! open proxy and the operator needs no exit policy. +//! +//! The exit's mixnet identity persists in `exit.data_dir`, so its mixnet +//! address is STABLE across restarts; the binary prints it at startup and +//! writes it to `/nym_address.txt`. Publish that address (for +//! example in the Floonet relay pool `exit` field) so wallets can prefer +//! it and fall back to the public mixnet path when it is down. +//! +//! Wallets run hostname-validated TLS end to end THROUGH the pipe, so the +//! exit only ever sees ciphertext. Point `exit.upstream` at your public +//! TLS endpoint (e.g. `relay.example.com:443`) so the certificate the +//! wallet sees over the mixnet matches the one it pins. + +use crate::config::Settings; +use crate::error::{Error, Result}; +use std::path::Path; +use std::time::Duration; +use tracing::{error, info, warn}; + +/// Validate the exit configuration at startup: fail fast on a bad toggle +/// instead of silently running without the exit. +pub fn validate(settings: &Settings) -> Result<()> { + if !settings.exit.enabled { + return Ok(()); + } + if !Path::new(&settings.exit.binary).is_file() { + let msg = format!( + "exit.enabled is true but exit.binary `{}` does not exist; \ + install floonet-mixexit or disable the exit", + settings.exit.binary + ); + error!("{msg}"); + return Err(Error::CustomError(msg)); + } + Ok(()) +} + +/// Spawn the supervision task. Must be called from within the tokio +/// runtime. The child is restarted with a backoff if it exits; it is +/// killed when the relay shuts down (kill-on-drop). +pub fn spawn(settings: &Settings) { + if !settings.exit.enabled { + return; + } + let binary = settings.exit.binary.clone(); + let data_dir = settings.exit.data_dir.clone(); + let upstream = if settings.exit.upstream.is_empty() { + format!("127.0.0.1:{}", settings.network.port) + } else { + settings.exit.upstream.clone() + }; + info!( + "mixnet exit enabled: supervising {} (upstream {}, identity in {})", + binary, upstream, data_dir + ); + tokio::spawn(async move { + loop { + let child = tokio::process::Command::new(&binary) + .env("FLOONET_MIXEXIT_DIR", &data_dir) + .env("FLOONET_EXIT_UPSTREAM", &upstream) + .kill_on_drop(true) + .spawn(); + match child { + Ok(mut child) => match child.wait().await { + Ok(status) => { + warn!("mixnet exit process ended ({status}); restarting in 10s"); + } + Err(e) => { + warn!("mixnet exit process wait failed ({e}); restarting in 10s"); + } + }, + Err(e) => { + error!("mixnet exit failed to start ({e}); retrying in 10s"); + } + } + tokio::time::sleep(Duration::from_secs(10)).await; + } + }); +} diff --git a/src/info.rs b/src/info.rs new file mode 100644 index 0000000..14e9693 --- /dev/null +++ b/src/info.rs @@ -0,0 +1,108 @@ +//! Relay metadata using NIP-11 +/// Relay Info +use crate::config::Settings; +use serde::{Deserialize, Serialize}; + +pub const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); +pub const UNIT: &str = "msats"; + +/// Limitations of the relay as specified in NIP-111 +/// (This nip isn't finalized so may change) +#[derive(Debug, Serialize, Deserialize)] +#[allow(unused)] +pub struct Limitation { + #[serde(skip_serializing_if = "Option::is_none")] + payment_required: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + restricted_writes: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(unused)] +pub struct Fees { + #[serde(skip_serializing_if = "Option::is_none")] + admission: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + publication: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(unused)] +pub struct Fee { + amount: u64, + unit: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[allow(unused)] +pub struct RelayInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub contact: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_nips: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub software: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub limitation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fees: Option, +} + +/// Convert an Info configuration into public Relay Info +impl From for RelayInfo { + fn from(c: Settings) -> Self { + let mut supported_nips = vec![1, 2, 9, 11, 12, 15, 16, 20, 22, 33, 40]; + + if c.authorization.nip42_auth { + supported_nips.push(42); + supported_nips.sort(); + } + + let i = c.info; + + // Floonet rule: the public relay information document never + // mentions payments, fees, or a payment URL. The relay only ever + // sees opaque gift-wrapped ciphertext, so payment wording would be + // both inaccurate and an operational liability. + let limitations = Limitation { + payment_required: None, + restricted_writes: Some( + c.pay_to_relay.enabled + || c.verified_users.is_enabled() + || c.authorization.pubkey_whitelist.is_some() + || c.authorization.require_auth_to_write + || c.grpc.restricts_write, + ), + }; + + RelayInfo { + id: i.relay_url, + name: i.name, + description: i.description, + pubkey: i.pubkey, + contact: i.contact, + supported_nips: Some(supported_nips), + software: Some("https://floonet.dev/floonet-rs".to_owned()), + version: CARGO_PKG_VERSION.map(std::borrow::ToOwned::to_owned), + limitation: Some(limitations), + payment_url: None, + fees: None, + icon: i.relay_icon, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4d6762b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +pub mod cli; +pub mod close; +pub mod config; +pub mod conn; +pub mod db; +pub mod delegation; +pub mod error; +pub mod event; +pub mod info; +pub mod admission; +pub mod exit; +pub mod name_authority; +pub mod nauthz; +pub mod nip05; +pub mod notice; +pub mod repo; +pub mod subscription; +pub mod utils; +// Public API for creating relays programmatically +pub mod payment; +pub mod server; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d68b2f2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,112 @@ +//! Server process +use clap::Parser; +use console_subscriber::ConsoleLayer; +use floonet_rs::cli::CLIArgs; +use floonet_rs::config; +use floonet_rs::server::start_server; +use std::fs; +use std::path::Path; +use std::process; +use std::sync::mpsc as syncmpsc; +use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender}; +use std::thread; +#[cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))] +use tikv_jemallocator::Jemalloc; +use tracing::info; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::EnvFilter; + +#[cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))] +#[global_allocator] +static GLOBAL: Jemalloc = Jemalloc; + +/// Start running a Nostr relay server. +fn main() { + let args = CLIArgs::parse(); + + // get config file name from args + let config_file_arg = args.config; + + // Ensure the config file is readable if it was explicitly set + if let Some(config_path) = config_file_arg.as_ref() { + let path = Path::new(&config_path); + if !path.exists() { + eprintln!("Config file not found: {}", &config_path); + process::exit(1); + } + if !path.is_file() { + eprintln!("Invalid config file path: {}", &config_path); + process::exit(1); + } + if let Err(err) = fs::metadata(path) { + eprintln!("Error while accessing file metadata: {}", err); + process::exit(1); + } + if let Err(err) = fs::File::open(path) { + eprintln!("Config file is not readable: {}", err); + process::exit(1); + } + } + + let mut _log_guard: Option = None; + + // configure settings from the config file (defaults to config.toml) + // replace default settings with those read from the config file + let mut settings = config::Settings::new(&config_file_arg).unwrap_or_else(|e| { + eprintln!("Error reading config file ({:?})", e); + process::exit(1); + }); + + // setup tracing + if settings.diagnostics.tracing { + // enable tracing with tokio-console + ConsoleLayer::builder().with_default_env().init(); + } else { + // standard logging + if let Some(path) = &settings.logging.folder_path { + // write logs to a folder + let prefix = match &settings.logging.file_prefix { + Some(p) => p.as_str(), + None => "relay", + }; + let file_appender = tracing_appender::rolling::daily(path, prefix); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + let filter = EnvFilter::from_default_env(); + // assign to a variable that is not dropped till the program ends + _log_guard = Some(guard); + + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(non_blocking) + .try_init() + .unwrap(); + } else { + // write to stdout + tracing_subscriber::fmt::try_init().unwrap(); + } + } + info!("Starting up from main"); + + // get database directory from args + let db_dir_arg = args.db; + + // update with database location from args, if provided + if let Some(db_dir) = db_dir_arg { + settings.database.data_directory = db_dir; + } + // we should have a 'control plane' channel to monitor and bump + // the server. this will let us do stuff like clear the database, + // shutdown, etc.; for now all this does is initiate shutdown if + // `()` is sent. This will change in the future, this is just a + // stopgap to shutdown the relay when it is used as a library. + let (_, ctrl_rx): (MpscSender<()>, MpscReceiver<()>) = syncmpsc::channel(); + // run this in a new thread + let handle = thread::spawn(move || { + if let Err(e) = start_server(&settings, ctrl_rx) { + eprintln!("server terminated with error: {e}"); + process::exit(1); + } + }); + // block on nostr thread to finish. + handle.join().unwrap(); +} diff --git a/src/name_authority/mod.rs b/src/name_authority/mod.rs new file mode 100644 index 0000000..f9baade --- /dev/null +++ b/src/name_authority/mod.rs @@ -0,0 +1,791 @@ +//! Built-in name authority (Floonet addition). +//! +//! `name@domain` NIP-05 resolution with NIP-98 authenticated self-service +//! registration, ported from goblin-nip05d and served in-process on the +//! relay's own HTTP listener. Claims live in the relay database +//! (`name_claims` table, sqlite engine); everything else (rate limits, +//! replay window, cooldowns) is in-memory and resets on restart. +//! +//! Endpoints: +//! * `GET /.well-known/nostr.json?name=` NIP-05 resolution +//! * `POST /api/v1/register` claim a name (NIP-98) +//! * `DELETE /api/v1/register/{name}` release a name (NIP-98) +//! * `GET /api/v1/name/{name}` availability +//! * `GET /api/v1/profile/{name}` name -> pubkey +//! * `GET /api/v1/by-pubkey/{pubkey}` pubkey -> name (reverse) +//! * `GET /api/v1/health` liveness +//! +//! Paid names: when `goblinpay.pay_mode = "name"`, a first-time claim +//! returns `402 {"error":"payment_required", "pay_url": ...}` carrying a +//! GoblinPay invoice. Once the payment confirms on chain the same +//! register call succeeds. Payment state reuses the relay's existing +//! `account`/`invoice` tables and the `PaymentProcessor` trait. + +pub mod names; +pub mod nip98; + +use crate::config::Settings; +use crate::error::{Error, Result}; +use crate::payment::goblinpay::GoblinPayPaymentProcessor; +use crate::payment::{InvoiceStatus, PaymentProcessor}; +use crate::repo::NostrRepo; +use crate::utils::unix_time; +use hyper::body::HttpBody; +use hyper::{Body, Method, Request, Response, StatusCode}; +use nostr::key::FromPkStr; +use nostr::Keys; +use serde_json::json; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::path::Path; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::{Duration, Instant}; +use tracing::{error, info, warn}; + +/// Largest register body we accept (fail closed on anything bigger). +const MAX_BODY_BYTES: u64 = 8192; + +/// True when this path belongs to the name authority. +#[must_use] +pub fn is_authority_path(path: &str) -> bool { + path == "/.well-known/nostr.json" || path.starts_with("/api/v1/") +} + +/// Resolved paid-names state. +enum PaidNames { + Free, + Paid { + processor: Arc, + price_nanogrin: u64, + price_grin: f64, + }, +} + +pub struct Authority { + cfg: crate::config::NameAuthority, + /// Relays advertised in `/.well-known/nostr.json`. + relays: Vec, + /// Operator domain labels + reserved-file names. + extra_reserved: Vec, + /// Header carrying the real client IP (set by the reverse proxy). + remote_ip_header: Option, + /// Claims store: an extra connection to the relay's own sqlite DB + /// (the `name_claims` table is created by the relay migration). + db: Mutex, + /// Per-IP sliding windows and per-pubkey cooldowns. + rate: Mutex>>, + /// Seen NIP-98 auth event ids (one-time use in the freshness window). + seen_auth: Mutex>, + /// Relay repository, reused for paid-name account/invoice state. + repo: Arc, + paid: PaidNames, +} + +impl Authority { + /// Build the authority from settings. The relay migration has already + /// created the `name_claims` table by the time this runs. + pub fn new(settings: &Settings, repo: Arc) -> Result { + let cfg = settings.name_authority.clone(); + let db_path = Path::new(&settings.database.data_directory).join(crate::db::DB_FILE); + let conn = rusqlite::Connection::open(db_path).map_err(Error::SqlError)?; + conn.busy_timeout(Duration::from_secs(5)) + .map_err(Error::SqlError)?; + // The relay migration (v19) creates this table for file-backed + // databases; applying the same idempotent DDL here keeps the + // authority working when the relay runs an in-memory event store. + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS name_claims ( + name TEXT PRIMARY KEY, + pubkey TEXT NOT NULL, + created_at INTEGER NOT NULL, + released_at INTEGER + ); + CREATE INDEX IF NOT EXISTS name_claims_pubkey_index ON name_claims(pubkey); + CREATE UNIQUE INDEX IF NOT EXISTS name_claims_active_pubkey + ON name_claims(pubkey) WHERE released_at IS NULL;", + ) + .map_err(Error::SqlError)?; + + let relays = match &cfg.relays { + Some(relays) if !relays.is_empty() => relays.clone(), + _ => settings.info.relay_url.iter().cloned().collect(), + }; + + // Reserve the operator's own domain labels, then any names from + // the optional reserved file. + let mut extra_reserved = names::domain_reserved(&cfg.domain); + if let Some(path) = cfg.reserved_file.as_ref().filter(|p| !p.is_empty()) { + let text = std::fs::read_to_string(path).map_err(|e| { + Error::CustomError(format!("name_authority.reserved_file `{path}` unreadable: {e}")) + })?; + extra_reserved.extend( + text.lines() + .map(str::trim) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .map(str::to_lowercase), + ); + } + + let paid = if settings.goblinpay.pay_mode == "name" { + info!( + "name authority: paid names enabled ({} GRIN per name)", + settings.goblinpay.name_price_grin + ); + PaidNames::Paid { + processor: Arc::new(GoblinPayPaymentProcessor::new( + &settings.goblinpay.url, + &settings.goblinpay.api_token, + )), + price_nanogrin: settings.goblinpay.name_price_nanogrin(), + price_grin: settings.goblinpay.name_price_grin, + } + } else { + PaidNames::Free + }; + + info!( + "name authority enabled: domain={} base_url={} relays={:?} names {}..={} chars", + cfg.domain, cfg.base_url, relays, cfg.name_min, cfg.name_max + ); + + Ok(Authority { + cfg, + relays, + extra_reserved, + remote_ip_header: settings.network.remote_ip_header.clone(), + db: Mutex::new(conn), + rate: Mutex::new(HashMap::new()), + seen_auth: Mutex::new(HashMap::new()), + repo, + paid, + }) + } + + // ------------------------------------------------------------------ + // Claims store + // ------------------------------------------------------------------ + + /// Active (non-released) pubkey for a name. + fn lookup(&self, name: &str) -> Option { + self.db + .lock() + .unwrap() + .query_row( + "SELECT pubkey FROM name_claims WHERE name = ?1 AND released_at IS NULL", + [name], + |r| r.get::<_, String>(0), + ) + .ok() + } + + /// Active name owned by a pubkey. + fn name_of(&self, pubkey: &str) -> Option { + self.db + .lock() + .unwrap() + .query_row( + "SELECT name FROM name_claims WHERE pubkey = ?1 AND released_at IS NULL", + [pubkey], + |r| r.get::<_, String>(0), + ) + .ok() + } + + // ------------------------------------------------------------------ + // Rate limiting / replay / cooldowns (in-memory, reset on restart) + // ------------------------------------------------------------------ + + /// Record a NIP-98 auth event id as used; false if replayed. + fn auth_event_fresh(&self, event_id: &str) -> bool { + let now = Instant::now(); + let window = Duration::from_secs(self.cfg.auth_max_age_secs.max(0) as u64 + 5); + let mut seen = self.seen_auth.lock().unwrap(); + seen.retain(|_, t| now.duration_since(*t) < window); + if seen.contains_key(event_id) { + return false; + } + seen.insert(event_id.to_string(), now); + true + } + + /// True when an operation in this bucket happened within the window. + fn cooldown_active(&self, bucket: &str, key: &str, window: Duration) -> bool { + let k = format!("{bucket}:{key}"); + let now = Instant::now(); + let mut map = self.rate.lock().unwrap(); + if let Some(hits) = map.get_mut(&k) { + hits.retain(|t| now.duration_since(*t) < window); + return !hits.is_empty(); + } + false + } + + /// Record a completed operation for cooldown tracking. + fn record_op(&self, bucket: &str, key: &str) { + let k = format!("{bucket}:{key}"); + self.rate + .lock() + .unwrap() + .entry(k) + .or_default() + .push(Instant::now()); + } + + /// Sliding-window per-IP limiter; true when the call is allowed. + fn allow(&self, bucket: &str, ip: &str, max: usize, window: Duration) -> bool { + let key = format!("{bucket}:{ip}"); + let now = Instant::now(); + let mut map = self.rate.lock().unwrap(); + let hits = map.entry(key).or_default(); + hits.retain(|t| now.duration_since(*t) < window); + if hits.len() >= max { + return false; + } + hits.push(now); + // Opportunistic global cleanup to bound memory. + if map.len() > 50_000 { + map.retain(|_, v| v.iter().any(|t| now.duration_since(*t) < window)); + } + true + } + + fn allow_read(&self, ip: &str) -> bool { + self.allow( + "na-read", + ip, + self.cfg.read_rate_max, + Duration::from_secs(self.cfg.read_rate_window_secs), + ) + } + + fn allow_write(&self, bucket: &str, ip: &str) -> bool { + self.allow( + bucket, + ip, + self.cfg.write_rate_max, + Duration::from_secs(self.cfg.write_rate_window_secs), + ) + } + + /// Client IP for rate limiting: the configured proxy header when + /// present (load-bearing behind a reverse proxy), else the socket. + fn client_ip(&self, request: &Request, remote_addr: &SocketAddr) -> String { + self.remote_ip_header + .as_ref() + .and_then(|h| request.headers().get(h.as_str())) + .and_then(|v| v.to_str().ok()) + .map(str::to_string) + .unwrap_or_else(|| remote_addr.ip().to_string()) + } + + // ------------------------------------------------------------------ + // HTTP dispatch + // ------------------------------------------------------------------ + + /// Handle one authority request. Callers route here for any path + /// where [`is_authority_path`] is true. + pub async fn handle( + self: &Arc, + request: Request, + remote_addr: SocketAddr, + ) -> Response { + let ip = self.client_ip(&request, &remote_addr); + let method = request.method().clone(); + let path = request.uri().path().to_string(); + match (method, path.as_str()) { + (Method::GET, "/.well-known/nostr.json") => self.well_known(&request, &ip), + (Method::GET, "/api/v1/health") => text_response(StatusCode::OK, "ok"), + (Method::GET, p) if p.starts_with("/api/v1/name/") => { + self.availability(strip(p, "/api/v1/name/"), &ip) + } + (Method::GET, p) if p.starts_with("/api/v1/profile/") => { + self.profile(strip(p, "/api/v1/profile/"), &ip) + } + (Method::GET, p) if p.starts_with("/api/v1/by-pubkey/") => { + self.by_pubkey(strip(p, "/api/v1/by-pubkey/"), &ip) + } + (Method::POST, "/api/v1/register") => self.register(request, &ip).await, + (Method::DELETE, p) if p.starts_with("/api/v1/register/") => { + self.unregister(strip(p, "/api/v1/register/"), &request, &ip) + } + _ => json_response(StatusCode::NOT_FOUND, json!({"error": "not found"})), + } + } + + // ------------------------------------------------------------------ + // Read endpoints + // ------------------------------------------------------------------ + + fn well_known(&self, request: &Request, ip: &str) -> Response { + if !self.allow_read(ip) { + return rate_limited(); + } + let mut result_names = serde_json::Map::new(); + let mut result_relays = serde_json::Map::new(); + if let Some(name) = query_param(request, "name").map(|n| n.to_lowercase()) { + if names::valid_name(&name, self.cfg.name_min, self.cfg.name_max) { + if let Some(pk) = self.lookup(&name) { + result_names.insert(name, json!(pk.clone())); + result_relays.insert(pk, json!(self.relays)); + } + } + } + json_response( + StatusCode::OK, + json!({ "names": result_names, "relays": result_relays }), + ) + } + + fn availability(&self, name: &str, ip: &str) -> Response { + if !self.allow_read(ip) { + return rate_limited(); + } + let name = name.to_lowercase(); + if !names::valid_name(&name, self.cfg.name_min, self.cfg.name_max) { + return json_response( + StatusCode::OK, + json!({"name": name, "available": false, "reason": "invalid"}), + ); + } + if names::is_reserved(&name, &self.extra_reserved) { + return json_response( + StatusCode::OK, + json!({"name": name, "available": false, "reason": "reserved"}), + ); + } + if self.lookup(&name).is_some() { + return json_response( + StatusCode::OK, + json!({"name": name, "available": false, "reason": "taken"}), + ); + } + json_response(StatusCode::OK, json!({"name": name, "available": true})) + } + + fn profile(&self, name: &str, ip: &str) -> Response { + if !self.allow_read(ip) { + return rate_limited(); + } + let name = name.to_lowercase(); + if !names::valid_name(&name, self.cfg.name_min, self.cfg.name_max) { + return json_response(StatusCode::NOT_FOUND, json!({"error": "not found"})); + } + match self.lookup(&name) { + Some(pubkey) => { + json_response(StatusCode::OK, json!({"name": name, "pubkey": pubkey})) + } + None => json_response(StatusCode::NOT_FOUND, json!({"error": "not found"})), + } + } + + fn by_pubkey(&self, pubkey: &str, ip: &str) -> Response { + if !self.allow_read(ip) { + return rate_limited(); + } + let pubkey = pubkey.to_lowercase(); + if !names::valid_pubkey_hex(&pubkey) { + return json_response(StatusCode::NOT_FOUND, json!({"error": "not found"})); + } + match self.name_of(&pubkey) { + Some(name) => { + json_response(StatusCode::OK, json!({"name": name, "pubkey": pubkey})) + } + None => json_response(StatusCode::NOT_FOUND, json!({"error": "not found"})), + } + } + + // ------------------------------------------------------------------ + // Write endpoints (NIP-98 authenticated) + // ------------------------------------------------------------------ + + async fn register(&self, request: Request, ip: &str) -> Response { + if !self.allow_write("na-reg", ip) { + return rate_limited(); + } + // Fail closed on oversized bodies before buffering anything. + if request + .body() + .size_hint() + .upper() + .map_or(true, |n| n > MAX_BODY_BYTES) + { + return json_response( + StatusCode::PAYLOAD_TOO_LARGE, + json!({"error": "body too large"}), + ); + } + let auth_header = request + .headers() + .get(hyper::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .map(str::to_string); + let body = match hyper::body::to_bytes(request.into_body()).await { + Ok(b) if (b.len() as u64) <= MAX_BODY_BYTES => b, + _ => { + return json_response( + StatusCode::BAD_REQUEST, + json!({"error": "invalid body"}), + ) + } + }; + + let (auth_pubkey, auth_id) = match nip98::verify_nip98( + auth_header.as_deref(), + "POST", + "/api/v1/register", + &body, + &self.cfg.base_url, + self.cfg.auth_max_age_secs, + ) { + Ok(v) => v, + Err(msg) => return json_response(StatusCode::UNAUTHORIZED, json!({"error": msg})), + }; + if !self.auth_event_fresh(&auth_id) { + return json_response( + StatusCode::UNAUTHORIZED, + json!({"error": "auth event replayed"}), + ); + } + + // The cooldown is armed by a *release*, not a claim: it blocks + // registering a new name for the window after letting one go + // (anti-churn). Checked after auth so strangers cannot probe it. + if self.cooldown_active( + "na-namechange", + &auth_pubkey, + Duration::from_secs(self.cfg.name_change_cooldown_secs), + ) { + return json_response( + StatusCode::TOO_MANY_REQUESTS, + json!({"error": "name_change_cooldown"}), + ); + } + + #[derive(serde::Deserialize)] + struct RegisterBody { + name: String, + pubkey: String, + } + let req: RegisterBody = match serde_json::from_slice(&body) { + Ok(r) => r, + Err(_) => { + return json_response(StatusCode::BAD_REQUEST, json!({"error": "invalid body"})) + } + }; + let name = req.name.to_lowercase(); + let pubkey = req.pubkey.to_lowercase(); + + if !names::valid_pubkey_hex(&pubkey) { + return json_response(StatusCode::BAD_REQUEST, json!({"error": "invalid pubkey"})); + } + if pubkey != auth_pubkey { + return json_response( + StatusCode::UNAUTHORIZED, + json!({"error": "auth pubkey does not match body pubkey"}), + ); + } + if !names::valid_name(&name, self.cfg.name_min, self.cfg.name_max) { + return json_response(StatusCode::BAD_REQUEST, json!({"error": "invalid name"})); + } + if names::is_reserved(&name, &self.extra_reserved) { + return json_response(StatusCode::FORBIDDEN, json!({"error": "name reserved"})); + } + + // Existing active registration of this exact name. + if let Some(owner) = self.lookup(&name) { + if owner == pubkey { + return json_response( + StatusCode::OK, + json!({"name": name, "nip05": format!("{name}@{}", self.cfg.domain)}), + ); + } + return json_response(StatusCode::CONFLICT, json!({"error": "name taken"})); + } + // One active name per pubkey. + if let Some(existing) = self.name_of(&pubkey) { + return json_response( + StatusCode::CONFLICT, + json!({"error": "pubkey already has a name", "name": existing}), + ); + } + + // Paid names: the claim only proceeds once this pubkey has a + // confirmed payment. All validity checks ran first, so nobody is + // asked to pay for an unclaimable name. + if let Some(resp) = self.paid_gate(&pubkey).await { + return resp; + } + + // INSERT guarded by the name PRIMARY KEY and the partial-unique + // pubkey index. The ON CONFLICT(name) only revives a released + // name; a concurrent double-register is caught by the unique + // index and surfaces as a constraint error -> 409. + let res = self.db.lock().unwrap().execute( + "INSERT INTO name_claims (name, pubkey, created_at) VALUES (?1, ?2, ?3) + ON CONFLICT(name) DO UPDATE SET pubkey = excluded.pubkey, + created_at = excluded.created_at, released_at = NULL + WHERE name_claims.released_at IS NOT NULL", + rusqlite::params![name, pubkey, unix_time()], + ); + match res { + // rows == 0 means the ON CONFLICT no-op fired (name already + // active): report a conflict rather than a false success. + Ok(0) => json_response(StatusCode::CONFLICT, json!({"error": "name taken"})), + Ok(_) => { + // Claiming must not arm a cooldown; only release does. + info!("name authority: registered {name} -> {pubkey}"); + json_response( + StatusCode::CREATED, + json!({"name": name, "nip05": format!("{name}@{}", self.cfg.domain)}), + ) + } + Err(rusqlite::Error::SqliteFailure(e, _)) + if e.code == rusqlite::ErrorCode::ConstraintViolation => + { + json_response( + StatusCode::CONFLICT, + json!({"error": "pubkey already has a name"}), + ) + } + Err(e) => { + error!("name authority: db insert failed: {e}"); + json_response( + StatusCode::INTERNAL_SERVER_ERROR, + json!({"error": "db error"}), + ) + } + } + } + + /// The paid gate. Returns None when the claim may proceed (free mode, + /// or this pubkey has a confirmed payment); otherwise the 402/5xx + /// response to send. Payment admits the PUBKEY (relay `account` row), + /// and each invoice is a GoblinPay invoice checked against the + /// GoblinPay server, which only reports paid after on-chain + /// confirmation. Fail closed: any error refuses the claim. + async fn paid_gate(&self, pubkey: &str) -> Option> { + let PaidNames::Paid { + processor, + price_nanogrin, + price_grin, + } = &self.paid + else { + return None; + }; + let keys = match Keys::from_pk_str(pubkey) { + Ok(k) => k, + Err(_) => { + return Some(json_response( + StatusCode::BAD_REQUEST, + json!({"error": "invalid pubkey"}), + )) + } + }; + + // Already paid? + if let Ok((admitted, _)) = self.repo.get_account_balance(&keys).await { + if admitted { + return None; + } + } + + // Outstanding invoice? Poll GoblinPay for its status. + if let Ok(Some(invoice)) = self.repo.get_unpaid_invoice(&keys).await { + return match processor.check_invoice(&invoice.payment_hash).await { + Ok(InvoiceStatus::Paid) => { + if self + .repo + .update_invoice(&invoice.payment_hash, InvoiceStatus::Paid) + .await + .is_err() + || self.repo.admit_account(&keys, *price_nanogrin).await.is_err() + { + return Some(server_error()); + } + info!("name authority: payment confirmed for {pubkey}"); + None + } + Ok(InvoiceStatus::Unpaid) => Some(payment_required( + &invoice.payment_hash, + &invoice.bolt11, + *price_grin, + *price_nanogrin, + )), + Ok(InvoiceStatus::Expired) => { + self.repo + .update_invoice(&invoice.payment_hash, InvoiceStatus::Expired) + .await + .ok(); + Some(self.new_invoice(processor, &keys, *price_grin, *price_nanogrin).await) + } + Err(e) => { + warn!("name authority: goblinpay status check failed: {e:?}"); + Some(server_error()) + } + }; + } + + // First contact: create the account row and a fresh invoice. + self.repo.create_account(&keys).await.ok(); + Some(self.new_invoice(processor, &keys, *price_grin, *price_nanogrin).await) + } + + /// Create and persist a fresh GoblinPay invoice; respond 402 with the + /// hosted pay page so the client can complete the payment. + async fn new_invoice( + &self, + processor: &Arc, + keys: &Keys, + price_grin: f64, + price_nanogrin: u64, + ) -> Response { + match processor.get_invoice(keys, price_nanogrin).await { + Ok(invoice) => { + if self + .repo + .create_invoice_record(keys, invoice.clone()) + .await + .is_err() + { + return server_error(); + } + payment_required( + &invoice.payment_hash, + &invoice.bolt11, + price_grin, + price_nanogrin, + ) + } + Err(e) => { + warn!("name authority: goblinpay invoice creation failed: {e:?}"); + server_error() + } + } + } + + fn unregister(&self, name: &str, request: &Request, ip: &str) -> Response { + if !self.allow_write("na-unreg", ip) { + return rate_limited(); + } + let name = name.to_lowercase(); + let path = format!("/api/v1/register/{name}"); + let auth_header = request + .headers() + .get(hyper::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()); + let (auth_pubkey, auth_id) = match nip98::verify_nip98( + auth_header, + "DELETE", + &path, + &[], + &self.cfg.base_url, + self.cfg.auth_max_age_secs, + ) { + Ok(v) => v, + Err(msg) => return json_response(StatusCode::UNAUTHORIZED, json!({"error": msg})), + }; + if !self.auth_event_fresh(&auth_id) { + return json_response( + StatusCode::UNAUTHORIZED, + json!({"error": "auth event replayed"}), + ); + } + // Release is always allowed; releasing is what arms the cooldown. + match self.lookup(&name) { + Some(owner) if owner == auth_pubkey => { + let res = self.db.lock().unwrap().execute( + "UPDATE name_claims SET released_at = ?2 + WHERE name = ?1 AND released_at IS NULL", + rusqlite::params![name, unix_time()], + ); + match res { + Ok(_) => { + self.record_op("na-namechange", &auth_pubkey); + info!("name authority: released {name}"); + json_response(StatusCode::OK, json!({"name": name, "released": true})) + } + Err(e) => { + error!("name authority: db release failed: {e}"); + server_error() + } + } + } + Some(_) => json_response(StatusCode::FORBIDDEN, json!({"error": "not the owner"})), + None => json_response(StatusCode::NOT_FOUND, json!({"error": "name not found"})), + } + } +} + +// ---------------------------------------------------------------------- +// Small response helpers +// ---------------------------------------------------------------------- + +fn strip<'a>(path: &'a str, prefix: &str) -> &'a str { + path.strip_prefix(prefix).unwrap_or("") +} + +fn query_param(request: &Request, key: &str) -> Option { + request.uri().query().and_then(|q| { + q.split('&').find_map(|pair| { + let mut parts = pair.splitn(2, '='); + if parts.next() == Some(key) { + parts.next().map(str::to_string) + } else { + None + } + }) + }) +} + +fn json_response(status: StatusCode, value: serde_json::Value) -> Response { + Response::builder() + .status(status) + .header("Content-Type", "application/json") + .header("Access-Control-Allow-Origin", "*") + .header("Cache-Control", "no-store") + .body(Body::from(value.to_string())) + .expect("response builder") +} + +fn text_response(status: StatusCode, text: &'static str) -> Response { + Response::builder() + .status(status) + .header("Content-Type", "text/plain") + .body(Body::from(text)) + .expect("response builder") +} + +fn rate_limited() -> Response { + json_response( + StatusCode::TOO_MANY_REQUESTS, + json!({"error": "rate_limited"}), + ) +} + +fn server_error() -> Response { + json_response( + StatusCode::INTERNAL_SERVER_ERROR, + json!({"error": "internal error"}), + ) +} + +/// 402 carrying everything a client needs to render or open the GoblinPay +/// pay page, then retry the claim once the payment confirms. +fn payment_required( + invoice_id: &str, + pay_url: &str, + price_grin: f64, + price_nanogrin: u64, +) -> Response { + json_response( + StatusCode::PAYMENT_REQUIRED, + json!({ + "error": "payment_required", + "invoice_id": invoice_id, + "pay_url": pay_url, + "price_grin": price_grin, + "price_nanogrin": price_nanogrin, + }), + ) +} diff --git a/src/name_authority/names.rs b/src/name_authority/names.rs new file mode 100644 index 0000000..f7c32fe --- /dev/null +++ b/src/name_authority/names.rs @@ -0,0 +1,224 @@ +//! Name and pubkey rules: validity, the reserved list, and look-alike +//! folding that stops digit/separator homographs of reserved terms. +//! Ported from goblin-nip05d (the reference name authority). + +/// Built-in reserved names. These are generic infrastructure, role and +/// finance terms that no operator should hand out as a payment identity; +/// they are domain-agnostic on purpose. The operator's own brand is +/// reserved separately and dynamically from their domain (see +/// [`domain_reserved`]); operators can add more via a reserved file. +pub const RESERVED: &[&str] = &[ + "admin", + "administrator", + "root", + "support", + "help", + "info", + "mail", + "email", + "www", + "relay", + "nostr", + "pay", + "payment", + "payments", + "wallet", + "official", + "security", + "abuse", + "postmaster", + "hostmaster", + "webmaster", + "contact", + "team", + "staff", + "mod", + "moderator", + "moderators", + "system", + "bot", + "api", + "app", + "dev", + "developer", + "test", + "testing", + "anonymous", + "anon", + "null", + "void", + "owner", + "ceo", + "register", + "registration", + "account", + "accounts", + "verify", + "verified", + "billing", + "donate", + "treasury", + "faucet", + "exchange", + "swap", + "bank", + "money", + "cash", + "fees", + "fee", + "node", + "miner", + "mining", + "explorer", + "status", + "blog", + "news", + "docs", + "wiki", + "store", + "shop", +]; + +/// True when `name` satisfies the length bounds and character rules: ASCII +/// lowercase alphanumerics plus `. _ -`, starting and ending alphanumeric. +#[must_use] +pub fn valid_name(name: &str, name_min: usize, name_max: usize) -> bool { + let len = name.chars().count(); + if !(name_min..=name_max).contains(&len) { + return false; + } + let bytes = name.as_bytes(); + let ok_char = + |c: u8| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, b'.' | b'_' | b'-'); + if !bytes.iter().all(|&c| ok_char(c)) { + return false; + } + let first = bytes[0]; + let last = bytes[bytes.len() - 1]; + (first.is_ascii_lowercase() || first.is_ascii_digit()) + && (last.is_ascii_lowercase() || last.is_ascii_digit()) +} + +/// Fold a name to catch separator/digit look-alikes of reserved terms, so +/// `g0blin`, `g-o-b-l-i-n` and `supp0rt` cannot impersonate a reserved or +/// brand term as a payment identity. Conservative: a name is only blocked +/// when its folded form exactly equals a reserved term's folded form. +#[must_use] +pub fn fold_lookalike(name: &str) -> String { + name.chars() + .filter_map(|c| match c { + '.' | '_' | '-' => None, + '0' => Some('o'), + '1' => Some('i'), + '3' => Some('e'), + '4' => Some('a'), + '5' => Some('s'), + '7' => Some('t'), + '8' => Some('b'), + '9' => Some('g'), + c => Some(c), + }) + .collect() +} + +/// True when `name` is reserved outright or folds onto a reserved term. +/// The `extra` slice holds the operator's domain labels plus any names +/// from the optional reserved file. +#[must_use] +pub fn is_reserved(name: &str, extra: &[String]) -> bool { + if RESERVED.contains(&name) || extra.iter().any(|r| r == name) { + return true; + } + let folded = fold_lookalike(name); + RESERVED.iter().any(|r| fold_lookalike(r) == folded) + || extra.iter().any(|r| fold_lookalike(r) == folded) +} + +/// Reserved names derived from the operator's own domain, so a domain's +/// brand cannot be claimed (or look-alike-folded) as a payment handle. +/// Each dot label except the final TLD is reserved: `example.com` becomes +/// `["example"]`, `names.acme.example` becomes `["names", "acme"]`. A +/// single-label host (e.g. `localhost`) reserves that label. +#[must_use] +pub fn domain_reserved(domain: &str) -> Vec { + let labels: Vec<&str> = domain + .trim() + .trim_end_matches('.') + .split('.') + .filter(|l| !l.is_empty()) + .collect(); + let keep = if labels.len() > 1 { + &labels[..labels.len() - 1] + } else { + &labels[..] + }; + keep.iter().map(|l| l.to_lowercase()).collect() +} + +/// Lowercase 64-char hex pubkey. +#[must_use] +pub fn valid_pubkey_hex(pk: &str) -> bool { + pk.len() == 64 + && pk + .bytes() + .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const MIN: usize = 3; + const MAX: usize = 20; + + #[test] + fn name_validation() { + assert!(valid_name("ada", MIN, MAX)); + assert!(valid_name("ada.wren-99_x", MIN, MAX)); + assert!(!valid_name("ab", MIN, MAX)); + assert!(!valid_name("Ada", MIN, MAX)); + assert!(!valid_name(".ada", MIN, MAX)); + assert!(!valid_name("ada.", MIN, MAX)); + assert!(!valid_name("a d a", MIN, MAX)); + assert!(!valid_name(&"a".repeat(21), MIN, MAX)); + assert!(valid_name(&"a".repeat(20), MIN, MAX)); + assert!(!valid_name("päge", MIN, MAX)); + } + + #[test] + fn reserved_and_lookalikes() { + assert!(is_reserved("support", &[])); + assert!(is_reserved("supp0rt", &[])); + assert!(is_reserved("adm1n", &[])); + // Brand terms are NOT built in; they come from the domain labels. + assert!(!is_reserved("goblin", &[])); + assert!(is_reserved("acme", &["acme".to_string()])); + assert!(is_reserved("acm3", &["acme".to_string()])); + assert!(!is_reserved("acmecorp", &["acme".to_string()])); + } + + #[test] + fn domain_labels_reserved() { + assert_eq!(domain_reserved("goblin.st"), vec!["goblin"]); + assert_eq!(domain_reserved("acme.example"), vec!["acme"]); + assert_eq!(domain_reserved("names.acme.example"), vec!["names", "acme"]); + assert_eq!(domain_reserved("GOBLIN.ST"), vec!["goblin"]); + assert_eq!(domain_reserved("localhost"), vec!["localhost"]); + let extra = domain_reserved("goblin.st"); + assert!(is_reserved("goblin", &extra)); + assert!(is_reserved("g0blin", &extra)); + assert!(is_reserved("g-o-b-l-i-n", &extra)); + assert!(!is_reserved("goblinfan", &extra)); + } + + #[test] + fn pubkey_validation() { + assert!(valid_pubkey_hex( + "91cf9dbbea5e6511fd2bbb190b112055ee4131c5d2bbb9faedf3ee8cbeac0d05" + )); + assert!(!valid_pubkey_hex( + "91CF9DBBEA5E6511FD2BBB190B112055EE4131C5D2BBB9FAEDF3EE8CBEAC0D05" + )); + assert!(!valid_pubkey_hex("abc")); + } +} diff --git a/src/name_authority/nip98.rs b/src/name_authority/nip98.rs new file mode 100644 index 0000000..ec0ff37 --- /dev/null +++ b/src/name_authority/nip98.rs @@ -0,0 +1,254 @@ +//! NIP-98 HTTP authorization: verify an `Authorization: Nostr ` +//! header carrying a signed kind-27235 event, including signature, +//! freshness, and the `u`/`method`/`payload` tags. The `u` tag is checked +//! against the configured public base URL, so a wrong base_url silently +//! fails every authenticated call (fail closed). +//! +//! Ported from goblin-nip05d, reusing this relay's own event validation +//! (id digest + schnorr signature) instead of an external nostr crate. + +use crate::event::Event; +use crate::utils::unix_time; +use base64::Engine; +use bitcoin_hashes::{sha256, Hash}; + +/// NIP-98 HTTP auth event kind. +pub const HTTP_AUTH_KIND: u64 = 27235; + +/// Verify a NIP-98 auth header for `method`+`url_path` over `body`. +/// On success returns (authenticated pubkey hex, auth event id hex). +pub fn verify_nip98( + auth_header: Option<&str>, + method: &str, + url_path: &str, + body: &[u8], + base_url: &str, + auth_max_age_secs: i64, +) -> Result<(String, String), String> { + let auth = auth_header.ok_or("missing Authorization header")?; + let b64 = auth + .strip_prefix("Nostr ") + .ok_or("Authorization scheme must be Nostr")?; + let raw = base64::engine::general_purpose::STANDARD + .decode(b64.trim()) + .map_err(|_| "invalid base64 auth event")?; + let event: Event = + serde_json::from_slice(&raw).map_err(|_| "invalid auth event json")?; + event.validate().map_err(|_| "bad event signature")?; + + if event.kind != HTTP_AUTH_KIND { + return Err("auth event kind must be 27235".to_string()); + } + let age = (unix_time() as i64) - (event.created_at as i64); + // Allow modest backward skew but only a few seconds forward, to bound + // the replay window (paired with one-time event-id enforcement at the + // caller). + if age > auth_max_age_secs || age < -5 { + return Err("auth event expired or post-dated".to_string()); + } + + let mut u_ok = false; + let mut method_ok = false; + let mut payload_hash: Option = None; + for tag in &event.tags { + match tag.first().map(String::as_str) { + Some("u") => { + if let Some(u) = tag.get(1) { + let expected = format!("{base_url}{url_path}"); + let normalized = u.trim_end_matches('/'); + u_ok = normalized == expected.trim_end_matches('/'); + } + } + Some("method") => { + if let Some(m) = tag.get(1) { + method_ok = m.eq_ignore_ascii_case(method); + } + } + Some("payload") => { + payload_hash = tag.get(1).cloned(); + } + _ => {} + } + } + if !u_ok { + return Err("auth event url mismatch".to_string()); + } + if !method_ok { + return Err("auth event method mismatch".to_string()); + } + if let Some(expect) = payload_hash { + let digest: sha256::Hash = sha256::Hash::hash(body); + let got = format!("{digest:x}"); + if !expect.eq_ignore_ascii_case(&got) { + return Err("auth event payload hash mismatch".to_string()); + } + } else if !body.is_empty() { + return Err("auth event missing payload hash".to_string()); + } + + Ok((event.pubkey.clone(), event.id.clone())) +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::Engine; + use bitcoin_hashes::{sha256, Hash}; + use secp256k1::{KeyPair, Secp256k1, XOnlyPublicKey}; + + /// Build and sign a real NIP-98 event for tests. + fn signed_auth_event( + url: &str, + method: &str, + body: Option<&[u8]>, + kind: u64, + created_at: u64, + ) -> String { + let secp = Secp256k1::new(); + let keypair = KeyPair::from_seckey_slice(&secp, &[7u8; 32]).unwrap(); + let pubkey = XOnlyPublicKey::from_keypair(&keypair); + let pubkey_hex = pubkey.to_string(); + + let mut tags: Vec> = vec![ + vec!["u".to_string(), url.to_string()], + vec!["method".to_string(), method.to_string()], + ]; + if let Some(body) = body { + let digest: sha256::Hash = sha256::Hash::hash(body); + tags.push(vec!["payload".to_string(), format!("{digest:x}")]); + } + let mut event = Event { + id: String::new(), + pubkey: pubkey_hex, + delegated_by: None, + created_at, + kind, + tags, + content: String::new(), + sig: String::new(), + tagidx: None, + }; + let canonical = event.to_canonical().unwrap(); + let digest: sha256::Hash = sha256::Hash::hash(canonical.as_bytes()); + event.id = format!("{digest:x}"); + let msg = secp256k1::Message::from_slice(digest.as_ref()).unwrap(); + event.sig = secp.sign_schnorr(&msg, &keypair).to_string(); + + let json = serde_json::to_string(&event).unwrap(); + format!( + "Nostr {}", + base64::engine::general_purpose::STANDARD.encode(json) + ) + } + + #[test] + fn accepts_valid_auth() { + let base = "https://names.example"; + let body = br#"{"name":"ada","pubkey":"aa"}"#; + let header = signed_auth_event( + &format!("{base}/api/v1/register"), + "POST", + Some(body), + HTTP_AUTH_KIND, + unix_time(), + ); + let res = verify_nip98( + Some(&header), + "POST", + "/api/v1/register", + body, + base, + 60, + ); + assert!(res.is_ok(), "{res:?}"); + } + + #[test] + fn rejects_wrong_kind() { + let base = "https://names.example"; + let header = signed_auth_event( + &format!("{base}/api/v1/register"), + "POST", + None, + 1, + unix_time(), + ); + assert!( + verify_nip98(Some(&header), "POST", "/api/v1/register", &[], base, 60).is_err() + ); + } + + #[test] + fn rejects_stale_event() { + let base = "https://names.example"; + let header = signed_auth_event( + &format!("{base}/api/v1/register"), + "POST", + None, + HTTP_AUTH_KIND, + unix_time() - 3600, + ); + assert!( + verify_nip98(Some(&header), "POST", "/api/v1/register", &[], base, 60).is_err() + ); + } + + #[test] + fn rejects_url_mismatch() { + let base = "https://names.example"; + let header = signed_auth_event( + "https://evil.example/api/v1/register", + "POST", + None, + HTTP_AUTH_KIND, + unix_time(), + ); + assert!( + verify_nip98(Some(&header), "POST", "/api/v1/register", &[], base, 60).is_err() + ); + } + + #[test] + fn rejects_method_mismatch() { + let base = "https://names.example"; + let header = signed_auth_event( + &format!("{base}/api/v1/register"), + "DELETE", + None, + HTTP_AUTH_KIND, + unix_time(), + ); + assert!( + verify_nip98(Some(&header), "POST", "/api/v1/register", &[], base, 60).is_err() + ); + } + + #[test] + fn rejects_payload_tampering() { + let base = "https://names.example"; + let body = br#"{"name":"ada"}"#; + let header = signed_auth_event( + &format!("{base}/api/v1/register"), + "POST", + Some(body), + HTTP_AUTH_KIND, + unix_time(), + ); + let tampered = br#"{"name":"eve"}"#; + assert!(verify_nip98( + Some(&header), + "POST", + "/api/v1/register", + tampered, + base, + 60 + ) + .is_err()); + } + + #[test] + fn rejects_missing_header_and_bad_scheme() { + assert!(verify_nip98(None, "POST", "/x", &[], "https://a", 60).is_err()); + assert!(verify_nip98(Some("Bearer zzz"), "POST", "/x", &[], "https://a", 60).is_err()); + } +} diff --git a/src/nauthz.rs b/src/nauthz.rs new file mode 100644 index 0000000..2413a2b --- /dev/null +++ b/src/nauthz.rs @@ -0,0 +1,111 @@ +use crate::error::{Error, Result}; +use crate::{event::Event, nip05::Nip05Name}; +use nauthz_grpc::authorization_client::AuthorizationClient; +use nauthz_grpc::event::TagEntry; +use nauthz_grpc::{Decision, Event as GrpcEvent, EventReply, EventRequest}; +use tracing::{info, warn}; + +pub mod nauthz_grpc { + tonic::include_proto!("nauthz"); +} + +// A decision for the DB to act upon +pub trait AuthzDecision: Send + Sync { + fn permitted(&self) -> bool; + fn message(&self) -> Option; +} + +impl AuthzDecision for EventReply { + fn permitted(&self) -> bool { + self.decision == Decision::Permit as i32 + } + fn message(&self) -> Option { + self.message.clone() + } +} + +// A connection to an event admission GRPC server +pub struct EventAuthzService { + server_addr: String, + conn: Option>, +} + +// conversion of Nip05Names into GRPC type +impl std::convert::From for nauthz_grpc::event_request::Nip05Name { + fn from(value: Nip05Name) -> Self { + nauthz_grpc::event_request::Nip05Name { + local: value.local.clone(), + domain: value.domain, + } + } +} + +// conversion of event tags into gprc struct +fn tags_to_protobuf(tags: &[Vec]) -> Vec { + tags.iter() + .map(|x| TagEntry { values: x.clone() }) + .collect() +} + +impl EventAuthzService { + pub async fn connect(server_addr: &str) -> EventAuthzService { + let mut eas = EventAuthzService { + server_addr: server_addr.to_string(), + conn: None, + }; + eas.ready_connection().await; + eas + } + + pub async fn ready_connection(&mut self) { + if self.conn.is_none() { + let client = AuthorizationClient::connect(self.server_addr.to_string()).await; + if let Err(ref msg) = client { + warn!("could not connect to nostr authz GRPC server: {:?}", msg); + } else { + info!("connected to nostr authorization GRPC server"); + } + self.conn = client.ok(); + } + } + + pub async fn admit_event( + &mut self, + event: &Event, + ip: &str, + origin: Option, + user_agent: Option, + nip05: Option, + auth_pubkey: Option>, + ) -> Result> { + self.ready_connection().await; + let id_blob = hex::decode(&event.id)?; + let pubkey_blob = hex::decode(&event.pubkey)?; + let sig_blob = hex::decode(&event.sig)?; + if let Some(ref mut c) = self.conn { + let gevent = GrpcEvent { + id: id_blob, + pubkey: pubkey_blob, + sig: sig_blob, + created_at: event.created_at, + kind: event.kind, + content: event.content.clone(), + tags: tags_to_protobuf(&event.tags), + }; + let svr_res = c + .event_admit(EventRequest { + event: Some(gevent), + ip_addr: Some(ip.to_string()), + origin, + user_agent, + auth_pubkey, + nip05: nip05.map(nauthz_grpc::event_request::Nip05Name::from), + }) + .await?; + let reply = svr_res.into_inner(); + Ok(Box::new(reply)) + } else { + Err(Error::AuthzError) + } + } +} diff --git a/src/nip05.rs b/src/nip05.rs new file mode 100644 index 0000000..81208fd --- /dev/null +++ b/src/nip05.rs @@ -0,0 +1,658 @@ +//! User verification using NIP-05 names +//! +//! NIP-05 defines a mechanism for authors to associate an internet +//! address with their public key, in metadata events. This module +//! consumes a stream of metadata events, and keeps a database table +//! updated with the current NIP-05 verification status. +use crate::config::VerifiedUsers; +use crate::error::{Error, Result}; +use crate::event::Event; +use crate::repo::NostrRepo; +use hyper::body::HttpBody; +use hyper::client::connect::HttpConnector; +use hyper::Client; +use hyper_rustls::HttpsConnector; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; +use std::time::SystemTime; +use tokio::time::Interval; +use tracing::{debug, info, warn}; + +/// NIP-05 verifier state +pub struct Verifier { + /// Repository for saving/retrieving events and records + repo: Arc, + /// Metadata events for us to inspect + metadata_rx: tokio::sync::broadcast::Receiver, + /// Newly validated events get written and then broadcast on this channel to subscribers + event_tx: tokio::sync::broadcast::Sender, + /// Settings + settings: crate::config::Settings, + /// HTTP client + client: hyper::Client, hyper::Body>, + /// After all accounts are updated, wait this long before checking again. + wait_after_finish: Duration, + /// Minimum amount of time between HTTP queries + http_wait_duration: Duration, + /// Interval for updating verification records + reverify_interval: Interval, +} + +/// A NIP-05 identifier is a local part and domain. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct Nip05Name { + pub local: String, + pub domain: String, +} + +impl Nip05Name { + /// Does this name represent the entire domain? + #[must_use] + pub fn is_domain_only(&self) -> bool { + self.local == "_" + } + + /// Determine the URL to query for verification + fn to_url(&self) -> Option { + format!( + "https://{}/.well-known/nostr.json?name={}", + self.domain, self.local + ) + .parse::() + .ok() + } +} + +// Parsing Nip05Names from strings +impl std::convert::TryFrom<&str> for Nip05Name { + type Error = Error; + fn try_from(inet: &str) -> Result { + // break full name at the @ boundary. + let components: Vec<&str> = inet.split('@').collect(); + if components.len() == 2 { + // check if local name is valid + let local = components[0]; + let domain = components[1]; + if local + .chars() + .all(|x| x.is_alphanumeric() || x == '_' || x == '-' || x == '.') + { + if domain + .chars() + .all(|x| x.is_alphanumeric() || x == '-' || x == '.') + { + Ok(Nip05Name { + local: local.to_owned(), + domain: domain.to_owned(), + }) + } else { + Err(Error::CustomError( + "invalid character in domain part".to_owned(), + )) + } + } else { + Err(Error::CustomError( + "invalid character in local part".to_owned(), + )) + } + } else { + Err(Error::CustomError("too many/few components".to_owned())) + } + } +} + +impl std::fmt::Display for Nip05Name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.local, self.domain) + } +} + +/// Check if the specified username and address are present and match in this response body +fn body_contains_user(username: &str, address: &str, bytes: &hyper::body::Bytes) -> Result { + // convert the body into json + let body: serde_json::Value = serde_json::from_slice(bytes)?; + // ensure we have a names object. + let names_map = body + .as_object() + .and_then(|x| x.get("names")) + .and_then(serde_json::Value::as_object) + .ok_or_else(|| Error::CustomError("not a map".to_owned()))?; + // get the pubkey for the requested user + let check_name = names_map.get(username).and_then(serde_json::Value::as_str); + // ensure the address is a match + Ok(check_name == Some(address)) +} + +impl Verifier { + pub fn new( + repo: Arc, + metadata_rx: tokio::sync::broadcast::Receiver, + event_tx: tokio::sync::broadcast::Sender, + settings: crate::config::Settings, + ) -> Result { + info!("creating NIP-05 verifier"); + // setup hyper client + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .build(); + + let client = Client::builder().build::<_, hyper::Body>(https); + + // After all accounts have been re-verified, don't check again + // for this long. + let wait_after_finish = Duration::from_secs(60 * 10); + // when we have an active queue of accounts to validate, we + // will wait this duration between HTTP requests. + let http_wait_duration = Duration::from_secs(1); + // setup initial interval for re-verification. If we find + // there is no work to be done, it will be reset to a longer + // duration. + let reverify_interval = tokio::time::interval(http_wait_duration); + Ok(Verifier { + repo, + metadata_rx, + event_tx, + settings, + client, + wait_after_finish, + http_wait_duration, + reverify_interval, + }) + } + + /// Perform web verification against a NIP-05 name and address. + pub async fn get_web_verification( + &mut self, + nip: &Nip05Name, + pubkey: &str, + ) -> UserWebVerificationStatus { + self.get_web_verification_res(nip, pubkey) + .await + .unwrap_or(UserWebVerificationStatus::Unknown) + } + + /// Perform web verification against an `Event` (must be metadata). + pub async fn get_web_verification_from_event( + &mut self, + e: &Event, + ) -> UserWebVerificationStatus { + let nip_parse = e.get_nip05_addr(); + if let Some(nip) = nip_parse { + self.get_web_verification_res(&nip, &e.pubkey) + .await + .unwrap_or(UserWebVerificationStatus::Unknown) + } else { + UserWebVerificationStatus::Unknown + } + } + + /// Perform web verification, with a `Result` return. + async fn get_web_verification_res( + &mut self, + nip: &Nip05Name, + pubkey: &str, + ) -> Result { + // determine if this domain should be checked + if !is_domain_allowed( + &nip.domain, + &self.settings.verified_users.domain_whitelist, + &self.settings.verified_users.domain_blacklist, + ) { + return Ok(UserWebVerificationStatus::DomainNotAllowed); + } + let url = nip + .to_url() + .ok_or_else(|| Error::CustomError("invalid NIP-05 URL".to_owned()))?; + let req = hyper::Request::builder() + .method(hyper::Method::GET) + .uri(url.clone()) + .header("Accept", "application/json") + .header( + "User-Agent", + format!( + "nostr-rs-relay/{} NIP-05 Verifier", + crate::info::CARGO_PKG_VERSION.unwrap() + ), + ) + .body(hyper::Body::empty()) + .expect("request builder"); + + let response_fut = self.client.request(req); + + if let Ok(response_res) = tokio::time::timeout(Duration::from_secs(5), response_fut).await { + // limit size of verification document to 1MB. + const MAX_ALLOWED_RESPONSE_SIZE: u64 = 1024 * 1024; + let response = response_res?; + let status = response.status(); + + // Log non-2XX status codes + if !status.is_success() { + info!( + "unexpected status code {} received for account {:?} at URL: {}", + status, + nip.to_string(), + url + ); + return Ok(UserWebVerificationStatus::Unknown); + } + + // determine content length from response + let response_content_length = match response.body().size_hint().upper() { + Some(v) => v, + None => { + info!( + "missing content length header for account {:?} at URL: {}", + nip.to_string(), + url + ); + return Ok(UserWebVerificationStatus::Unknown); + } + }; + + if response_content_length > MAX_ALLOWED_RESPONSE_SIZE { + info!( + "content length {} exceeded limit of {} bytes for account {:?} at URL: {}", + response_content_length, + MAX_ALLOWED_RESPONSE_SIZE, + nip.to_string(), + url + ); + return Ok(UserWebVerificationStatus::Unknown); + } + + let (parts, body) = response.into_parts(); + // TODO: consider redirects + if parts.status == http::StatusCode::OK { + // parse body, determine if the username / key / address is present + let body_bytes = match hyper::body::to_bytes(body).await { + Ok(bytes) => bytes, + Err(e) => { + info!( + "failed to read response body for account {:?} at URL: {}: {:?}", + nip.to_string(), + url, + e + ); + return Ok(UserWebVerificationStatus::Unknown); + } + }; + + match body_contains_user(&nip.local, pubkey, &body_bytes) { + Ok(true) => Ok(UserWebVerificationStatus::Verified), + Ok(false) => Ok(UserWebVerificationStatus::Unverified), + Err(e) => { + info!( + "error parsing response body for account {:?}: {:?}", + nip.to_string(), + e + ); + Ok(UserWebVerificationStatus::Unknown) + } + } + } else { + info!( + "unexpected status code {} for account {:?}", + parts.status, + nip.to_string() + ); + Ok(UserWebVerificationStatus::Unknown) + } + } else { + info!("timeout verifying account {:?}", nip); + Ok(UserWebVerificationStatus::Unknown) + } + } + + /// Perform NIP-05 verifier tasks. + pub async fn run(&mut self) { + // use this to schedule periodic re-validation tasks + // run a loop, restarting on failure + loop { + let res = self.run_internal().await; + match res { + Err(Error::ChannelClosed) => { + // channel was closed, we are shutting down + return; + } + Err(e) => { + info!("error in verifier: {:?}", e); + } + _ => {} + } + } + } + + /// Internal select loop for performing verification + async fn run_internal(&mut self) -> Result<()> { + tokio::select! { + m = self.metadata_rx.recv() => { + match m { + Ok(e) => { + if let Some(naddr) = e.get_nip05_addr() { + info!("got metadata event for ({:?},{:?})", naddr.to_string() ,e.get_author_prefix()); + // Process a new author, checking if they are verified: + let check_verified = self.repo.get_latest_user_verification(&e.pubkey).await; + // ensure the event we got is more recent than the one we have, otherwise we can ignore it. + if let Ok(last_check) = check_verified { + if e.created_at <= last_check.event_created { + // this metadata is from the same author as an existing verification. + // it is older than what we have, so we can ignore it. + debug!("received older metadata event for author {:?}", e.get_author_prefix()); + return Ok(()); + } + } + // old, or no existing record for this user. In either case, we just create a new one. + let start = Instant::now(); + let v = self.get_web_verification_from_event(&e).await; + info!( + "checked name {:?}, result: {:?}, in: {:?}", + naddr.to_string(), + v, + start.elapsed() + ); + // sleep to limit how frequently we make HTTP requests for new metadata events. This should limit us to 4 req/sec. + tokio::time::sleep(Duration::from_millis(250)).await; + // if this user was verified, we need to write the + // record, persist the event, and broadcast. + if let UserWebVerificationStatus::Verified = v { + self.create_new_verified_user(&naddr.to_string(), &e).await?; + } + } + }, + Err(tokio::sync::broadcast::error::RecvError::Lagged(c)) => { + warn!("incoming metadata events overwhelmed buffer, {} events dropped",c); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => { + info!("metadata broadcast channel closed"); + return Err(Error::ChannelClosed); + } + } + }, + _ = self.reverify_interval.tick() => { + // check and see if there is an old account that needs + // to be reverified + self.do_reverify().await?; + }, + } + Ok(()) + } + + /// Reverify the oldest user verification record. + async fn do_reverify(&mut self) -> Result<()> { + let reverify_setting = self + .settings + .verified_users + .verify_update_frequency_duration; + let max_failures = self.settings.verified_users.max_consecutive_failures; + // get from settings, but default to 6hrs between re-checking an account + let reverify_dur = reverify_setting.unwrap_or_else(|| Duration::from_secs(60 * 60 * 6)); + // find all verification records that have success or failure OLDER than the reverify_dur. + let now = SystemTime::now(); + let earliest = now - reverify_dur; + let earliest_epoch = earliest + .duration_since(SystemTime::UNIX_EPOCH) + .map(|x| x.as_secs()) + .unwrap_or(0); + let vr = self.repo.get_oldest_user_verification(earliest_epoch).await; + match vr { + Ok(ref v) => { + let new_status = self.get_web_verification(&v.name, &v.address).await; + match new_status { + UserWebVerificationStatus::Verified => { + // freshly verified account, update the + // timestamp. + self.repo.update_verification_timestamp(v.rowid).await?; + info!("verification updated for {}", v.to_string()); + } + UserWebVerificationStatus::DomainNotAllowed + | UserWebVerificationStatus::Unknown => { + // server may be offline, or temporarily + // blocked by the config file. Note the + // failure so we can process something + // else. + + // have we had enough failures to give up? + if v.failure_count >= max_failures as u64 { + info!( + "giving up on verifying {:?} after {} failures", + v.name, v.failure_count + ); + self.repo.delete_verification(v.rowid).await?; + } else { + // record normal failure, incrementing failure count + info!("verification failed for {}", v.to_string()); + self.repo.fail_verification(v.rowid).await?; + } + } + UserWebVerificationStatus::Unverified => { + // domain has removed the verification, drop + // the record on our side. + info!("verification rescinded for {}", v.to_string()); + self.repo.delete_verification(v.rowid).await?; + } + } + } + Err( + Error::SqlError(rusqlite::Error::QueryReturnedNoRows) + | Error::SqlxError(sqlx::Error::RowNotFound), + ) => { + // No users need verification. Reset the interval to + // the next verification attempt. + let start = tokio::time::Instant::now() + self.wait_after_finish; + self.reverify_interval = tokio::time::interval_at(start, self.http_wait_duration); + } + Err(ref e) => { + warn!( + "Error when checking for NIP-05 verification records: {:?}", + e + ); + } + } + Ok(()) + } + + /// Persist an event, create a verification record, and broadcast. + // TODO: have more event-writing logic handled in the db module. + // Right now, these events avoid the rate limit. That is + // acceptable since as soon as the user is registered, this path + // is no longer used. + // TODO: refactor these into spawn_blocking + // calls to get them off the async executors. + async fn create_new_verified_user(&mut self, name: &str, event: &Event) -> Result<()> { + let start = Instant::now(); + // we should only do this if we are enabled. if we are + // disabled/passive, the event has already been persisted. + let should_write_event = self.settings.verified_users.is_enabled(); + if should_write_event { + match self.repo.write_event(event).await { + Ok(updated) => { + if updated != 0 { + info!( + "persisted event (new verified pubkey): {:?} in {:?}", + event.get_event_id_prefix(), + start.elapsed() + ); + self.event_tx.send(event.clone()).ok(); + } + } + Err(err) => { + warn!("event insert failed: {:?}", err); + if let Error::SqlError(r) = err { + warn!("because: : {:?}", r); + } + } + } + } + // write the verification record + self.repo + .create_verification_record(&event.id, name) + .await?; + Ok(()) + } +} + +/// Result of checking user's verification status against DNS/HTTP. +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum UserWebVerificationStatus { + Verified, // user is verified, as of now. + DomainNotAllowed, // domain blacklist or whitelist denied us from attempting a verification + Unknown, // user's status could not be determined (timeout, server error) + Unverified, // user's status is not verified (successful check, name / addr do not match) +} + +/// A NIP-05 verification record. +#[derive(PartialEq, Eq, Debug, Clone)] +// Basic information for a verification event. Gives us all we need to assert a NIP-05 address is good. +pub struct VerificationRecord { + pub rowid: u64, // database row for this verification event + pub name: Nip05Name, // address being verified + pub address: String, // pubkey + pub event: String, // event ID hash providing the verification + pub event_created: u64, // when the metadata event was published + pub last_success: Option, // the most recent time a verification was provided. None if verification under this name has never succeeded. + pub last_failure: Option, // the most recent time verification was attempted, but could not be completed. + pub failure_count: u64, // how many consecutive failures have been observed. +} + +/// Check with settings to determine if a given domain is allowed to +/// publish. +#[must_use] +pub fn is_domain_allowed( + domain: &str, + whitelist: &Option>, + blacklist: &Option>, +) -> bool { + // if there is a whitelist, domain must be present in it. + if let Some(wl) = whitelist { + // workaround for Vec contains not accepting &str + return wl.iter().any(|x| x == domain); + } + // otherwise, check that user is not in the blacklist + if let Some(bl) = blacklist { + return !bl.iter().any(|x| x == domain); + } + true +} + +impl VerificationRecord { + /// Check if the record is recent enough to be considered valid, + /// and the domain is allowed. + #[must_use] + pub fn is_valid(&self, verified_users_settings: &VerifiedUsers) -> bool { + //let settings = SETTINGS.read().unwrap(); + // how long a verification record is good for + let nip05_expiration = &verified_users_settings.verify_expiration_duration; + if let Some(e) = nip05_expiration { + if !self.is_current(e) { + return false; + } + } + // check domains + is_domain_allowed( + &self.name.domain, + &verified_users_settings.domain_whitelist, + &verified_users_settings.domain_blacklist, + ) + } + + /// Check if this record has been validated since the given + /// duration. + fn is_current(&self, d: &Duration) -> bool { + match self.last_success { + Some(s) => { + // current time - duration + let now = SystemTime::now(); + let cutoff = now - *d; + let cutoff_epoch = cutoff + .duration_since(SystemTime::UNIX_EPOCH) + .map(|x| x.as_secs()) + .unwrap_or(0); + s > cutoff_epoch + } + None => false, + } + } +} + +impl std::fmt::Display for VerificationRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "({:?},{:?})", + self.name.to_string(), + self.address.chars().take(8).collect::() + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn local_from_inet() { + let addr = "bob@example.com"; + let parsed = Nip05Name::try_from(addr); + assert!(parsed.is_ok()); + let v = parsed.unwrap(); + assert_eq!(v.local, "bob"); + assert_eq!(v.domain, "example.com"); + } + + #[test] + fn not_enough_sep() { + let addr = "bob_example.com"; + let parsed = Nip05Name::try_from(addr); + assert!(parsed.is_err()); + } + + #[test] + fn too_many_sep() { + let addr = "foo@bob@example.com"; + let parsed = Nip05Name::try_from(addr); + assert!(parsed.is_err()); + } + + #[test] + fn invalid_local_name() { + // non-permitted ascii chars + assert!(Nip05Name::try_from("foo!@example.com").is_err()); + assert!(Nip05Name::try_from("foo @example.com").is_err()); + assert!(Nip05Name::try_from(" foo@example.com").is_err()); + assert!(Nip05Name::try_from("f oo@example.com").is_err()); + assert!(Nip05Name::try_from("foo<@example.com").is_err()); + // unicode dash + assert!(Nip05Name::try_from("foo‐bar@example.com").is_err()); + // emoji + assert!(Nip05Name::try_from("foo😭bar@example.com").is_err()); + } + #[test] + fn invalid_domain_name() { + // non-permitted ascii chars + assert!(Nip05Name::try_from("foo@examp!e.com").is_err()); + assert!(Nip05Name::try_from("foo@ example.com").is_err()); + assert!(Nip05Name::try_from("foo@exa mple.com").is_err()); + assert!(Nip05Name::try_from("foo@example .com").is_err()); + assert!(Nip05Name::try_from("foo@exa bool { + match self { + Self::Duplicate | Self::Saved => true, + Self::Invalid | Self::Blocked | Self::RateLimited | Self::Error | Self::Restricted => { + false + } + } + } + + #[must_use] + pub fn prefix(&self) -> &'static str { + match self { + Self::Saved => "saved", + Self::Duplicate => "duplicate", + Self::Invalid => "invalid", + Self::Blocked => "blocked", + Self::RateLimited => "rate-limited", + Self::Error => "error", + Self::Restricted => "restricted", + } + } +} + +impl Notice { + //pub fn err(err: error::Error, id: String) -> Notice { + // Notice::err_msg(format!("{}", err), id) + //} + + #[must_use] + pub fn message(msg: String) -> Notice { + Notice::Message(msg) + } + + fn prefixed(id: String, msg: &str, status: EventResultStatus) -> Notice { + let msg = format!("{}: {}", status.prefix(), msg); + Notice::EventResult(EventResult { id, msg, status }) + } + + #[must_use] + pub fn invalid(id: String, msg: &str) -> Notice { + Notice::prefixed(id, msg, EventResultStatus::Invalid) + } + + #[must_use] + pub fn blocked(id: String, msg: &str) -> Notice { + Notice::prefixed(id, msg, EventResultStatus::Blocked) + } + + #[must_use] + pub fn rate_limited(id: String, msg: &str) -> Notice { + Notice::prefixed(id, msg, EventResultStatus::RateLimited) + } + + #[must_use] + pub fn duplicate(id: String) -> Notice { + Notice::prefixed(id, "", EventResultStatus::Duplicate) + } + + #[must_use] + pub fn error(id: String, msg: &str) -> Notice { + Notice::prefixed(id, msg, EventResultStatus::Error) + } + + #[must_use] + pub fn restricted(id: String, msg: &str) -> Notice { + Notice::prefixed(id, msg, EventResultStatus::Restricted) + } + + /// NIP-42: an OK=false with the machine-readable `auth-required:` + /// prefix, telling the client to AUTH and resend. + #[must_use] + pub fn auth_required(id: String, msg: &str) -> Notice { + Notice::EventResult(EventResult { + id, + msg: format!("auth-required: {msg}"), + status: EventResultStatus::Restricted, + }) + } + + #[must_use] + pub fn saved(id: String) -> Notice { + Notice::EventResult(EventResult { + id, + msg: "".into(), + status: EventResultStatus::Saved, + }) + } +} diff --git a/src/payment/cln_rest.rs b/src/payment/cln_rest.rs new file mode 100644 index 0000000..f5c9f7a --- /dev/null +++ b/src/payment/cln_rest.rs @@ -0,0 +1,137 @@ +use std::{fs, str::FromStr}; + +use async_trait::async_trait; +use cln_rpc::{ + model::{ + requests::InvoiceRequest, + responses::{InvoiceResponse, ListinvoicesInvoicesStatus, ListinvoicesResponse}, + }, + primitives::{Amount, AmountOrAny}, +}; +use config::ConfigError; +use http::{header::CONTENT_TYPE, HeaderValue, Uri}; +use hyper::{client::HttpConnector, Client}; +use hyper_rustls::HttpsConnector; +use nostr::Keys; +use rand::random; + +use crate::{ + config::Settings, + error::{Error, Result}, +}; + +use super::{InvoiceInfo, InvoiceStatus, PaymentProcessor}; + +#[derive(Clone)] +pub struct ClnRestPaymentProcessor { + client: hyper::Client, hyper::Body>, + settings: Settings, + rune_header: HeaderValue, +} + +impl ClnRestPaymentProcessor { + pub fn new(settings: &Settings) -> Result { + let rune_path = settings + .pay_to_relay + .rune_path + .clone() + .ok_or(ConfigError::NotFound("rune_path".to_string()))?; + let rune = String::from_utf8(fs::read(rune_path)?) + .map_err(|_| ConfigError::Message("Rune should be UTF8".to_string()))?; + let mut rune_header = HeaderValue::from_str(rune.trim()) + .map_err(|_| ConfigError::Message("Invalid Rune header".to_string()))?; + rune_header.set_sensitive(true); + + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .https_only() + .enable_http1() + .build(); + let client = Client::builder().build::<_, hyper::Body>(https); + + Ok(Self { + client, + settings: settings.clone(), + rune_header, + }) + } +} + +#[async_trait] +impl PaymentProcessor for ClnRestPaymentProcessor { + async fn get_invoice(&self, key: &Keys, amount: u64) -> Result { + let random_number: u16 = random(); + let memo = format!("{}: {}", random_number, key.public_key()); + + let body = InvoiceRequest { + cltv: None, + deschashonly: None, + expiry: None, + preimage: None, + exposeprivatechannels: None, + fallbacks: None, + amount_msat: AmountOrAny::Amount(Amount::from_sat(amount)), + description: memo.clone(), + label: "Nostr".to_string(), + }; + let uri = Uri::from_str(&format!( + "{}/v1/invoice", + &self.settings.pay_to_relay.node_url + )) + .map_err(|_| ConfigError::Message("Bad node URL".to_string()))?; + + let req = hyper::Request::builder() + .method(hyper::Method::POST) + .uri(uri) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .header("Rune", self.rune_header.clone()) + .body(hyper::Body::from(serde_json::to_string(&body)?)) + .expect("request builder"); + + let res = self.client.request(req).await?; + + let body = hyper::body::to_bytes(res.into_body()).await?; + let invoice_response: InvoiceResponse = serde_json::from_slice(&body)?; + + Ok(InvoiceInfo { + pubkey: key.public_key().to_string(), + payment_hash: invoice_response.payment_hash.to_string(), + bolt11: invoice_response.bolt11, + amount, + memo, + status: InvoiceStatus::Unpaid, + confirmed_at: None, + }) + } + + async fn check_invoice(&self, payment_hash: &str) -> Result { + let uri = Uri::from_str(&format!( + "{}/v1/listinvoices?payment_hash={}", + &self.settings.pay_to_relay.node_url, payment_hash + )) + .map_err(|_| ConfigError::Message("Bad node URL".to_string()))?; + + let req = hyper::Request::builder() + .method(hyper::Method::POST) + .uri(uri) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .header("Rune", self.rune_header.clone()) + .body(hyper::Body::empty()) + .expect("request builder"); + + let res = self.client.request(req).await?; + + let body = hyper::body::to_bytes(res.into_body()).await?; + let invoice_response: ListinvoicesResponse = serde_json::from_slice(&body)?; + let invoice = invoice_response + .invoices + .first() + .ok_or(Error::CustomError("Invoice not found".to_string()))?; + let status = match invoice.status { + ListinvoicesInvoicesStatus::PAID => InvoiceStatus::Paid, + ListinvoicesInvoicesStatus::UNPAID => InvoiceStatus::Unpaid, + ListinvoicesInvoicesStatus::EXPIRED => InvoiceStatus::Expired, + }; + Ok(status) + } +} diff --git a/src/payment/goblinpay.rs b/src/payment/goblinpay.rs new file mode 100644 index 0000000..ff60f4f --- /dev/null +++ b/src/payment/goblinpay.rs @@ -0,0 +1,166 @@ +//! GoblinPay payment processor (Floonet addition). +//! +//! Talks to a GoblinPay server (the Grin payment backend) over its REST +//! API, using the same `PaymentProcessor` trait the Lightning processors +//! implement: +//! +//! * `POST /invoice` (Bearer `GP_API_TOKEN`) creates an invoice; the +//! response carries a hosted `pay_url` the payer opens to complete a +//! Grin payment (GoblinPay, manual slatepack, or a `grin1` address if +//! the operator enabled that method). +//! * `GET /invoice/{id}` returns the invoice's current status +//! (`open` / `paid` / `expired`); GoblinPay marks it paid only after +//! the payment confirms on chain (payment proof held server-side). +//! +//! Mapping onto the relay's invoice model: `payment_hash` holds the +//! GoblinPay `invoice_id`, and the `invoice` column (named `bolt11` in +//! code, an upstream Lightning artifact) holds the hosted `pay_url`. +//! Amounts are nanogrin (1 GRIN = 1_000_000_000 nanogrin). +//! +//! A GoblinPay webhook may POST `{"invoice_id": ...}` to this relay's +//! `/goblinpay` endpoint to speed up admission; the relay always +//! re-verifies with the GoblinPay server before admitting, so a forged +//! webhook cannot fake a payment (fail closed). + +use http::Uri; +use hyper::client::connect::HttpConnector; +use hyper::Client; +use hyper_rustls::HttpsConnector; +use nostr::Keys; +use serde::{Deserialize, Serialize}; + +use async_trait::async_trait; +use std::str::FromStr; + +use crate::error::Error; + +use super::{InvoiceInfo, InvoiceStatus, PaymentProcessor}; + +/// JSON body for `POST /invoice`. +#[derive(Serialize, Debug)] +struct CreateInvoiceBody { + /// Amount in nanogrin. + amount_grin: u64, + /// The relay's reference for this invoice. + order_ref: String, + memo: String, +} + +/// The slice of GoblinPay's invoice JSON the relay needs. +#[derive(Deserialize, Debug)] +struct InvoiceResponse { + invoice_id: String, + pay_url: String, + status: String, +} + +#[derive(Clone)] +pub struct GoblinPayPaymentProcessor { + client: hyper::Client, hyper::Body>, + /// GoblinPay server base URL, no trailing slash. + base_url: String, + /// Bearer token (`GP_API_TOKEN`). + api_token: String, +} + +impl GoblinPayPaymentProcessor { + pub fn new(base_url: &str, api_token: &str) -> Self { + // https in production; plain http is accepted so a local GoblinPay + // instance can be tested without certificates. + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .build(); + let client = Client::builder().build::<_, hyper::Body>(https); + Self { + client, + base_url: base_url.trim_end_matches('/').to_string(), + api_token: api_token.to_string(), + } + } + + fn status_from(&self, status: &str) -> InvoiceStatus { + match status { + "paid" => InvoiceStatus::Paid, + "open" => InvoiceStatus::Unpaid, + // Unknown statuses are treated as expired: fail closed. + _ => InvoiceStatus::Expired, + } + } +} + +#[async_trait] +impl PaymentProcessor for GoblinPayPaymentProcessor { + /// Create a GoblinPay invoice for `amount` nanogrin. + async fn get_invoice(&self, key: &Keys, amount: u64) -> Result { + let pubkey = key.public_key().to_string(); + let memo = format!("floonet relay: {pubkey}"); + let body = CreateInvoiceBody { + amount_grin: amount, + order_ref: format!("floonet:{pubkey}"), + memo: memo.clone(), + }; + let uri = Uri::from_str(&format!("{}/invoice", self.base_url)) + .map_err(|_| Error::CustomError("invalid goblinpay url".to_string()))?; + let req = hyper::Request::builder() + .method(hyper::Method::POST) + .uri(uri) + .header("Authorization", format!("Bearer {}", self.api_token)) + .header("Content-Type", "application/json") + .body(hyper::Body::from(serde_json::to_string(&body)?)) + .expect("request builder"); + + let res = self.client.request(req).await?; + if !res.status().is_success() { + return Err(Error::CustomError(format!( + "goblinpay create invoice failed: HTTP {}", + res.status() + ))); + } + let body = hyper::body::to_bytes(res.into_body()).await?; + let invoice: InvoiceResponse = serde_json::from_slice(&body)?; + + Ok(InvoiceInfo { + pubkey, + payment_hash: invoice.invoice_id, + bolt11: invoice.pay_url, + amount, + memo, + status: self.status_from(&invoice.status), + confirmed_at: None, + }) + } + + /// Ask GoblinPay for the invoice's current status. GoblinPay only + /// reports `paid` after the Grin payment confirmed on chain. + async fn check_invoice(&self, payment_hash: &str) -> Result { + // The id is server-generated, but never let a crafted value alter + // the request path. + if !payment_hash + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return Err(Error::CustomError("invalid invoice id".to_string())); + } + let uri = Uri::from_str(&format!("{}/invoice/{}", self.base_url, payment_hash)) + .map_err(|_| Error::CustomError("invalid goblinpay url".to_string()))?; + let req = hyper::Request::builder() + .method(hyper::Method::GET) + .uri(uri) + .header("Authorization", format!("Bearer {}", self.api_token)) + .body(hyper::Body::empty()) + .expect("request builder"); + + let res = self.client.request(req).await?; + if !res.status().is_success() { + return Err(Error::CustomError(format!( + "goblinpay check invoice failed: HTTP {}", + res.status() + ))); + } + let body = hyper::body::to_bytes(res.into_body()).await?; + let invoice: InvoiceResponse = serde_json::from_slice(&body)?; + Ok(self.status_from(&invoice.status)) + } +} diff --git a/src/payment/lnbits.rs b/src/payment/lnbits.rs new file mode 100644 index 0000000..a5c15d7 --- /dev/null +++ b/src/payment/lnbits.rs @@ -0,0 +1,176 @@ +//! LNBits payment processor +use http::Uri; +use hyper::client::connect::HttpConnector; +use hyper::Client; +use hyper_rustls::HttpsConnector; +use nostr::Keys; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use async_trait::async_trait; +use rand::Rng; + +use std::str::FromStr; +use url::Url; + +use crate::{config::Settings, error::Error}; + +use super::{InvoiceInfo, InvoiceStatus, PaymentProcessor}; + +const APIPATH: &str = "/api/v1/payments/"; + +/// Info LNBits expects in create invoice request +#[derive(Serialize, Deserialize, Debug)] +pub struct LNBitsCreateInvoice { + out: bool, + amount: u64, + memo: String, + webhook: String, + unit: String, + internal: bool, + expiry: u64, +} + +/// Invoice response for LN bits +#[derive(Debug, Serialize, Deserialize)] +pub struct LNBitsCreateInvoiceResponse { + payment_hash: String, + payment_request: String, +} + +/// LNBits call back response +/// Used when an invoice is paid +/// lnbits to post the status change to relay +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct LNBitsCallback { + pub checking_id: String, + pub pending: bool, + pub amount: u64, + pub memo: String, + pub time: u64, + pub bolt11: String, + pub preimage: String, + pub payment_hash: String, + pub wallet_id: String, + pub webhook: String, + pub webhook_status: Option, +} + +/// LN Bits repose for check invoice endpoint +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct LNBitsCheckInvoiceResponse { + paid: bool, +} + +#[derive(Clone)] +pub struct LNBitsPaymentProcessor { + /// HTTP client + client: hyper::Client, hyper::Body>, + settings: Settings, +} + +impl LNBitsPaymentProcessor { + pub fn new(settings: &Settings) -> Self { + // setup hyper client + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .https_only() + .enable_http1() + .build(); + let client = Client::builder().build::<_, hyper::Body>(https); + + Self { + client, + settings: settings.clone(), + } + } +} + +#[async_trait] +impl PaymentProcessor for LNBitsPaymentProcessor { + /// Calls LNBits api to ger new invoice + async fn get_invoice(&self, key: &Keys, amount: u64) -> Result { + let random_number: u16 = rand::thread_rng().gen(); + let memo = format!("{}: {}", random_number, key.public_key()); + + let callback_url = Url::parse( + &self + .settings + .info + .relay_url + .clone() + .unwrap() + .replace("ws", "http"), + )? + .join("lnbits")?; + + let body = LNBitsCreateInvoice { + out: false, + amount, + memo: memo.clone(), + webhook: callback_url.to_string(), + unit: "sat".to_string(), + internal: false, + expiry: 3600, + }; + let url = Url::parse(&self.settings.pay_to_relay.node_url)?.join(APIPATH)?; + let uri = Uri::from_str(url.as_str().strip_suffix('/').unwrap_or(url.as_str())).unwrap(); + + let req = hyper::Request::builder() + .method(hyper::Method::POST) + .uri(uri) + .header("X-Api-Key", &self.settings.pay_to_relay.api_secret) + .body(hyper::Body::from(serde_json::to_string(&body)?)) + .expect("request builder"); + + let res = self.client.request(req).await?; + + // Json to Struct of LNbits callback + let body = hyper::body::to_bytes(res.into_body()).await?; + let invoice_response: LNBitsCreateInvoiceResponse = serde_json::from_slice(&body)?; + + Ok(InvoiceInfo { + pubkey: key.public_key().to_string(), + payment_hash: invoice_response.payment_hash, + bolt11: invoice_response.payment_request, + amount, + memo, + status: InvoiceStatus::Unpaid, + confirmed_at: None, + }) + } + + /// Calls LNBits Api to check the payment status of invoice + async fn check_invoice(&self, payment_hash: &str) -> Result { + let url = Url::parse(&self.settings.pay_to_relay.node_url)? + .join(APIPATH)? + .join(payment_hash)?; + let uri = Uri::from_str(url.as_str()).unwrap(); + + let req = hyper::Request::builder() + .method(hyper::Method::GET) + .uri(uri) + .header("X-Api-Key", &self.settings.pay_to_relay.api_secret) + .body(hyper::Body::empty()) + .expect("request builder"); + + let res = self.client.request(req).await?; + // Json to Struct of LNbits callback + let body = hyper::body::to_bytes(res.into_body()).await?; + let invoice_response: Value = serde_json::from_slice(&body)?; + + let status = if let Ok(invoice_response) = + serde_json::from_value::(invoice_response) + { + if invoice_response.paid { + InvoiceStatus::Paid + } else { + InvoiceStatus::Unpaid + } + } else { + InvoiceStatus::Expired + }; + + Ok(status) + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs new file mode 100644 index 0000000..aa430a3 --- /dev/null +++ b/src/payment/mod.rs @@ -0,0 +1,287 @@ +use crate::error::{Error, Result}; +use crate::event::Event; +use crate::payment::cln_rest::ClnRestPaymentProcessor; +use crate::payment::goblinpay::GoblinPayPaymentProcessor; +use crate::payment::lnbits::LNBitsPaymentProcessor; +use crate::repo::NostrRepo; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tracing::{info, warn}; + +use async_trait::async_trait; +use nostr::key::{FromPkStr, FromSkStr}; +use nostr::{key::Keys, Event as NostrEvent, EventBuilder}; + +pub mod cln_rest; +pub mod goblinpay; +pub mod lnbits; + +/// Payment handler +pub struct Payment { + /// Repository for saving/retrieving events and events + repo: Arc, + /// Newly validated events get written and then broadcast on this channel to subscribers + event_tx: tokio::sync::broadcast::Sender, + /// Payment message sender + payment_tx: tokio::sync::broadcast::Sender, + /// Payment message receiver + payment_rx: tokio::sync::broadcast::Receiver, + /// Settings + settings: crate::config::Settings, + // Nostr Keys + nostr_keys: Option, + /// Payment Processor + processor: Arc, +} + +#[async_trait] +pub trait PaymentProcessor: Send + Sync { + /// Get invoice from processor + async fn get_invoice(&self, keys: &Keys, amount: u64) -> Result; + /// Check payment status of an invoice + async fn check_invoice(&self, payment_hash: &str) -> Result; +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum Processor { + LNBits, + ClnRest, + GoblinPay, +} + +/// Possible states of an invoice +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, sqlx::Type)] +#[sqlx(type_name = "status")] +pub enum InvoiceStatus { + Unpaid, + Paid, + Expired, +} + +impl std::fmt::Display for InvoiceStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InvoiceStatus::Paid => write!(f, "Paid"), + InvoiceStatus::Unpaid => write!(f, "Unpaid"), + InvoiceStatus::Expired => write!(f, "Expired"), + } + } +} + +/// Invoice information +#[derive(Debug, Clone)] +pub struct InvoiceInfo { + pub pubkey: String, + pub payment_hash: String, + pub bolt11: String, + pub amount: u64, + pub status: InvoiceStatus, + pub memo: String, + pub confirmed_at: Option, +} + +/// Message variants for the payment channel +#[derive(Debug, Clone)] +pub enum PaymentMessage { + /// New account + NewAccount(String), + /// Check account, + CheckAccount(String), + /// Account Admitted + AccountAdmitted(String), + /// Invoice generated + Invoice(String, InvoiceInfo), + /// Invoice call back + /// Payment hash is passed + // This may have to be changed to better support other processors + InvoicePaid(String), +} + +impl Payment { + pub fn new( + repo: Arc, + payment_tx: tokio::sync::broadcast::Sender, + payment_rx: tokio::sync::broadcast::Receiver, + event_tx: tokio::sync::broadcast::Sender, + settings: crate::config::Settings, + ) -> Result { + info!("Create payment handler"); + + // Create nostr key from sk string + let nostr_keys = if let Some(secret_key) = &settings.pay_to_relay.secret_key { + Some(Keys::from_sk_str(secret_key)?) + } else { + None + }; + + // Create processor kind defined in settings + let processor: Arc = match &settings.pay_to_relay.processor { + Processor::LNBits => Arc::new(LNBitsPaymentProcessor::new(&settings)), + Processor::ClnRest => Arc::new(ClnRestPaymentProcessor::new(&settings)?), + Processor::GoblinPay => Arc::new(GoblinPayPaymentProcessor::new( + &settings.pay_to_relay.node_url, + &settings.pay_to_relay.api_secret, + )), + }; + + Ok(Payment { + repo, + payment_tx, + payment_rx, + event_tx, + settings, + nostr_keys, + processor, + }) + } + + /// Perform Payment tasks + pub async fn run(&mut self) { + loop { + let res = self.run_internal().await; + if let Err(e) = res { + info!("error in payment: {:?}", e); + } + } + } + + /// Internal select loop for preforming payment operations + async fn run_internal(&mut self) -> Result<()> { + tokio::select! { + m = self.payment_rx.recv() => { + match m { + Ok(PaymentMessage::NewAccount(pubkey)) => { + info!("payment event for {:?}", pubkey); + // REVIEW: This will need to change for cost per event + let amount = self.settings.pay_to_relay.admission_cost; + let invoice_info = self.get_invoice_info(&pubkey, amount).await?; + // TODO: should handle this error + self.payment_tx.send(PaymentMessage::Invoice(pubkey, invoice_info)).ok(); + }, + // Gets the most recent unpaid invoice from database + // Checks LNbits to verify if paid/unpaid + Ok(PaymentMessage::CheckAccount(pubkey)) => { + let keys = Keys::from_pk_str(&pubkey)?; + + if let Ok(Some(invoice_info)) = self.repo.get_unpaid_invoice(&keys).await { + match self.check_invoice_status(&invoice_info.payment_hash).await? { + InvoiceStatus::Paid => { + self.repo.admit_account(&keys, self.settings.pay_to_relay.admission_cost).await?; + self.payment_tx.send(PaymentMessage::AccountAdmitted(pubkey)).ok(); + } + _ => { + self.payment_tx.send(PaymentMessage::Invoice(pubkey, invoice_info)).ok(); + } + } + } else { + let amount = self.settings.pay_to_relay.admission_cost; + let invoice_info = self.get_invoice_info(&pubkey, amount).await?; + self.payment_tx.send(PaymentMessage::Invoice(pubkey, invoice_info)).ok(); + } + } + Ok(PaymentMessage::InvoicePaid(payment_hash)) => { + if self.check_invoice_status(&payment_hash).await?.eq(&InvoiceStatus::Paid) { + let pubkey = self.repo + .update_invoice(&payment_hash, InvoiceStatus::Paid) + .await?; + + let key = Keys::from_pk_str(&pubkey)?; + self.repo.admit_account(&key, self.settings.pay_to_relay.admission_cost).await?; + } + } + Ok(_) => { + // For this variant nothing need to be done here + // it is used by `server` + } + Err(err) => warn!("Payment RX: {err}") + } + } + } + + Ok(()) + } + + /// Sends Nostr DM to pubkey that requested invoice + /// Two events the terms followed by the bolt11 invoice + pub async fn send_admission_message( + &self, + pubkey: &str, + invoice_info: &InvoiceInfo, + ) -> Result<()> { + let nostr_keys = match &self.nostr_keys { + Some(key) => key, + None => return Err(Error::CustomError("Nostr key not defined".to_string())), + }; + + // Create Nostr key from pk + let key = Keys::from_pk_str(pubkey)?; + + let pubkey = key.public_key(); + + // Event DM with terms of service + let message_event: NostrEvent = EventBuilder::new_encrypted_direct_msg( + nostr_keys, + pubkey, + &self.settings.pay_to_relay.terms_message, + )? + .to_event(nostr_keys)?; + + // Event DM with invoice + let invoice_event: NostrEvent = + EventBuilder::new_encrypted_direct_msg(nostr_keys, pubkey, &invoice_info.bolt11)? + .to_event(nostr_keys)?; + + // Persist DM events to DB + self.repo.write_event(&message_event.clone().into()).await?; + self.repo.write_event(&invoice_event.clone().into()).await?; + + // Broadcast DM events + self.event_tx.send(message_event.clone().into()).ok(); + self.event_tx.send(invoice_event.clone().into()).ok(); + + Ok(()) + } + + /// Get Invoice Info + /// If the has an active invoice that will be return + /// Otherwise a new invoice will be generated by the payment processor + pub async fn get_invoice_info(&self, pubkey: &str, amount: u64) -> Result { + // If user is already in DB this will be false + // This avoids recreating admission invoices + // I think it will continue to send DMs with the invoice + // If client continues to try and write to the relay (will be same invoice) + let key = Keys::from_pk_str(pubkey)?; + if !self.repo.create_account(&key).await? { + if let Ok(Some(invoice_info)) = self.repo.get_unpaid_invoice(&key).await { + return Ok(invoice_info); + } + } + + let key = Keys::from_pk_str(pubkey)?; + + let invoice_info = self.processor.get_invoice(&key, amount).await?; + + // Persist invoice to DB + self.repo + .create_invoice_record(&key, invoice_info.clone()) + .await?; + + if self.settings.pay_to_relay.direct_message { + // Admission event invoice and terms to pubkey that is joining + self.send_admission_message(pubkey, &invoice_info).await?; + } + + Ok(invoice_info) + } + + /// Check paid status of invoice with LNbits + pub async fn check_invoice_status(&self, payment_hash: &str) -> Result { + // Check base if passed expiry time + let status = self.processor.check_invoice(payment_hash).await?; + self.repo + .update_invoice(payment_hash, status.clone()) + .await?; + + Ok(status) + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs new file mode 100644 index 0000000..b4dafb9 --- /dev/null +++ b/src/repo/mod.rs @@ -0,0 +1,98 @@ +use crate::db::QueryResult; +use crate::error::Result; +use crate::event::Event; +use crate::nip05::VerificationRecord; +use crate::payment::{InvoiceInfo, InvoiceStatus}; +use crate::subscription::Subscription; +use crate::utils::unix_time; +use async_trait::async_trait; +use nostr::Keys; +use rand::Rng; + +pub mod postgres; +pub mod postgres_migration; +pub mod sqlite; +pub mod sqlite_migration; + +#[async_trait] +pub trait NostrRepo: Send + Sync { + /// Start the repository (any initialization or maintenance tasks can be kicked off here) + async fn start(&self) -> Result<()>; + + /// Run migrations and return current version + async fn migrate_up(&self) -> Result; + + /// Persist event to database + async fn write_event(&self, e: &Event) -> Result; + + /// Perform a database query using a subscription. + /// + /// The [`Subscription`] is converted into a SQL query. Each result + /// is published on the `query_tx` channel as it is returned. If a + /// message becomes available on the `abandon_query_rx` channel, the + /// query is immediately aborted. + async fn query_subscription( + &self, + sub: Subscription, + client_id: String, + query_tx: tokio::sync::mpsc::Sender, + mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>, + ) -> Result<()>; + + /// Perform normal maintenance + async fn optimize_db(&self) -> Result<()>; + + /// Create a new verification record connected to a specific event + async fn create_verification_record(&self, event_id: &str, name: &str) -> Result<()>; + + /// Update verification timestamp + async fn update_verification_timestamp(&self, id: u64) -> Result<()>; + + /// Update verification record as failed + async fn fail_verification(&self, id: u64) -> Result<()>; + + /// Delete verification record + async fn delete_verification(&self, id: u64) -> Result<()>; + + /// Get the latest verification record for a given pubkey. + async fn get_latest_user_verification(&self, pub_key: &str) -> Result; + + /// Get oldest verification before timestamp + async fn get_oldest_user_verification(&self, before: u64) -> Result; + + /// Create a new account + async fn create_account(&self, pubkey: &Keys) -> Result; + + /// Admit an account + async fn admit_account(&self, pubkey: &Keys, admission_cost: u64) -> Result<()>; + + /// Gets user balance if they are an admitted pubkey + async fn get_account_balance(&self, pubkey: &Keys) -> Result<(bool, u64)>; + + /// Update account balance + async fn update_account_balance( + &self, + pub_key: &Keys, + positive: bool, + new_balance: u64, + ) -> Result<()>; + + /// Create invoice record + async fn create_invoice_record(&self, pubkey: &Keys, invoice_info: InvoiceInfo) -> Result<()>; + + /// Update Invoice for given payment hash + async fn update_invoice(&self, payment_hash: &str, status: InvoiceStatus) -> Result; + + /// Get the most recent invoice for a given pubkey + /// invoice must be unpaid and not expired + async fn get_unpaid_invoice(&self, pubkey: &Keys) -> Result>; +} + +// Current time, with a slight forward jitter in seconds +pub(crate) fn now_jitter(sec: u64) -> u64 { + // random time between now, and 10min in future. + let mut rng = rand::thread_rng(); + let jitter_amount = rng.gen_range(0..sec); + let now = unix_time(); + now.saturating_add(jitter_amount) +} diff --git a/src/repo/postgres.rs b/src/repo/postgres.rs new file mode 100644 index 0000000..15f87f6 --- /dev/null +++ b/src/repo/postgres.rs @@ -0,0 +1,1063 @@ +use crate::db::QueryResult; +use crate::error; +use crate::error::Result; +use crate::event::{single_char_tagname, Event}; +use crate::nip05::{Nip05Name, VerificationRecord}; +use crate::payment::{InvoiceInfo, InvoiceStatus}; +use crate::repo::postgres_migration::run_migrations; +use crate::repo::{now_jitter, NostrRepo}; +use crate::server::NostrMetrics; +use crate::subscription::{ReqFilter, Subscription, TagOperand}; +use crate::utils::{self, is_hex, is_lower_hex}; +use async_std::stream::StreamExt; +use async_trait::async_trait; +use chrono::{DateTime, TimeZone, Utc}; +use itertools::Itertools; +use nostr::key::Keys; +use sqlx::postgres::PgRow; +use sqlx::Error::RowNotFound; +use sqlx::{Error, Execute, FromRow, Postgres, QueryBuilder, Row}; +use std::ops::Deref; +use std::time::{Duration, Instant}; +use tokio::sync::mpsc::Sender; +use tokio::sync::oneshot::Receiver; +use tracing::{debug, error, info, trace, warn}; + +pub type PostgresPool = sqlx::pool::Pool; + +pub struct PostgresRepo { + conn: PostgresPool, + conn_write: PostgresPool, + metrics: NostrMetrics, +} + +impl PostgresRepo { + pub fn new(c: PostgresPool, cw: PostgresPool, m: NostrMetrics) -> PostgresRepo { + PostgresRepo { + conn: c, + conn_write: cw, + metrics: m, + } + } +} + +/// Cleanup expired events on a regular basis +async fn cleanup_expired(conn: PostgresPool, frequency: Duration) -> Result<()> { + tokio::task::spawn(async move { + loop { + tokio::select! { + _ = tokio::time::sleep(frequency) => { + let start = Instant::now(); + let exp_res = delete_expired(conn.clone()).await; + match exp_res { + Ok(exp_count) => { + if exp_count > 0 { + info!("removed {} expired events in: {:?}", exp_count, start.elapsed()); + } + }, + Err(e) => { + warn!("could not remove expired events due to error: {:?}", e); + } + } + } + }; + } + }); + Ok(()) +} + +/// One-time deletion of all expired events +async fn delete_expired(conn: PostgresPool) -> Result { + let mut tx = conn.begin().await?; + let update_count = sqlx::query("DELETE FROM \"event\" WHERE expires_at <= $1;") + .bind(Utc.timestamp_opt(utils::unix_time() as i64, 0).unwrap()) + .execute(&mut tx) + .await? + .rows_affected(); + tx.commit().await?; + Ok(update_count) +} + +#[async_trait] +impl NostrRepo for PostgresRepo { + async fn start(&self) -> Result<()> { + // begin a cleanup task for expired events. + cleanup_expired(self.conn_write.clone(), Duration::from_secs(600)).await?; + Ok(()) + } + + async fn migrate_up(&self) -> Result { + Ok(run_migrations(&self.conn_write).await?) + } + + async fn write_event(&self, e: &Event) -> Result { + // start transaction + let mut tx = self.conn_write.begin().await?; + let start = Instant::now(); + + // get relevant fields from event and convert to blobs. + let id_blob = hex::decode(&e.id).ok(); + let pubkey_blob: Option> = hex::decode(&e.pubkey).ok(); + let delegator_blob: Option> = + e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok()); + let event_str = serde_json::to_string(&e).unwrap(); + + // determine if this event would be shadowed by an existing + // replaceable event or parameterized replaceable event. + if e.is_replaceable() { + let repl_count = sqlx::query( + "SELECT e.id FROM event e WHERE e.pub_key=$1 AND e.kind=$2 AND e.created_at >= $3 LIMIT 1;") + .bind(&pubkey_blob) + .bind(e.kind as i64) + .bind(Utc.timestamp_opt(e.created_at as i64, 0).unwrap()) + .fetch_optional(&mut tx) + .await?; + if repl_count.is_some() { + return Ok(0); + } + } + if let Some(d_tag) = e.distinct_param() { + let repl_count: i64 = if is_lower_hex(&d_tag) && (d_tag.len() % 2 == 0) { + sqlx::query_scalar( + "SELECT count(*) AS count FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.pub_key=$1 AND e.kind=$2 AND t.name='d' AND t.value_hex=$3 AND e.created_at >= $4 LIMIT 1;") + .bind(hex::decode(&e.pubkey).ok()) + .bind(e.kind as i64) + .bind(hex::decode(d_tag).ok()) + .bind(Utc.timestamp_opt(e.created_at as i64, 0).unwrap()) + .fetch_one(&mut tx) + .await? + } else { + sqlx::query_scalar( + "SELECT count(*) AS count FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.pub_key=$1 AND e.kind=$2 AND t.name='d' AND t.value=$3 AND e.created_at >= $4 LIMIT 1;") + .bind(hex::decode(&e.pubkey).ok()) + .bind(e.kind as i64) + .bind(d_tag.as_bytes()) + .bind(Utc.timestamp_opt(e.created_at as i64, 0).unwrap()) + .fetch_one(&mut tx) + .await? + }; + // if any rows were returned, then some newer event with + // the same author/kind/tag value exist, and we can ignore + // this event. + if repl_count > 0 { + return Ok(0); + } + } + // ignore if the event hash is a duplicate. + let mut ins_count = sqlx::query( + r#"INSERT INTO "event" +(id, pub_key, created_at, expires_at, kind, "content", delegated_by) +VALUES($1, $2, $3, $4, $5, $6, $7) +ON CONFLICT (id) DO NOTHING"#, + ) + .bind(&id_blob) + .bind(&pubkey_blob) + .bind(Utc.timestamp_opt(e.created_at as i64, 0).unwrap()) + .bind( + e.expiration() + .and_then(|x| Utc.timestamp_opt(x as i64, 0).latest()), + ) + .bind(e.kind as i64) + .bind(event_str.into_bytes()) + .bind(delegator_blob) + .execute(&mut tx) + .await? + .rows_affected(); + + if ins_count == 0 { + // if the event was a duplicate, no need to insert event or + // pubkey references. This will abort the txn. + return Ok(0); + } + + // add all tags to the tag table + for tag in e.tags.iter() { + // ensure we have 2 values. + if tag.len() >= 2 { + let tag_name = &tag[0]; + let tag_val = &tag[1]; + // only single-char tags are searchable + let tag_char_opt = single_char_tagname(tag_name); + if tag_char_opt.is_some() { + // if tag value is lowercase hex; + if is_lower_hex(tag_val) && (tag_val.len() % 2 == 0) { + sqlx::query("INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES($1, $2, NULL, $3) \ + ON CONFLICT (event_id, \"name\", value, value_hex) DO NOTHING") + .bind(&id_blob) + .bind(tag_name) + .bind(hex::decode(tag_val).ok()) + .execute(&mut tx) + .await?; + } else { + sqlx::query("INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES($1, $2, $3, NULL) \ + ON CONFLICT (event_id, \"name\", value, value_hex) DO NOTHING") + .bind(&id_blob) + .bind(tag_name) + .bind(tag_val.as_bytes()) + .execute(&mut tx) + .await?; + } + } + } + } + if e.is_replaceable() { + let update_count = sqlx::query("DELETE FROM \"event\" WHERE kind=$1 and pub_key = $2 and id not in (select id from \"event\" where kind=$1 and pub_key=$2 order by created_at desc limit 1);") + .bind(e.kind as i64) + .bind(hex::decode(&e.pubkey).ok()) + .execute(&mut tx) + .await?.rows_affected(); + if update_count > 0 { + info!( + "hid {} older replaceable kind {} events for author: {:?}", + update_count, + e.kind, + e.get_author_prefix() + ); + } + } + // parameterized replaceable events + // check for parameterized replaceable events that would be hidden; don't insert these either. + if let Some(d_tag) = e.distinct_param() { + let update_count = if is_lower_hex(&d_tag) && (d_tag.len() % 2 == 0) { + sqlx::query("DELETE FROM event WHERE kind=$1 AND pub_key=$2 AND id IN (SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.kind=$1 AND e.pub_key=$2 AND t.name='d' AND t.value_hex=$3 ORDER BY created_at DESC OFFSET 1);") + .bind(e.kind as i64) + .bind(hex::decode(&e.pubkey).ok()) + .bind(hex::decode(d_tag).ok()) + .execute(&mut tx) + .await?.rows_affected() + } else { + sqlx::query("DELETE FROM event WHERE kind=$1 AND pub_key=$2 AND id IN (SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.kind=$1 AND e.pub_key=$2 AND t.name='d' AND t.value=$3 ORDER BY created_at DESC OFFSET 1);") + .bind(e.kind as i64) + .bind(hex::decode(&e.pubkey).ok()) + .bind(d_tag.as_bytes()) + .execute(&mut tx) + .await?.rows_affected() + }; + if update_count > 0 { + info!( + "removed {} older parameterized replaceable kind {} events for author: {:?}", + update_count, + e.kind, + e.get_author_prefix() + ); + } + } + // if this event is a deletion, hide the referenced events from the same author. + if e.kind == 5 { + let event_candidates = e.tag_values_by_name("e"); + let pub_keys: Vec> = event_candidates + .iter() + .filter(|x| is_hex(x) && x.len() == 64) + .filter_map(|x| hex::decode(x).ok()) + .collect(); + + let mut builder = QueryBuilder::new( + "UPDATE \"event\" SET hidden = 1::bit(1) WHERE kind != 5 AND pub_key = ", + ); + builder.push_bind(hex::decode(&e.pubkey).ok()); + builder.push(" AND id IN ("); + + let mut sep = builder.separated(", "); + for pk in pub_keys { + sep.push_bind(pk); + } + sep.push_unseparated(")"); + + let update_count = builder.build().execute(&mut tx).await?.rows_affected(); + info!( + "hid {} deleted events for author {:?}", + update_count, + e.get_author_prefix() + ); + } else { + // check if a deletion has already been recorded for this event. + // Only relevant for non-deletion events + let del_count = sqlx::query( + "SELECT e.id FROM \"event\" e \ + LEFT JOIN tag t ON e.id = t.event_id \ + WHERE e.pub_key = $1 AND t.\"name\" = 'e' AND e.kind = 5 AND t.value = $2 LIMIT 1", + ) + .bind(&pubkey_blob) + .bind(&id_blob) + .fetch_optional(&mut tx) + .await?; + + // check if a the query returned a result, meaning we should + // hid the current event + if del_count.is_some() { + // a deletion already existed, mark original event as hidden. + info!( + "hid event: {:?} due to existing deletion by author: {:?}", + e.get_event_id_prefix(), + e.get_author_prefix() + ); + sqlx::query("UPDATE \"event\" SET hidden = 1::bit(1) WHERE id = $1") + .bind(&id_blob) + .execute(&mut tx) + .await?; + // event was deleted, so let caller know nothing new + // arrived, preventing this from being sent to active + // subscriptions + ins_count = 0; + } + } + tx.commit().await?; + self.metrics + .write_events + .observe(start.elapsed().as_secs_f64()); + Ok(ins_count) + } + + async fn query_subscription( + &self, + sub: Subscription, + client_id: String, + query_tx: Sender, + mut abandon_query_rx: Receiver<()>, + ) -> Result<()> { + let start = Instant::now(); + let mut row_count: usize = 0; + let metrics = &self.metrics; + + for filter in sub.filters.iter() { + let start = Instant::now(); + // generate SQL query + let q_filter = query_from_filter(filter); + if q_filter.is_none() { + debug!("Failed to generate query!"); + continue; + } + + debug!("SQL generated in {:?}", start.elapsed()); + + // cutoff for displaying slow queries + let slow_cutoff = Duration::from_millis(2000); + + // any client that doesn't cause us to generate new rows in 5 + // seconds gets dropped. + let abort_cutoff = Duration::from_secs(5); + + let start = Instant::now(); + let mut slow_first_event; + let mut last_successful_send = Instant::now(); + + // execute the query. Don't cache, since queries vary so much. + let mut q_filter = q_filter.unwrap(); + let q_build = q_filter.build(); + let sql = q_build.sql(); + let mut results = q_build.fetch(&self.conn); + + let mut first_result = true; + while let Some(row) = results.next().await { + if let Err(e) = row { + error!("Query failed: {} {} {:?}", e, sql, filter); + break; + } + let first_event_elapsed = start.elapsed(); + slow_first_event = first_event_elapsed >= slow_cutoff; + if first_result { + debug!( + "first result in {:?} (cid: {}, sub: {:?})", + first_event_elapsed, client_id, sub.id + ); + first_result = false; + } + + // logging for slow queries; show sub and SQL. + // to reduce logging; only show 1/16th of clients (leading 0) + if slow_first_event && client_id.starts_with("00") { + debug!( + "query req (slow): {:?} (cid: {}, sub: {:?})", + &sub, client_id, sub.id + ); + } else { + trace!( + "query req: {:?} (cid: {}, sub: {:?})", + &sub, + client_id, + sub.id + ); + } + + // check if this is still active; every 100 rows + if row_count % 100 == 0 && abandon_query_rx.try_recv().is_ok() { + debug!( + "query cancelled by client (cid: {}, sub: {:?})", + client_id, sub.id + ); + return Ok(()); + } + + row_count += 1; + let event_json: Vec = row.unwrap().get(0); + loop { + if query_tx.capacity() != 0 { + // we have capacity to add another item + break; + } else { + // the queue is full + trace!("db reader thread is stalled"); + if last_successful_send + abort_cutoff < Instant::now() { + // the queue has been full for too long, abort + info!("aborting database query due to slow client"); + metrics + .query_aborts + .with_label_values(&["slowclient"]) + .inc(); + return Ok(()); + } + // give the queue a chance to clear before trying again + async_std::task::sleep(Duration::from_millis(100)).await; + } + } + + // TODO: we could use try_send, but we'd have to juggle + // getting the query result back as part of the error + // result. + query_tx + .send(QueryResult { + sub_id: sub.get_id(), + event: String::from_utf8(event_json).unwrap(), + }) + .await + .ok(); + last_successful_send = Instant::now(); + } + } + query_tx + .send(QueryResult { + sub_id: sub.get_id(), + event: "EOSE".to_string(), + }) + .await + .ok(); + self.metrics + .query_sub + .observe(start.elapsed().as_secs_f64()); + debug!( + "query completed in {:?} (cid: {}, sub: {:?}, db_time: {:?}, rows: {})", + start.elapsed(), + client_id, + sub.id, + start.elapsed(), + row_count + ); + Ok(()) + } + + async fn optimize_db(&self) -> Result<()> { + // Not implemented + Ok(()) + } + + async fn create_verification_record(&self, event_id: &str, name: &str) -> Result<()> { + let mut tx = self.conn_write.begin().await?; + + sqlx::query("DELETE FROM user_verification WHERE \"name\" = $1") + .bind(name) + .execute(&mut tx) + .await?; + + sqlx::query("INSERT INTO user_verification (event_id, \"name\", verified_at) VALUES ($1, $2, now())") + .bind(hex::decode(event_id).ok()) + .bind(name) + .execute(&mut tx) + .await?; + + tx.commit().await?; + info!("saved new verification record for ({:?})", name); + Ok(()) + } + + async fn update_verification_timestamp(&self, id: u64) -> Result<()> { + // add some jitter to the verification to prevent everything from stacking up together. + let verify_time = now_jitter(600); + + // update verification time and reset any failure count + sqlx::query("UPDATE user_verification SET verified_at = $1, fail_count = 0 WHERE id = $2") + .bind(Utc.timestamp_opt(verify_time as i64, 0).unwrap()) + .bind(id as i64) + .execute(&self.conn_write) + .await?; + + info!("verification updated for {}", id); + Ok(()) + } + + async fn fail_verification(&self, id: u64) -> Result<()> { + sqlx::query("UPDATE user_verification SET failed_at = now(), fail_count = fail_count + 1 WHERE id = $1") + .bind(id as i64) + .execute(&self.conn_write) + .await?; + Ok(()) + } + + async fn delete_verification(&self, id: u64) -> Result<()> { + sqlx::query("DELETE FROM user_verification WHERE id = $1") + .bind(id as i64) + .execute(&self.conn_write) + .await?; + Ok(()) + } + + async fn get_latest_user_verification(&self, pub_key: &str) -> Result { + let query = r#"SELECT + v.id, + v."name", + e.id as event_id, + e.pub_key, + e.created_at, + v.verified_at, + v.failed_at, + v.fail_count + FROM user_verification v + LEFT JOIN "event" e ON e.id = v.event_id + WHERE e.pub_key = $1 + ORDER BY e.created_at DESC, v.verified_at DESC, v.failed_at DESC + LIMIT 1"#; + sqlx::query_as::<_, VerificationRecord>(query) + .bind(hex::decode(pub_key).ok()) + .fetch_optional(&self.conn) + .await? + .ok_or(error::Error::SqlxError(RowNotFound)) + } + + async fn get_oldest_user_verification(&self, before: u64) -> Result { + let query = r#"SELECT + v.id, + v."name", + e.id as event_id, + e.pub_key, + e.created_at, + v.verified_at, + v.failed_at, + v.fail_count + FROM user_verification v + LEFT JOIN "event" e ON e.id = v.event_id + WHERE (v.verified_at < $1 OR v.verified_at IS NULL) + AND (v.failed_at < $1 OR v.failed_at IS NULL) + ORDER BY v.verified_at ASC, v.failed_at ASC + LIMIT 1"#; + sqlx::query_as::<_, VerificationRecord>(query) + .bind(Utc.timestamp_opt(before as i64, 0).unwrap()) + .fetch_optional(&self.conn) + .await? + .ok_or(error::Error::SqlxError(RowNotFound)) + } + + async fn create_account(&self, pub_key: &Keys) -> Result { + let pub_key = pub_key.public_key().to_string(); + let mut tx = self.conn_write.begin().await?; + + let result = sqlx::query("INSERT INTO account (pubkey, balance) VALUES ($1, 0);") + .bind(pub_key) + .execute(&mut tx) + .await; + + let success = match result { + Ok(res) => { + tx.commit().await?; + res.rows_affected() == 1 + } + Err(_err) => false, + }; + + Ok(success) + } + + /// Admit account + async fn admit_account(&self, pub_key: &Keys, admission_cost: u64) -> Result<()> { + let pub_key = pub_key.public_key().to_string(); + sqlx::query( + "UPDATE account SET is_admitted = TRUE, balance = balance - $1 WHERE pubkey = $2", + ) + .bind(admission_cost as i64) + .bind(pub_key) + .execute(&self.conn_write) + .await?; + Ok(()) + } + + /// Gets if the account is admitted and balance + async fn get_account_balance(&self, pub_key: &Keys) -> Result<(bool, u64)> { + let pub_key = pub_key.public_key().to_string(); + let query = r#"SELECT + is_admitted, + balance + FROM account + WHERE pubkey = $1 + LIMIT 1"#; + + let result = sqlx::query_as::<_, (bool, i64)>(query) + .bind(pub_key) + .fetch_optional(&self.conn_write) + .await? + .ok_or(error::Error::SqlxError(RowNotFound))?; + + Ok((result.0, result.1 as u64)) + } + + /// Update account balance + async fn update_account_balance( + &self, + pub_key: &Keys, + positive: bool, + new_balance: u64, + ) -> Result<()> { + let pub_key = pub_key.public_key().to_string(); + match positive { + true => { + sqlx::query("UPDATE account SET balance = balance + $1 WHERE pubkey = $2") + .bind(new_balance as i64) + .bind(pub_key) + .execute(&self.conn_write) + .await? + } + false => { + sqlx::query("UPDATE account SET balance = balance - $1 WHERE pubkey = $2") + .bind(new_balance as i64) + .bind(pub_key) + .execute(&self.conn_write) + .await? + } + }; + Ok(()) + } + + /// Create invoice record + async fn create_invoice_record(&self, pub_key: &Keys, invoice_info: InvoiceInfo) -> Result<()> { + let pub_key = pub_key.public_key().to_string(); + let mut tx = self.conn_write.begin().await?; + + sqlx::query( + "INSERT INTO invoice (pubkey, payment_hash, amount, status, description, created_at, invoice) VALUES ($1, $2, $3, $4, $5, now(), $6)", + ) + .bind(pub_key) + .bind(invoice_info.payment_hash) + .bind(invoice_info.amount as i64) + .bind(invoice_info.status) + .bind(invoice_info.memo) + .bind(invoice_info.bolt11) + .execute(&mut tx) + .await.unwrap(); + + debug!("Invoice added"); + + tx.commit().await?; + Ok(()) + } + + /// Update invoice record + async fn update_invoice(&self, payment_hash: &str, status: InvoiceStatus) -> Result { + debug!("Payment Hash: {}", payment_hash); + let query = "SELECT pubkey, status, amount FROM invoice WHERE payment_hash=$1;"; + let (pubkey, prev_invoice_status, amount) = + sqlx::query_as::<_, (String, InvoiceStatus, i64)>(query) + .bind(payment_hash) + .fetch_optional(&self.conn_write) + .await? + .ok_or(error::Error::SqlxError(RowNotFound))?; + + // If the invoice is paid update the confirmed at timestamp + let query = if status.eq(&InvoiceStatus::Paid) { + "UPDATE invoice SET status=$1, confirmed_at = now() WHERE payment_hash=$2;" + } else { + "UPDATE invoice SET status=$1 WHERE payment_hash=$2;" + }; + + sqlx::query(query) + .bind(&status) + .bind(payment_hash) + .execute(&self.conn_write) + .await?; + + if prev_invoice_status.eq(&InvoiceStatus::Unpaid) && status.eq(&InvoiceStatus::Paid) { + sqlx::query("UPDATE account SET balance = balance + $1 WHERE pubkey = $2") + .bind(amount) + .bind(&pubkey) + .execute(&self.conn_write) + .await?; + } + + Ok(pubkey) + } + + /// Get the most recent invoice for a given pubkey + /// invoice must be unpaid and not expired + async fn get_unpaid_invoice(&self, pubkey: &Keys) -> Result> { + let query = r#" +SELECT amount, payment_hash, description, invoice +FROM invoice +WHERE pubkey = $1 +ORDER BY created_at DESC +LIMIT 1; + "#; + match sqlx::query_as::<_, (i64, String, String, String)>(query) + .bind(pubkey.public_key().to_string()) + .fetch_optional(&self.conn_write) + .await + .unwrap() + { + Some((amount, payment_hash, description, invoice)) => Ok(Some(InvoiceInfo { + pubkey: pubkey.public_key().to_string(), + payment_hash, + bolt11: invoice, + amount: amount as u64, + status: InvoiceStatus::Unpaid, + memo: description, + confirmed_at: None, + })), + None => Ok(None), + } + } +} + +/// Create a dynamic SQL query and params from a subscription filter. +fn query_from_filter(f: &'_ ReqFilter) -> Option> { + // if the filter is malformed, don't return anything. + if f.force_no_match { + return None; + } + + let mut query = QueryBuilder::new("SELECT e.\"content\", e.created_at FROM \"event\" e"); + + // This tracks whether we need to push a prefix AND before adding another clause + let mut push_and = false; + + // Query for tags + if let Some(map) = &f.tags { + if !map.is_empty() { + let mut tag_ctr = 1; + for (key, val) in map.iter().sorted_by(|(k1, _), (k2, _)| k1.cmp(k2)) { + if val.is_empty() { + return None; + } + let has_plain_values = val.deref().into_iter().any(|v| !is_lower_hex(&v)); + let has_hex_values = val.deref().into_iter().any(|v| is_lower_hex(&v)); + + if let TagOperand::Or(v_or) = val { + query + .push(format!( + " JOIN tag t{0} on e.id = t{0}.event_id AND t{0}.\"name\" = ", + tag_ctr + )) + .push_bind(key.to_string()) + .push(" AND ("); + + if has_plain_values { + query.push(format!("t{0}.\"value\" in (", tag_ctr)); + let mut tag_query = query.separated(", "); + for v in v_or.iter().filter(|v| !is_lower_hex(v)) { + tag_query.push_bind(v.as_bytes()); + } + } + if has_plain_values && has_hex_values { + query.push(") OR "); + } + if has_hex_values { + query.push(format!("t{0}.\"value_hex\" in (", tag_ctr)); + let mut tag_query = query.separated(", "); + for v in v_or.iter().filter(|v| v.len() % 2 == 0 && is_lower_hex(v)) { + tag_query.push_bind(hex::decode(v).ok()); + } + } + + tag_ctr += 1; + query.push("))"); + } else if let TagOperand::And(v_and) = val { + let mut sorted_values: Vec<_> = v_and.iter().collect(); + sorted_values.sort(); + for vx in sorted_values { + query + .push(format!( + " JOIN \"tag\" t{0} on e.id = t{0}.event_id AND t{0}.\"name\" = ", + tag_ctr + )) + .push_bind(key.to_string()) + .push(" AND "); + + if !is_lower_hex(vx) { + query + .push(format!("t{0}.\"value\" = ", tag_ctr)) + .push_bind(vx.as_bytes()); + } else { + query + .push(format!("t{0}.\"value_hex\" = ", tag_ctr)) + .push_bind(hex::decode(vx).ok()); + } + + tag_ctr += 1; + } + } + } + } + } + + query.push(" WHERE "); + // Query for "authors", allowing prefix matches + if let Some(auth_vec) = &f.authors { + // filter out non-hex values + let auth_vec: Vec<&String> = auth_vec.iter().filter(|a| is_hex(a)).collect(); + + if auth_vec.is_empty() { + return None; + } + query.push("(e.pub_key in ("); + + let mut pk_sep = query.separated(", "); + for pk in auth_vec.iter() { + pk_sep.push_bind(hex::decode(pk).ok()); + } + query.push(") OR e.delegated_by in ("); + let mut pk_delegated_sep = query.separated(", "); + for pk in auth_vec.iter() { + pk_delegated_sep.push_bind(hex::decode(pk).ok()); + } + push_and = true; + query.push("))"); + } + + // Query for Kind + if let Some(ks) = &f.kinds { + if ks.is_empty() { + return None; + } + if push_and { + query.push(" AND "); + } + push_and = true; + + query.push("e.kind in ("); + let mut list_query = query.separated(", "); + for k in ks.iter() { + list_query.push_bind(*k as i64); + } + query.push(")"); + } + + // Query for event, + if let Some(id_vec) = &f.ids { + // filter out non-hex values + let id_vec: Vec<&String> = id_vec.iter().filter(|a| is_hex(a)).collect(); + if id_vec.is_empty() { + return None; + } + if push_and { + query.push(" AND ("); + } else { + query.push("("); + } + push_and = true; + + query.push("id in ("); + let mut sep = query.separated(", "); + for id in id_vec.iter() { + sep.push_bind(hex::decode(id).ok()); + } + query.push("))"); + } + + // Query for timestamp + if f.since.is_some() { + if push_and { + query.push(" AND "); + } + push_and = true; + query + .push("e.created_at >= ") + .push_bind(Utc.timestamp_opt(f.since.unwrap() as i64, 0).unwrap()); + } + + // Query for timestamp + if f.until.is_some() { + if push_and { + query.push(" AND "); + } + push_and = true; + query + .push("e.created_at <= ") + .push_bind(Utc.timestamp_opt(f.until.unwrap() as i64, 0).unwrap()); + } + + // never display hidden events + if push_and { + query.push(" AND e.hidden != 1::bit(1)"); + } else { + query.push("e.hidden != 1::bit(1)"); + } + // never display expired events + query.push(" AND (e.expires_at IS NULL OR e.expires_at > now())"); + + // Apply per-filter limit to this query. + // The use of a LIMIT implies a DESC order, to capture only the most recent events. + if let Some(lim) = f.limit { + query.push(" ORDER BY e.created_at DESC LIMIT "); + query.push(lim.min(1000)); + } else { + query.push(" ORDER BY e.created_at ASC LIMIT "); + query.push(1000); + } + Some(query) +} + +impl FromRow<'_, PgRow> for VerificationRecord { + fn from_row(row: &'_ PgRow) -> std::result::Result { + let name = Nip05Name::try_from(row.get::<'_, &str, &str>("name")).or(Err(RowNotFound))?; + Ok(VerificationRecord { + rowid: row.get::<'_, i64, &str>("id") as u64, + name, + address: hex::encode(row.get::<'_, Vec, &str>("pub_key")), + event: hex::encode(row.get::<'_, Vec, &str>("event_id")), + event_created: row.get::<'_, DateTime, &str>("created_at").timestamp() as u64, + last_success: match row.try_get::<'_, DateTime, &str>("verified_at") { + Ok(x) => Some(x.timestamp() as u64), + _ => None, + }, + last_failure: match row.try_get::<'_, DateTime, &str>("failed_at") { + Ok(x) => Some(x.timestamp() as u64), + _ => None, + }, + failure_count: row.get::<'_, i32, &str>("fail_count") as u64, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::subscription::TagOperand; + use std::collections::{HashMap, HashSet}; + + #[test] + fn test_query_gen_tag_value_hex() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: Some(vec![ + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ]), + limit: None, + tags: Some(HashMap::from([( + 'p', + TagOperand::Or(HashSet::from([ + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed".to_owned(), + ])), + )])), + force_no_match: false, + }; + + let q = query_from_filter(&filter).unwrap(); + assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e JOIN tag t1 on e.id = t1.event_id AND t1.\"name\" = $1 AND (t1.\"value_hex\" in ($2)) WHERE (e.pub_key in ($3) OR e.delegated_by in ($4)) AND e.kind in ($5) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000") + } + + #[test] + fn test_query_gen_tag_value() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: Some(vec![ + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ]), + limit: None, + tags: Some(HashMap::from([( + 'd', + TagOperand::Or(HashSet::from(["test".to_owned()])), + )])), + force_no_match: false, + }; + + let q = query_from_filter(&filter).unwrap(); + assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e JOIN tag t1 on e.id = t1.event_id AND t1.\"name\" = $1 AND (t1.\"value\" in ($2)) WHERE (e.pub_key in ($3) OR e.delegated_by in ($4)) AND e.kind in ($5) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000") + } + + #[test] + fn test_query_gen_tag_value_and_value_hex() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: Some(vec![ + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ]), + limit: None, + tags: Some(HashMap::from([( + 'd', + TagOperand::And(HashSet::from([ + "test".to_owned(), + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed".to_owned(), + ])), + )])), + force_no_match: false, + }; + + let q = query_from_filter(&filter).unwrap(); + assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e JOIN \"tag\" t1 on e.id = t1.event_id AND t1.\"name\" = $1 AND t1.\"value_hex\" = $2 JOIN \"tag\" t2 on e.id = t2.event_id AND t2.\"name\" = $3 AND t2.\"value\" = $4 WHERE (e.pub_key in ($5) OR e.delegated_by in ($6)) AND e.kind in ($7) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000") + } + + #[test] + fn test_query_multiple_tags() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![30_001]), + since: None, + until: None, + authors: None, + limit: None, + tags: Some(HashMap::from([( + 'd', + TagOperand::Or(HashSet::from([ + "test".to_owned(), + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed".to_owned(), + ])), + )])), + force_no_match: false, + }; + + let q = query_from_filter(&filter).unwrap(); + assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e JOIN tag t1 on e.id = t1.event_id AND t1.\"name\" = $1 AND (t1.\"value\" in ($2) OR t1.\"value_hex\" in ($3)) WHERE e.kind in ($4) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000") + } + + #[test] + fn test_query_gen_tag_value_hex_and() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: Some(vec![ + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ]), + limit: None, + tags: Some(HashMap::from([( + 'p', + TagOperand::And(HashSet::from([ + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed".to_owned(), + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ])), + )])), + force_no_match: false, + }; + let q = query_from_filter(&filter).unwrap(); + assert_eq!(q.sql(), "SELECT e.\"content\", e.created_at FROM \"event\" e JOIN \"tag\" t1 on e.id = t1.event_id AND t1.\"name\" = $1 AND t1.\"value_hex\" = $2 JOIN \"tag\" t2 on e.id = t2.event_id AND t2.\"name\" = $3 AND t2.\"value_hex\" = $4 WHERE (e.pub_key in ($5) OR e.delegated_by in ($6)) AND e.kind in ($7) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000") + } + + #[test] + fn test_query_empty_tags() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1, 6, 16, 30023, 1063, 6969]), + since: Some(1700697846), + until: None, + authors: None, + limit: None, + tags: Some(HashMap::from([('a', TagOperand::And(HashSet::new()))])), + force_no_match: false, + }; + assert!(query_from_filter(&filter).is_none()); + } +} diff --git a/src/repo/postgres_migration.rs b/src/repo/postgres_migration.rs new file mode 100644 index 0000000..6fe14de --- /dev/null +++ b/src/repo/postgres_migration.rs @@ -0,0 +1,320 @@ +use crate::repo::postgres::PostgresPool; +use async_trait::async_trait; +use sqlx::{Executor, Postgres, Transaction}; + +#[async_trait] +pub trait Migration { + fn serial_number(&self) -> i64; + async fn run(&self, tx: &mut Transaction); +} + +struct SimpleSqlMigration { + pub serial_number: i64, + pub sql: Vec<&'static str>, +} + +#[async_trait] +impl Migration for SimpleSqlMigration { + fn serial_number(&self) -> i64 { + self.serial_number + } + + async fn run(&self, tx: &mut Transaction) { + for sql in self.sql.iter() { + tx.execute(*sql).await.unwrap(); + } + } +} + +/// Execute all migrations on the database. +pub async fn run_migrations(db: &PostgresPool) -> crate::error::Result { + prepare_migrations_table(db).await; + run_migration(m001::migration(), db).await; + let m002_result = run_migration(m002::migration(), db).await; + if m002_result == MigrationResult::Upgraded { + m002::rebuild_tags(db).await?; + } + run_migration(m003::migration(), db).await; + run_migration(m004::migration(), db).await; + run_migration(m005::migration(), db).await; + Ok(current_version(db).await as usize) +} + +async fn current_version(db: &PostgresPool) -> i64 { + sqlx::query_scalar("SELECT max(serial_number) FROM migrations;") + .fetch_one(db) + .await + .unwrap() +} + +async fn prepare_migrations_table(db: &PostgresPool) { + sqlx::query("CREATE TABLE IF NOT EXISTS migrations (serial_number bigint)") + .execute(db) + .await + .unwrap(); +} + +// Running a migration was either unnecessary, or completed +#[derive(PartialEq, Eq, Debug, Clone)] +enum MigrationResult { + Upgraded, + NotNeeded, +} + +async fn run_migration(migration: impl Migration, db: &PostgresPool) -> MigrationResult { + let row: i64 = + sqlx::query_scalar("SELECT COUNT(*) AS count FROM migrations WHERE serial_number = $1") + .bind(migration.serial_number()) + .fetch_one(db) + .await + .unwrap(); + + if row > 0 { + return MigrationResult::NotNeeded; + } + + let mut transaction = db.begin().await.unwrap(); + migration.run(&mut transaction).await; + + sqlx::query("INSERT INTO migrations VALUES ($1)") + .bind(migration.serial_number()) + .execute(&mut transaction) + .await + .unwrap(); + + transaction.commit().await.unwrap(); + MigrationResult::Upgraded +} + +mod m001 { + use crate::repo::postgres_migration::{Migration, SimpleSqlMigration}; + + pub const VERSION: i64 = 1; + + pub fn migration() -> impl Migration { + SimpleSqlMigration { + serial_number: VERSION, + sql: vec![ + r#" +-- Events table +CREATE TABLE "event" ( + id bytea NOT NULL, + pub_key bytea NOT NULL, + created_at timestamp with time zone NOT NULL, + kind integer NOT NULL, + "content" bytea NOT NULL, + hidden bit(1) NOT NULL DEFAULT 0::bit(1), + delegated_by bytea NULL, + first_seen timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT event_pkey PRIMARY KEY (id) +); +CREATE INDEX event_created_at_idx ON "event" (created_at,kind); +CREATE INDEX event_pub_key_idx ON "event" (pub_key); +CREATE INDEX event_delegated_by_idx ON "event" (delegated_by); + +-- Tags table +CREATE TABLE "tag" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + event_id bytea NOT NULL, + "name" varchar NOT NULL, + value bytea NOT NULL, + CONSTRAINT tag_fk FOREIGN KEY (event_id) REFERENCES "event"(id) ON DELETE CASCADE +); +CREATE INDEX tag_event_id_idx ON tag USING btree (event_id, name); +CREATE INDEX tag_value_idx ON tag USING btree (value); + +-- NIP-05 Verification table +CREATE TABLE "user_verification" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + event_id bytea NOT NULL, + "name" varchar NOT NULL, + verified_at timestamptz NULL, + failed_at timestamptz NULL, + fail_count int4 NULL DEFAULT 0, + CONSTRAINT user_verification_pk PRIMARY KEY (id), + CONSTRAINT user_verification_fk FOREIGN KEY (event_id) REFERENCES "event"(id) ON DELETE CASCADE +); +CREATE INDEX user_verification_event_id_idx ON user_verification USING btree (event_id); +CREATE INDEX user_verification_name_idx ON user_verification USING btree (name); + "#, + ], + } + } +} + +mod m002 { + use async_std::stream::StreamExt; + use indicatif::{ProgressBar, ProgressStyle}; + use sqlx::Row; + use std::time::Instant; + use tracing::info; + + use crate::event::{single_char_tagname, Event}; + use crate::repo::postgres::PostgresPool; + use crate::repo::postgres_migration::{Migration, SimpleSqlMigration}; + use crate::utils::is_lower_hex; + + pub const VERSION: i64 = 2; + + pub fn migration() -> impl Migration { + SimpleSqlMigration { + serial_number: VERSION, + sql: vec![ + r#" +-- Add tag value column +ALTER TABLE tag ADD COLUMN value_hex bytea; +-- Remove not-null constraint +ALTER TABLE tag ALTER COLUMN value DROP NOT NULL; +-- Add value index +CREATE INDEX tag_value_hex_idx ON tag USING btree (value_hex); + "#, + ], + } + } + + pub async fn rebuild_tags(db: &PostgresPool) -> crate::error::Result<()> { + // Check how many events we have to process + let start = Instant::now(); + let mut tx = db.begin().await.unwrap(); + let mut update_tx = db.begin().await.unwrap(); + // Clear out table + sqlx::query("DELETE FROM tag;") + .execute(&mut update_tx) + .await?; + { + let event_count: i64 = sqlx::query_scalar("SELECT COUNT(*) from event;") + .fetch_one(&mut tx) + .await + .unwrap(); + let bar = ProgressBar::new(event_count.try_into().unwrap()) + .with_message("rebuilding tags table"); + bar.set_style( + ProgressStyle::with_template( + "[{elapsed_precise}] {bar:40.white/blue} {pos:>7}/{len:7} [{percent}%] {msg}", + ) + .unwrap(), + ); + let mut events = + sqlx::query("SELECT id, content FROM event ORDER BY id;").fetch(&mut tx); + while let Some(row) = events.next().await { + bar.inc(1); + // get the row id and content + let row = row.unwrap(); + let event_id: Vec = row.get(0); + let event_bytes: Vec = row.get(1); + let event: Event = serde_json::from_str(&String::from_utf8(event_bytes).unwrap())?; + + for t in event.tags.iter().filter(|x| x.len() > 1) { + let tagname = t.first().unwrap(); + let tagnamechar_opt = single_char_tagname(tagname); + if tagnamechar_opt.is_none() { + continue; + } + // safe because len was > 1 + let tagval = t.get(1).unwrap(); + // insert as BLOB if we can restore it losslessly. + // this means it needs to be even length and lowercase. + if (tagval.len() % 2 == 0) && is_lower_hex(tagval) { + let q = "INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES ($1, $2, NULL, $3) ON CONFLICT DO NOTHING;"; + sqlx::query(q) + .bind(&event_id) + .bind(tagname) + .bind(hex::decode(tagval).ok()) + .execute(&mut update_tx) + .await?; + } else { + let q = "INSERT INTO tag (event_id, \"name\", value, value_hex) VALUES ($1, $2, $3, NULL) ON CONFLICT DO NOTHING;"; + sqlx::query(q) + .bind(&event_id) + .bind(tagname) + .bind(tagval.as_bytes()) + .execute(&mut update_tx) + .await?; + } + } + } + update_tx.commit().await?; + bar.finish(); + } + info!("rebuilt tags in {:?}", start.elapsed()); + Ok(()) + } +} + +mod m003 { + use crate::repo::postgres_migration::{Migration, SimpleSqlMigration}; + + pub const VERSION: i64 = 3; + + pub fn migration() -> impl Migration { + SimpleSqlMigration { + serial_number: VERSION, + sql: vec![ + r#" +-- Add unique constraint on tag +ALTER TABLE tag ADD CONSTRAINT unique_constraint_name UNIQUE (event_id, "name", value, value_hex); + "#, + ], + } + } +} + +mod m004 { + use crate::repo::postgres_migration::{Migration, SimpleSqlMigration}; + + pub const VERSION: i64 = 4; + + pub fn migration() -> impl Migration { + SimpleSqlMigration { + serial_number: VERSION, + sql: vec![ + r#" +-- Add expiration time for events +ALTER TABLE event ADD COLUMN expires_at timestamp(0) with time zone; +-- Index expiration time +CREATE INDEX event_expires_at_idx ON "event" (expires_at); + "#, + ], + } + } +} + +mod m005 { + use crate::repo::postgres_migration::{Migration, SimpleSqlMigration}; + + pub const VERSION: i64 = 5; + + pub fn migration() -> impl Migration { + SimpleSqlMigration { + serial_number: VERSION, + sql: vec![ + r#" +-- Create account table +CREATE TABLE "account" ( + pubkey varchar NOT NULL, + is_admitted BOOLEAN NOT NULL DEFAULT FALSE, + balance BIGINT NOT NULL DEFAULT 0, + tos_accepted_at TIMESTAMP, + CONSTRAINT account_pkey PRIMARY KEY (pubkey) +); + +CREATE TYPE status AS ENUM ('Paid', 'Unpaid', 'Expired'); + + +CREATE TABLE "invoice" ( + payment_hash varchar NOT NULL, + pubkey varchar NOT NULL, + invoice varchar NOT NULL, + amount BIGINT NOT NULL, + status status NOT NULL DEFAULT 'Unpaid', + description varchar, + created_at timestamp, + confirmed_at timestamp, + CONSTRAINT invoice_payment_hash PRIMARY KEY (payment_hash), + CONSTRAINT invoice_pubkey_fkey FOREIGN KEY (pubkey) REFERENCES account (pubkey) ON DELETE CASCADE +); + "#, + ], + } + } +} diff --git a/src/repo/sqlite.rs b/src/repo/sqlite.rs new file mode 100644 index 0000000..3b9c0b7 --- /dev/null +++ b/src/repo/sqlite.rs @@ -0,0 +1,1614 @@ +//! Event persistence and querying +//use crate::config::SETTINGS; +use crate::config::Settings; +use crate::db::QueryResult; +use crate::error::{Error::SqlError, Result}; +use crate::event::{single_char_tagname, Event}; +use crate::nip05::{Nip05Name, VerificationRecord}; +use crate::payment::{InvoiceInfo, InvoiceStatus}; +use crate::repo::sqlite_migration::{upgrade_db, STARTUP_SQL}; +use crate::server::NostrMetrics; +use crate::subscription::{ReqFilter, Subscription, TagOperand}; +use crate::utils::{is_hex, unix_time}; +use async_trait::async_trait; +use hex; +use itertools::Itertools; +use r2d2; +use r2d2_sqlite::SqliteConnectionManager; +use rusqlite::params; +use rusqlite::types::ToSql; +use rusqlite::OpenFlags; +use std::fmt::Write as _; +use std::path::Path; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use std::time::Instant; +use tokio::sync::{Mutex, MutexGuard, Semaphore}; +use tokio::task; +use tracing::{debug, info, trace, warn}; + +use crate::repo::{now_jitter, NostrRepo}; +use nostr::key::Keys; + +pub type SqlitePool = r2d2::Pool; +pub type PooledConnection = r2d2::PooledConnection; +pub const DB_FILE: &str = "nostr.db"; + +#[derive(Clone)] +pub struct SqliteRepo { + /// Metrics + metrics: NostrMetrics, + /// Pool for reading events and NIP-05 status + read_pool: SqlitePool, + /// Pool for writing events and NIP-05 verification + write_pool: SqlitePool, + /// Pool for performing checkpoints/optimization + maint_pool: SqlitePool, + /// Flag to indicate a checkpoint is underway + checkpoint_in_progress: Arc>, + /// Flag to limit writer concurrency + write_in_progress: Arc>, + /// Semaphore for readers to acquire blocking threads + reader_threads_ready: Arc, +} + +impl SqliteRepo { + // build all the pools needed + #[must_use] + pub fn new(settings: &Settings, metrics: NostrMetrics) -> SqliteRepo { + let write_pool = build_pool( + "writer", + settings, + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, + 0, + 2, + false, + ); + let maint_pool = build_pool( + "maintenance", + settings, + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, + 0, + 2, + true, + ); + let read_pool = build_pool( + "reader", + settings, + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, + settings.database.min_conn, + settings.database.max_conn, + true, + ); + + // this is used to block new reads during critical checkpoints + let checkpoint_in_progress = Arc::new(Mutex::new(0)); + // SQLite can only effectively write single threaded, so don't + // block multiple worker threads unnecessarily. + let write_in_progress = Arc::new(Mutex::new(0)); + // configure the number of worker threads that can be spawned + // to match the number of database reader connections. + let max_conn = settings.database.max_conn as usize; + let reader_threads_ready = Arc::new(Semaphore::new(max_conn)); + SqliteRepo { + metrics, + read_pool, + write_pool, + maint_pool, + checkpoint_in_progress, + write_in_progress, + reader_threads_ready, + } + } + + /// Persist an event to the database, returning rows added. + pub fn persist_event(conn: &mut PooledConnection, e: &Event) -> Result { + // enable auto vacuum + conn.execute_batch("pragma auto_vacuum = FULL")?; + + // start transaction + let tx = conn.transaction()?; + // get relevant fields from event and convert to blobs. + let id_blob = hex::decode(&e.id).ok(); + let pubkey_blob: Option> = hex::decode(&e.pubkey).ok(); + let delegator_blob: Option> = + e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok()); + let event_str = serde_json::to_string(&e).ok(); + // check for replaceable events that would hide this one; we won't even attempt to insert these. + if e.is_replaceable() { + let repl_count = tx.query_row( + "SELECT e.id FROM event e INDEXED BY author_index WHERE e.author=? AND e.kind=? AND e.created_at >= ? LIMIT 1;", + params![pubkey_blob, e.kind, e.created_at], |row| row.get::(0)); + if repl_count.ok().is_some() { + return Ok(0); + } + } + // check for parameterized replaceable events that would be hidden; don't insert these either. + if let Some(d_tag) = e.distinct_param() { + let repl_count = tx.query_row( + "SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.author=? AND e.kind=? AND t.name='d' AND t.value=? AND e.created_at >= ? LIMIT 1;", + params![pubkey_blob, e.kind, d_tag, e.created_at],|row| row.get::(0)); + // if any rows were returned, then some newer event with + // the same author/kind/tag value exist, and we can ignore + // this event. + if repl_count.ok().is_some() { + return Ok(0); + } + } + // ignore if the event hash is a duplicate. + let mut ins_count = tx.execute( + "INSERT OR IGNORE INTO event (event_hash, created_at, expires_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, strftime('%s','now'), FALSE);", + params![id_blob, e.created_at, e.expiration(), e.kind, pubkey_blob, delegator_blob, event_str] + )? as u64; + if ins_count == 0 { + // if the event was a duplicate, no need to insert event or + // pubkey references. + tx.rollback().ok(); + return Ok(ins_count); + } + // remember primary key of the event most recently inserted. + let ev_id = tx.last_insert_rowid(); + // add all tags to the tag table + for tag in &e.tags { + // ensure we have 2 values. + if tag.len() >= 2 { + let tagname = &tag[0]; + let tagval = &tag[1]; + // only single-char tags are searchable + let tagchar_opt = single_char_tagname(tagname); + if tagchar_opt.is_some() { + tx.execute( + "INSERT OR IGNORE INTO tag (event_id, name, value, kind, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", + params![ev_id, &tagname, &tagval, e.kind, e.created_at], + )?; + } + } + } + // if this event is replaceable update, remove other replaceable + // event with the same kind from the same author that was issued + // earlier than this. + if e.is_replaceable() { + let author = hex::decode(&e.pubkey).ok(); + // this is a backwards check - hide any events that were older. + let update_count = tx.execute( + "DELETE FROM event WHERE kind=? and author=? and id NOT IN (SELECT id FROM event INDEXED BY author_kind_index WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1)", + params![e.kind, author, e.kind, author], + )?; + if update_count > 0 { + info!( + "removed {} older replaceable kind {} events for author: {:?}", + update_count, + e.kind, + e.get_author_prefix() + ); + } + } + // if this event is parameterized replaceable, remove other events. + if let Some(d_tag) = e.distinct_param() { + let update_count = tx.execute( + "DELETE FROM event WHERE kind=? AND author=? AND id IN (SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.kind=? AND e.author=? AND t.name='d' AND t.value=? ORDER BY t.created_at DESC LIMIT -1 OFFSET 1);", + params![e.kind, pubkey_blob, e.kind, pubkey_blob, d_tag])?; + if update_count > 0 { + info!( + "removed {} older parameterized replaceable kind {} events for author: {:?}", + update_count, + e.kind, + e.get_author_prefix() + ); + } + } + // if this event is a deletion, hide the referenced events from the same author. + if e.kind == 5 { + let event_candidates = e.tag_values_by_name("e"); + // first parameter will be author + let mut params: Vec> = vec![Box::new(hex::decode(&e.pubkey)?)]; + event_candidates + .iter() + .filter(|x| is_hex(x) && x.len() == 64) + .filter_map(|x| hex::decode(x).ok()) + .for_each(|x| params.push(Box::new(x))); + let query = format!( + "UPDATE event SET hidden=TRUE WHERE kind!=5 AND author=? AND event_hash IN ({})", + repeat_vars(params.len() - 1) + ); + let mut stmt = tx.prepare(&query)?; + let update_count = stmt.execute(rusqlite::params_from_iter(params))?; + info!( + "hid {} deleted events for author {:?}", + update_count, + e.get_author_prefix() + ); + } else { + // check if a deletion has already been recorded for this event. + // Only relevant for non-deletion events + let del_count = tx.query_row( + "SELECT e.id FROM event e WHERE e.author=? AND e.id IN (SELECT t.event_id FROM tag t WHERE t.name='e' AND t.kind=5 AND t.value=?) LIMIT 1;", + params![pubkey_blob, e.id], |row| row.get::(0)); + // check if a the query returned a result, meaning we should + // hid the current event + if del_count.ok().is_some() { + // a deletion already existed, mark original event as hidden. + info!( + "hid event: {:?} due to existing deletion by author: {:?}", + e.get_event_id_prefix(), + e.get_author_prefix() + ); + let _update_count = + tx.execute("UPDATE event SET hidden=TRUE WHERE id=?", params![ev_id])?; + // event was deleted, so let caller know nothing new + // arrived, preventing this from being sent to active + // subscriptions + ins_count = 0; + } + } + tx.commit()?; + Ok(ins_count) + } +} + +#[async_trait] +impl NostrRepo for SqliteRepo { + async fn start(&self) -> Result<()> { + db_checkpoint_task( + self.maint_pool.clone(), + Duration::from_secs(60), + self.write_in_progress.clone(), + self.checkpoint_in_progress.clone(), + ) + .await?; + cleanup_expired( + self.maint_pool.clone(), + Duration::from_secs(600), + self.write_in_progress.clone(), + ) + .await + } + + async fn migrate_up(&self) -> Result { + let _write_guard = self.write_in_progress.lock().await; + let mut conn = self.write_pool.get()?; + task::spawn_blocking(move || upgrade_db(&mut conn)).await? + } + /// Persist event to database + async fn write_event(&self, e: &Event) -> Result { + let start = Instant::now(); + let max_write_attempts = 10; + let mut attempts = 0; + let _write_guard = self.write_in_progress.lock().await; + // spawn a blocking thread + //let mut conn = self.write_pool.get()?; + let pool = self.write_pool.clone(); + let e = e.clone(); + let event_count = task::spawn_blocking(move || { + let mut conn = pool.get()?; + // this could fail because the database was busy; try + // multiple times before giving up. + loop { + attempts += 1; + let wr = SqliteRepo::persist_event(&mut conn, &e); + match wr { + Err(SqlError(rusqlite::Error::SqliteFailure(e, _))) => { + // this basically means that NIP-05 or another + // writer was using the database between us + // reading and promoting the connection to a + // write lock. + info!( + "event write failed, DB locked (attempt: {}); sqlite err: {}", + attempts, e.extended_code + ); + } + _ => { + return wr; + } + } + if attempts >= max_write_attempts { + return wr; + } + } + }) + .await?; + self.metrics + .write_events + .observe(start.elapsed().as_secs_f64()); + event_count + } + + /// Perform a database query using a subscription. + /// + /// The [`Subscription`] is converted into a SQL query. Each result + /// is published on the `query_tx` channel as it is returned. If a + /// message becomes available on the `abandon_query_rx` channel, the + /// query is immediately aborted. + async fn query_subscription( + &self, + sub: Subscription, + client_id: String, + query_tx: tokio::sync::mpsc::Sender, + mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>, + ) -> Result<()> { + let pre_spawn_start = Instant::now(); + // if we let every request spawn a thread, we'll exhaust the + // thread pool waiting for queries to finish under high load. + // Instead, don't bother spawning threads when they will just + // block on a database connection. + let sem = self + .reader_threads_ready + .clone() + .acquire_owned() + .await + .unwrap(); + let self = self.clone(); + let metrics = self.metrics.clone(); + task::spawn_blocking(move || { + { + // if we are waiting on a checkpoint, stop until it is complete + let _x = self.checkpoint_in_progress.blocking_lock(); + } + let db_queue_time = pre_spawn_start.elapsed(); + // if the queue time was very long (>5 seconds), spare the DB and abort. + if db_queue_time > Duration::from_secs(5) { + info!( + "shedding DB query load queued for {:?} (cid: {}, sub: {:?})", + db_queue_time, client_id, sub.id + ); + metrics.query_aborts.with_label_values(&["loadshed"]).inc(); + return Ok(()); + } + // otherwise, report queuing time if it is slow + else if db_queue_time > Duration::from_secs(1) { + debug!( + "(slow) DB query queued for {:?} (cid: {}, sub: {:?})", + db_queue_time, client_id, sub.id + ); + } + // check before getting a DB connection if the client still wants the results + if abandon_query_rx.try_recv().is_ok() { + debug!( + "query cancelled by client (before execution) (cid: {}, sub: {:?})", + client_id, sub.id + ); + return Ok(()); + } + + let start = Instant::now(); + let mut row_count: usize = 0; + // cutoff for displaying slow queries + let slow_cutoff = Duration::from_millis(250); + let mut filter_count = 0; + // remove duplicates from the filter list. + if let Ok(mut conn) = self.read_pool.get() { + { + let pool_state = self.read_pool.state(); + metrics + .db_connections + .set((pool_state.connections - pool_state.idle_connections).into()); + } + for filter in sub.filters.iter() { + let filter_start = Instant::now(); + filter_count += 1; + let sql_gen_elapsed = filter_start.elapsed(); + let (q, p, idx) = query_from_filter(filter); + if sql_gen_elapsed > Duration::from_millis(10) { + debug!("SQL (slow) generated in {:?}", filter_start.elapsed()); + } + // any client that doesn't cause us to generate new rows in 2 + // seconds gets dropped. + let abort_cutoff = Duration::from_secs(2); + let mut slow_first_event; + let mut last_successful_send = Instant::now(); + // execute the query. + // make the actual SQL query (with parameters inserted) available + conn.trace(Some(|x| trace!("SQL trace: {:?}", x))); + let mut stmt = conn.prepare_cached(&q)?; + let mut event_rows = stmt.query(rusqlite::params_from_iter(p))?; + + let mut first_result = true; + while let Some(row) = event_rows.next()? { + let first_event_elapsed = filter_start.elapsed(); + slow_first_event = first_event_elapsed >= slow_cutoff; + if first_result { + debug!( + "first result in {:?} (cid: {}, sub: {:?}, filter: {}) [used index: {:?}]", + first_event_elapsed, client_id, sub.id, filter_count, idx + ); + // logging for slow queries; show filter and SQL. + // to reduce logging; only show 1/16th of clients (leading 0) + if slow_first_event && client_id.starts_with('0') { + debug!( + "filter first result in {:?} (slow): {} (cid: {}, sub: {:?})", + first_event_elapsed, + serde_json::to_string(&filter)?, + client_id, + sub.id + ); + } + first_result = false; + } + // check if a checkpoint is trying to run, and abort + if row_count % 100 == 0 { + { + if self.checkpoint_in_progress.try_lock().is_err() { + // lock was held, abort this query + debug!( + "query aborted due to checkpoint (cid: {}, sub: {:?})", + client_id, sub.id + ); + metrics + .query_aborts + .with_label_values(&["checkpoint"]) + .inc(); + return Ok(()); + } + } + } + + // check if this is still active; every 100 rows + if row_count % 100 == 0 && abandon_query_rx.try_recv().is_ok() { + debug!( + "query cancelled by client (cid: {}, sub: {:?})", + client_id, sub.id + ); + return Ok(()); + } + row_count += 1; + let event_json = row.get(0)?; + loop { + if query_tx.capacity() != 0 { + // we have capacity to add another item + break; + } + // the queue is full + trace!("db reader thread is stalled"); + if last_successful_send + abort_cutoff < Instant::now() { + // the queue has been full for too long, abort + info!("aborting database query due to slow client (cid: {}, sub: {:?})", + client_id, sub.id); + metrics + .query_aborts + .with_label_values(&["slowclient"]) + .inc(); + let ok: Result<()> = Ok(()); + return ok; + } + // check if a checkpoint is trying to run, and abort + if self.checkpoint_in_progress.try_lock().is_err() { + // lock was held, abort this query + debug!( + "query aborted due to checkpoint (cid: {}, sub: {:?})", + client_id, sub.id + ); + metrics + .query_aborts + .with_label_values(&["checkpoint"]) + .inc(); + return Ok(()); + } + // give the queue a chance to clear before trying again + debug!( + "query thread sleeping due to full query_tx (cid: {}, sub: {:?})", + client_id, sub.id + ); + thread::sleep(Duration::from_millis(500)); + } + // TODO: we could use try_send, but we'd have to juggle + // getting the query result back as part of the error + // result. + query_tx + .blocking_send(QueryResult { + sub_id: sub.get_id(), + event: event_json, + }) + .ok(); + last_successful_send = Instant::now(); + } + metrics + .query_db + .observe(filter_start.elapsed().as_secs_f64()); + // if the filter took too much db_time, print out the JSON. + if filter_start.elapsed() > slow_cutoff && client_id.starts_with('0') { + debug!( + "query filter req (slow): {} (cid: {}, sub: {:?}, filter: {})", + serde_json::to_string(&filter)?, + client_id, + sub.id, + filter_count + ); + } + } + } else { + warn!("Could not get a database connection for querying"); + } + drop(sem); // new query can begin + debug!( + "query completed in {:?} (cid: {}, sub: {:?}, db_time: {:?}, rows: {})", + pre_spawn_start.elapsed(), + client_id, + sub.id, + start.elapsed(), + row_count + ); + query_tx + .blocking_send(QueryResult { + sub_id: sub.get_id(), + event: "EOSE".to_string(), + }) + .ok(); + metrics + .query_sub + .observe(pre_spawn_start.elapsed().as_secs_f64()); + let ok: Result<()> = Ok(()); + ok + }); + Ok(()) + } + + /// Perform normal maintenance + async fn optimize_db(&self) -> Result<()> { + let conn = self.write_pool.get()?; + task::spawn_blocking(move || { + let start = Instant::now(); + conn.execute_batch("PRAGMA optimize;").ok(); + info!("optimize ran in {:?}", start.elapsed()); + }) + .await?; + Ok(()) + } + + /// Create a new verification record connected to a specific event + async fn create_verification_record(&self, event_id: &str, name: &str) -> Result<()> { + let e = hex::decode(event_id).ok(); + let n = name.to_owned(); + let mut conn = self.write_pool.get()?; + let _write_guard = self.write_in_progress.lock().await; + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + { + // if we create a /new/ one, we should get rid of any old ones. or group the new ones by name and only consider the latest. + let query = "INSERT INTO user_verification (metadata_event, name, verified_at) VALUES ((SELECT id from event WHERE event_hash=?), ?, strftime('%s','now'));"; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![e, n])?; + // get the row ID + let v_id = tx.last_insert_rowid(); + // delete everything else by this name + let del_query = "DELETE FROM user_verification WHERE name = ? AND id != ?;"; + let mut del_stmt = tx.prepare(del_query)?; + let count = del_stmt.execute(params![n,v_id])?; + if count > 0 { + info!("removed {} old verification records for ({:?})", count, n); + } + } + tx.commit()?; + info!("saved new verification record for ({:?})", n); + let ok: Result<()> = Ok(()); + ok + }).await? + } + + /// Update verification timestamp + async fn update_verification_timestamp(&self, id: u64) -> Result<()> { + let mut conn = self.write_pool.get()?; + let _write_guard = self.write_in_progress.lock().await; + tokio::task::spawn_blocking(move || { + // add some jitter to the verification to prevent everything from stacking up together. + let verif_time = now_jitter(600); + let tx = conn.transaction()?; + { + // update verification time and reset any failure count + let query = + "UPDATE user_verification SET verified_at=?, failure_count=0 WHERE id=?"; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![verif_time, id])?; + } + tx.commit()?; + let ok: Result<()> = Ok(()); + ok + }) + .await? + } + + /// Update verification record as failed + async fn fail_verification(&self, id: u64) -> Result<()> { + let mut conn = self.write_pool.get()?; + let _write_guard = self.write_in_progress.lock().await; + tokio::task::spawn_blocking(move || { + // add some jitter to the verification to prevent everything from stacking up together. + let fail_time = now_jitter(600); + let tx = conn.transaction()?; + { + let query = "UPDATE user_verification SET failed_at=?, failure_count=failure_count+1 WHERE id=?"; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![fail_time, id])?; + } + tx.commit()?; + let ok: Result<()> = Ok(()); + ok + }) + .await? + } + + /// Delete verification record + async fn delete_verification(&self, id: u64) -> Result<()> { + let mut conn = self.write_pool.get()?; + let _write_guard = self.write_in_progress.lock().await; + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + { + let query = "DELETE FROM user_verification WHERE id=?;"; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![id])?; + } + tx.commit()?; + let ok: Result<()> = Ok(()); + ok + }) + .await? + } + + /// Get the latest verification record for a given pubkey. + async fn get_latest_user_verification(&self, pub_key: &str) -> Result { + let mut conn = self.read_pool.get()?; + let pub_key = pub_key.to_owned(); + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + let query = "SELECT v.id, v.name, e.event_hash, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v LEFT JOIN event e ON e.id=v.metadata_event WHERE e.author=? ORDER BY e.created_at DESC, v.verified_at DESC, v.failed_at DESC LIMIT 1;"; + let mut stmt = tx.prepare_cached(query)?; + let fields = stmt.query_row(params![hex::decode(&pub_key).ok()], |r| { + let rowid: u64 = r.get(0)?; + let rowname: String = r.get(1)?; + let eventid: Vec = r.get(2)?; + let created_at: u64 = r.get(3)?; + // create a tuple since we can't throw non-rusqlite errors in this closure + Ok(( + rowid, + rowname, + eventid, + created_at, + r.get(4).ok(), + r.get(5).ok(), + r.get(6)?, + )) + })?; + Ok(VerificationRecord { + rowid: fields.0, + name: Nip05Name::try_from(&fields.1[..])?, + address: pub_key, + event: hex::encode(fields.2), + event_created: fields.3, + last_success: fields.4, + last_failure: fields.5, + failure_count: fields.6, + }) + }).await? + } + + /// Get oldest verification before timestamp + async fn get_oldest_user_verification(&self, before: u64) -> Result { + let mut conn = self.read_pool.get()?; + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + let query = "SELECT v.id, v.name, e.event_hash, e.author, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v INNER JOIN event e ON e.id=v.metadata_event WHERE (v.verified_at < ? OR v.verified_at IS NULL) AND (v.failed_at < ? OR v.failed_at IS NULL) ORDER BY v.verified_at ASC, v.failed_at ASC LIMIT 1;"; + let mut stmt = tx.prepare_cached(query)?; + let fields = stmt.query_row(params![before, before], |r| { + let rowid: u64 = r.get(0)?; + let rowname: String = r.get(1)?; + let eventid: Vec = r.get(2)?; + let pubkey: Vec = r.get(3)?; + let created_at: u64 = r.get(4)?; + // create a tuple since we can't throw non-rusqlite errors in this closure + Ok(( + rowid, + rowname, + eventid, + pubkey, + created_at, + r.get(5).ok(), + r.get(6).ok(), + r.get(7)?, + )) + })?; + let vr = VerificationRecord { + rowid: fields.0, + name: Nip05Name::try_from(&fields.1[..])?, + address: hex::encode(fields.3), + event: hex::encode(fields.2), + event_created: fields.4, + last_success: fields.5, + last_failure: fields.6, + failure_count: fields.7, + }; + Ok(vr) + }).await? + } + + /// Create account + async fn create_account(&self, pub_key: &Keys) -> Result { + let pub_key = pub_key.public_key().to_string(); + + let mut conn = self.write_pool.get()?; + let ins_count = tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + let ins_count: u64; + { + // Ignore if user is already in db + let query = "INSERT OR IGNORE INTO account (pubkey, is_admitted, balance) VALUES (?1, ?2, ?3);"; + let mut stmt = tx.prepare(query)?; + ins_count = stmt.execute(params![&pub_key, false, 0])? as u64; + } + tx.commit()?; + let ok: Result = Ok(ins_count); + ok + }).await??; + + if ins_count != 1 { + return Ok(false); + } + + Ok(true) + } + + /// Admit account + async fn admit_account(&self, pub_key: &Keys, admission_cost: u64) -> Result<()> { + let pub_key = pub_key.public_key().to_string(); + let mut conn = self.write_pool.get()?; + let pub_key = pub_key.to_owned(); + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + { + let query = "UPDATE account SET is_admitted = TRUE, tos_accepted_at = strftime('%s','now'), balance = balance - ?1 WHERE pubkey=?2;"; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![admission_cost, pub_key])?; + } + tx.commit()?; + let ok: Result<()> = Ok(()); + ok + }) + .await? + } + + /// Gets if the account is admitted and balance + async fn get_account_balance(&self, pub_key: &Keys) -> Result<(bool, u64)> { + let pub_key = pub_key.public_key().to_string(); + let mut conn = self.write_pool.get()?; + let pub_key = pub_key.to_owned(); + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + let query = "SELECT is_admitted, balance FROM account WHERE pubkey = ?1;"; + let mut stmt = tx.prepare_cached(query)?; + let fields = stmt.query_row(params![pub_key], |r| { + let is_admitted: bool = r.get(0)?; + let balance: u64 = r.get(1)?; + // create a tuple since we can't throw non-rusqlite errors in this closure + Ok((is_admitted, balance)) + })?; + Ok(fields) + }) + .await? + } + + /// Update account balance + async fn update_account_balance( + &self, + pub_key: &Keys, + positive: bool, + new_balance: u64, + ) -> Result<()> { + let pub_key = pub_key.public_key().to_string(); + + let mut conn = self.write_pool.get()?; + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + { + let query = if positive { + "UPDATE account SET balance=balance + ?1 WHERE pubkey=?2" + } else { + "UPDATE account SET balance=balance - ?1 WHERE pubkey=?2" + }; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![new_balance, pub_key])?; + } + tx.commit()?; + let ok: Result<()> = Ok(()); + ok + }) + .await? + } + + /// Create invoice record + async fn create_invoice_record(&self, pub_key: &Keys, invoice_info: InvoiceInfo) -> Result<()> { + let pub_key = pub_key.public_key().to_string(); + let pub_key = pub_key.to_owned(); + let mut conn = self.write_pool.get()?; + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + { + let query = "INSERT INTO invoice (pubkey, payment_hash, amount, status, description, created_at, invoice) VALUES (?1, ?2, ?3, ?4, ?5, strftime('%s','now'), ?6);"; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![&pub_key, invoice_info.payment_hash, invoice_info.amount, invoice_info.status.to_string(), invoice_info.memo, invoice_info.bolt11])?; + } + tx.commit()?; + let ok: Result<()> = Ok(()); + ok + }).await??; + + Ok(()) + } + + /// Update invoice record + async fn update_invoice(&self, payment_hash: &str, status: InvoiceStatus) -> Result { + let mut conn = self.write_pool.get()?; + let payment_hash = payment_hash.to_owned(); + + tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + let pubkey: String; + { + + // Get required invoice info for given payment hash + let query = "SELECT pubkey, status, amount FROM invoice WHERE payment_hash=?1;"; + let mut stmt = tx.prepare(query)?; + let (pub_key, prev_status, amount) = stmt.query_row(params![payment_hash], |r| { + let pub_key: String = r.get(0)?; + let status: String = r.get(1)?; + let amount: u64 = r.get(2)?; + + + Ok((pub_key, status, amount)) + + })?; + + // If the invoice is paid update the confirmed_at timestamp + let query = if status.eq(&InvoiceStatus::Paid) { + "UPDATE invoice SET status=?1, confirmed_at = strftime('%s', 'now') WHERE payment_hash=?2;" + } else { + "UPDATE invoice SET status=?1 WHERE payment_hash=?2;" + }; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![status.to_string(), payment_hash])?; + + // Increase account balance by given invoice amount + if prev_status == "Unpaid" && status.eq(&InvoiceStatus::Paid) { + let query = + "UPDATE account SET balance = balance + ?1 WHERE pubkey = ?2;"; + let mut stmt = tx.prepare(query)?; + stmt.execute(params![amount, pub_key])?; + } + + pubkey = pub_key; + } + + tx.commit()?; + let ok: Result = Ok(pubkey); + ok + }) + .await? + } + + /// Get the most recent invoice for a given pubkey + /// invoice must be unpaid and not expired + async fn get_unpaid_invoice(&self, pubkey: &Keys) -> Result> { + let mut conn = self.write_pool.get()?; + + let pubkey = pubkey.to_owned(); + let pubkey_str = pubkey.clone().public_key().to_string(); + let (payment_hash, invoice, amount, description) = tokio::task::spawn_blocking(move || { + let tx = conn.transaction()?; + + let query = r#" +SELECT amount, payment_hash, description, invoice +FROM invoice +WHERE pubkey = ?1 AND status = 'Unpaid' +ORDER BY created_at DESC +LIMIT 1; + "#; + let mut stmt = tx.prepare(query).unwrap(); + stmt.query_row(params![&pubkey_str], |r| { + let amount: u64 = r.get(0)?; + let payment_hash: String = r.get(1)?; + let description: String = r.get(2)?; + let invoice: String = r.get(3)?; + + Ok((payment_hash, invoice, amount, description)) + }) + }) + .await??; + + Ok(Some(InvoiceInfo { + pubkey: pubkey.public_key().to_string(), + payment_hash, + bolt11: invoice, + amount, + status: InvoiceStatus::Unpaid, + memo: description, + confirmed_at: None, + })) + } +} + +/// Decide if there is an index that should be used explicitly +fn override_index(f: &ReqFilter) -> Option { + if f.ids.is_some() { + return Some("event_hash_index".into()); + } + // queries for multiple kinds default to kind_index, which is + // significantly slower than kind_created_at_index. + if let Some(ks) = &f.kinds { + if f.ids.is_none() + && ks.len() > 1 + && f.since.is_none() + && f.until.is_none() + && f.tags.is_none() + && f.authors.is_none() + { + return Some("kind_created_at_index".into()); + } + } + // if there is an author, it is much better to force the authors index. + if f.authors.is_some() { + if f.since.is_none() && f.until.is_none() && f.limit.is_none() { + if f.kinds.is_none() { + // with no use of kinds/created_at, just author + return Some("author_index".into()); + } + // prefer author_kind if there are kinds + return Some("author_kind_index".into()); + } + // finally, prefer author_created_at if time is provided + return Some("author_created_at_index".into()); + } + None +} + +/// Create a dynamic SQL subquery and params from a subscription filter (and optional explicit index used) +fn query_from_filter(f: &ReqFilter) -> (String, Vec>, Option) { + // build a dynamic SQL query. all user-input is either an integer + // (sqli-safe), or a string that is filtered to only contain + // hexadecimal characters. Strings that require escaping (tag + // names/values) use parameters. + + // if the filter is malformed, don't return anything. + if f.force_no_match { + let empty_query = "SELECT e.content FROM event e WHERE 1=0".to_owned(); + // query parameters for SQLite + let empty_params: Vec> = vec![]; + return (empty_query, empty_params, None); + } + + // check if the index needs to be overridden + let idx_name = override_index(f); + let idx_stmt = idx_name + .as_ref() + .map_or_else(|| "".to_owned(), |i| format!("INDEXED BY {i}")); + let mut query = format!("SELECT e.content FROM event e {idx_stmt}"); + // query parameters for SQLite + let mut params: Vec> = vec![]; + + // individual filter components (single conditions such as an author or event ID) + let mut filter_components: Vec = Vec::new(); + // Query for "authors", allowing prefix matches + if let Some(authvec) = &f.authors { + // take each author and convert to a hexsearch + let mut auth_searches: Vec = vec![]; + for auth in authvec { + auth_searches.push("author=?".to_owned()); + let auth_bin = hex::decode(auth).ok(); + params.push(Box::new(auth_bin)); + } + if !authvec.is_empty() { + let auth_clause = format!("({})", auth_searches.join(" OR ")); + filter_components.push(auth_clause); + } else { + filter_components.push("false".to_owned()); + } + } + // Query for Kind + if let Some(ks) = &f.kinds { + // kind is number, no escaping needed + let str_kinds: Vec = ks.iter().map(std::string::ToString::to_string).collect(); + let kind_clause = format!("kind IN ({})", str_kinds.join(", ")); + filter_components.push(kind_clause); + } + // Query for event, allowing prefix matches + if let Some(idvec) = &f.ids { + // take each author and convert to a hexsearch + let mut id_searches: Vec = vec![]; + for id in idvec { + id_searches.push("event_hash=?".to_owned()); + let id_bin = hex::decode(id).ok(); + params.push(Box::new(id_bin)); + } + if idvec.is_empty() { + // if the ids list was empty, we should never return + // any results. + filter_components.push("false".to_owned()); + } else { + let id_clause = format!("({})", id_searches.join(" OR ")); + filter_components.push(id_clause); + } + } + // Query for tags + if let Some(map) = &f.tags { + for (key, val) in map.iter().sorted_by(|(k1, _), (k2, _)| k1.cmp(k2)) { + if val.is_empty() { + continue; + } + + // Query for Kind/Since/Until additionally, to reduce the number of tags that come back. + let kind_clause; + if let Some(ks) = &f.kinds { + // kind is number, no escaping needed + let str_kinds: Vec = + ks.iter().map(std::string::ToString::to_string).collect(); + kind_clause = format!("AND kind IN ({})", str_kinds.join(", ")); + } else { + kind_clause = String::new(); + }; + let since_clause = if f.since.is_some() { + format!("AND created_at >= {}", f.since.unwrap()) + } else { + String::new() + }; + // Query for timestamp + let until_clause = if f.until.is_some() { + format!("AND created_at <= {}", f.until.unwrap()) + } else { + String::new() + }; + + match val { + TagOperand::Or(v_or) => { + // Sort values for consistent query generation + let mut sorted_values: Vec = v_or.iter().cloned().collect(); + sorted_values.sort(); + + let mut str_vals: Vec> = vec![]; + for v in &sorted_values { + str_vals.push(Box::new(v.clone())); + } + // create clauses with "?" params for each tag value being searched + let str_clause = format!("AND value IN ({})", repeat_vars(str_vals.len())); + + // Build WHERE clause components + let mut tag_where_parts = vec![format!("name=? {str_clause}")]; + if !kind_clause.is_empty() { + tag_where_parts.push(kind_clause.clone()); + } + if !since_clause.is_empty() { + tag_where_parts.push(since_clause.clone()); + } + if !until_clause.is_empty() { + tag_where_parts.push(until_clause.clone()); + } + + // find evidence of the target tag name/value existing for this event. + let tag_clause = format!( + "e.id IN (SELECT t.event_id FROM tag t WHERE ({}))", + tag_where_parts.join(" ") + ); + + // add the tag name as the first parameter + params.push(Box::new(key.to_string())); + // add all tag values that are blobs as params + params.append(&mut str_vals); + filter_components.push(tag_clause); + } + TagOperand::And(v_and) => { + // Sort values for consistent query generation + let mut sorted_values: Vec = v_and.iter().cloned().collect(); + sorted_values.sort(); + + for v in &sorted_values { + // Build WHERE clause components + let mut tag_where_parts = vec!["name=? AND value=?".to_string()]; + if !kind_clause.is_empty() { + tag_where_parts.push(kind_clause.clone()); + } + if !since_clause.is_empty() { + tag_where_parts.push(since_clause.clone()); + } + if !until_clause.is_empty() { + tag_where_parts.push(until_clause.clone()); + } + + // For AND operations, each value must exist, so we create a separate subquery for each + let tag_clause = format!( + "e.id IN (SELECT t.event_id FROM tag t WHERE ({}))", + tag_where_parts.join(" ") + ); + + // add the tag name as the first parameter + params.push(Box::new(key.to_string())); + // add the tag value + params.push(Box::new(v.clone())); + filter_components.push(tag_clause); + } + } + } + } + } + // Query for timestamp + if f.since.is_some() { + let created_clause = format!("created_at >= {}", f.since.unwrap()); + filter_components.push(created_clause); + } + // Query for timestamp + if f.until.is_some() { + let until_clause = format!("created_at <= {}", f.until.unwrap()); + filter_components.push(until_clause); + } + // never display hidden events + query.push_str(" WHERE hidden!=TRUE"); + // never display hidden events + filter_components.push("(expires_at IS NULL OR expires_at > ?)".to_string()); + params.push(Box::new(unix_time())); + // build filter component conditions + if !filter_components.is_empty() { + query.push_str(" AND "); + query.push_str(&filter_components.join(" AND ")); + } + // Apply per-filter limit to this subquery. + // The use of a LIMIT implies a DESC order, to capture only the most recent events. + if let Some(lim) = f.limit { + let _ = write!(query, " ORDER BY e.created_at DESC LIMIT {lim}"); + } else { + query.push_str(" ORDER BY e.created_at ASC"); + } + (query, params, idx_name) +} + +/// Create a dynamic SQL query string and params from a subscription. +fn _query_from_sub(sub: &Subscription) -> (String, Vec>, Vec) { + // build a dynamic SQL query for an entire subscription, based on + // SQL subqueries for filters. + let mut subqueries: Vec = Vec::new(); + let mut indexes = vec![]; + // subquery params + let mut params: Vec> = vec![]; + // for every filter in the subscription, generate a subquery + for f in &sub.filters { + let (f_subquery, mut f_params, index) = query_from_filter(f); + if let Some(i) = index { + indexes.push(i); + } + subqueries.push(f_subquery); + params.append(&mut f_params); + } + // encapsulate subqueries into select statements + let subqueries_selects: Vec = subqueries + .iter() + .map(|s| format!("SELECT distinct content, created_at FROM ({s})")) + .collect(); + let query: String = subqueries_selects.join(" UNION "); + (query, params, indexes) +} + +/// Build a database connection pool. +/// # Panics +/// +/// Will panic if the pool could not be created. +#[must_use] +pub fn build_pool( + name: &str, + settings: &Settings, + flags: OpenFlags, + min_size: u32, + max_size: u32, + wait_for_db: bool, +) -> SqlitePool { + let db_dir = &settings.database.data_directory; + let full_path = Path::new(db_dir).join(DB_FILE); + + // small hack; if the database doesn't exist yet, that means the + // writer thread hasn't finished. Give it a chance to work. This + // is only an issue with the first time we run. + if !settings.database.in_memory { + while !full_path.exists() && wait_for_db { + debug!("Database reader pool is waiting on the database to be created..."); + thread::sleep(Duration::from_millis(500)); + } + } + let manager = if settings.database.in_memory { + SqliteConnectionManager::file("file::memory:?cache=shared") + .with_flags(flags) + .with_init(|c| c.execute_batch(STARTUP_SQL)) + } else { + SqliteConnectionManager::file(&full_path) + .with_flags(flags) + .with_init(|c| c.execute_batch(STARTUP_SQL)) + }; + let pool: SqlitePool = r2d2::Pool::builder() + .test_on_check_out(true) // no noticeable performance hit + .min_idle(Some(min_size)) + .max_size(max_size) + .idle_timeout(Some(Duration::from_secs(10))) + .max_lifetime(Some(Duration::from_secs(30))) + .build(manager) + .unwrap(); + // retrieve a connection to ensure the startup statements run immediately + { + let _ = pool.get(); + } + + info!( + "Built a connection pool {:?} (min={}, max={})", + name, min_size, max_size + ); + pool +} + +/// Cleanup expired events on a regular basis +async fn cleanup_expired( + pool: SqlitePool, + frequency: Duration, + write_in_progress: Arc>, +) -> Result<()> { + tokio::task::spawn(async move { + loop { + tokio::select! { + _ = tokio::time::sleep(frequency) => { + if let Ok(mut conn) = pool.get() { + let mut _guard:Option> = None; + // take a write lock to prevent event writes + // from proceeding while we are deleting + // events. This isn't necessary, but + // minimizes the chances of forcing event + // persistence to be retried. + _guard = Some(write_in_progress.lock().await); + let start = Instant::now(); + let exp_res = tokio::task::spawn_blocking(move || { + delete_expired(&mut conn) + }).await; + match exp_res { + Ok(Ok(count)) => { + if count > 0 { + info!("removed {} expired events in: {:?}", count, start.elapsed()); + } + }, + _ => { + // either the task or underlying query failed + info!("there was an error cleaning up expired events: {:?}", exp_res); + } + } + } + } + }; + } + }); + Ok(()) +} + +/// Execute a query to delete all expired events +pub fn delete_expired(conn: &mut PooledConnection) -> Result { + let tx = conn.transaction()?; + let update_count = tx.execute( + "DELETE FROM event WHERE expires_at <= ?", + params![unix_time()], + )?; + tx.commit()?; + Ok(update_count) +} + +/// Perform database WAL checkpoint on a regular basis +pub async fn db_checkpoint_task( + pool: SqlitePool, + frequency: Duration, + write_in_progress: Arc>, + checkpoint_in_progress: Arc>, +) -> Result<()> { + // TODO; use acquire_many on the reader semaphore to stop them from interrupting this. + tokio::task::spawn(async move { + // WAL size in pages. + let mut current_wal_size = 0; + // WAL threshold for more aggressive checkpointing (10,000 pages, or about 40MB) + let wal_threshold = 1000 * 10; + // default threshold for the busy timer + let busy_wait_default = Duration::from_secs(1); + // if the WAL file is getting too big, switch to this + let busy_wait_default_long = Duration::from_secs(10); + loop { + tokio::select! { + _ = tokio::time::sleep(frequency) => { + if let Ok(mut conn) = pool.get() { + // block all other writers + let _write_guard = write_in_progress.lock().await; + let mut _guard:Option> = None; + // the busy timer will block writers, so don't set + // this any higher than you want max latency for event + // writes. + if current_wal_size <= wal_threshold { + conn.busy_timeout(busy_wait_default).ok(); + } else { + // if the wal size has exceeded a threshold, increase the busy timeout. + conn.busy_timeout(busy_wait_default_long).ok(); + // take a lock that will prevent new readers. + info!("blocking new readers to perform wal_checkpoint"); + _guard = Some(checkpoint_in_progress.lock().await); + } + debug!("running wal_checkpoint(TRUNCATE)"); + if let Ok(new_size) = checkpoint_db(&mut conn) { + current_wal_size = new_size; + } + } + } + }; + } + }); + + Ok(()) +} + +#[derive(Debug)] +#[allow(dead_code)] +enum SqliteStatus { + Ok, + Busy, + Error, + Other(u64), +} + +/// Checkpoint/Truncate WAL. Returns the number of WAL pages remaining. +pub fn checkpoint_db(conn: &mut PooledConnection) -> Result { + let query = "PRAGMA wal_checkpoint(TRUNCATE);"; + let start = Instant::now(); + let (cp_result, wal_size, _frames_checkpointed) = conn.query_row(query, [], |row| { + let checkpoint_result: u64 = row.get(0)?; + let wal_size: u64 = row.get(1)?; + let frames_checkpointed: u64 = row.get(2)?; + Ok((checkpoint_result, wal_size, frames_checkpointed)) + })?; + let result = match cp_result { + 0 => SqliteStatus::Ok, + 1 => SqliteStatus::Busy, + 2 => SqliteStatus::Error, + x => SqliteStatus::Other(x), + }; + info!( + "checkpoint ran in {:?} (result: {:?}, WAL size: {})", + start.elapsed(), + result, + wal_size + ); + Ok(wal_size as usize) +} + +/// Produce a arbitrary list of '?' parameters. +fn repeat_vars(count: usize) -> String { + if count == 0 { + return "".to_owned(); + } + let mut s = "?, ".repeat(count); + // Remove trailing comma and space + s.pop(); + s.pop(); + s +} + +/// Display database pool stats every 1 minute +pub async fn monitor_pool(name: &str, pool: SqlitePool) { + let sleep_dur = Duration::from_secs(60); + loop { + log_pool_stats(name, &pool); + tokio::time::sleep(sleep_dur).await; + } +} + +/// Log pool stats +fn log_pool_stats(name: &str, pool: &SqlitePool) { + let state: r2d2::State = pool.state(); + let in_use_cxns = state.connections - state.idle_connections; + debug!( + "DB pool {:?} usage (in_use: {}, available: {}, max: {})", + name, + in_use_cxns, + state.connections, + pool.max_size() + ); +} + +/// Check if the pool is fully utilized +fn _pool_at_capacity(pool: &SqlitePool) -> bool { + let state: r2d2::State = pool.state(); + state.idle_connections == 0 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::subscription::TagOperand; + use std::collections::{HashMap, HashSet}; + + #[test] + fn test_query_gen_tag_value_or() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: Some(vec![ + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ]), + limit: None, + tags: Some(HashMap::from([( + 'd', + TagOperand::Or(HashSet::from(["test".to_owned()])), + )])), + force_no_match: false, + }; + + let (query, _params, _idx) = query_from_filter(&filter); + assert_eq!( + query, + "SELECT e.content FROM event e INDEXED BY author_kind_index WHERE hidden!=TRUE AND (author=?) AND kind IN (1000) AND e.id IN (SELECT t.event_id FROM tag t WHERE (name=? AND value IN (?) AND kind IN (1000))) AND (expires_at IS NULL OR expires_at > ?) ORDER BY e.created_at ASC" + ); + } + + #[test] + fn test_query_gen_tag_value_or_multiple() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![30_001]), + since: None, + until: None, + authors: None, + limit: None, + tags: Some(HashMap::from([( + 'd', + TagOperand::Or(HashSet::from(["test".to_owned(), "test2".to_owned()])), + )])), + force_no_match: false, + }; + + let (query, _params, _idx) = query_from_filter(&filter); + // Values are sorted, so test comes before test2 + assert_eq!( + query, + "SELECT e.content FROM event e WHERE hidden!=TRUE AND kind IN (30001) AND e.id IN (SELECT t.event_id FROM tag t WHERE (name=? AND value IN (?, ?) AND kind IN (30001))) AND (expires_at IS NULL OR expires_at > ?) ORDER BY e.created_at ASC" + ); + } + + #[test] + fn test_query_gen_tag_value_and() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: Some(vec![ + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ]), + limit: None, + tags: Some(HashMap::from([( + 'd', + TagOperand::And(HashSet::from(["test".to_owned(), "test2".to_owned()])), + )])), + force_no_match: false, + }; + + let (query, _params, _idx) = query_from_filter(&filter); + // For AND queries, we should have multiple subqueries + assert!(query.contains("SELECT e.content FROM event e")); + assert!(query.contains("WHERE hidden!=TRUE")); + assert!(query.contains("(author=?)")); + assert!(query.contains("kind IN (1000)")); + // Should contain two separate subqueries for AND operation + let subquery_count = query + .matches("e.id IN (SELECT t.event_id FROM tag t WHERE (name=? AND value=?") + .count(); + assert_eq!( + subquery_count, 2, + "Should have 2 subqueries for AND operation" + ); + assert!(query.contains("ORDER BY e.created_at ASC")); + } + + #[test] + fn test_query_gen_tag_value_and_hex() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: Some(vec![ + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ]), + limit: None, + tags: Some(HashMap::from([( + 'p', + TagOperand::And(HashSet::from([ + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed".to_owned(), + "84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864".to_owned(), + ])), + )])), + force_no_match: false, + }; + + let (query, _params, _idx) = query_from_filter(&filter); + // For AND queries with hex values + assert!(query.contains("SELECT e.content FROM event e")); + assert!(query.contains("WHERE hidden!=TRUE")); + assert!(query.contains("(author=?)")); + assert!(query.contains("kind IN (1000)")); + // Should contain two separate subqueries for AND operation + let subquery_count = query + .matches("e.id IN (SELECT t.event_id FROM tag t WHERE (name=? AND value=?") + .count(); + assert_eq!( + subquery_count, 2, + "Should have 2 subqueries for AND operation with hex values" + ); + } + + #[test] + fn test_query_empty_tags() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1, 6, 16, 30023, 1063, 6969]), + since: Some(1700697846), + until: None, + authors: None, + limit: None, + tags: Some(HashMap::from([('a', TagOperand::And(HashSet::new()))])), + force_no_match: false, + }; + + let (query, _params, _idx) = query_from_filter(&filter); + // Empty tags should still generate a valid query, just without tag filters + assert!(query.contains("SELECT e.content FROM event e")); + assert!(query.contains("WHERE hidden!=TRUE")); + assert!(query.contains("kind IN (1, 6, 16, 30023, 1063, 6969)")); + // Should not contain tag subqueries since the tag set is empty + assert!(!query.contains("e.id IN (SELECT t.event_id FROM tag t")); + } + + #[test] + fn test_query_tag_with_since_until() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: Some(1234567890), + until: Some(9876543210), + authors: None, + limit: None, + tags: Some(HashMap::from([( + 'd', + TagOperand::Or(HashSet::from(["test".to_owned()])), + )])), + force_no_match: false, + }; + + let (query, _params, _idx) = query_from_filter(&filter); + // Should include since and until in both the main query and the tag subquery + assert!(query.contains("created_at >= 1234567890")); + assert!(query.contains("created_at <= 9876543210")); + // Tag subquery should also include the time constraints + assert!(query.contains("AND created_at >= 1234567890")); + assert!(query.contains("AND created_at <= 9876543210")); + } + + #[test] + fn test_query_multiple_tag_keys_or() { + let filter = ReqFilter { + ids: None, + kinds: Some(vec![1000]), + since: None, + until: None, + authors: None, + limit: None, + tags: Some(HashMap::from([ + ('d', TagOperand::Or(HashSet::from(["test".to_owned()]))), + ('e', TagOperand::Or(HashSet::from(["event1".to_owned()]))), + ])), + force_no_match: false, + }; + + let (query, _params, _idx) = query_from_filter(&filter); + // Should have two separate tag subqueries, one for each key + let subquery_count = query + .matches("e.id IN (SELECT t.event_id FROM tag t WHERE (name=?") + .count(); + assert_eq!( + subquery_count, 2, + "Should have 2 subqueries for 2 different tag keys" + ); + } +} diff --git a/src/repo/sqlite_migration.rs b/src/repo/sqlite_migration.rs new file mode 100644 index 0000000..5abd60d --- /dev/null +++ b/src/repo/sqlite_migration.rs @@ -0,0 +1,885 @@ +//! Database schema and migrations +use crate::db::PooledConnection; +use crate::error::Result; +use crate::event::{single_char_tagname, Event}; +use crate::utils::is_lower_hex; +use const_format::formatcp; +use indicatif::{ProgressBar, ProgressStyle}; +use rusqlite::limits::Limit; +use rusqlite::params; +use rusqlite::Connection; +use std::cmp::Ordering; +use std::time::Instant; +use tracing::{debug, error, info}; + +/// Startup DB Pragmas +pub const STARTUP_SQL: &str = r##" +PRAGMA main.synchronous = NORMAL; +PRAGMA foreign_keys = ON; +PRAGMA journal_size_limit = 32768; +PRAGMA temp_store = 2; -- use memory, not temp files +PRAGMA main.cache_size = 20000; -- 80MB max cache size per conn +pragma mmap_size = 0; -- disable mmap (default) +"##; + +/// Latest database version +pub const DB_VERSION: usize = 19; + +/// Schema definition +const INIT_SQL: &str = formatcp!( + r##" +-- Database settings +PRAGMA encoding = "UTF-8"; +PRAGMA journal_mode = WAL; +PRAGMA auto_vacuum = FULL; +PRAGMA main.synchronous=NORMAL; +PRAGMA foreign_keys = ON; +PRAGMA application_id = 1654008667; +PRAGMA user_version = {}; + +-- Event Table +CREATE TABLE IF NOT EXISTS event ( +id INTEGER PRIMARY KEY, +event_hash BLOB NOT NULL, -- 32-byte SHA256 hash +first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970) +created_at INTEGER NOT NULL, -- when the event was authored +expires_at INTEGER, -- when the event expires and may be deleted +author BLOB NOT NULL, -- author pubkey +delegated_by BLOB, -- delegator pubkey (NIP-26) +kind INTEGER NOT NULL, -- event kind +hidden INTEGER, -- relevant for queries +content TEXT NOT NULL -- serialized json of event object +); + +-- Event Indexes +CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash); +CREATE INDEX IF NOT EXISTS author_index ON event(author); +CREATE INDEX IF NOT EXISTS kind_index ON event(kind); +CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at); +CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by); +CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at); +CREATE INDEX IF NOT EXISTS kind_author_index ON event(kind,author); +CREATE INDEX IF NOT EXISTS kind_created_at_index ON event(kind,created_at); +CREATE INDEX IF NOT EXISTS author_created_at_index ON event(author,created_at); +CREATE INDEX IF NOT EXISTS author_kind_index ON event(author,kind); +CREATE INDEX IF NOT EXISTS event_expiration ON event(expires_at); + +-- Tag Table +-- Tag values are stored as either a BLOB (if they come in as a +-- hex-string), or TEXT otherwise. +-- This means that searches need to select the appropriate column. +-- We duplicate the kind/created_at to make indexes much more efficient. +CREATE TABLE IF NOT EXISTS tag ( +id INTEGER PRIMARY KEY, +event_id INTEGER NOT NULL, -- an event ID that contains a tag. +name TEXT, -- the tag name ("p", "e", whatever) +value TEXT, -- the tag value, if not hex. +value_hex BLOB, -- the tag value, if it can be interpreted as a lowercase hex string. +created_at INTEGER NOT NULL, -- when the event was authored +kind INTEGER NOT NULL, -- event kind +FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS tag_val_index ON tag(value); +CREATE INDEX IF NOT EXISTS tag_composite_index ON tag(event_id,name,value); +CREATE INDEX IF NOT EXISTS tag_name_eid_index ON tag(name,event_id,value); +CREATE INDEX IF NOT EXISTS tag_covering_index ON tag(name,kind,value,created_at,event_id); + +-- NIP-05 User Validation +CREATE TABLE IF NOT EXISTS user_verification ( +id INTEGER PRIMARY KEY, +metadata_event INTEGER NOT NULL, -- the metadata event used for this validation. +name TEXT NOT NULL, -- the nip05 field value (user@domain). +verified_at INTEGER, -- timestamp this author/nip05 was most recently verified. +failed_at INTEGER, -- timestamp a verification attempt failed (host down). +failure_count INTEGER DEFAULT 0, -- number of consecutive failures. +FOREIGN KEY(metadata_event) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS user_verification_name_index ON user_verification(name); +CREATE INDEX IF NOT EXISTS user_verification_event_index ON user_verification(metadata_event); + +-- Create account table +CREATE TABLE IF NOT EXISTS account ( +pubkey TEXT PRIMARY KEY, +is_admitted INTEGER NOT NULL DEFAULT 0, +balance INTEGER NOT NULL DEFAULT 0, +tos_accepted_at INTEGER +); + +-- Create account index +CREATE INDEX IF NOT EXISTS user_pubkey_index ON account(pubkey); + +-- Invoice table +CREATE TABLE IF NOT EXISTS invoice ( +payment_hash TEXT PRIMARY KEY, +pubkey TEXT NOT NULL, +invoice TEXT NOT NULL, +amount INTEGER NOT NULL, +status TEXT CHECK ( status IN ('Paid', 'Unpaid', 'Expired' ) ) NOT NUll DEFAULT 'Unpaid', +description TEXT, +created_at INTEGER NOT NULL, +confirmed_at INTEGER, +CONSTRAINT invoice_pubkey_fkey FOREIGN KEY (pubkey) REFERENCES account (pubkey) ON DELETE CASCADE +); + +-- Create invoice index +CREATE INDEX IF NOT EXISTS invoice_pubkey_index ON invoice(pubkey); + +-- Name authority claims (Floonet). One row per claimed name; a release +-- sets released_at instead of deleting, and the partial unique index +-- enforces one ACTIVE name per pubkey at the database layer. +CREATE TABLE IF NOT EXISTS name_claims ( +name TEXT PRIMARY KEY, +pubkey TEXT NOT NULL, +created_at INTEGER NOT NULL, +released_at INTEGER +); +CREATE INDEX IF NOT EXISTS name_claims_pubkey_index ON name_claims(pubkey); +CREATE UNIQUE INDEX IF NOT EXISTS name_claims_active_pubkey + ON name_claims(pubkey) WHERE released_at IS NULL; + +"##, + DB_VERSION +); + +/// Determine the current application database schema version. +pub fn curr_db_version(conn: &mut Connection) -> Result { + let query = "PRAGMA user_version;"; + let curr_version = conn.query_row(query, [], |row| row.get(0))?; + Ok(curr_version) +} + +/// Determine event count +pub fn db_event_count(conn: &mut Connection) -> Result { + let query = "SELECT count(*) FROM event;"; + let count = conn.query_row(query, [], |row| row.get(0))?; + Ok(count) +} + +/// Determine tag count +pub fn db_tag_count(conn: &mut Connection) -> Result { + let query = "SELECT count(*) FROM tag;"; + let count = conn.query_row(query, [], |row| row.get(0))?; + Ok(count) +} + +fn mig_init(conn: &mut PooledConnection) -> usize { + match conn.execute_batch(INIT_SQL) { + Ok(()) => { + info!( + "database pragma/schema initialized to v{}, and ready", + DB_VERSION + ); + } + Err(err) => { + error!("update (init) failed: {}", err); + panic!("database could not be initialized"); + } + } + DB_VERSION +} + +/// Upgrade DB to latest version, and execute pragma settings +pub fn upgrade_db(conn: &mut PooledConnection) -> Result { + // check the version. + let mut curr_version = curr_db_version(conn)?; + info!("DB version = {:?}", curr_version); + + debug!( + "SQLite max query parameters: {}", + conn.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) + ); + debug!( + "SQLite max table/blob/text length: {} MB", + (f64::from(conn.limit(Limit::SQLITE_LIMIT_LENGTH)) / f64::from(1024 * 1024)).floor() + ); + debug!( + "SQLite max SQL length: {} MB", + (f64::from(conn.limit(Limit::SQLITE_LIMIT_SQL_LENGTH)) / f64::from(1024 * 1024)).floor() + ); + + match curr_version.cmp(&DB_VERSION) { + // Database is new or not current + Ordering::Less => { + // initialize from scratch + if curr_version == 0 { + curr_version = mig_init(conn); + } + // for initialized but out-of-date schemas, proceed to + // upgrade sequentially until we are current. + if curr_version == 1 { + curr_version = mig_1_to_2(conn)?; + } + if curr_version == 2 { + curr_version = mig_2_to_3(conn)?; + } + if curr_version == 3 { + curr_version = mig_3_to_4(conn)?; + } + if curr_version == 4 { + curr_version = mig_4_to_5(conn)?; + } + if curr_version == 5 { + curr_version = mig_5_to_6(conn)?; + } + if curr_version == 6 { + curr_version = mig_6_to_7(conn)?; + } + if curr_version == 7 { + curr_version = mig_7_to_8(conn)?; + } + if curr_version == 8 { + curr_version = mig_8_to_9(conn)?; + } + if curr_version == 9 { + curr_version = mig_9_to_10(conn)?; + } + if curr_version == 10 { + curr_version = mig_10_to_11(conn)?; + } + if curr_version == 11 { + curr_version = mig_11_to_12(conn)?; + } + if curr_version == 12 { + curr_version = mig_12_to_13(conn)?; + } + if curr_version == 13 { + curr_version = mig_13_to_14(conn)?; + } + if curr_version == 14 { + curr_version = mig_14_to_15(conn)?; + } + if curr_version == 15 { + curr_version = mig_15_to_16(conn)?; + } + if curr_version == 16 { + curr_version = mig_16_to_17(conn)?; + } + if curr_version == 17 { + curr_version = mig_17_to_18(conn)?; + } + if curr_version == 18 { + curr_version = mig_18_to_19(conn)?; + } + + if curr_version == DB_VERSION { + info!( + "All migration scripts completed successfully. Welcome to v{}.", + DB_VERSION + ); + } + } + // Database is current, all is good + Ordering::Equal => { + debug!("Database version was already current (v{DB_VERSION})"); + } + // Database is newer than what this code understands, abort + Ordering::Greater => { + panic!( + "Database version is newer than supported by this executable (v{curr_version} > v{DB_VERSION})", + ); + } + } + + // Setup PRAGMA + conn.execute_batch(STARTUP_SQL)?; + debug!("SQLite PRAGMA startup completed"); + Ok(DB_VERSION) +} + +pub fn rebuild_tags(conn: &mut PooledConnection) -> Result<()> { + // Check how many events we have to process + let count = db_event_count(conn)?; + let update_each_percent = 0.05; + let mut percent_done = 0.0; + let mut events_processed = 0; + let start = Instant::now(); + let tx = conn.transaction()?; + { + // Clear out table + tx.execute("DELETE FROM tag;", [])?; + let mut stmt = tx.prepare("select id, content from event order by id;")?; + let mut tag_rows = stmt.query([])?; + while let Some(row) = tag_rows.next()? { + if (events_processed as f32) / (count as f32) > percent_done { + info!("Tag update {}% complete...", (100.0 * percent_done).round()); + percent_done += update_each_percent; + } + // we want to capture the event_id that had the tag, the tag name, and the tag hex value. + let event_id: u64 = row.get(0)?; + let event_json: String = row.get(1)?; + let event: Event = serde_json::from_str(&event_json)?; + // look at each event, and each tag, creating new tag entries if appropriate. + for t in event.tags.iter().filter(|x| x.len() > 1) { + let tagname = t.first().unwrap(); + let tagnamechar_opt = single_char_tagname(tagname); + if tagnamechar_opt.is_none() { + continue; + } + // safe because len was > 1 + let tagval = t.get(1).unwrap(); + // insert as BLOB if we can restore it losslessly. + // this means it needs to be even length and lowercase. + if (tagval.len() % 2 == 0) && is_lower_hex(tagval) { + tx.execute( + "INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);", + params![event_id, tagname, hex::decode(tagval).ok()], + )?; + } else { + // otherwise, insert as text + tx.execute( + "INSERT INTO tag (event_id, name, value) VALUES (?1, ?2, ?3);", + params![event_id, tagname, &tagval], + )?; + } + } + events_processed += 1; + } + } + tx.commit()?; + info!("rebuilt tags in {:?}", start.elapsed()); + Ok(()) +} + +// Migration Scripts + +fn mig_1_to_2(conn: &mut PooledConnection) -> Result { + // only change is adding a hidden column to events. + let upgrade_sql = r##" +ALTER TABLE event ADD hidden INTEGER; +UPDATE event SET hidden=FALSE; +PRAGMA user_version = 2; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v1 -> v2"); + } + Err(err) => { + error!("update (v1->v2) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(2) +} + +fn mig_2_to_3(conn: &mut PooledConnection) -> Result { + // this version lacks the tag column + info!("database schema needs update from 2->3"); + let upgrade_sql = r##" +CREATE TABLE IF NOT EXISTS tag ( +id INTEGER PRIMARY KEY, +event_id INTEGER NOT NULL, -- an event ID that contains a tag. +name TEXT, -- the tag name ("p", "e", whatever) +value TEXT, -- the tag value, if not hex. +value_hex BLOB, -- the tag value, if it can be interpreted as a hex string. +FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE +); +PRAGMA user_version = 3; +"##; + // TODO: load existing refs into tag table + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v2 -> v3"); + } + Err(err) => { + error!("update (v2->v3) failed: {}", err); + panic!("database could not be upgraded"); + } + } + // iterate over every event/pubkey tag + let tx = conn.transaction()?; + { + let mut stmt = tx.prepare("select event_id, \"e\", lower(hex(referenced_event)) from event_ref union select event_id, \"p\", lower(hex(referenced_pubkey)) from pubkey_ref;")?; + let mut tag_rows = stmt.query([])?; + while let Some(row) = tag_rows.next()? { + // we want to capture the event_id that had the tag, the tag name, and the tag hex value. + let event_id: u64 = row.get(0)?; + let tag_name: String = row.get(1)?; + let tag_value: String = row.get(2)?; + // this will leave behind p/e tags that were non-hex, but they are invalid anyways. + if is_lower_hex(&tag_value) { + tx.execute( + "INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);", + params![event_id, tag_name, hex::decode(&tag_value).ok()], + )?; + } + } + } + info!("Updated tag values"); + tx.commit()?; + Ok(3) +} + +fn mig_3_to_4(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 3->4"); + let upgrade_sql = r##" +-- incoming metadata events with nip05 +CREATE TABLE IF NOT EXISTS user_verification ( +id INTEGER PRIMARY KEY, +metadata_event INTEGER NOT NULL, -- the metadata event used for this validation. +name TEXT NOT NULL, -- the nip05 field value (user@domain). +verified_at INTEGER, -- timestamp this author/nip05 was most recently verified. +failed_at INTEGER, -- timestamp a verification attempt failed (host down). +failure_count INTEGER DEFAULT 0, -- number of consecutive failures. +FOREIGN KEY(metadata_event) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS user_verification_name_index ON user_verification(name); +CREATE INDEX IF NOT EXISTS user_verification_event_index ON user_verification(metadata_event); +PRAGMA user_version = 4; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v3 -> v4"); + } + Err(err) => { + error!("update (v3->v4) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(4) +} + +fn mig_4_to_5(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 4->5"); + let upgrade_sql = r##" +DROP TABLE IF EXISTS event_ref; +DROP TABLE IF EXISTS pubkey_ref; +PRAGMA user_version=5; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v4 -> v5"); + } + Err(err) => { + error!("update (v4->v5) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(5) +} + +fn mig_5_to_6(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 5->6"); + // We need to rebuild the tags table. iterate through the + // event table. build event from json, insert tags into a + // fresh tag table. This was needed due to a logic error in + // how hex-like tags got indexed. + let start = Instant::now(); + let tx = conn.transaction()?; + { + // Clear out table + tx.execute("DELETE FROM tag;", [])?; + let mut stmt = tx.prepare("select id, content from event order by id;")?; + let mut tag_rows = stmt.query([])?; + while let Some(row) = tag_rows.next()? { + let event_id: u64 = row.get(0)?; + let event_json: String = row.get(1)?; + let event: Event = serde_json::from_str(&event_json)?; + // look at each event, and each tag, creating new tag entries if appropriate. + for t in event.tags.iter().filter(|x| x.len() > 1) { + let tagname = t.first().unwrap(); + let tagnamechar_opt = single_char_tagname(tagname); + if tagnamechar_opt.is_none() { + continue; + } + // safe because len was > 1 + let tagval = t.get(1).unwrap(); + // insert as BLOB if we can restore it losslessly. + // this means it needs to be even length and lowercase. + if (tagval.len() % 2 == 0) && is_lower_hex(tagval) { + tx.execute( + "INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);", + params![event_id, tagname, hex::decode(tagval).ok()], + )?; + } else { + // otherwise, insert as text + tx.execute( + "INSERT INTO tag (event_id, name, value) VALUES (?1, ?2, ?3);", + params![event_id, tagname, &tagval], + )?; + } + } + } + tx.execute("PRAGMA user_version = 6;", [])?; + } + tx.commit()?; + info!("database schema upgraded v5 -> v6 in {:?}", start.elapsed()); + // vacuum after large table modification + let start = Instant::now(); + conn.execute("VACUUM;", [])?; + info!("vacuumed DB after tags rebuild in {:?}", start.elapsed()); + Ok(6) +} + +fn mig_6_to_7(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 6->7"); + let upgrade_sql = r##" +ALTER TABLE event ADD delegated_by BLOB; +CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by); +PRAGMA user_version = 7; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v6 -> v7"); + } + Err(err) => { + error!("update (v6->v7) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(7) +} + +fn mig_7_to_8(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 7->8"); + // Remove redundant indexes, and add a better multi-column index. + let upgrade_sql = r##" +DROP INDEX IF EXISTS created_at_index; +DROP INDEX IF EXISTS kind_index; +CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at); +PRAGMA user_version = 8; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v7 -> v8"); + } + Err(err) => { + error!("update (v7->v8) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(8) +} + +fn mig_8_to_9(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 8->9"); + // Those old indexes were actually helpful... + let upgrade_sql = r##" +CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at); +CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at); +PRAGMA user_version = 9; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v8 -> v9"); + } + Err(err) => { + error!("update (v8->v9) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(9) +} + +fn mig_9_to_10(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 9->10"); + // Those old indexes were actually helpful... + let upgrade_sql = r##" +CREATE INDEX IF NOT EXISTS tag_composite_index ON tag(event_id,name,value_hex,value); +PRAGMA user_version = 10; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v9 -> v10"); + } + Err(err) => { + error!("update (v9->v10) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(10) +} + +fn mig_10_to_11(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 10->11"); + // Those old indexes were actually helpful... + let upgrade_sql = r##" +CREATE INDEX IF NOT EXISTS tag_name_eid_index ON tag(name,event_id,value_hex); +reindex; +pragma optimize; +PRAGMA user_version = 11; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v10 -> v11"); + } + Err(err) => { + error!("update (v10->v11) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(11) +} + +fn mig_11_to_12(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 11->12"); + let start = Instant::now(); + let tx = conn.transaction()?; + { + // Lookup every replaceable event + let mut stmt = tx.prepare("select kind,author from event where kind in (0,3,41) or (kind>=10000 and kind<20000) order by id;")?; + let mut replaceable_rows = stmt.query([])?; + info!("updating replaceable events; this could take awhile..."); + while let Some(row) = replaceable_rows.next()? { + // we want to capture the event_id that had the tag, the tag name, and the tag hex value. + let event_kind: u64 = row.get(0)?; + let event_author: Vec = row.get(1)?; + tx.execute( + "UPDATE event SET hidden=TRUE WHERE hidden!=TRUE and kind=? and author=? and id NOT IN (SELECT id FROM event WHERE kind=? AND author=? ORDER BY created_at DESC LIMIT 1)", + params![event_kind, event_author, event_kind, event_author], + )?; + } + tx.execute("PRAGMA user_version = 12;", [])?; + } + tx.commit()?; + info!( + "database schema upgraded v11 -> v12 in {:?}", + start.elapsed() + ); + // vacuum after large table modification + let start = Instant::now(); + conn.execute("VACUUM;", [])?; + info!( + "vacuumed DB after hidden event cleanup in {:?}", + start.elapsed() + ); + Ok(12) +} + +fn mig_12_to_13(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 12->13"); + let upgrade_sql = r##" +CREATE INDEX IF NOT EXISTS kind_author_index ON event(kind,author); +reindex; +pragma optimize; +PRAGMA user_version = 13; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v12 -> v13"); + } + Err(err) => { + error!("update (v12->v13) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(13) +} + +fn mig_13_to_14(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 13->14"); + let upgrade_sql = r##" +CREATE INDEX IF NOT EXISTS kind_index ON event(kind); +CREATE INDEX IF NOT EXISTS kind_created_at_index ON event(kind,created_at); +pragma optimize; +PRAGMA user_version = 14; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v13 -> v14"); + } + Err(err) => { + error!("update (v13->v14) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(14) +} + +fn mig_14_to_15(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 14->15"); + let upgrade_sql = r##" +CREATE INDEX IF NOT EXISTS author_created_at_index ON event(author,created_at); +CREATE INDEX IF NOT EXISTS author_kind_index ON event(author,kind); +PRAGMA user_version = 15; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v14 -> v15"); + } + Err(err) => { + error!("update (v14->v15) failed: {}", err); + panic!("database could not be upgraded"); + } + } + // clear out hidden events + let clear_hidden_sql = r##"DELETE FROM event WHERE HIDDEN=true;"##; + info!("removing hidden events; this may take awhile..."); + match conn.execute_batch(clear_hidden_sql) { + Ok(()) => { + info!("all hidden events removed"); + } + Err(err) => { + error!("delete failed: {}", err); + panic!("could not remove hidden events"); + } + } + Ok(15) +} + +fn mig_15_to_16(conn: &mut PooledConnection) -> Result { + let count = db_event_count(conn)?; + info!("database schema needs update from 15->16 (this may take a few minutes)"); + let upgrade_sql = r##" +DROP TABLE tag; +CREATE TABLE tag ( +id INTEGER PRIMARY KEY, +event_id INTEGER NOT NULL, -- an event ID that contains a tag. +name TEXT, -- the tag name ("p", "e", whatever) +value TEXT, -- the tag value, if not hex. +created_at INTEGER NOT NULL, -- when the event was authored +kind INTEGER NOT NULL, -- event kind +FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS tag_val_index ON tag(value); +CREATE INDEX IF NOT EXISTS tag_composite_index ON tag(event_id,name,value); +CREATE INDEX IF NOT EXISTS tag_name_eid_index ON tag(name,event_id,value); +CREATE INDEX IF NOT EXISTS tag_covering_index ON tag(name,kind,value,created_at,event_id); +"##; + + let start = Instant::now(); + let tx = conn.transaction()?; + + let bar = ProgressBar::new(count.try_into().unwrap()).with_message("rebuilding tags table"); + bar.set_style( + ProgressStyle::with_template( + "[{elapsed_precise}] {bar:40.white/blue} {pos:>7}/{len:7} [{percent}%] {msg}", + ) + .unwrap(), + ); + { + tx.execute_batch(upgrade_sql)?; + let mut stmt = + tx.prepare("select id, kind, created_at, content from event order by id;")?; + let mut tag_rows = stmt.query([])?; + let mut count = 0; + while let Some(row) = tag_rows.next()? { + count += 1; + if count % 10 == 0 { + bar.inc(10); + } + let event_id: u64 = row.get(0)?; + let kind: u64 = row.get(1)?; + let created_at: u64 = row.get(2)?; + let event_json: String = row.get(3)?; + let event: Event = serde_json::from_str(&event_json)?; + // look at each event, and each tag, creating new tag entries if appropriate. + for t in event.tags.iter().filter(|x| x.len() > 1) { + let tagname = t.first().unwrap(); + let tagnamechar_opt = single_char_tagname(tagname); + if tagnamechar_opt.is_none() { + continue; + } + // safe because len was > 1 + let tagval = t.get(1).unwrap(); + // otherwise, insert as text + tx.execute( + "INSERT INTO tag (event_id, name, value, kind, created_at) VALUES (?1, ?2, ?3, ?4, ?5);", + params![event_id, tagname, &tagval, kind, created_at], + )?; + } + } + tx.execute("PRAGMA user_version = 16;", [])?; + } + bar.finish(); + tx.commit()?; + info!( + "database schema upgraded v15 -> v16 in {:?}", + start.elapsed() + ); + Ok(16) +} + +fn mig_16_to_17(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 16->17"); + let upgrade_sql = r##" +ALTER TABLE event ADD COLUMN expires_at INTEGER; +CREATE INDEX IF NOT EXISTS event_expiration ON event(expires_at); +PRAGMA user_version = 17; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v16 -> v17"); + } + Err(err) => { + error!("update (v16->v17) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(17) +} + +fn mig_17_to_18(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 17->18"); + let upgrade_sql = r##" +-- Create invoices table +CREATE TABLE IF NOT EXISTS invoice ( +payment_hash TEXT PRIMARY KEY, +pubkey TEXT NOT NULL, +invoice TEXT NOT NULL, +amount INTEGER NOT NULL, +status TEXT CHECK ( status IN ('Paid', 'Unpaid', 'Expired' ) ) NOT NUll DEFAULT 'Unpaid', +description TEXT, +created_at INTEGER NOT NULL, +confirmed_at INTEGER, +CONSTRAINT invoice_pubkey_fkey FOREIGN KEY (pubkey) REFERENCES account (pubkey) ON DELETE CASCADE +); + +-- Create invoice index +CREATE INDEX IF NOT EXISTS invoice_pubkey_index ON invoice(pubkey); + +-- Create account table + +CREATE TABLE IF NOT EXISTS account ( +pubkey TEXT PRIMARY KEY, +is_admitted INTEGER NOT NULL DEFAULT 0, +balance INTEGER NOT NULL DEFAULT 0, +tos_accepted_at INTEGER +); + +-- Create account index +CREATE INDEX IF NOT EXISTS account_pubkey_index ON account(pubkey); + + +pragma optimize; +PRAGMA user_version = 18; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v17 -> v18"); + } + Err(err) => { + error!("update (v17->v18) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(18) +} + +fn mig_18_to_19(conn: &mut PooledConnection) -> Result { + info!("database schema needs update from 18->19"); + let upgrade_sql = r##" +-- Name authority claims (Floonet). One row per claimed name; a release +-- sets released_at instead of deleting, and the partial unique index +-- enforces one ACTIVE name per pubkey at the database layer. +CREATE TABLE IF NOT EXISTS name_claims ( +name TEXT PRIMARY KEY, +pubkey TEXT NOT NULL, +created_at INTEGER NOT NULL, +released_at INTEGER +); +CREATE INDEX IF NOT EXISTS name_claims_pubkey_index ON name_claims(pubkey); +CREATE UNIQUE INDEX IF NOT EXISTS name_claims_active_pubkey + ON name_claims(pubkey) WHERE released_at IS NULL; +PRAGMA user_version = 19; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v18 -> v19"); + } + Err(err) => { + error!("update (v18->v19) failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(19) +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..7fa9a04 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,1671 @@ +//! Server process +use crate::admission::{Admission, Decision}; +use crate::close::Close; +use crate::close::CloseCmd; +use crate::config::{Settings, VerifiedUsersMode}; +use crate::conn; +use crate::db; +use crate::db::SubmittedEvent; +use crate::error::{Error, Result}; +use crate::event::Event; +use crate::event::EventCmd; +use crate::event::EventWrapper; +use crate::exit; +use crate::info::RelayInfo; +use crate::name_authority; +use crate::nip05; +use crate::notice::Notice; +use crate::payment; +use crate::payment::InvoiceInfo; +use crate::payment::PaymentMessage; +use crate::repo::NostrRepo; +use crate::server::Error::CommandUnknownError; +use crate::server::EventWrapper::{WrappedAuth, WrappedEvent}; +use crate::subscription::Subscription; +use futures::SinkExt; +use futures::StreamExt; +use governor::{Jitter, Quota, RateLimiter}; +use http::header::HeaderMap; +use hyper::body::to_bytes; +use hyper::header::ACCEPT; +use hyper::service::{make_service_fn, service_fn}; +use hyper::upgrade::Upgraded; +use hyper::{ + header, server::conn::AddrStream, upgrade, Body, Request, Response, Server, StatusCode, +}; +use nostr::key::FromPkStr; +use nostr::key::Keys; +use prometheus::IntCounterVec; +use prometheus::IntGauge; +use prometheus::{Encoder, Histogram, HistogramOpts, IntCounter, Opts, Registry, TextEncoder}; +use qrcode::render::svg; +use qrcode::QrCode; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::HashMap; +use std::convert::Infallible; +use std::fs::File; +use std::io::BufReader; +use std::io::Read; +use std::net::SocketAddr; +use std::path::Path; +use std::sync::atomic::Ordering; +use std::sync::mpsc::Receiver as MpscReceiver; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; +use tokio::runtime::Builder; +use tokio::sync::broadcast::{self, Receiver, Sender}; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio_tungstenite::WebSocketStream; +use tracing::{debug, error, info, trace, warn}; +use tungstenite::error::CapacityError::MessageTooLong; +use tungstenite::error::Error as WsError; +use tungstenite::handshake; +use tungstenite::protocol::Message; +use tungstenite::protocol::WebSocketConfig; + +/// The Floonet logo, embedded at compile time and served at `/logo.svg`. +const FLOONET_LOGO_SVG: &str = include_str!("../assets/floonet-logo.svg"); + +/// Handle arbitrary HTTP requests, including for `WebSocket` upgrades. +#[allow(clippy::too_many_arguments)] +async fn handle_web_request( + mut request: Request, + repo: Arc, + settings: Settings, + remote_addr: SocketAddr, + broadcast: Sender, + event_tx: tokio::sync::mpsc::Sender, + payment_tx: tokio::sync::broadcast::Sender, + shutdown: Receiver<()>, + favicon: Option>, + registry: Registry, + metrics: NostrMetrics, + admission: Arc, + authority: Option>, +) -> Result, Infallible> { + // Own the path so arms can consume `request` freely. + let request_path = request.uri().path().to_owned(); + // Name authority endpoints (Floonet): the authority owns everything + // under /api/v1/ plus the NIP-05 well-known document. + if let Some(authority) = &authority { + if name_authority::is_authority_path(&request_path) { + return Ok(authority.handle(request, remote_addr).await); + } + } + match ( + request_path.as_str(), + request.headers().contains_key(header::UPGRADE), + ) { + // Request for / as websocket + ("/", true) => { + trace!("websocket with upgrade request"); + //assume request is a handshake, so create the handshake response + let response = match handshake::server::create_response_with_body(&request, || { + Body::empty() + }) { + Ok(response) => { + //in case the handshake response creation succeeds, + //spawn a task to handle the websocket connection + tokio::spawn(async move { + //using the hyper feature of upgrading a connection + match upgrade::on(&mut request).await { + //if successfully upgraded + Ok(upgraded) => { + // set WebSocket configuration options + let config = WebSocketConfig { + max_send_queue: Some(1024), + max_message_size: settings.limits.max_ws_message_bytes, + max_frame_size: settings.limits.max_ws_frame_bytes, + ..Default::default() + }; + //create a websocket stream from the upgraded object + let ws_stream = WebSocketStream::from_raw_socket( + //pass the upgraded object + //as the base layer stream of the Websocket + upgraded, + tokio_tungstenite::tungstenite::protocol::Role::Server, + Some(config), + ) + .await; + let origin = get_header_string("origin", request.headers()); + let user_agent = get_header_string("user-agent", request.headers()); + // determine the remote IP from headers if the exist + let header_ip = settings + .network + .remote_ip_header + .as_ref() + .and_then(|x| get_header_string(x, request.headers())); + // use the socket addr as a backup + let remote_ip = + header_ip.unwrap_or_else(|| remote_addr.ip().to_string()); + let client_info = ClientInfo { + remote_ip, + user_agent, + origin, + }; + // spawn a nostr server with our websocket + tokio::spawn(nostr_server( + repo, + client_info, + settings, + ws_stream, + broadcast, + event_tx, + shutdown, + metrics, + admission, + )); + } + // todo: trace, don't print... + Err(e) => println!( + "error when trying to upgrade connection \ + from address {remote_addr} to websocket connection. \ + Error is: {e}", + ), + } + }); + //return the response to the handshake request + response + } + Err(error) => { + warn!("websocket response failed"); + let mut res = + Response::new(Body::from(format!("Failed to create websocket: {error}"))); + *res.status_mut() = StatusCode::BAD_REQUEST; + return Ok(res); + } + }; + Ok::<_, Infallible>(response) + } + // Request for Relay info + ("/", false) => { + // handle request at root with no upgrade header + // Check if this is a nostr server info request + let accept_header = &request.headers().get(ACCEPT); + // check if application/nostr+json is included + if let Some(media_types) = accept_header { + if let Ok(mt_str) = media_types.to_str() { + if mt_str.contains("application/nostr+json") { + // build a relay info response + debug!("Responding to server info request"); + let rinfo = RelayInfo::from(settings); + let b = Body::from(serde_json::to_string_pretty(&rinfo).unwrap()); + return Ok(Response::builder() + .status(200) + .header("Content-Type", "application/nostr+json") + .header("Access-Control-Allow-Origin", "*") + .body(b) + .unwrap()); + } + } + } + + if let Some(relay_file_path) = settings.info.relay_page { + match file_bytes(&relay_file_path) { + Ok(file_content) => { + return Ok(Response::builder() + .status(200) + .header("Content-Type", "text/html; charset=UTF-8") + .body(Body::from(file_content)) + .expect("request builder")); + } + Err(err) => { + error!("Failed to read relay_page file: {}. Will use default", err); + } + } + } + + // Default landing page: neutral Floonet branding, logo front + // and center, and no payment wording anywhere. + let name = settings + .info + .name + .as_deref() + .unwrap_or("floonet-rs-relay") + .to_owned(); + let description = settings + .info + .description + .as_deref() + .unwrap_or("A Floonet relay for the Grin community Nostr network.") + .to_owned(); + let html = format!( + r#" + +{name} +
+Floonet +

{name}

+

{description}

+

Please use a Nostr client to connect.

+
"# + ); + Ok(Response::builder() + .status(200) + .header("Content-Type", "text/html; charset=UTF-8") + .body(Body::from(html)) + .unwrap()) + } + // The Floonet logo, served for the landing page. + ("/logo.svg", false) => Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "image/svg+xml") + // 1 day cache + .header("Cache-Control", "public, max-age=86400") + .body(Body::from(FLOONET_LOGO_SVG)) + .unwrap()), + // GoblinPay webhook: a payment server may POST {"invoice_id": ...} + // here to hint that an invoice was paid. The relay only forwards + // the id to the payment thread, which re-verifies the status with + // the GoblinPay server before admitting anything, so a forged + // webhook cannot fake a payment (fail closed). + ("/goblinpay", false) => { + let bytes = match to_bytes(request.into_body()).await { + Ok(b) if b.len() <= 16384 => b, + _ => { + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("invalid body")) + .unwrap()) + } + }; + let invoice_id = serde_json::from_slice::(&bytes) + .ok() + .and_then(|v| { + v.get("invoice_id") + .and_then(|id| id.as_str().map(std::string::ToString::to_string)) + }); + if let Some(invoice_id) = invoice_id { + debug!("goblinpay webhook for invoice {invoice_id}"); + payment_tx + .send(PaymentMessage::InvoicePaid(invoice_id)) + .ok(); + } + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from("ok")) + .unwrap()) + } + ("/metrics", false) => { + let mut buffer = vec![]; + let encoder = TextEncoder::new(); + let metric_families = registry.gather(); + encoder.encode(&metric_families, &mut buffer).unwrap(); + + Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "text/plain") + .body(Body::from(buffer)) + .unwrap()) + } + ("/favicon.ico", false) => { + if let Some(favicon_bytes) = favicon { + info!("returning favicon"); + Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "image/x-icon") + // 1 month cache + .header("Cache-Control", "public, max-age=2419200") + .body(Body::from(favicon_bytes)) + .unwrap()) + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("")) + .unwrap()) + } + } + // LN bits callback endpoint for paid invoices + ("/lnbits", false) => { + let callback: payment::lnbits::LNBitsCallback = + serde_json::from_slice(&to_bytes(request.into_body()).await.unwrap()).unwrap(); + debug!("LNBits callback: {callback:?}"); + + if let Err(e) = payment_tx.send(PaymentMessage::InvoicePaid(callback.payment_hash)) { + warn!("Could not send invoice update: {}", e); + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Error processing callback")) + .unwrap()); + } + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from("ok")) + .unwrap()) + } + // Endpoint for relays terms + ("/terms", false) => Ok(Response::builder() + .status(200) + .header("Content-Type", "text/plain") + .body(Body::from(settings.pay_to_relay.terms_message)) + .unwrap()), + // Endpoint to allow users to sign up + ("/join", false) => { + // Stops sign ups if disabled + if !settings.pay_to_relay.sign_ups { + return Ok(Response::builder() + .status(401) + .header("Content-Type", "text/plain") + .body(Body::from("Sorry, joining is not allowed at the moment")) + .unwrap()); + } + + let html = r#" + + + + + + +
+

Enter your pubkey

+
+

+ +

+ +
+ +
+ + + + "#; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(html)) + .unwrap()) + } + // Endpoint to display invoice + ("/invoice", false) => { + // Stops sign ups if disabled + if !settings.pay_to_relay.sign_ups { + return Ok(Response::builder() + .status(401) + .header("Content-Type", "text/plain") + .body(Body::from("Sorry, joining is not allowed at the moment")) + .unwrap()); + } + + // Get query pubkey from query string + let pubkey = get_pubkey(request); + + // Redirect back to join page if no pub key is found in query string + if pubkey.is_none() { + return Ok(Response::builder() + .status(404) + .header("location", "/join") + .body(Body::empty()) + .unwrap()); + } + + // Checks key is valid + let pubkey = pubkey.unwrap(); + let key = Keys::from_pk_str(&pubkey); + if key.is_err() { + return Ok(Response::builder() + .status(401) + .header("Content-Type", "text/plain") + .body(Body::from("Looks like your key is invalid")) + .unwrap()); + } + + // Checks if user is already admitted + let payment_message; + if let Ok((admission_status, _)) = repo.get_account_balance(&key.unwrap()).await { + if admission_status { + return Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from("Already admitted")) + .unwrap()); + } else { + payment_message = PaymentMessage::CheckAccount(pubkey.clone()); + } + } else { + payment_message = PaymentMessage::NewAccount(pubkey.clone()); + } + + // Send message on payment channel requesting invoice + if payment_tx.send(payment_message).is_err() { + warn!("Could not send payment tx"); + return Ok(Response::builder() + .status(501) + .header("Content-Type", "text/plain") + .body(Body::from("Sorry, something went wrong")) + .unwrap()); + } + + // wait for message with invoice back that matched the pub key + let mut invoice_info: Option = None; + while let Ok(msg) = payment_tx.subscribe().recv().await { + match msg { + PaymentMessage::Invoice(m_pubkey, m_invoice_info) => { + if m_pubkey == pubkey.clone() { + invoice_info = Some(m_invoice_info); + break; + } + } + PaymentMessage::AccountAdmitted(m_pubkey) => { + if m_pubkey == pubkey.clone() { + return Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from("Already admitted")) + .unwrap()); + } + } + _ => (), + } + } + + // Return early if cant get invoice + if invoice_info.is_none() { + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Sorry, could not get invoice")) + .unwrap()); + } + + // Since invoice is checked to be not none, unwrap + let invoice_info = invoice_info.unwrap(); + + let qr_code: String; + if let Ok(code) = QrCode::new(invoice_info.bolt11.as_bytes()) { + qr_code = code + .render() + .min_dimensions(200, 200) + .dark_color(svg::Color("#800000")) + .light_color(svg::Color("#ffff80")) + .build(); + } else { + qr_code = "Could not render image".to_string(); + } + + let html_result = format!( + r#" + + + + + + + +
+

+ To use this relay, an admission fee of {} sats is required. By paying the fee, you agree to the terms. +

+
+
+
+ {} +
+
+
+
+

{}

+ +
+
+

This page will not refresh

+

Verify admission here once you have paid

+
+
+ + + + + +"#, + settings.pay_to_relay.admission_cost, + qr_code, + invoice_info.bolt11, + pubkey, + invoice_info.bolt11 + ); + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(html_result)) + .unwrap()) + } + ("/account", false) => { + // Stops sign ups if disabled + if !settings.pay_to_relay.enabled { + return Ok(Response::builder() + .status(401) + .header("Content-Type", "text/plain") + .body(Body::from("This relay is not paid")) + .unwrap()); + } + + // Gets the pubkey from query string + let pubkey = get_pubkey(request); + + // Redirect back to join page if no pub key is found in query string + if pubkey.is_none() { + return Ok(Response::builder() + .status(404) + .header("location", "/join") + .body(Body::empty()) + .unwrap()); + } + + // Checks key is valid + let pubkey = pubkey.unwrap(); + let key = Keys::from_pk_str(&pubkey); + if key.is_err() { + return Ok(Response::builder() + .status(401) + .header("Content-Type", "text/plain") + .body(Body::from("Looks like your key is invalid")) + .unwrap()); + } + + // Account is checked async so user will have to refresh the page a couple times after + // they have paid. + if let Err(e) = payment_tx.send(PaymentMessage::CheckAccount(pubkey.clone())) { + warn!("Could not check account: {}", e); + } + // Checks if user is already admitted + let text = + if let Ok((admission_status, _)) = repo.get_account_balance(&key.unwrap()).await { + if admission_status { + r#"is"# + } else { + r#"is not"# + } + } else { + "Could not get admission status" + }; + + let html_result = format!( + r#" + + + + + + + +
+
{} {} admitted
+
+ + + + + "#, + pubkey, text + ); + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(html_result)) + .unwrap()) + } + // later balance + (_, _) => { + // handle any other url + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("Nothing here.")) + .unwrap()) + } + } +} + +// Get pubkey from request query string +fn get_pubkey(request: Request) -> Option { + let query = request.uri().query().unwrap_or("").to_string(); + + // Gets the pubkey value from query string + query.split('&').fold(None, |acc, pair| { + let mut parts = pair.splitn(2, '='); + let key = parts.next(); + let value = parts.next(); + if key == Some("pubkey") { + return value.map(|s| s.to_owned()); + } + acc + }) +} + +fn get_header_string(header: &str, headers: &HeaderMap) -> Option { + headers + .get(header) + .and_then(|x| x.to_str().ok().map(std::string::ToString::to_string)) +} + +// return on a control-c or internally requested shutdown signal +async fn ctrl_c_or_signal(mut shutdown_signal: Receiver<()>) { + let mut term_signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("could not define signal"); + #[allow(clippy::never_loop)] + loop { + tokio::select! { + _ = shutdown_signal.recv() => { + info!("Shutting down webserver as requested"); + // server shutting down, exit loop + break; + }, + _ = tokio::signal::ctrl_c() => { + info!("Shutting down webserver due to SIGINT"); + break; + }, + _ = term_signal.recv() => { + info!("Shutting down webserver due to SIGTERM"); + break; + }, + } + } +} + +fn create_metrics() -> (Registry, NostrMetrics) { + // setup prometheus registry + let registry = Registry::new(); + + let query_sub = Histogram::with_opts(HistogramOpts::new( + "nostr_query_seconds", + "Subscription response times", + )) + .unwrap(); + let query_db = Histogram::with_opts(HistogramOpts::new( + "nostr_filter_seconds", + "Filter SQL query times", + )) + .unwrap(); + let write_events = Histogram::with_opts(HistogramOpts::new( + "nostr_events_write_seconds", + "Event writing response times", + )) + .unwrap(); + let sent_events = IntCounterVec::new( + Opts::new("nostr_events_sent_total", "Events sent to clients"), + vec!["source"].as_slice(), + ) + .unwrap(); + let connections = + IntCounter::with_opts(Opts::new("nostr_connections_total", "New connections")).unwrap(); + let db_connections = IntGauge::with_opts(Opts::new( + "nostr_db_connections", + "Active database connections", + )) + .unwrap(); + let query_aborts = IntCounterVec::new( + Opts::new("nostr_query_abort_total", "Aborted queries"), + vec!["reason"].as_slice(), + ) + .unwrap(); + let cmd_req = IntCounter::with_opts(Opts::new("nostr_cmd_req_total", "REQ commands")).unwrap(); + let cmd_event = + IntCounter::with_opts(Opts::new("nostr_cmd_event_total", "EVENT commands")).unwrap(); + let cmd_close = + IntCounter::with_opts(Opts::new("nostr_cmd_close_total", "CLOSE commands")).unwrap(); + let cmd_auth = + IntCounter::with_opts(Opts::new("nostr_cmd_auth_total", "AUTH commands")).unwrap(); + let disconnects = IntCounterVec::new( + Opts::new("nostr_disconnects_total", "Client disconnects"), + vec!["reason"].as_slice(), + ) + .unwrap(); + registry.register(Box::new(query_sub.clone())).unwrap(); + registry.register(Box::new(query_db.clone())).unwrap(); + registry.register(Box::new(write_events.clone())).unwrap(); + registry.register(Box::new(sent_events.clone())).unwrap(); + registry.register(Box::new(connections.clone())).unwrap(); + registry.register(Box::new(db_connections.clone())).unwrap(); + registry.register(Box::new(query_aborts.clone())).unwrap(); + registry.register(Box::new(cmd_req.clone())).unwrap(); + registry.register(Box::new(cmd_event.clone())).unwrap(); + registry.register(Box::new(cmd_close.clone())).unwrap(); + registry.register(Box::new(cmd_auth.clone())).unwrap(); + registry.register(Box::new(disconnects.clone())).unwrap(); + let metrics = NostrMetrics { + query_sub, + query_db, + write_events, + sent_events, + connections, + db_connections, + disconnects, + query_aborts, + cmd_req, + cmd_event, + cmd_close, + cmd_auth, + }; + (registry, metrics) +} + +fn file_bytes(path: &str) -> Result> { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + let mut buffer = Vec::new(); + // Read file into vector. + reader.read_to_end(&mut buffer)?; + Ok(buffer) +} + +/// Start running a Nostr relay server. +pub fn start_server(settings: &Settings, shutdown_rx: MpscReceiver<()>) -> Result<(), Error> { + trace!("Config: {:?}", settings); + // do some config validation. + if !Path::new(&settings.database.data_directory).is_dir() { + error!("Database directory does not exist"); + return Err(Error::DatabaseDirError); + } + // Fail fast on a broken mixnet exit toggle. + exit::validate(settings)?; + let addr = format!( + "{}:{}", + settings.network.address.trim(), + settings.network.port + ); + let socket_addr = addr.parse().expect("listening address not valid"); + // address whitelisting settings + if let Some(addr_whitelist) = &settings.authorization.pubkey_whitelist { + info!( + "Event publishing restricted to {} pubkey(s)", + addr_whitelist.len() + ); + } + // check if NIP-05 enforced user verification is on + if settings.verified_users.is_active() { + info!( + "NIP-05 user verification mode:{:?}", + settings.verified_users.mode + ); + if let Some(d) = settings.verified_users.verify_update_duration() { + info!("NIP-05 check user verification every: {:?}", d); + } + if let Some(d) = settings.verified_users.verify_expiration_duration() { + info!("NIP-05 user verification expires after: {:?}", d); + } + if let Some(wl) = &settings.verified_users.domain_whitelist { + info!("NIP-05 domain whitelist: {:?}", wl); + } + if let Some(bl) = &settings.verified_users.domain_blacklist { + info!("NIP-05 domain blacklist: {:?}", bl); + } + } + // configure tokio runtime + let rt = Builder::new_multi_thread() + .enable_all() + .thread_name_fn(|| { + // give each thread a unique numeric name + static ATOMIC_ID: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); + format!("tokio-ws-{id}") + }) + // limit concurrent SQLite blocking threads + .max_blocking_threads(settings.limits.max_blocking_threads) + .on_thread_start(|| { + trace!("started new thread: {:?}", std::thread::current().name()); + }) + .on_thread_stop(|| { + trace!("stopped thread: {:?}", std::thread::current().name()); + }) + .build() + .unwrap(); + // start tokio + rt.block_on(async { + let broadcast_buffer_limit = settings.limits.broadcast_buffer; + let persist_buffer_limit = settings.limits.event_persist_buffer; + let verified_users_active = settings.verified_users.is_active(); + let settings = settings.clone(); + info!("listening on: {}", socket_addr); + // all client-submitted valid events are broadcast to every + // other client on this channel. This should be large enough + // to accommodate slower readers (messages are dropped if + // clients can not keep up). + let (bcast_tx, _) = broadcast::channel::(broadcast_buffer_limit); + // validated events that need to be persisted are sent to the + // database on via this channel. + let (event_tx, event_rx) = mpsc::channel::(persist_buffer_limit); + // establish a channel for letting all threads now about a + // requested server shutdown. + let (invoke_shutdown, shutdown_listen) = broadcast::channel::<()>(1); + // create a channel for sending any new metadata event. These + // will get processed relatively slowly (a potentially + // multi-second blocking HTTP call) on a single thread, so we + // buffer requests on the channel. No harm in dropping events + // here, since we are protecting against DoS. This can make + // it difficult to setup initial metadata in bulk, since + // overwhelming this will drop events and won't register + // metadata events. + let (metadata_tx, metadata_rx) = broadcast::channel::(4096); + + let (payment_tx, payment_rx) = broadcast::channel::(4096); + + let (registry, metrics) = create_metrics(); + + // build a repository for events + let repo = db::build_repo(&settings, metrics.clone()).await; + // Floonet: event admission pipeline (kind whitelist keystone). + let admission = Arc::new(Admission::from_settings(&settings)); + // Floonet: built-in name authority, if enabled. + let authority = if settings.name_authority.enabled { + match name_authority::Authority::new(&settings, repo.clone()) { + Ok(a) => Some(Arc::new(a)), + Err(e) => { + error!("failed to start name authority: {:?}", e); + std::process::exit(1); + } + } + } else { + None + }; + // Floonet: co-located mixnet exit, if enabled. + exit::spawn(&settings); + // start the database writer task. Give it a channel for + // writing events, and for publishing events that have been + // written (to all connected clients). + tokio::task::spawn(db::db_writer( + repo.clone(), + settings.clone(), + event_rx, + bcast_tx.clone(), + metadata_tx.clone(), + payment_tx.clone(), + shutdown_listen, + )); + info!("db writer created"); + + // create a nip-05 verifier thread; if enabled. + if settings.verified_users.mode != VerifiedUsersMode::Disabled { + let verifier_opt = nip05::Verifier::new( + repo.clone(), + metadata_rx, + bcast_tx.clone(), + settings.clone(), + ); + if let Ok(mut v) = verifier_opt { + if verified_users_active { + tokio::task::spawn(async move { + info!("starting up NIP-05 verifier..."); + v.run().await; + }); + } + } + } + + // Create payments thread if pay to relay enabled + if settings.pay_to_relay.enabled { + let payment_opt = payment::Payment::new( + repo.clone(), + payment_tx.clone(), + payment_rx, + bcast_tx.clone(), + settings.clone(), + ); + match payment_opt { + Ok(mut p) => { + tokio::task::spawn(async move { + info!("starting payment process ..."); + p.run().await; + }); + } + Err(e) => { + error!("Failed to start payment process {e}"); + std::process::exit(1); + } + } + } + + // listen for (external to tokio) shutdown request + let controlled_shutdown = invoke_shutdown.clone(); + tokio::spawn(async move { + info!("control message listener started"); + match shutdown_rx.recv() { + Ok(()) => { + info!("control message requesting shutdown"); + controlled_shutdown.send(()).ok(); + } + Err(std::sync::mpsc::RecvError) => { + trace!("shutdown requestor is disconnected (this is normal)"); + } + }; + }); + // listen for ctrl-c interruupts + let ctrl_c_shutdown = invoke_shutdown.clone(); + // listener for webserver shutdown + let webserver_shutdown_listen = invoke_shutdown.subscribe(); + + tokio::spawn(async move { + tokio::signal::ctrl_c().await.unwrap(); + info!("shutting down due to SIGINT (main)"); + ctrl_c_shutdown.send(()).ok(); + }); + // spawn a task to check the pool size. + //let pool_monitor = pool.clone(); + //tokio::spawn(async move {db::monitor_pool("reader", pool_monitor).await;}); + + // Read in the favicon if it exists + let favicon = settings.info.favicon.as_ref().and_then(|x| { + info!("reading favicon..."); + file_bytes(x).ok() + }); + + // A `Service` is needed for every connection, so this + // creates one from our `handle_request` function. + let make_svc = make_service_fn(|conn: &AddrStream| { + let repo = repo.clone(); + let remote_addr = conn.remote_addr(); + let bcast = bcast_tx.clone(); + let event = event_tx.clone(); + let payment_tx = payment_tx.clone(); + let stop = invoke_shutdown.clone(); + let settings = settings.clone(); + let favicon = favicon.clone(); + let registry = registry.clone(); + let metrics = metrics.clone(); + let admission = admission.clone(); + let authority = authority.clone(); + async move { + // service_fn converts our function into a `Service` + Ok::<_, Infallible>(service_fn(move |request: Request| { + handle_web_request( + request, + repo.clone(), + settings.clone(), + remote_addr, + bcast.clone(), + event.clone(), + payment_tx.clone(), + stop.subscribe(), + favicon.clone(), + registry.clone(), + metrics.clone(), + admission.clone(), + authority.clone(), + ) + })) + } + }); + let server = Server::bind(&socket_addr) + .serve(make_svc) + .with_graceful_shutdown(ctrl_c_or_signal(webserver_shutdown_listen)); + // run hyper in this thread. This is why the thread does not return. + if let Err(e) = server.await { + eprintln!("server error: {e}"); + } + }); + Ok(()) +} + +/// Nostr protocol messages from a client +#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)] +#[serde(untagged)] +pub enum NostrMessage { + /// `EVENT` and `AUTH` messages + EventMsg(EventCmd), + /// A `REQ` message + SubMsg(Subscription), + /// A `CLOSE` message + CloseMsg(CloseCmd), +} + +/// Convert Message to `NostrMessage` +fn convert_to_msg(msg: &str, max_bytes: Option) -> Result { + let parsed_res: Result = + serde_json::from_str(msg).map_err(std::convert::Into::into); + match parsed_res { + Ok(m) => { + if let NostrMessage::SubMsg(_) = m { + // note; this only prints the first 16k of a REQ and then truncates. + trace!("REQ: {:?}", msg); + }; + if let NostrMessage::EventMsg(_) = m { + if let Some(max_size) = max_bytes { + // check length, ensure that some max size is set. + if msg.len() > max_size && max_size > 0 { + return Err(Error::EventMaxLengthError(msg.len())); + } + } + } + Ok(m) + } + Err(e) => { + trace!("proto parse error: {:?}", e); + trace!("parse error on message: {:?}", msg.trim()); + Err(Error::ProtoParseError) + } + } +} + +/// Turn a string into a NOTICE message ready to send over a `WebSocket` +fn make_notice_message(notice: &Notice) -> Message { + let json = match notice { + Notice::Message(ref msg) => json!(["NOTICE", msg]), + Notice::EventResult(ref res) => json!(["OK", res.id, res.status.to_bool(), res.msg]), + Notice::AuthChallenge(ref challenge) => json!(["AUTH", challenge]), + }; + + Message::text(json.to_string()) +} + +fn allowed_to_send(event_str: &str, conn: &conn::ClientConn, settings: &Settings) -> bool { + // TODO: pass in kind so that we can avoid deserialization for most events + if settings.authorization.nip42_dms { + match serde_json::from_str::(event_str) { + Ok(event) => { + if event.kind == 4 || event.kind == 44 || event.kind == 1059 { + match (conn.auth_pubkey(), event.tag_values_by_name("p").first()) { + (Some(auth_pubkey), Some(recipient_pubkey)) => { + recipient_pubkey == auth_pubkey || &event.pubkey == auth_pubkey + } + (_, _) => false, + } + } else { + true + } + } + Err(_) => false, + } + } else { + true + } +} + +struct ClientInfo { + remote_ip: String, + user_agent: Option, + origin: Option, +} + +/// Handle new client connections. This runs through an event loop +/// for all client communication. +#[allow(clippy::too_many_arguments)] +async fn nostr_server( + repo: Arc, + client_info: ClientInfo, + settings: Settings, + mut ws_stream: WebSocketStream, + broadcast: Sender, + event_tx: mpsc::Sender, + mut shutdown: Receiver<()>, + metrics: NostrMetrics, + admission: Arc, +) { + // the time this websocket nostr server started + let orig_start = Instant::now(); + // get a broadcast channel for clients to communicate on + let mut bcast_rx = broadcast.subscribe(); + // Track internal client state + let mut conn = conn::ClientConn::new(client_info.remote_ip); + // subscription creation rate limiting + let mut sub_lim_opt = None; + // 100ms jitter when the rate limiter returns + let jitter = Jitter::up_to(Duration::from_millis(100)); + let sub_per_min_setting = settings.limits.subscriptions_per_min; + if let Some(sub_per_min) = sub_per_min_setting { + if sub_per_min > 0 { + trace!("Rate limits for sub creation ({}/min)", sub_per_min); + let quota_time = core::num::NonZeroU32::new(sub_per_min).unwrap(); + let quota = Quota::per_minute(quota_time); + sub_lim_opt = Some(RateLimiter::direct(quota)); + } + } + // Use the remote IP as the client identifier + let cid = conn.get_client_prefix(); + // Create a channel for receiving query results from the database. + // we will send out the tx handle to any query we generate. + // this has capacity for some of the larger requests we see, which + // should allow the DB thread to release the handle earlier. + let (query_tx, mut query_rx) = mpsc::channel::(20_000); + // Create channel for receiving NOTICEs + let (notice_tx, mut notice_rx) = mpsc::channel::(128); + + // last time this client sent data (message, ping, etc.) + let mut last_message_time = Instant::now(); + + // ping interval (every 5 minutes) + let default_ping_dur = Duration::from_secs(settings.network.ping_interval_seconds.into()); + + // disconnect after 20 minutes without a ping response or event. + let max_quiet_time = Duration::from_secs(60 * 20); + + let start = tokio::time::Instant::now() + default_ping_dur; + let mut ping_interval = tokio::time::interval_at(start, default_ping_dur); + + // maintain a hashmap of a oneshot channel for active subscriptions. + // when these subscriptions are cancelled, make a message + // available to the executing query so it knows to stop. + let mut running_queries: HashMap> = HashMap::new(); + // for stats, keep track of how many events the client published, + // and how many it received from queries. + let mut client_published_event_count: usize = 0; + let mut client_received_event_count: usize = 0; + + let unspec = "".to_string(); + info!("new client connection (cid: {}, ip: {:?})", cid, conn.ip()); + let origin = client_info.origin.as_ref().unwrap_or(&unspec); + let user_agent = client_info.user_agent.as_ref().unwrap_or(&unspec); + info!( + "cid: {}, origin: {:?}, user-agent: {:?}", + cid, origin, user_agent + ); + + // Measure connections + metrics.connections.inc(); + + if settings.authorization.nip42_auth { + conn.generate_auth_challenge(); + if let Some(challenge) = conn.auth_challenge() { + ws_stream + .send(make_notice_message(&Notice::AuthChallenge( + challenge.to_string(), + ))) + .await + .ok(); + } + } + + loop { + tokio::select! { + _ = shutdown.recv() => { + metrics.disconnects.with_label_values(&["shutdown"]).inc(); + info!("Close connection down due to shutdown, client: {}, ip: {:?}, connected: {:?}", cid, conn.ip(), orig_start.elapsed()); + // server shutting down, exit loop + break; + }, + _ = ping_interval.tick() => { + // check how long since we talked to client + // if it has been too long, disconnect + if last_message_time.elapsed() > max_quiet_time { + debug!("ending connection due to lack of client ping response"); + metrics.disconnects.with_label_values(&["timeout"]).inc(); + break; + } + // Send a ping + if ws_stream.send(Message::Ping(Vec::new())).await.is_err() { + debug!("failed to send ping, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + }, + Some(notice_msg) = notice_rx.recv() => { + if ws_stream.send(make_notice_message(¬ice_msg)).await.is_err() { + debug!("failed to send ping, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + }, + Some(query_result) = query_rx.recv() => { + // database informed us of a query result we asked for + let subesc = query_result.sub_id.replace('"', ""); + if query_result.event == "EOSE" { + let send_str = format!("[\"EOSE\",\"{subesc}\"]"); + if ws_stream.send(Message::Text(send_str)).await.is_err() { + debug!("failed to send message, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + } else if allowed_to_send(&query_result.event, &conn, &settings) { + metrics.sent_events.with_label_values(&["db"]).inc(); + client_received_event_count += 1; + // send a result + let send_str = format!("[\"EVENT\",\"{}\",{}]", subesc, &query_result.event); + if ws_stream.send(Message::Text(send_str)).await.is_err() { + debug!("failed to send message, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + } + }, + // TODO: consider logging the LaggedRecv error + Ok(global_event) = bcast_rx.recv() => { + let mut should_disconnect = false; + // an event has been broadcast to all clients + // first check if there is a subscription for this event. + for (s, sub) in conn.subscriptions() { + if !sub.interested_in_event(&global_event) { + continue; + } + // TODO: serialize at broadcast time, instead of + // once for each consumer. + if let Ok(event_str) = serde_json::to_string(&global_event) { + if allowed_to_send(&event_str, &conn, &settings) { + // create an event response and send it + trace!("sub match for client: {}, sub: {:?}, event: {:?}", + cid, s, + global_event.get_event_id_prefix()); + let subesc = s.replace('"', ""); + metrics.sent_events.with_label_values(&["realtime"]).inc(); + if ws_stream.send(Message::Text(format!("[\"EVENT\",\"{subesc}\",{event_str}]"))).await.is_err() { + debug!("failed to send message, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + should_disconnect = true; + break; + } + } + } else { + warn!("could not serialize event: {:?}", global_event.get_event_id_prefix()); + } + } + if should_disconnect { + break; + } + }, + ws_next = ws_stream.next() => { + // update most recent message time for client + last_message_time = Instant::now(); + // Consume text messages from the client, parse into Nostr messages. + let nostr_msg = match ws_next { + Some(Ok(Message::Text(m))) => { + convert_to_msg(&m,settings.limits.max_event_bytes) + }, + Some(Ok(Message::Binary(_))) => { + + if ws_stream.send( + make_notice_message(&Notice::message("binary messages are not accepted".into()))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + continue; + }, + Some(Ok(Message::Ping(_) | Message::Pong(_))) => { + // get a ping/pong, ignore. tungstenite will + // send responses automatically. + continue; + }, + Some(Err(WsError::Capacity(MessageTooLong{size, max_size}))) => { + if ws_stream.send( + make_notice_message(&Notice::message(format!("message too large ({size} > {max_size})")))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + continue; + }, + None | + Some(Ok(Message::Close(_)) | + Err(WsError::AlreadyClosed | WsError::ConnectionClosed | + WsError::Protocol(tungstenite::error::ProtocolError::ResetWithoutClosingHandshake))) + => { + debug!("websocket close from client (cid: {}, ip: {:?})",cid, conn.ip()); + metrics.disconnects.with_label_values(&["normal"]).inc(); + break; + }, + Some(Err(WsError::Io(e))) => { + // IO errors are considered fatal + warn!("IO error (cid: {}, ip: {:?}): {:?}", cid, conn.ip(), e); + metrics.disconnects.with_label_values(&["error"]).inc(); + break; + } + x => { + // default condition on error is to close the client connection + info!("unknown error (cid: {}, ip: {:?}): {:?} (closing conn)", cid, conn.ip(), x); + metrics.disconnects.with_label_values(&["error"]).inc(); + break; + } + }; + + // convert ws_next into proto_next + match nostr_msg { + Ok(NostrMessage::EventMsg(ec)) => { + // An EventCmd needs to be validated to be converted into an Event + // handle each type of message + let evid = ec.event_id().to_owned(); + let parsed : Result = Result::::from(ec); + match parsed { + Ok(WrappedEvent(e)) => { + metrics.cmd_event.inc(); + let id_prefix:String = e.id.chars().take(8).collect(); + debug!("successfully parsed/validated event: {:?} (cid: {}, kind: {})", id_prefix, cid, e.kind); + // check if event is expired + if e.is_expired() { + let notice = Notice::invalid(e.id, "The event has already expired"); + if ws_stream.send(make_notice_message(¬ice)).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + // check if the event is too far in the future. + } else if e.is_valid_timestamp(settings.options.reject_future_seconds) { + // Floonet admission: the composed policy + // pipeline (default-deny kind whitelist, + // optional NIP-42 write gate, author + // whitelist) decides before anything is + // queued for persistence. + match admission.check(&e, conn.auth_pubkey().map(String::as_str)) { + Decision::Allow => { + // Write this to the database. + let auth_pubkey = conn.auth_pubkey().and_then(|pubkey| hex::decode(pubkey).ok()); + let submit_event = SubmittedEvent { + event: e.clone(), + notice_tx: notice_tx.clone(), + source_ip: conn.ip().to_string(), + origin: client_info.origin.clone(), + user_agent: client_info.user_agent.clone(), + auth_pubkey }; + event_tx.send(submit_event).await.ok(); + client_published_event_count += 1; + } + Decision::Deny { reason, auth_required } => { + debug!("admission denied event (kind: {}, cid: {}): {}", e.kind, cid, reason); + let notice = if auth_required { + Notice::auth_required(e.id.clone(), &reason) + } else { + Notice::blocked(e.id.clone(), &reason) + }; + if ws_stream.send(make_notice_message(¬ice)).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + } + } + } else { + info!("client: {} sent a far future-dated event", cid); + if let Some(fut_sec) = settings.options.reject_future_seconds { + let msg = format!("The event created_at field is out of the acceptable range (+{fut_sec}sec) for this relay."); + let notice = Notice::invalid(e.id, &msg); + if ws_stream.send(make_notice_message(¬ice)).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + + } + } + } + }, + Ok(WrappedAuth(event)) => { + metrics.cmd_auth.inc(); + if settings.authorization.nip42_auth { + let id_prefix:String = event.id.chars().take(8).collect(); + debug!("successfully parsed auth: {:?} (cid: {})", id_prefix, cid); + match &settings.info.relay_url { + None => { + error!("AUTH command received, but relay_url is not set in the config file (cid: {})", cid); + }, + Some(relay) => { + match conn.authenticate(&event, relay) { + Ok(_) => { + let pubkey = match conn.auth_pubkey() { + Some(k) => k.chars().take(8).collect(), + None => "".to_string(), + }; + info!("client is authenticated: (cid: {}, pubkey: {:?})", cid, pubkey); + // Send OK message to confirm successful authentication (NIP-42) + if ws_stream.send(make_notice_message(&Notice::saved(event.id))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + }, + Err(e) => { + info!("authentication error: {} (cid: {})", e, cid); + if ws_stream.send(make_notice_message(&Notice::restricted(event.id, format!("authentication error: {e}").as_str()))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + }, + } + } + } + } else { + let e = CommandUnknownError; + info!("client sent an invalid event (cid: {})", cid); + if ws_stream.send(make_notice_message(&Notice::invalid(evid, &format!("{e}")))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + } + }, + Err(e) => { + metrics.cmd_event.inc(); + info!("client sent an invalid event (cid: {})", cid); + if ws_stream.send(make_notice_message(&Notice::invalid(evid, &format!("{e}")))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + } + } + }, + Ok(NostrMessage::SubMsg(s)) => { + debug!("subscription requested (cid: {}, sub: {:?})", cid, s.id); + // subscription handling consists of: + // * check for rate limits + // * registering the subscription so future events can be matched + // * making a channel to cancel to request later + // * sending a request for a SQL query + // Do nothing if the sub already exists. + if conn.has_subscription(&s) { + info!("client sent duplicate subscription, ignoring (cid: {}, sub: {:?})", cid, s.id); + } else { + metrics.cmd_req.inc(); + if let Some(ref lim) = sub_lim_opt { + lim.until_ready_with_jitter(jitter).await; + } + if settings.limits.limit_scrapers && s.is_scraper() { + info!("subscription was scraper, ignoring (cid: {}, sub: {:?})", cid, s.id); + if ws_stream.send(Message::Text(format!("[\"EOSE\",\"{}\"]", s.id))).await.is_err() { + debug!("failed to send message, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + continue + } + let (abandon_query_tx, abandon_query_rx) = oneshot::channel::<()>(); + match conn.subscribe(s.clone()) { + Ok(()) => { + // when we insert, if there was a previous query running with the same name, cancel it. + if let Some(previous_query) = running_queries.insert(s.id.clone(), abandon_query_tx) { + previous_query.send(()).ok(); + } + if s.needs_historical_events() { + // start a database query. this spawns a blocking database query on a worker thread. + repo.query_subscription(s, cid.clone(), query_tx.clone(), abandon_query_rx).await.ok(); + } + }, + Err(e) => { + info!("Subscription error: {} (cid: {}, sub: {:?})", e, cid, s.id); + if ws_stream.send(make_notice_message(&Notice::message(format!("Subscription error: {e}")))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + + } + } + } + } + }, + Ok(NostrMessage::CloseMsg(cc)) => { + // closing a request simply removes the subscription. + let parsed : Result = Result::::from(cc); + if let Ok(c) = parsed { + metrics.cmd_close.inc(); + // check if a query is currently + // running, and remove it if so. + let stop_tx = running_queries.remove(&c.id); + if let Some(tx) = stop_tx { + tx.send(()).ok(); + } + // stop checking new events against + // the subscription + conn.unsubscribe(&c); + } else { + info!("invalid command ignored"); + if ws_stream.send(make_notice_message(&Notice::message("could not parse command".into()))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + } + }, + Err(Error::ConnError) => { + debug!("got connection close/error, disconnecting cid: {}, ip: {:?}",cid, conn.ip()); + break; + } + Err(Error::EventMaxLengthError(s)) => { + info!("client sent command larger ({} bytes) than max size (cid: {})", s, cid); + if ws_stream.send(make_notice_message(&Notice::message("event exceeded max size".into()))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + }, + Err(Error::ProtoParseError) => { + info!("client sent command that could not be parsed (cid: {})", cid); + if ws_stream.send(make_notice_message(&Notice::message("could not parse command".into()))).await.is_err() { + debug!("failed to send notice, closing connection (cid: {})", cid); + metrics.disconnects.with_label_values(&["send_error"]).inc(); + break; + } + }, + Err(e) => { + info!("got non-fatal error from client (cid: {}, error: {:?}", cid, e); + }, + } + }, + } + } + // connection cleanup - ensure any still running queries are terminated. + for (_, stop_tx) in running_queries { + stop_tx.send(()).ok(); + } + info!( + "stopping client connection (cid: {}, ip: {:?}, sent: {} events, recv: {} events, connected: {:?})", + cid, + conn.ip(), + client_published_event_count, + client_received_event_count, + orig_start.elapsed() + ); +} + +#[derive(Clone)] +pub struct NostrMetrics { + pub query_sub: Histogram, // response time of successful subscriptions + pub query_db: Histogram, // individual database query execution time + pub db_connections: IntGauge, // database connections in use + pub write_events: Histogram, // response time of event writes + pub sent_events: IntCounterVec, // count of events sent to clients + pub connections: IntCounter, // count of websocket connections + pub disconnects: IntCounterVec, // client disconnects + pub query_aborts: IntCounterVec, // count of queries aborted by server + pub cmd_req: IntCounter, // count of REQ commands received + pub cmd_event: IntCounter, // count of EVENT commands received + pub cmd_close: IntCounter, // count of CLOSE commands received + pub cmd_auth: IntCounter, // count of AUTH commands received +} diff --git a/src/subscription.rs b/src/subscription.rs new file mode 100644 index 0000000..8f7204e --- /dev/null +++ b/src/subscription.rs @@ -0,0 +1,703 @@ +//! Subscription and filter parsing +use crate::error::Result; +use crate::event::Event; +use serde::de::Unexpected; +use serde::ser::SerializeMap; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; +use std::collections::HashMap; +use std::collections::HashSet; +use std::ops::Deref; + +/// Subscription identifier and set of request filters +#[derive(Serialize, PartialEq, Eq, Debug, Clone)] +pub struct Subscription { + pub id: String, + pub filters: Vec, +} + +/// Tag query is AND or OR operation +#[derive(Serialize, PartialEq, Eq, Debug, Clone)] +pub enum TagOperand { + And(HashSet), + Or(HashSet), +} + +impl Deref for TagOperand { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + match self { + TagOperand::Or(v) => v, + TagOperand::And(v) => v, + } + } +} + +/// Filter for requests +/// +/// Corresponds to client-provided subscription request elements. Any +/// element can be present if it should be used in filtering, or +/// absent ([`None`]) if it should be ignored. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct ReqFilter { + /// Event hashes + pub ids: Option>, + /// Event kinds + pub kinds: Option>, + /// Events published after this time + pub since: Option, + /// Events published before this time + pub until: Option, + /// List of author public keys + pub authors: Option>, + /// Limit number of results + pub limit: Option, + /// Set of tags + pub tags: Option>, + /// Force no matches due to malformed data + // we can't represent it in the req filter, so we don't want to + // erroneously match. This basically indicates the req tried to + // do something invalid. + pub force_no_match: bool, +} + +impl Serialize for ReqFilter { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(None)?; + if let Some(ids) = &self.ids { + map.serialize_entry("ids", &ids)?; + } + if let Some(kinds) = &self.kinds { + map.serialize_entry("kinds", &kinds)?; + } + if let Some(until) = &self.until { + map.serialize_entry("until", until)?; + } + if let Some(since) = &self.since { + map.serialize_entry("since", since)?; + } + if let Some(limit) = &self.limit { + map.serialize_entry("limit", limit)?; + } + if let Some(authors) = &self.authors { + map.serialize_entry("authors", &authors)?; + } + // serialize tags + if let Some(tags) = &self.tags { + for (k, v) in tags { + map.serialize_entry(&format!("#{k}"), v)?; + } + } + map.end() + } +} + +impl<'de> Deserialize<'de> for ReqFilter { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let received: Value = Deserialize::deserialize(deserializer)?; + let filter = received.as_object().ok_or_else(|| { + serde::de::Error::invalid_type( + Unexpected::Other("reqfilter is not an object"), + &"a json object", + ) + })?; + let mut rf = ReqFilter { + ids: None, + kinds: None, + since: None, + until: None, + authors: None, + limit: None, + tags: None, + force_no_match: false, + }; + let empty_string = "".into(); + let mut ts: Option> = None; + // iterate through each key, and assign values that exist + for (key, val) in filter { + // ids + if key == "ids" { + let raw_ids: Option> = Deserialize::deserialize(val).ok(); + if let Some(a) = raw_ids.as_ref() { + if a.contains(&empty_string) { + return Err(serde::de::Error::invalid_type( + Unexpected::Other("prefix matches must not be empty strings"), + &"a json object", + )); + } + } + rf.ids = raw_ids; + } else if key == "kinds" { + rf.kinds = Deserialize::deserialize(val).ok(); + } else if key == "since" { + rf.since = Deserialize::deserialize(val).ok(); + } else if key == "until" { + rf.until = Deserialize::deserialize(val).ok(); + } else if key == "limit" { + rf.limit = Deserialize::deserialize(val).ok(); + } else if key == "authors" { + let raw_authors: Option> = Deserialize::deserialize(val).ok(); + if let Some(a) = raw_authors.as_ref() { + if a.contains(&empty_string) { + return Err(serde::de::Error::invalid_type( + Unexpected::Other("prefix matches must not be empty strings"), + &"a json object", + )); + } + } + rf.authors = raw_authors; + } else if key.starts_with('#') && key.len() > 1 && key.len() < 4 && val.is_array() { + if ts.is_none() { + // Initialize the tag if necessary + ts = Some(HashMap::new()); + } + if let Some(m) = ts.as_mut() { + let tag_vals: Option> = Deserialize::deserialize(val).ok(); + if let Some(v) = tag_vals { + let hs = v.into_iter().collect::>(); + let hs_op = match key.len() { + 2 => Some(TagOperand::Or(hs)), + 3 => { + if key.chars().nth(2).unwrap() == '&' { + Some(TagOperand::And(hs)) + } else { + None + } + } + _ => None, + }; + if let Some(hs_some) = hs_op { + m.insert(key.chars().nth(1).unwrap(), hs_some); + } + } + }; + } + } + rf.tags = ts; + Ok(rf) + } +} + +impl<'de> Deserialize<'de> for Subscription { + /// Custom deserializer for subscriptions, which have a more + /// complex structure than the other message types. + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut v: Value = Deserialize::deserialize(deserializer)?; + // this should be a 3-or-more element array. + // verify the first element is a String, REQ + // get the subscription from the second element. + // convert each of the remaining objects into filters + + // check for array + let va = v + .as_array_mut() + .ok_or_else(|| serde::de::Error::custom("not array"))?; + + // check length + if va.len() < 3 { + return Err(serde::de::Error::custom("not enough fields")); + } + let mut i = va.iter_mut(); + // get command ("REQ") and ensure it is a string + let req_cmd_str: serde_json::Value = i.next().unwrap().take(); + let req = req_cmd_str + .as_str() + .ok_or_else(|| serde::de::Error::custom("first element of request was not a string"))?; + if req != "REQ" { + return Err(serde::de::Error::custom("missing REQ command")); + } + + // ensure sub id is a string + let sub_id_str: serde_json::Value = i.next().unwrap().take(); + let sub_id = sub_id_str + .as_str() + .ok_or_else(|| serde::de::Error::custom("missing subscription id"))?; + + let mut filters = vec![]; + for fv in i { + let f: ReqFilter = serde_json::from_value(fv.take()) + .map_err(|_| serde::de::Error::custom("could not parse filter"))?; + // create indexes + filters.push(f); + } + filters.dedup(); + Ok(Subscription { + id: sub_id.to_owned(), + filters, + }) + } +} + +impl Subscription { + /// Get a copy of the subscription identifier. + #[must_use] + pub fn get_id(&self) -> String { + self.id.clone() + } + + /// Determine if any filter is requesting historical (database) + /// queries. If every filter has limit:0, we do not need to query the DB. + #[must_use] + pub fn needs_historical_events(&self) -> bool { + self.filters.iter().any(|f| f.limit != Some(0)) + } + + /// Determine if this subscription matches a given [`Event`]. Any + /// individual filter match is sufficient. + #[must_use] + pub fn interested_in_event(&self, event: &Event) -> bool { + for f in &self.filters { + if f.interested_in_event(event) { + return true; + } + } + false + } + + /// Is this subscription defined as a scraper query + pub fn is_scraper(&self) -> bool { + for f in &self.filters { + let mut precision = 0; + if f.ids.is_some() { + precision += 2; + } + if f.authors.is_some() { + precision += 1; + } + if f.kinds.is_some() { + precision += 1; + } + if f.tags.is_some() { + precision += 1; + } + if precision < 2 { + return true; + } + } + false + } +} + +fn prefix_match(prefixes: &[String], target: &str) -> bool { + for prefix in prefixes { + if target.starts_with(prefix) { + return true; + } + } + // none matched + false +} + +impl ReqFilter { + fn ids_match(&self, event: &Event) -> bool { + self.ids + .as_ref() + .map_or(true, |vs| prefix_match(vs, &event.id)) + } + + fn authors_match(&self, event: &Event) -> bool { + self.authors + .as_ref() + .map_or(true, |vs| prefix_match(vs, &event.pubkey)) + } + + fn delegated_authors_match(&self, event: &Event) -> bool { + if let Some(delegated_pubkey) = &event.delegated_by { + self.authors + .as_ref() + .map_or(true, |vs| prefix_match(vs, delegated_pubkey)) + } else { + false + } + } + + fn tag_match(&self, event: &Event) -> bool { + // get the hashset from the filter. + if let Some(map) = &self.tags { + for (key, val) in map.iter() { + let tag_match = event.generic_tag_val_intersect(*key, val); + // if there is no match for this tag, the match fails. + if !tag_match { + return false; + } + // if there was a match, we move on to the next one. + } + } + // if the tag map is empty, the match succeeds (there was no filter) + true + } + + /// Check if this filter either matches, or does not care about the kind. + fn kind_match(&self, kind: u64) -> bool { + self.kinds.as_ref().map_or(true, |ks| ks.contains(&kind)) + } + + /// Determine if all populated fields in this filter match the provided event. + #[must_use] + pub fn interested_in_event(&self, event: &Event) -> bool { + // self.id.as_ref().map(|v| v == &event.id).unwrap_or(true) + self.ids_match(event) + && self.since.map_or(true, |t| event.created_at >= t) + && self.until.map_or(true, |t| event.created_at <= t) + && self.kind_match(event.kind) + && (self.authors_match(event) || self.delegated_authors_match(event)) + && self.tag_match(event) + && !self.force_no_match + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_request_parse() -> Result<()> { + let raw_json = "[\"REQ\",\"some-id\",{}]"; + let s: Subscription = serde_json::from_str(raw_json)?; + assert_eq!(s.id, "some-id"); + assert_eq!(s.filters.len(), 1); + assert_eq!(s.filters.first().unwrap().authors, None); + Ok(()) + } + + #[test] + fn incorrect_header() { + let raw_json = "[\"REQUEST\",\"some-id\",\"{}\"]"; + assert!(serde_json::from_str::(raw_json).is_err()); + } + + #[test] + fn req_missing_filters() { + let raw_json = "[\"REQ\",\"some-id\"]"; + assert!(serde_json::from_str::(raw_json).is_err()); + } + + #[test] + fn req_empty_authors_prefix() { + let raw_json = "[\"REQ\",\"some-id\",{\"authors\": [\"\"]}]"; + assert!(serde_json::from_str::(raw_json).is_err()); + } + + #[test] + fn req_empty_ids_prefix() { + let raw_json = "[\"REQ\",\"some-id\",{\"ids\": [\"\"]}]"; + assert!(serde_json::from_str::(raw_json).is_err()); + } + + #[test] + fn req_empty_ids_prefix_mixed() { + let raw_json = "[\"REQ\",\"some-id\",{\"ids\": [\"\",\"aaa\"]}]"; + assert!(serde_json::from_str::(raw_json).is_err()); + } + + #[test] + fn legacy_filter() { + // legacy field in filter + let raw_json = "[\"REQ\",\"some-id\",{\"kind\": 3}]"; + assert!(serde_json::from_str::(raw_json).is_ok()); + } + + #[test] + fn dupe_filter() -> Result<()> { + let raw_json = r#"["REQ","some-id",{"kinds": [1984]}, {"kinds": [1984]}]"#; + let s: Subscription = serde_json::from_str(raw_json)?; + assert_eq!(s.filters.len(), 1); + Ok(()) + } + + #[test] + fn dupe_filter_many() -> Result<()> { + // duplicate filters in different order + let raw_json = r#"["REQ","some-id",{"kinds":[1984]},{"kinds":[1984]},{"kinds":[1984]},{"kinds":[1984]}]"#; + let s: Subscription = serde_json::from_str(raw_json)?; + assert_eq!(s.filters.len(), 1); + Ok(()) + } + + #[test] + fn author_filter() -> Result<()> { + let raw_json = r#"["REQ","some-id",{"authors": ["test-author-id"]}]"#; + let s: Subscription = serde_json::from_str(raw_json)?; + assert_eq!(s.id, "some-id"); + assert_eq!(s.filters.len(), 1); + let first_filter = s.filters.first().unwrap(); + assert_eq!( + first_filter.authors, + Some(vec!("test-author-id".to_owned())) + ); + Ok(()) + } + + #[test] + fn interest_author_prefix_match() -> Result<()> { + // subscription with a filter for ID + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors": ["abc"]}]"#)?; + let e = Event { + id: "foo".to_owned(), + pubkey: "abcd".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn interest_id_prefix_match() -> Result<()> { + // subscription with a filter for ID + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"]}]"#)?; + let e = Event { + id: "abcd".to_owned(), + pubkey: "".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn interest_id_nomatch() -> Result<()> { + // subscription with a filter for ID + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"ids": ["xyz"]}]"#)?; + let e = Event { + id: "abcde".to_owned(), + pubkey: "".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(!s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn interest_until() -> Result<()> { + // subscription with a filter for ID and time + let s: Subscription = + serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "until": 1000}]"#)?; + let e = Event { + id: "abc".to_owned(), + pubkey: "".to_owned(), + delegated_by: None, + created_at: 50, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn interest_range() -> Result<()> { + // subscription with a filter for ID and time + let s_in: Subscription = + serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 100, "until": 200}]"#)?; + let s_before: Subscription = + serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 100, "until": 140}]"#)?; + let s_after: Subscription = + serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 160, "until": 200}]"#)?; + let e = Event { + id: "abc".to_owned(), + pubkey: "".to_owned(), + delegated_by: None, + created_at: 150, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s_in.interested_in_event(&e)); + assert!(!s_before.interested_in_event(&e)); + assert!(!s_after.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn interest_time_and_id() -> Result<()> { + // subscription with a filter for ID and time + let s: Subscription = + serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 1000}]"#)?; + let e = Event { + id: "abc".to_owned(), + pubkey: "".to_owned(), + delegated_by: None, + created_at: 50, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(!s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn interest_time_and_id2() -> Result<()> { + // subscription with a filter for ID and time + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc", "since": 1000}]"#)?; + let e = Event { + id: "abc".to_owned(), + pubkey: "".to_owned(), + delegated_by: None, + created_at: 1001, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn interest_id() -> Result<()> { + // subscription with a filter for ID + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc"}]"#)?; + let e = Event { + id: "abc".to_owned(), + pubkey: "".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn authors_single() -> Result<()> { + // subscription with a filter for ID + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc"]}]"#)?; + let e = Event { + id: "123".to_owned(), + pubkey: "abc".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn authors_multi_pubkey() -> Result<()> { + // check for any of a set of authors, against the pubkey + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc", "bcd"]}]"#)?; + let e = Event { + id: "123".to_owned(), + pubkey: "bcd".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn authors_multi_no_match() -> Result<()> { + // check for any of a set of authors, against the pubkey + let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc", "bcd"]}]"#)?; + let e = Event { + id: "123".to_owned(), + pubkey: "xyz".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: Vec::new(), + content: "".to_owned(), + sig: "".to_owned(), + tagidx: None, + }; + assert!(!s.interested_in_event(&e)); + Ok(()) + } + + #[test] + fn serialize_filter() -> Result<()> { + let s: Subscription = serde_json::from_str( + r##"["REQ","xyz",{"authors":["abc", "bcd"], "since": 10, "until": 20, "limit":100, "#e": ["foo", "bar"], "#d": ["test"]}]"##, + )?; + let f = s.filters.first(); + let serialized = serde_json::to_string(&f)?; + let serialized_wrapped = format!(r##"["REQ", "xyz",{}]"##, serialized); + let parsed: Subscription = serde_json::from_str(&serialized_wrapped)?; + let parsed_filter = parsed.filters.first(); + if let Some(pf) = parsed_filter { + assert_eq!(pf.since, Some(10)); + assert_eq!(pf.until, Some(20)); + assert_eq!(pf.limit, Some(100)); + } else { + assert!(false, "filter could not be parsed"); + } + Ok(()) + } + + #[test] + fn is_scraper() -> Result<()> { + assert!(serde_json::from_str::( + r#"["REQ","some-id",{"kinds": [1984],"since": 123,"limit":1}]"# + )? + .is_scraper()); + assert!(serde_json::from_str::( + r#"["REQ","some-id",{"kinds": [1984]},{"kinds": [1984],"authors":["aaaa"]}]"# + )? + .is_scraper()); + assert!(!serde_json::from_str::( + r#"["REQ","some-id",{"kinds": [1984],"authors":["aaaa"]}]"# + )? + .is_scraper()); + assert!( + !serde_json::from_str::(r#"["REQ","some-id",{"ids": ["aaaa"]}]"#)? + .is_scraper() + ); + assert!(!serde_json::from_str::( + r##"["REQ","some-id",{"#p": ["aaaa"],"kinds":[1,4]}]"## + )? + .is_scraper()); + Ok(()) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..eae6846 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,72 @@ +//! Common utility functions +use bech32::FromBase32; +use std::time::SystemTime; +use url::Url; + +/// Seconds since 1970. +#[must_use] +pub fn unix_time() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|x| x.as_secs()) + .unwrap_or(0) +} + +/// Check if a string contains only hex characters. +#[must_use] +pub fn is_hex(s: &str) -> bool { + s.chars().all(|x| char::is_ascii_hexdigit(&x)) +} + +/// Check if string is a nip19 string +pub fn is_nip19(s: &str) -> bool { + s.starts_with("npub") || s.starts_with("note") +} + +pub fn nip19_to_hex(s: &str) -> Result { + let (_hrp, data, _checksum) = bech32::decode(s)?; + let data = Vec::::from_base32(&data)?; + Ok(hex::encode(data)) +} + +/// Check if a string contains only lower-case hex chars. +#[must_use] +pub fn is_lower_hex(s: &str) -> bool { + s.chars().all(|x| { + (char::is_ascii_lowercase(&x) || char::is_ascii_digit(&x)) && char::is_ascii_hexdigit(&x) + }) +} + +pub fn host_str(url: &str) -> Option { + Url::parse(url) + .ok() + .and_then(|u| u.host_str().map(|s| s.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lower_hex() { + let hexstr = "abcd0123"; + assert!(is_lower_hex(hexstr)); + } + + #[test] + fn nip19() { + let hexkey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; + let nip19key = "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"; + assert!(!is_nip19(hexkey)); + assert!(is_nip19(nip19key)); + } + + #[test] + fn nip19_hex() { + let nip19key = "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"; + let expected = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; + let got = nip19_to_hex(nip19key).unwrap(); + + assert_eq!(expected, got); + } +} diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..16e15d9 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,10 @@ +#[cfg(test)] +mod tests { + use floonet_rs::cli::CLIArgs; + + #[test] + fn cli_tests() { + use clap::CommandFactory; + CLIArgs::command().debug_assert(); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..a3a4f0c --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,115 @@ +use anyhow::{anyhow, Result}; +use floonet_rs::config; +use floonet_rs::server::start_server; +//use http::{Request, Response}; +use hyper::{Client, StatusCode, Uri}; +use std::net::TcpListener; +use std::sync::atomic::{AtomicU16, Ordering}; +use std::sync::mpsc as syncmpsc; +use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender}; +use std::thread; +use std::thread::JoinHandle; +use std::time::Duration; +use tracing::{debug, info}; + +pub struct Relay { + pub port: u16, + pub handle: JoinHandle<()>, + pub shutdown_tx: MpscSender<()>, +} + +pub fn start_relay() -> Result { + start_relay_with(|_| {}) +} + +/// Start a relay, letting the caller adjust settings after the defaults +/// (port and loopback bind are already set; the closure may read +/// `settings.network.port`). +pub fn start_relay_with(configure: impl FnOnce(&mut config::Settings)) -> Result { + // setup tracing + let _trace_sub = tracing_subscriber::fmt::try_init(); + info!("Starting a new relay"); + // replace default settings + let mut settings = config::Settings::default(); + // identify open port + info!("Checking for address..."); + let port = get_available_port().unwrap(); + info!("Found open port: {}", port); + // bind to local interface only + settings.network.address = "127.0.0.1".to_owned(); + settings.network.port = port; + // create an in-memory DB with multiple readers + settings.database.in_memory = true; + settings.database.min_conn = 4; + settings.database.max_conn = 8; + configure(&mut settings); + let (shutdown_tx, shutdown_rx): (MpscSender<()>, MpscReceiver<()>) = syncmpsc::channel(); + let handle = thread::spawn(move || { + // server will block the thread it is run on. + let _ = start_server(&settings, shutdown_rx); + }); + // how do we know the relay has finished starting up? + Ok(Relay { + port, + handle, + shutdown_tx, + }) +} + +// check if the server is healthy via HTTP request +async fn server_ready(relay: &Relay) -> Result { + let uri: String = format!("http://127.0.0.1:{}/", relay.port); + let client = Client::new(); + let uri: Uri = uri.parse().unwrap(); + let res = client.get(uri).await?; + Ok(res.status() == StatusCode::OK) +} + +pub async fn wait_for_healthy_relay(relay: &Relay) -> Result<()> { + // TODO: maximum time to wait for server to become healthy. + // give it a little time to start up before we start polling + tokio::time::sleep(Duration::from_millis(10)).await; + loop { + let server_check = server_ready(relay).await; + match server_check { + Ok(true) => { + // server responded with 200-OK. + break; + } + Ok(false) => { + // server responded with an error, we're done. + return Err(anyhow!("Got non-200-OK from relay")); + } + Err(_) => { + // server is not yet ready, probably connection refused... + debug!("Relay not ready, will try again..."); + tokio::time::sleep(Duration::from_millis(10)).await; + } + } + } + info!("relay is ready"); + Ok(()) + // simple message sent to web browsers + //let mut request = Request::builder() + // .uri("https://www.rust-lang.org/") + // .header("User-Agent", "my-awesome-agent/1.0"); +} + +// from https://elliotekj.com/posts/2017/07/25/find-available-tcp-port-rust/ +// This needed some modification; if multiple tasks all ask for open ports, they will tend to get the same one. +// instead we should try to try these incrementally/globally. + +static PORT_COUNTER: AtomicU16 = AtomicU16::new(4030); + +fn get_available_port() -> Option { + let startsearch = PORT_COUNTER.fetch_add(10, Ordering::SeqCst); + if startsearch >= 20000 { + // wrap around + PORT_COUNTER.store(4030, Ordering::Relaxed); + } + (startsearch..20000).find(|port| port_is_available(*port)) +} +pub fn port_is_available(port: u16) -> bool { + info!("checking on port {}", port); + TcpListener::bind(("127.0.0.1", port)).is_ok() +} diff --git a/tests/conn.rs b/tests/conn.rs new file mode 100644 index 0000000..ea3ec51 --- /dev/null +++ b/tests/conn.rs @@ -0,0 +1,356 @@ +#[cfg(test)] +mod tests { + use bitcoin_hashes::hex::ToHex; + use bitcoin_hashes::sha256; + use bitcoin_hashes::Hash; + use secp256k1::rand; + use secp256k1::{KeyPair, Secp256k1, XOnlyPublicKey}; + + use floonet_rs::conn::ClientConn; + use floonet_rs::error::Error; + use floonet_rs::event::Event; + use floonet_rs::utils::unix_time; + + const RELAY: &str = "wss://nostr.example.com/"; + + #[test] + fn test_generate_auth_challenge() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let last_auth_challenge = client_conn.auth_challenge().cloned(); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_ne!( + client_conn.auth_challenge().unwrap(), + &last_auth_challenge.unwrap() + ); + assert_eq!(client_conn.auth_pubkey(), None); + } + + #[test] + fn test_authenticate_with_valid_event() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap(); + let event = auth_event(challenge); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Ok(()))); + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey)); + } + + #[test] + fn test_fail_to_authenticate_in_invalid_state() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let event = auth_event(&"challenge".into()); + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_authenticate_when_already_authenticated() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap().clone(); + + let event = auth_event(&challenge); + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Ok(()))); + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey)); + + let event1 = auth_event(&challenge); + let result1 = client_conn.authenticate(&event1, RELAY); + + assert!(matches!(result1, Ok(()))); + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey)); + assert_ne!(client_conn.auth_pubkey(), Some(&event1.pubkey)); + } + + #[test] + fn test_fail_to_authenticate_with_invalid_event() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap(); + let mut event = auth_event(challenge); + event.sig = event.sig.chars().rev().collect::(); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_with_invalid_event_kind() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap(); + let event = auth_event_with_kind(challenge, 9999999999999999); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_with_expired_timestamp() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap(); + let event = auth_event_with_created_at(challenge, unix_time() - 1200); // 20 minutes + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_with_future_timestamp() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap(); + let event = auth_event_with_created_at(challenge, unix_time() + 1200); // 20 minutes + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_without_tags() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let event = auth_event_without_tags(); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_without_challenge() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let event = auth_event_without_challenge(); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_without_relay() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap(); + let event = auth_event_without_relay(challenge); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_with_invalid_challenge() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let event = auth_event(&"invalid challenge".into()); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + #[test] + fn test_fail_to_authenticate_with_invalid_relay() { + let mut client_conn = ClientConn::new("127.0.0.1".into()); + + assert_eq!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + client_conn.generate_auth_challenge(); + + assert_ne!(client_conn.auth_challenge(), None); + assert_eq!(client_conn.auth_pubkey(), None); + + let challenge = client_conn.auth_challenge().unwrap(); + let event = auth_event_with_relay(challenge, &"xyz".into()); + + let result = client_conn.authenticate(&event, RELAY); + + assert!(matches!(result, Err(Error::AuthFailure))); + } + + fn auth_event(challenge: &String) -> Event { + create_auth_event(Some(challenge), Some(&RELAY.into()), 22242, unix_time()) + } + + fn auth_event_with_kind(challenge: &String, kind: u64) -> Event { + create_auth_event(Some(challenge), Some(&RELAY.into()), kind, unix_time()) + } + + fn auth_event_with_created_at(challenge: &String, created_at: u64) -> Event { + create_auth_event(Some(challenge), Some(&RELAY.into()), 22242, created_at) + } + + fn auth_event_without_challenge() -> Event { + create_auth_event(None, Some(&RELAY.into()), 22242, unix_time()) + } + + fn auth_event_without_relay(challenge: &String) -> Event { + create_auth_event(Some(challenge), None, 22242, unix_time()) + } + + fn auth_event_without_tags() -> Event { + create_auth_event(None, None, 22242, unix_time()) + } + + fn auth_event_with_relay(challenge: &String, relay: &String) -> Event { + create_auth_event(Some(challenge), Some(relay), 22242, unix_time()) + } + + fn create_auth_event( + challenge: Option<&String>, + relay: Option<&String>, + kind: u64, + created_at: u64, + ) -> Event { + let secp = Secp256k1::new(); + let key_pair = KeyPair::new(&secp, &mut rand::thread_rng()); + let public_key = XOnlyPublicKey::from_keypair(&key_pair); + + let mut tags: Vec> = vec![]; + + if let Some(c) = challenge { + let tag = vec!["challenge".into(), c.into()]; + tags.push(tag); + } + + if let Some(r) = relay { + let tag = vec!["relay".into(), r.into()]; + tags.push(tag); + } + + let mut event = Event { + id: "0".to_owned(), + pubkey: public_key.to_hex(), + delegated_by: None, + created_at, + kind, + tags, + content: "".to_owned(), + sig: "0".to_owned(), + tagidx: None, + }; + + let c = event.to_canonical().unwrap(); + let digest: sha256::Hash = sha256::Hash::hash(c.as_bytes()); + + let msg = secp256k1::Message::from_slice(digest.as_ref()).unwrap(); + let sig = secp.sign_schnorr(&msg, &key_pair); + + event.id = format!("{digest:x}"); + event.sig = sig.to_hex(); + + event + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..dfc614b --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,80 @@ +use anyhow::Result; +use futures::SinkExt; +use futures::StreamExt; +use std::thread; +use std::time::Duration; +use tokio_tungstenite::connect_async; +use tracing::info; +mod common; + +#[tokio::test] +async fn start_and_stop() -> Result<()> { + // this will be the common pattern for acquiring a new relay: + // start a fresh relay, on a port to-be-provided back to us: + let relay = common::start_relay()?; + // wait for the relay's webserver to start up and deliver a page: + common::wait_for_healthy_relay(&relay).await?; + let port = relay.port; + // just make sure we can startup and shut down. + // if we send a shutdown message before the server is listening, + // we will get a SendError. Keep sending until someone is + // listening. + loop { + let shutdown_res = relay.shutdown_tx.send(()); + match shutdown_res { + Ok(()) => { + break; + } + Err(_) => { + thread::sleep(Duration::from_millis(100)); + } + } + } + // wait for relay to shutdown + let thread_join = relay.handle.join(); + assert!(thread_join.is_ok()); + // assert that port is now available. + assert!(common::port_is_available(port)); + Ok(()) +} + +#[tokio::test] +async fn relay_home_page() -> Result<()> { + // get a relay and wait for startup... + let relay = common::start_relay()?; + common::wait_for_healthy_relay(&relay).await?; + // tell relay to shutdown + let _res = relay.shutdown_tx.send(()); + Ok(()) +} + +//#[tokio::test] +// Still inwork +#[allow(dead_code)] +async fn publish_test() -> Result<()> { + // get a relay and wait for startup + let relay = common::start_relay()?; + common::wait_for_healthy_relay(&relay).await?; + // open a non-secure websocket connection. + let (mut ws, _res) = connect_async(format!("ws://localhost:{}", relay.port)).await?; + // send a simple pre-made message + let simple_event = r#"["EVENT", {"content": "hello world","created_at": 1691239763, + "id":"f3ce6798d70e358213ebbeba4886bbdfacf1ecfd4f65ee5323ef5f404de32b86", + "kind": 1, + "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "sig": "30ca29e8581eeee75bf838171dec818af5e6de2b74f5337de940f5cc91186534c0b20d6cf7ad1043a2c51dbd60b979447720a471d346322103c83f6cb66e4e98", + "tags": []}]"#; + ws.send(simple_event.into()).await?; + // get response from server, confirm it is an array with first element "OK" + let event_confirm = ws.next().await; + ws.close(None).await?; + info!("event confirmed: {:?}", event_confirm); + // open a new connection, and wait for some time to get the event. + let (mut sub_ws, _res) = connect_async(format!("ws://localhost:{}", relay.port)).await?; + let event_sub = r#"["REQ", "simple", {}]"#; + sub_ws.send(event_sub.into()).await?; + // read from subscription + let _ws_next = sub_ws.next().await; + let _res = relay.shutdown_tx.send(()); + Ok(()) +} diff --git a/tests/name_authority.rs b/tests/name_authority.rs new file mode 100644 index 0000000..5d8bb99 --- /dev/null +++ b/tests/name_authority.rs @@ -0,0 +1,377 @@ +//! End-to-end tests for the built-in name authority: NIP-98 registration +//! round trip, NIP-05 resolution, reverse lookup, one-name-per-key, +//! reserved names, release + cooldown, plus the paid-name flow against a +//! fake GoblinPay server (402 until the invoice reports paid). Also +//! verifies the NIP-11 document stays payment-free. + +use anyhow::Result; +use base64::Engine; +use bitcoin_hashes::{sha256, Hash}; +use floonet_rs::event::Event; +use floonet_rs::utils::unix_time; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Client, Request, Server, StatusCode}; +use secp256k1::{KeyPair, Secp256k1, XOnlyPublicKey}; +use serde_json::{json, Value}; +use std::convert::Infallible; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +mod common; + +/// A test identity that can sign NIP-98 auth events. +struct Signer { + secp: Secp256k1, + keypair: KeyPair, + pub pubkey_hex: String, +} + +impl Signer { + fn new(seed: u8) -> Signer { + let secp = Secp256k1::new(); + let keypair = KeyPair::from_seckey_slice(&secp, &[seed; 32]).unwrap(); + let pubkey_hex = XOnlyPublicKey::from_keypair(&keypair).to_string(); + Signer { + secp, + keypair, + pubkey_hex, + } + } + + /// `Authorization: Nostr ` header for method+url over body. + fn nip98(&self, url: &str, method: &str, body: &[u8]) -> String { + let mut tags: Vec> = vec![ + vec!["u".to_string(), url.to_string()], + vec!["method".to_string(), method.to_string()], + // A nonce keeps every auth event id unique, so back-to-back + // requests in the same second are not misread as replays. + vec!["nonce".to_string(), format!("{:x}", rand_u64())], + ]; + if !body.is_empty() { + let digest: sha256::Hash = sha256::Hash::hash(body); + tags.push(vec!["payload".to_string(), format!("{digest:x}")]); + } + let mut event = Event { + id: "0".to_owned(), + pubkey: self.pubkey_hex.clone(), + delegated_by: None, + created_at: unix_time(), + kind: 27235, + tags, + content: String::new(), + sig: "0".to_owned(), + tagidx: None, + }; + let canonical = event.to_canonical().unwrap(); + let digest: sha256::Hash = sha256::Hash::hash(canonical.as_bytes()); + let msg = secp256k1::Message::from_slice(digest.as_ref()).unwrap(); + event.id = format!("{digest:x}"); + event.sig = self.secp.sign_schnorr(&msg, &self.keypair).to_string(); + let json = serde_json::to_string(&event).unwrap(); + format!( + "Nostr {}", + base64::engine::general_purpose::STANDARD.encode(json) + ) + } +} + +fn rand_u64() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + // Cheap uniqueness for test nonces. + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64 +} + +async fn get_json(url: &str) -> Result<(StatusCode, Value)> { + let client = Client::new(); + let res = client.get(url.parse()?).await?; + let status = res.status(); + let bytes = hyper::body::to_bytes(res.into_body()).await?; + Ok((status, serde_json::from_slice(&bytes)?)) +} + +async fn register( + base: &str, + signer: Option<&Signer>, + name: &str, + pubkey: &str, +) -> Result<(StatusCode, Value)> { + let body = json!({"name": name, "pubkey": pubkey}).to_string(); + let mut builder = Request::builder() + .method("POST") + .uri(format!("{base}/api/v1/register")) + .header("Content-Type", "application/json"); + if let Some(signer) = signer { + builder = builder.header( + "Authorization", + signer.nip98(&format!("{base}/api/v1/register"), "POST", body.as_bytes()), + ); + } + let res = Client::new() + .request(builder.body(Body::from(body))?) + .await?; + let status = res.status(); + let bytes = hyper::body::to_bytes(res.into_body()).await?; + Ok((status, serde_json::from_slice(&bytes)?)) +} + +async fn unregister(base: &str, signer: &Signer, name: &str) -> Result<(StatusCode, Value)> { + let url = format!("{base}/api/v1/register/{name}"); + let req = Request::builder() + .method("DELETE") + .uri(&url) + .header("Authorization", signer.nip98(&url, "DELETE", &[])) + .body(Body::empty())?; + let res = Client::new().request(req).await?; + let status = res.status(); + let bytes = hyper::body::to_bytes(res.into_body()).await?; + Ok((status, serde_json::from_slice(&bytes)?)) +} + +/// Fresh file-backed data directory (exercises the v19 migration). +fn temp_data_dir(tag: &str) -> String { + let dir = std::env::temp_dir().join(format!("floonet-rs-test-{tag}-{}", rand_u64())); + std::fs::create_dir_all(&dir).unwrap(); + dir.to_string_lossy().into_owned() +} + +fn authority_relay(data_dir: &str) -> Result { + let data_dir = data_dir.to_owned(); + common::start_relay_with(move |settings| { + settings.database.in_memory = false; + settings.database.data_directory = data_dir; + settings.name_authority.enabled = true; + settings.name_authority.domain = "names.example".to_owned(); + settings.name_authority.base_url = format!("http://127.0.0.1:{}", settings.network.port); + settings.name_authority.name_change_cooldown_secs = 600; + }) +} + +#[tokio::test] +async fn name_authority_round_trip() -> Result<()> { + let data_dir = temp_data_dir("authority"); + let relay = authority_relay(&data_dir)?; + common::wait_for_healthy_relay(&relay).await?; + let base = format!("http://127.0.0.1:{}", relay.port); + let alice = Signer::new(11); + let bob = Signer::new(22); + + // Health. + let res = Client::new() + .get(format!("{base}/api/v1/health").parse()?) + .await?; + assert_eq!(res.status(), StatusCode::OK); + + // Availability before any claim. + let (status, body) = get_json(&format!("{base}/api/v1/name/ada")).await?; + assert_eq!(status, StatusCode::OK); + assert_eq!(body["available"], json!(true)); + + // Unauthenticated register is refused. + let (status, _) = register(&base, None, "ada", &alice.pubkey_hex).await?; + assert_eq!(status, StatusCode::UNAUTHORIZED); + + // NIP-98 authenticated register succeeds. + let (status, body) = register(&base, Some(&alice), "ada", &alice.pubkey_hex).await?; + assert_eq!(status, StatusCode::CREATED, "{body}"); + assert_eq!(body["nip05"], json!("ada@names.example")); + + // NIP-05 resolution. + let (status, body) = + get_json(&format!("{base}/.well-known/nostr.json?name=ada")).await?; + assert_eq!(status, StatusCode::OK); + assert_eq!(body["names"]["ada"], json!(alice.pubkey_hex)); + + // Reverse lookup. + let (status, body) = + get_json(&format!("{base}/api/v1/by-pubkey/{}", alice.pubkey_hex)).await?; + assert_eq!(status, StatusCode::OK); + assert_eq!(body["name"], json!("ada")); + + // Same name, different key: conflict. + let (status, _) = register(&base, Some(&bob), "ada", &bob.pubkey_hex).await?; + assert_eq!(status, StatusCode::CONFLICT); + + // One active name per key. + let (status, body) = register(&base, Some(&alice), "ada2", &alice.pubkey_hex).await?; + assert_eq!(status, StatusCode::CONFLICT, "{body}"); + + // Reserved and look-alike names are refused. + let (status, _) = register(&base, Some(&bob), "admin", &bob.pubkey_hex).await?; + assert_eq!(status, StatusCode::FORBIDDEN); + let (status, _) = register(&base, Some(&bob), "supp0rt", &bob.pubkey_hex).await?; + assert_eq!(status, StatusCode::FORBIDDEN); + // The operator's own domain label is reserved too. + let (status, _) = register(&base, Some(&bob), "names", &bob.pubkey_hex).await?; + assert_eq!(status, StatusCode::FORBIDDEN); + + // Release, then the release-armed cooldown blocks a fresh claim. + let (status, body) = unregister(&base, &alice, "ada").await?; + assert_eq!(status, StatusCode::OK, "{body}"); + let (status, body) = register(&base, Some(&alice), "lovelace", &alice.pubkey_hex).await?; + assert_eq!(status, StatusCode::TOO_MANY_REQUESTS, "{body}"); + assert_eq!(body["error"], json!("name_change_cooldown")); + + // The released name resolves to nobody. + let (_, body) = get_json(&format!("{base}/.well-known/nostr.json?name=ada")).await?; + assert_eq!(body["names"], json!({})); + + let _res = relay.shutdown_tx.send(()); + std::fs::remove_dir_all(&data_dir).ok(); + Ok(()) +} + +#[tokio::test] +async fn nip11_and_landing_are_payment_free() -> Result<()> { + let relay = common::start_relay()?; + common::wait_for_healthy_relay(&relay).await?; + let base = format!("http://127.0.0.1:{}", relay.port); + + // NIP-11 document: neutral Floonet identity, zero payment wording. + let req = Request::builder() + .method("GET") + .uri(&base) + .header("Accept", "application/nostr+json") + .body(Body::empty())?; + let res = Client::new().request(req).await?; + assert_eq!(res.status(), StatusCode::OK); + let bytes = hyper::body::to_bytes(res.into_body()).await?; + let text = String::from_utf8(bytes.to_vec())?; + let info: Value = serde_json::from_str(&text)?; + assert_eq!(info["name"], json!("floonet-rs-relay")); + for banned in ["payment", "fees", "sats", "msats", "invoice", "slatepack"] { + assert!( + !text.to_lowercase().contains(banned), + "NIP-11 must not mention `{banned}`: {text}" + ); + } + + // Landing page shows the Floonet branding and references the logo. + let res = Client::new().get(base.parse()?).await?; + let bytes = hyper::body::to_bytes(res.into_body()).await?; + let html = String::from_utf8(bytes.to_vec())?; + assert!(html.contains("/logo.svg"), "landing must show the logo"); + assert!( + !html.to_lowercase().contains("payment"), + "landing must not mention payments" + ); + + // The logo itself is served. + let res = Client::new().get(format!("{base}/logo.svg").parse()?).await?; + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get("content-type").unwrap(), + "image/svg+xml" + ); + + let _res = relay.shutdown_tx.send(()); + Ok(()) +} + +/// Minimal fake GoblinPay: POST /invoice and GET /invoice/{id}, with a +/// shared "paid" flag the test flips. +async fn fake_goblinpay(paid: Arc) -> Result { + let make_svc = make_service_fn(move |_conn| { + let paid = paid.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |req: Request| { + let paid = paid.clone(); + async move { + let authed = req + .headers() + .get("Authorization") + .and_then(|v| v.to_str().ok()) + == Some("Bearer test-gp-token"); + let status = if paid.load(Ordering::SeqCst) { + "paid" + } else { + "open" + }; + let response = if !authed { + hyper::Response::builder() + .status(401) + .body(Body::from(r#"{"error":"unauthorized"}"#)) + .unwrap() + } else { + hyper::Response::builder() + .status(200) + .header("Content-Type", "application/json") + .body(Body::from( + json!({ + "invoice_id": "inv-test-1", + "token": "tok1", + "pay_url": "http://pay.invalid/pay/tok1", + "status": status, + }) + .to_string(), + )) + .unwrap() + }; + Ok::<_, Infallible>(response) + } + })) + } + }); + let server = Server::bind(&"127.0.0.1:0".parse()?).serve(make_svc); + let addr = server.local_addr(); + tokio::spawn(async move { + let _ = server.await; + }); + Ok(format!("http://{addr}")) +} + +#[tokio::test] +async fn paid_names_require_confirmed_goblinpay_payment() -> Result<()> { + let paid = Arc::new(AtomicBool::new(false)); + let gp_url = fake_goblinpay(paid.clone()).await?; + + let data_dir = temp_data_dir("paid"); + let relay = { + let data_dir = data_dir.clone(); + common::start_relay_with(move |settings| { + settings.database.in_memory = false; + settings.database.data_directory = data_dir; + settings.name_authority.enabled = true; + settings.name_authority.domain = "names.example".to_owned(); + settings.name_authority.base_url = + format!("http://127.0.0.1:{}", settings.network.port); + settings.goblinpay.pay_mode = "name".to_owned(); + settings.goblinpay.url = gp_url; + settings.goblinpay.api_token = "test-gp-token".to_owned(); + settings.goblinpay.name_price_grin = 2.5; + })? + }; + common::wait_for_healthy_relay(&relay).await?; + let base = format!("http://127.0.0.1:{}", relay.port); + let alice = Signer::new(33); + + // Unpaid: register answers 402 with the GoblinPay pay page. + let (status, body) = register(&base, Some(&alice), "ada", &alice.pubkey_hex).await?; + assert_eq!(status, StatusCode::PAYMENT_REQUIRED, "{body}"); + assert_eq!(body["error"], json!("payment_required")); + assert_eq!(body["pay_url"], json!("http://pay.invalid/pay/tok1")); + assert_eq!(body["invoice_id"], json!("inv-test-1")); + assert_eq!(body["price_grin"], json!(2.5)); + assert_eq!(body["price_nanogrin"], json!(2_500_000_000u64)); + + // Still unpaid on retry: the same outstanding invoice comes back. + let (status, body) = register(&base, Some(&alice), "ada", &alice.pubkey_hex).await?; + assert_eq!(status, StatusCode::PAYMENT_REQUIRED, "{body}"); + assert_eq!(body["invoice_id"], json!("inv-test-1")); + + // Payment confirms on chain (GoblinPay now reports paid): claim works. + paid.store(true, Ordering::SeqCst); + let (status, body) = register(&base, Some(&alice), "ada", &alice.pubkey_hex).await?; + assert_eq!(status, StatusCode::CREATED, "{body}"); + assert_eq!(body["nip05"], json!("ada@names.example")); + + // And the name resolves. + let (_, body) = get_json(&format!("{base}/.well-known/nostr.json?name=ada")).await?; + assert_eq!(body["names"]["ada"], json!(alice.pubkey_hex)); + + let _res = relay.shutdown_tx.send(()); + std::fs::remove_dir_all(&data_dir).ok(); + Ok(()) +} diff --git a/tests/whitelist.rs b/tests/whitelist.rs new file mode 100644 index 0000000..c2fd741 --- /dev/null +++ b/tests/whitelist.rs @@ -0,0 +1,103 @@ +//! End-to-end tests for the Floonet default-deny kind whitelist: a real +//! relay, a real websocket, real signed events. An allowed kind gets +//! OK=true; a disallowed kind gets OK=false with a `blocked:` reason. + +use anyhow::Result; +use bitcoin_hashes::{sha256, Hash}; +use floonet_rs::event::Event; +use floonet_rs::utils::unix_time; +use futures::SinkExt; +use futures::StreamExt; +use secp256k1::{rand, KeyPair, Secp256k1, XOnlyPublicKey}; +use serde_json::Value; + +mod common; + +/// Build a signed event of `kind` and return (event_json, event_id). +fn signed_event(kind: u64, content: &str) -> (String, String) { + let secp = Secp256k1::new(); + let key_pair = KeyPair::new(&secp, &mut rand::thread_rng()); + let public_key = XOnlyPublicKey::from_keypair(&key_pair); + + let mut event = Event { + id: "0".to_owned(), + pubkey: public_key.to_string(), + delegated_by: None, + created_at: unix_time(), + kind, + tags: vec![], + content: content.to_owned(), + sig: "0".to_owned(), + tagidx: None, + }; + let canonical = event.to_canonical().unwrap(); + let digest: sha256::Hash = sha256::Hash::hash(canonical.as_bytes()); + let msg = secp256k1::Message::from_slice(digest.as_ref()).unwrap(); + let sig = secp.sign_schnorr(&msg, &key_pair); + event.id = format!("{digest:x}"); + event.sig = sig.to_string(); + let json = serde_json::to_string(&event).unwrap(); + (format!(r#"["EVENT",{json}]"#), event.id) +} + +/// Publish a message and return the relay's OK frame for that event id. +async fn publish_and_get_ok(port: u16, msg: &str, event_id: &str) -> Result { + let (mut ws, _res) = tokio_tungstenite::connect_async(format!("ws://127.0.0.1:{port}")).await?; + ws.send(msg.into()).await?; + // Read frames until the OK for our event id shows up. + while let Some(frame) = ws.next().await { + let frame = frame?; + if let Ok(text) = frame.into_text() { + if let Ok(value) = serde_json::from_str::(&text) { + if value.get(0).and_then(Value::as_str) == Some("OK") + && value.get(1).and_then(Value::as_str) == Some(event_id) + { + ws.close(None).await.ok(); + return Ok(value); + } + } + } + } + anyhow::bail!("no OK frame received for event {event_id}"); +} + +#[tokio::test] +async fn whitelist_accepts_allowed_kind_and_rejects_disallowed() -> Result<()> { + let relay = common::start_relay()?; + common::wait_for_healthy_relay(&relay).await?; + + // Kind 1 (short text note) is NOT in the Floonet whitelist: rejected. + let (msg, id) = signed_event(1, "hello world"); + let ok = publish_and_get_ok(relay.port, &msg, &id).await?; + assert_eq!( + ok.get(2).and_then(Value::as_bool), + Some(false), + "kind 1 must be rejected: {ok}" + ); + let reason = ok.get(3).and_then(Value::as_str).unwrap_or_default(); + assert!( + reason.starts_with("blocked:"), + "rejection must be a blocked: OK message, got {reason}" + ); + + // Kind 0 (profile metadata) IS in the whitelist: accepted. + let (msg, id) = signed_event(0, r#"{"name":"floonet-test"}"#); + let ok = publish_and_get_ok(relay.port, &msg, &id).await?; + assert_eq!( + ok.get(2).and_then(Value::as_bool), + Some(true), + "kind 0 must be accepted: {ok}" + ); + + // Kind 1059 (gift wrap) IS in the whitelist: accepted. + let (msg, id) = signed_event(1059, "opaque ciphertext"); + let ok = publish_and_get_ok(relay.port, &msg, &id).await?; + assert_eq!( + ok.get(2).and_then(Value::as_bool), + Some(true), + "kind 1059 must be accepted: {ok}" + ); + + let _res = relay.shutdown_tx.send(()); + Ok(()) +}