rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

commit e309980bd60b4dda8b26bba5d82967044494747c
parent 29e05021f8d84ba1d1d885a8528dc90c3a29d223
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 20:25:52 +0000

rhi: migrate trade attestation dependencies

- pin final Radroots package contracts without sibling paths
- move lifecycle paths logging and key custody into the RHI host
- replace the retired Nostr prelude with canonical package APIs
- verify tests clippy docs and the standalone package artifact

Diffstat:
MCargo.lock | 1145++++++++++++++++++++-----------------------------------------------------------
MCargo.toml | 40++++++++++++++++++++++------------------
Mrust-toolchain.toml | 2+-
Msrc/adapters/nostr/event.rs | 46++++++++++++++++++----------------------------
Msrc/cli.rs | 4++--
Msrc/config.rs | 28++++++++++++++--------------
Msrc/features/trade_agreement_attestation.rs | 231+++++++++++++++++++++++++------------------------------------------------------
Asrc/host_identity.rs | 327+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/host_nostr.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/host_paths/error.rs | 35+++++++++++++++++++++++++++++++++++
Asrc/host_paths/mod.rs | 11+++++++++++
Asrc/host_paths/namespace.rs | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/host_paths/platform.rs | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/host_paths/roots.rs | 315+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/host_runtime.rs | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/identity_storage.rs | 366++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/lib.rs | 110++++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/main.rs | 85+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/paths.rs | 9+++++----
Msrc/rhi.rs | 54+++++++++++++++++++++++++++---------------------------
Mtests/source_guards.rs | 23++++++++++++-----------
21 files changed, 2085 insertions(+), 1257 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -24,18 +24,6 @@ dependencies = [ ] [[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" @@ -45,21 +33,6 @@ dependencies = [ ] [[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 = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -116,29 +89,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[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", -] - -[[package]] name = "async-utility" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -176,22 +132,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "base64" -version = "0.21.7" +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" @@ -244,9 +194,6 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "serde_core", -] [[package]] name = "block-buffer" @@ -304,12 +251,6 @@ 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 = "chacha20" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -334,19 +275,6 @@ dependencies = [ ] [[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -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" @@ -404,58 +332,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] -name = "config" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust2", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cpufeatures" @@ -482,10 +362,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crunchy" -version = "0.2.4" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] [[package]] name = "crypto-common" @@ -505,6 +391,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -536,27 +432,26 @@ dependencies = [ ] [[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - -[[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "elliptic-curve" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "cfg-if", + "base16ct", + "crypto-bigint", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] @@ -582,6 +477,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[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", +] + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -633,6 +538,17 @@ 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-io" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -674,6 +590,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -696,11 +613,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -729,13 +644,14 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.14.5" +name = "group" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ahash", - "allocator-api2", + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] @@ -754,15 +670,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[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 = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -803,120 +710,12 @@ dependencies = [ ] [[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[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", - "http-body", - "pin-project-lite", -] - -[[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.6", -] - -[[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", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -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" @@ -1005,6 +804,16 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" @@ -1059,22 +868,6 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1103,14 +896,13 @@ dependencies = [ ] [[package]] -name = "json5" -version = "0.4.1" +name = "k256" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ - "pest", - "pest_derive", - "serde", + "cfg-if", + "elliptic-curve", ] [[package]] @@ -1165,12 +957,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[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" @@ -1192,12 +978,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] name = "mio" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1215,23 +995,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" [[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 = "nostr" version = "0.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" dependencies = [ "aes", - "base64 0.22.1", + "base64", "bech32", "bip39", "bitcoin_hashes", @@ -1247,6 +1017,7 @@ dependencies = [ "serde_json", "unicode-normalization", "url", + "url-fork", ] [[package]] @@ -1271,9 +1042,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.44.0" +version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +checksum = "c85c54d6ca9aae4ae2bf19a7663ba9db5f45f783f1d24aff55f006386b8b99a1" dependencies = [ "async-utility", "async-wsocket", @@ -1345,16 +1116,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1389,12 +1150,6 @@ dependencies = [ ] [[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1411,61 +1166,12 @@ 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", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[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 = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1520,61 +1226,6 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "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", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1596,15 +1247,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "radroots_authority" -version = "1.0.0-alpha.1" -dependencies = [ - "radroots_event", -] - -[[package]] name = "radroots_blossom" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "mediatype", "serde", @@ -1615,21 +1259,22 @@ dependencies = [ [[package]] name = "radroots_core" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "rust_decimal", - "rust_decimal_macros", "serde", ] [[package]] name = "radroots_event" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "hex", "jiff-tzdb", "radroots_blossom", "radroots_core", + "radroots_identity", + "radroots_protocol", "serde", "serde_json", "sha2", @@ -1639,119 +1284,69 @@ dependencies = [ [[package]] name = "radroots_event_codec" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ - "nostr", "radroots_blossom", "radroots_core", "radroots_event", + "radroots_identity", + "radroots_protocol", + "secp256k1", "serde", "serde_json", ] [[package]] name = "radroots_identity" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ - "nostr", - "radroots_protected_store", - "radroots_runtime", - "radroots_runtime_paths", - "radroots_secret_vault", + "k256", "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", -] - -[[package]] -name = "radroots_log" -version = "1.0.0-alpha.1" -dependencies = [ - "chrono", - "serde_json", - "thiserror 1.0.69", - "tracing", - "tracing-appender", - "tracing-subscriber", + "thiserror 2.0.18", ] [[package]] name = "radroots_nostr" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "nostr", - "nostr-sdk", "radroots_event", "radroots_event_codec", "radroots_identity", - "reqwest", "serde", "serde_json", "thiserror 1.0.69", ] [[package]] -name = "radroots_protected_store" -version = "1.0.0-alpha.1" +name = "radroots_protocol" +version = "0.1.0-alpha" dependencies = [ - "chacha20poly1305", - "getrandom 0.2.17", - "radroots_secret_vault", "serde", - "serde_json", - "zeroize", ] [[package]] -name = "radroots_runtime" -version = "1.0.0-alpha.1" +name = "radroots_secrets" +version = "0.1.0-alpha" dependencies = [ - "anyhow", "chacha20poly1305", - "clap", - "config", - "getrandom 0.2.17", - "radroots_log", - "radroots_protected_store", - "radroots_runtime_paths", - "radroots_secret_vault", "serde", - "serde_json", - "tempfile", - "thiserror 1.0.69", - "tokio", - "toml", - "tracing", "zeroize", ] [[package]] -name = "radroots_runtime_paths" -version = "1.0.0-alpha.1" -dependencies = [ - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "radroots_secret_vault" -version = "1.0.0-alpha.1" - -[[package]] name = "radroots_trade" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ - "base64 0.22.1", + "base64", "hex", - "radroots_authority", "radroots_core", "radroots_event", "radroots_event_codec", + "radroots_identity", "serde", "serde_json", "sha2", - "thiserror 1.0.69", ] [[package]] @@ -1827,71 +1422,36 @@ 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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.6", +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] name = "rhi" version = "0.1.0" dependencies = [ "anyhow", + "chacha20poly1305", "clap", - "radroots_core", + "futures-executor", + "nostr", + "nostr-sdk", "radroots_event", "radroots_event_codec", "radroots_identity", - "radroots_log", "radroots_nostr", - "radroots_runtime", - "radroots_runtime_paths", + "radroots_protocol", + "radroots_secrets", "radroots_trade", + "rand 0.9.2", "serde", "serde_json", "sha2", @@ -1900,6 +1460,9 @@ dependencies = [ "tokio", "toml", "tracing", + "tracing-appender", + "tracing-subscriber", + "zeroize", ] [[package]] @@ -1917,28 +1480,6 @@ dependencies = [ ] [[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags", - "serde", - "serde_derive", -] - -[[package]] -name = "rust-ini" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] name = "rust_decimal" version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1950,22 +1491,6 @@ dependencies = [ ] [[package]] -name = "rust_decimal_macros" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1998,7 +1523,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "web-time", "zeroize", ] @@ -2020,12 +1544,6 @@ 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 = "salsa20" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2053,6 +1571,19 @@ dependencies = [ ] [[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] name = "secp256k1" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2131,18 +1662,6 @@ dependencies = [ ] [[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 = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2241,15 +1760,6 @@ dependencies = [ ] [[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" @@ -2354,15 +1864,6 @@ dependencies = [ ] [[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2495,51 +1996,6 @@ 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", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[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" @@ -2613,12 +2069,6 @@ dependencies = [ ] [[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.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2644,10 +2094,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "unicode-bidi" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-general-category" @@ -2671,12 +2121,6 @@ dependencies = [ ] [[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2705,13 +2149,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", - "idna", + "idna 1.1.0", "percent-encoding", "serde", "serde_derive", ] [[package]] +name = "url-fork" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa3323c39b8e786154d3000b70ae9af0e9bd746c9791456da0d4a1f68ad89d6" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", + "serde", +] + +[[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2742,15 +2198,6 @@ 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.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2878,16 +2325,6 @@ dependencies = [ ] [[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-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2906,80 +2343,18 @@ dependencies = [ ] [[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", -] - -[[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", -] - -[[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.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2997,31 +2372,14 @@ 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", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3031,96 +2389,48 @@ 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 = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3224,17 +2534,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] -name = "yaml-rust2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", -] - -[[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3342,3 +2641,123 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[patch.unused]] +name = "radroots_authority" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_event_store" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_geonames" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_log" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_mesh" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_nostr_accounts" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_nostr_connect" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_nostr_runtime" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_nostr_signer" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_nostrdb" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_outbox" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_protected_store" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_protocol_contract_v1" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_replica_schema" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_replica_store" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_replica_sync" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_runtime" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_runtime_paths" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_runtime_store" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_secret_vault" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_signing" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_sql_core" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_storage" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_storage_sqlite" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_sync" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_test_fixtures" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_transport" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_transport_nostr" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_transport_publish_protocol" +version = "0.1.0-alpha" + +[[patch.unused]] +name = "radroots_transport_reticulum" +version = "0.1.0-alpha" diff --git a/Cargo.toml b/Cargo.toml @@ -3,23 +3,21 @@ name = "rhi" version = "0.1.0" edition = "2024" authors = ["Radroots Authors"] -rust-version = "1.97.0" +rust-version = "1.97.1" license = "AGPL-3.0-or-later" description = "Radroots trade agreement attestation worker" [workspace] -resolver = "2" +resolver = "3" [workspace.dependencies] -radroots_core = { version = "=1.0.0-alpha.1", path = "../lib/crates/core" } -radroots_event = { version = "=1.0.0-alpha.1", path = "../lib/crates/event" } -radroots_event_codec = { version = "=1.0.0-alpha.1", path = "../lib/crates/event_codec" } -radroots_identity = { version = "=1.0.0-alpha.1", path = "../lib/crates/identity" } -radroots_log = { version = "=1.0.0-alpha.1", path = "../lib/crates/log" } -radroots_nostr = { version = "=1.0.0-alpha.1", path = "../lib/crates/nostr" } -radroots_runtime = { version = "=1.0.0-alpha.1", path = "../lib/crates/runtime" } -radroots_runtime_paths = { version = "=1.0.0-alpha.1", path = "../lib/crates/runtime_paths" } -radroots_trade = { version = "=1.0.0-alpha.1", path = "../lib/crates/trade" } +radroots_event = "=0.1.0-alpha" +radroots_event_codec = "=0.1.0-alpha" +radroots_identity = "=0.1.0-alpha" +radroots_nostr = "=0.1.0-alpha" +radroots_protocol = "=0.1.0-alpha" +radroots_secrets = "=0.1.0-alpha" +radroots_trade = "=0.1.0-alpha" [features] default = [] @@ -28,25 +26,31 @@ default = [] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] -radroots_core = { workspace = true, features = ["std", "serde"] } radroots_event = { workspace = true, features = ["serde"] } -radroots_event_codec = { workspace = true, features = ["nostr", "serde_json"] } +radroots_event_codec = { workspace = true, features = ["json"] } radroots_identity = { workspace = true } -radroots_log = { workspace = true } -radroots_nostr = { workspace = true, features = ["client", "codec", "events", "http"] } -radroots_runtime = { workspace = true, features = ["cli"] } -radroots_runtime_paths = { workspace = true } +radroots_nostr = { workspace = true, features = ["events"] } +radroots_protocol = { workspace = true } +radroots_secrets = { workspace = true } radroots_trade = { workspace = true } anyhow = { version = "1" } +chacha20poly1305 = { version = "0.10" } clap = { version = "4", features = ["derive"] } +futures-executor = { version = "0.3" } +nostr = { version = "0.44.2", features = ["nip49"] } +nostr-sdk = { version = "0.44.1" } +rand = { version = "0.9" } serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false } sha2 = { version = "0.10" } tokio = { version = "1", features = ["full"] } thiserror = { version = "2" } +tempfile = { version = "3" } toml = { version = "0.8" } tracing = { version = "0.1" } +tracing-appender = { version = "0.2" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +zeroize = { version = "1" } [dev-dependencies] -tempfile = { version = "3" } diff --git a/rust-toolchain.toml b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.97.0" +channel = "1.97.1" diff --git a/src/adapters/nostr/event.rs b/src/adapters/nostr/event.rs @@ -1,16 +1,16 @@ +use crate::host_nostr::{Event, Kind}; use radroots_event_codec::job::traits::{JobEventBorrow, JobEventLike}; -use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrKind}; #[derive(Clone, Debug)] pub struct NostrEventAdapter<'a> { - evt: &'a RadrootsNostrEvent, + evt: &'a Event, id_hex: String, author_hex: String, } impl<'a> NostrEventAdapter<'a> { #[inline] - pub fn new(evt: &'a RadrootsNostrEvent) -> Self { + pub fn new(evt: &'a Event) -> Self { Self { evt, id_hex: evt.id.to_hex(), @@ -30,12 +30,12 @@ impl<'a> NostrEventAdapter<'a> { impl<'a> JobEventBorrow<'a> for NostrEventAdapter<'a> { #[inline] - fn raw_id(&'a self) -> &'a str { - &self.id_hex + fn raw_id(&'a self) -> String { + self.id_hex.clone() } #[inline] - fn raw_author(&'a self) -> &'a str { - &self.author_hex + fn raw_author(&'a self) -> String { + self.author_hex.clone() } #[inline] fn raw_content(&'a self) -> &'a str { @@ -44,7 +44,7 @@ impl<'a> JobEventBorrow<'a> for NostrEventAdapter<'a> { #[inline] fn raw_kind(&'a self) -> u32 { match self.evt.kind { - RadrootsNostrKind::Custom(v) => v as u32, + Kind::Custom(v) => v as u32, _ => 0, } } @@ -62,7 +62,7 @@ impl JobEventLike for NostrEventAdapter<'_> { } fn raw_kind(&self) -> u32 { match self.evt.kind { - RadrootsNostrKind::Custom(v) => v as u32, + Kind::Custom(v) => v as u32, _ => 0, } } @@ -81,18 +81,11 @@ impl JobEventLike for NostrEventAdapter<'_> { #[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::NostrEventAdapter; + use crate::host_nostr::{Event, GenericBuilder, Keys, Kind, Tag, TagKind}; use radroots_event_codec::job::traits::{JobEventBorrow, JobEventLike}; - use radroots_nostr::prelude::{ - RadrootsNostrEvent, RadrootsNostrGenericEventBuilder, RadrootsNostrKeys, RadrootsNostrKind, - RadrootsNostrTag, RadrootsNostrTagKind, - }; - fn build_event( - keys: &RadrootsNostrKeys, - kind: RadrootsNostrKind, - tags: Vec<RadrootsNostrTag>, - ) -> RadrootsNostrEvent { - RadrootsNostrGenericEventBuilder::new(kind, "content") + fn build_event(keys: &Keys, kind: Kind, tags: Vec<Tag>) -> Event { + GenericBuilder::new(kind, "content") .tags(tags) .sign_with_keys(keys) .expect("event must sign") @@ -100,14 +93,11 @@ mod tests { #[test] fn adapter_exposes_borrow_and_owned_fields_for_custom_kind() { - let keys = RadrootsNostrKeys::generate(); - let recipient = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); + let recipient = Keys::generate(); let recipient_hex = recipient.public_key().to_hex(); - let tags = vec![RadrootsNostrTag::custom( - RadrootsNostrTagKind::p(), - vec![recipient_hex.clone()], - )]; - let event = build_event(&keys, RadrootsNostrKind::Custom(5322), tags); + let tags = vec![Tag::custom(TagKind::p(), vec![recipient_hex.clone()])]; + let event = build_event(&keys, Kind::Custom(5322), tags); let adapter = NostrEventAdapter::new(&event); assert_eq!(JobEventBorrow::raw_id(&adapter), event.id.to_hex()); @@ -131,8 +121,8 @@ mod tests { #[test] fn adapter_maps_non_custom_kind_to_zero() { - let keys = RadrootsNostrKeys::generate(); - let event = build_event(&keys, RadrootsNostrKind::Repost, Vec::new()); + let keys = Keys::generate(); + let event = build_event(&keys, Kind::Repost, Vec::new()); let adapter = NostrEventAdapter::new(&event); assert_eq!(JobEventBorrow::raw_kind(&adapter), 0); diff --git a/src/cli.rs b/src/cli.rs @@ -1,5 +1,5 @@ +use crate::host_runtime::ServiceCliArgs; use clap::Parser; -use radroots_runtime::RadrootsServiceCliArgs; use std::path::PathBuf; #[derive(Parser, Debug, Clone)] @@ -12,7 +12,7 @@ pub struct Args { #[command(subcommand)] pub command: Option<Command>, #[command(flatten)] - pub service: RadrootsServiceCliArgs, + pub service: ServiceCliArgs, } #[derive(clap::Subcommand, Debug, Clone)] diff --git a/src/config.rs b/src/config.rs @@ -1,6 +1,6 @@ +use crate::host_nostr::Metadata; +use crate::host_runtime::{BackoffConfig, NostrServiceConfig}; use anyhow::{Context, Result, bail}; -use radroots_nostr::prelude::RadrootsNostrMetadata; -use radroots_runtime::{BackoffConfig, RadrootsNostrServiceConfig}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -83,8 +83,8 @@ struct RawServiceConfig { } impl RawServiceConfig { - fn into_service_config(self) -> RadrootsNostrServiceConfig { - RadrootsNostrServiceConfig { + fn into_service_config(self) -> NostrServiceConfig { + NostrServiceConfig { logs_dir: self.logging.output_dir.display().to_string(), relays: self.relays.urls, nip89_identifier: self.nostr.nip89.identifier, @@ -96,7 +96,7 @@ impl RawServiceConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Configuration { #[serde(flatten)] - pub service: RadrootsNostrServiceConfig, + pub service: NostrServiceConfig, pub logging: LoggingConfig, #[serde(default)] pub subscriber: SubscriberConfig, @@ -184,7 +184,7 @@ impl Default for SubscriberStateConfig { #[derive(Debug, Deserialize, Clone)] #[serde(deny_unknown_fields)] struct RawSettings { - pub metadata: RadrootsNostrMetadata, + pub metadata: Metadata, #[serde(default)] pub logging: RawLoggingConfig, #[serde(default)] @@ -221,14 +221,14 @@ impl RawSettings { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { - pub metadata: RadrootsNostrMetadata, + pub metadata: Metadata, pub config: Configuration, } fn load_settings_from_path_with_resolver( path: &Path, - resolver: &radroots_runtime_paths::RadrootsPathResolver, - profile: radroots_runtime_paths::RadrootsPathProfile, + resolver: &crate::host_paths::RadrootsPathResolver, + profile: crate::host_paths::RadrootsPathProfile, repo_local_root: Option<&Path>, ) -> Result<Settings> { let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?; @@ -245,7 +245,7 @@ pub fn load_settings_from_path(path: &Path) -> Result<Settings> { let (profile, repo_local_root) = crate::paths::process_path_selection()?; load_settings_from_path_with_resolver( path, - &radroots_runtime_paths::RadrootsPathResolver::current(), + &crate::host_paths::RadrootsPathResolver::current(), profile, repo_local_root.as_deref(), ) @@ -255,14 +255,14 @@ pub fn load_settings_from_path(path: &Path) -> Result<Settings> { mod tests { use super::load_settings_from_path_with_resolver; use crate::features::trade_agreement_attestation::TradeAgreementAttestationBackend; + use crate::host_paths::{ + RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, RadrootsRuntimeNamespace, + }; use crate::paths::{ default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver, runtime_contract_with_resolver, }; - use radroots_runtime_paths::{ - RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPlatform, RadrootsRuntimeNamespace, - }; use std::path::PathBuf; fn linux_resolver() -> RadrootsPathResolver { diff --git a/src/features/trade_agreement_attestation.rs b/src/features/trade_agreement_attestation.rs @@ -7,25 +7,25 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -use anyhow::{Result, anyhow}; -use radroots_event::ids::{ - RadrootsAddressableCoordinate, RadrootsEventId, RadrootsPublicKey, RadrootsTradeId, - RadrootsTradeMutationId, +use crate::host_nostr::{ + Client, Event, Filter, Keys, Kind, RelayPoolNotification, SubscriptionId, Timestamp, }; -use radroots_event::kinds::{TRADE_MUTATION_EVENT_KINDS, is_trade_mutation_event_kind}; +use anyhow::{Result, anyhow}; +use radroots_event::envelope::kind::{TRADE_MUTATION_EVENT_KINDS, is_trade_mutation_event_kind}; +use radroots_event::id::{AddressableCoordinate, EventId, MutationId, TradeId}; use radroots_event::trade::{ - RadrootsTradeMutationEnvelopeV1, canonical_jcs_value, trade_mutation_from_canonical_content, + TradeMutationEnvelopeV1, canonical_jcs_value, trade_mutation_from_canonical_content, +}; +use radroots_identity::PublicKey; +use radroots_trade::evidence::{ + RadrootsTradeAttestationResultV1, RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1, }; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKeys, - RadrootsNostrKind, RadrootsNostrRelayPoolNotification, RadrootsNostrSubscriptionId, - RadrootsNostrTimestamp, +use radroots_trade::model::{ + RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, RadrootsTradeProjectionV1, }; -use radroots_trade::workflow::{ +use radroots_trade::reducer::{ RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION, - RadrootsTradeAgreementStateV1, RadrootsTradeAttestationResultV1, - RadrootsTradeAttestationStateV1, RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1, - RadrootsTradeProjectionV1, RadrootsTradeReductionInputV1, reduce_trade_records, + RadrootsTradeReductionInputV1, reduce_trade_records, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -76,10 +76,10 @@ impl TradeAgreementAttestationPolicy { self.validator_set_event_id.as_deref(), ) { (Some(addr), Some(event_id)) => { - RadrootsAddressableCoordinate::parse(addr).map_err(|_| { + AddressableCoordinate::parse(addr).map_err(|_| { TradeAgreementAttestationError::InvalidValidatorSetBinding("validator_set_addr") })?; - RadrootsEventId::parse(event_id).map_err(|_| { + EventId::parse(event_id).map_err(|_| { TradeAgreementAttestationError::InvalidValidatorSetBinding( "validator_set_event_id", ) @@ -223,7 +223,7 @@ pub enum TradeAgreementAttestationError { #[error("invalid signed event")] InvalidSignedEvent, #[error("trade protocol error: {0}")] - TradeProtocol(#[from] radroots_event::trade::RadrootsTradeProtocolError), + TradeProtocol(#[from] radroots_event::trade::TradeProtocolError), #[error("serde error: {0}")] Serde(#[from] serde_json::Error), #[error("state error: {0}")] @@ -305,18 +305,16 @@ impl TradeAgreementAttestationRuntime { self.persist().await } - pub async fn recovery_filter(&self, kinds: Vec<RadrootsNostrKind>) -> RadrootsNostrFilter { + pub async fn recovery_filter(&self, kinds: Vec<Kind>) -> Filter { let since = { let state = self.state.lock().await; state.replay_since( - RadrootsNostrTimestamp::now().as_secs(), + Timestamp::now().as_secs(), self.config.replay_window_secs, self.config.replay_overlap_secs, ) }; - RadrootsNostrFilter::new() - .kinds(kinds) - .since(RadrootsNostrTimestamp::from(since)) + Filter::new().kinds(kinds).since(Timestamp::from(since)) } pub async fn reports(&self) -> Vec<TradeAgreementAttestationReportV1> { @@ -331,17 +329,17 @@ impl TradeAgreementAttestationRuntime { async fn observe_mutation_event( &self, - event: &RadrootsNostrEvent, - mutation: &RadrootsTradeMutationEnvelopeV1, - mutation_id: &RadrootsTradeMutationId, + event: &Event, + mutation: &TradeMutationEnvelopeV1, + mutation_id: &MutationId, kind: u32, ) -> Result<bool, TradeAgreementAttestationRuntimeError> { let observation = TradeMutationObservationV1 { event_id: event.id.to_hex(), event_kind: kind, event_pubkey: event.pubkey.to_hex(), - trade_id: mutation.trade_id.as_str().to_owned(), - mutation_id: mutation_id.as_str().to_owned(), + trade_id: mutation.trade_id.to_hex(), + mutation_id: mutation_id.to_hex(), content: event.content.clone(), observed_at_unix_s: event.created_at.as_secs(), }; @@ -357,32 +355,33 @@ impl TradeAgreementAttestationRuntime { async fn reduce_trade( &self, - trade_id: &RadrootsTradeId, + trade_id: &TradeId, ) -> Result<RadrootsTradeProjectionV1, TradeAgreementAttestationError> { let observations = { let state = self.state.lock().await; state .mutation_events .values() - .filter(|observation| observation.trade_id == trade_id.as_str()) + .filter(|observation| observation.trade_id == trade_id.to_hex().as_str()) .cloned() .collect::<Vec<_>>() }; - let mut input = RadrootsTradeReductionInputV1::new(trade_id.clone()); - input.evidence_state = RadrootsTradeEvidenceStateV1::Complete; - input.mutations = observations + let mutations = observations .iter() .map(|observation| { let mutation = trade_mutation_from_canonical_content(observation.content.as_str())?; - let transport_event_id = RadrootsEventId::parse(observation.event_id.as_str()) + let transport_event_id = EventId::parse(observation.event_id.as_str()) .map(Some) .map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?; - Ok(RadrootsTradeMutationRecordV1 { + Ok(RadrootsTradeMutationRecordV1::new( transport_event_id, mutation, - }) + )) }) .collect::<Result<Vec<_>, TradeAgreementAttestationError>>()?; + let input = RadrootsTradeReductionInputV1::new(*trade_id) + .with_evidence_state(RadrootsTradeEvidenceStateV1::Complete) + .with_mutations(mutations); Ok(reduce_trade_records(input)) } @@ -483,7 +482,7 @@ fn temp_state_path(path: &Path) -> Result<PathBuf, TradeAgreementAttestationRunt } pub async fn handle_trade_mutation_event( - event: RadrootsNostrEvent, + event: Event, runtime: TradeAgreementAttestationRuntime, policy: &TradeAgreementAttestationPolicy, ) -> Result<Option<TradeAgreementAttestationReportV1>, TradeAgreementAttestationError> { @@ -496,14 +495,13 @@ pub async fn handle_trade_mutation_event( if mutation.mutation_kind().nostr_kind() != kind { return Err(TradeAgreementAttestationError::UnsupportedKind); } - let event_author = RadrootsPublicKey::parse(event.pubkey.to_hex()) + let event_author = PublicKey::from_hex(&event.pubkey.to_hex()) .map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?; if event_author != mutation.author_pubkey { return Err(TradeAgreementAttestationError::InvalidEventAuthor); } let mutation_id = mutation .mutation_id - .clone() .ok_or(TradeAgreementAttestationError::MissingMutationId)?; if !runtime .observe_mutation_event(&event, &mutation, &mutation_id, kind) @@ -513,10 +511,10 @@ pub async fn handle_trade_mutation_event( } let projection = runtime.reduce_trade(&mutation.trade_id).await?; let Some(claim_id) = projection - .active_agreement_claim_ids + .active_agreement_claim_ids() .iter() .find(|claim_id| **claim_id == mutation_id) - .or_else(|| projection.active_agreement_claim_ids.first()) + .or_else(|| projection.active_agreement_claim_ids().first()) .cloned() else { return Ok(None); @@ -528,14 +526,14 @@ pub async fn handle_trade_mutation_event( pub fn attest_projection_claim( projection: &RadrootsTradeProjectionV1, - claim_mutation_id: &RadrootsTradeMutationId, + claim_mutation_id: &MutationId, policy: &TradeAgreementAttestationPolicy, ) -> Result<TradeAgreementAttestationReportV1, TradeAgreementAttestationError> { policy.validate()?; if !projection - .agreement_claims + .agreement_claims() .iter() - .any(|claim| claim.claim_mutation_id == *claim_mutation_id) + .any(|claim| claim.claim_mutation_id() == claim_mutation_id) { return Err(TradeAgreementAttestationError::MissingAgreementClaim); } @@ -544,32 +542,34 @@ pub fn attest_projection_claim( schema_version: RHI_AGREEMENT_ATTESTATION_REPORT_VERSION, reducer_contract_id: RADROOTS_TRADE_REDUCER_CONTRACT_ID.to_owned(), reducer_version: RADROOTS_TRADE_REDUCER_VERSION, - trade_id: projection.trade_id.as_str().to_owned(), - claim_mutation_id: claim_mutation_id.as_str().to_owned(), - projection_digest: projection.projection_digest.clone(), - agreement_state: projection.agreement_state, - attestation_state_before_report: projection.attestation_state, - active_agreement_claim_ids: mutation_ids_to_strings(&projection.active_agreement_claim_ids), - contested_claim_ids: mutation_ids_to_strings(&projection.contested_claim_ids), - cancelled_claim_ids: mutation_ids_to_strings(&projection.cancelled_claim_ids), - evidence_state: projection.evidence_state, + trade_id: projection.trade_id().to_hex(), + claim_mutation_id: claim_mutation_id.to_hex(), + projection_digest: projection.projection_digest().to_owned(), + agreement_state: projection.agreement_state(), + attestation_state_before_report: projection.attestation_state(), + active_agreement_claim_ids: mutation_ids_to_strings( + projection.active_agreement_claim_ids(), + ), + contested_claim_ids: mutation_ids_to_strings(projection.contested_claim_ids()), + cancelled_claim_ids: mutation_ids_to_strings(projection.cancelled_claim_ids()), + evidence_state: projection.evidence_state(), validator_set: policy.validator_set_binding()?, }; let statement_hash = hash_canonical_value( b"radroots:rhi-agreement-attestation-statement:v1\0", &statement, )?; - let result = if projection.agreement_state == RadrootsTradeAgreementStateV1::Agreed + let result = if projection.agreement_state() == RadrootsTradeAgreementStateV1::Agreed && projection - .active_agreement_claim_ids + .active_agreement_claim_ids() .iter() .any(|claim| claim == claim_mutation_id) && !projection - .contested_claim_ids + .contested_claim_ids() .iter() .any(|claim| claim == claim_mutation_id) && !projection - .cancelled_claim_ids + .cancelled_claim_ids() .iter() .any(|claim| claim == claim_mutation_id) { @@ -609,16 +609,13 @@ pub fn trade_mutation_subscription_kinds() -> Vec<u32> { TRADE_MUTATION_EVENT_KINDS.to_vec() } -fn mutation_ids_to_strings(values: &[RadrootsTradeMutationId]) -> Vec<String> { - values - .iter() - .map(|value| value.as_str().to_owned()) - .collect() +fn mutation_ids_to_strings(values: &[MutationId]) -> Vec<String> { + values.iter().map(MutationId::to_hex).collect() } -fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeAgreementAttestationError> { +fn event_kind_u32(event: &Event) -> Result<u32, TradeAgreementAttestationError> { match event.kind { - RadrootsNostrKind::Custom(value) => Ok(u32::from(value)), + Kind::Custom(value) => Ok(u32::from(value)), _ => Err(TradeAgreementAttestationError::UnsupportedKind), } } @@ -651,23 +648,16 @@ fn hash_canonical_value( } fn map_notification_recv_result( - result: Result<RadrootsNostrRelayPoolNotification, tokio::sync::broadcast::error::RecvError>, -) -> Result<RadrootsNostrRelayPoolNotification, ()> { + result: Result<RelayPoolNotification, tokio::sync::broadcast::error::RecvError>, +) -> Result<RelayPoolNotification, ()> { result.map_err(|_| ()) } -async fn subscribe_io( - client: &RadrootsNostrClient, - filter: RadrootsNostrFilter, -) -> Result<RadrootsNostrSubscriptionId> { - let subscription = client.subscribe(filter, None).await?; - Ok(subscription.val) +async fn subscribe_io(client: &Client, filter: Filter) -> Result<SubscriptionId> { + client.subscribe(filter).await.map_err(Into::into) } -async fn unsubscribe_io( - client: &RadrootsNostrClient, - subscription_id: &RadrootsNostrSubscriptionId, -) { +async fn unsubscribe_io(client: &Client, subscription_id: &SubscriptionId) { client.unsubscribe(subscription_id).await; } @@ -676,7 +666,7 @@ fn should_delay_before_event_handle() -> bool { } async fn process_event_notification( - event: RadrootsNostrEvent, + event: Event, runtime: TradeAgreementAttestationRuntime, policy: TradeAgreementAttestationPolicy, ) -> Result<()> { @@ -698,8 +688,8 @@ async fn process_event_notification( } pub async fn subscriber( - client: RadrootsNostrClient, - _keys: RadrootsNostrKeys, + client: Client, + _keys: Keys, runtime: TradeAgreementAttestationRuntime, policy: TradeAgreementAttestationPolicy, mut stop_rx: watch::Receiver<bool>, @@ -710,10 +700,10 @@ pub async fn subscriber( subscribed_kinds ); - let kinds: Vec<RadrootsNostrKind> = subscribed_kinds + let kinds: Vec<Kind> = subscribed_kinds .iter() .map(|kind| u16::try_from(*kind).expect("trade mutation kinds fit in nostr custom range")) - .map(RadrootsNostrKind::Custom) + .map(Kind::Custom) .collect(); let filter = runtime.recovery_filter(kinds).await; @@ -742,7 +732,7 @@ pub async fn subscriber( } }; - if let RadrootsNostrRelayPoolNotification::Event { event, .. } = n { + if let RelayPoolNotification::Event { event, .. } = n { let event = (*event).clone(); process_event_notification(event, runtime.clone(), policy.clone()).await?; } @@ -864,83 +854,10 @@ fn write_output(path: Option<&Path>, bytes: &[u8]) -> anyhow::Result<()> { #[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::{ - RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, TradeAgreementAttestationPolicy, - TradeAgreementAttestationRuntime, TradeAgreementAttestationRuntimeConfig, - TradeAgreementAttestationSmokeOperation, TradeAgreementAttestationSmokeRequest, - attest_projection_claim, handle_smoke_request_bytes, - }; - use radroots_event::ids::{RadrootsTradeId, RadrootsTradeMutationId}; - use radroots_trade::workflow::{ - RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, - RadrootsTradeEvidenceStateV1, RadrootsTradeProjectionV1, + RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, TradeAgreementAttestationRuntime, + TradeAgreementAttestationRuntimeConfig, TradeAgreementAttestationSmokeOperation, + TradeAgreementAttestationSmokeRequest, handle_smoke_request_bytes, }; - - fn hex_64(ch: char) -> String { - std::iter::repeat_n(ch, 64).collect() - } - - fn hex_32(ch: char) -> String { - std::iter::repeat_n(ch, 32).collect() - } - - fn projection(claim_id: RadrootsTradeMutationId) -> RadrootsTradeProjectionV1 { - RadrootsTradeProjectionV1 { - reducer_contract_id: radroots_trade::workflow::RADROOTS_TRADE_REDUCER_CONTRACT_ID - .to_owned(), - reducer_version: radroots_trade::workflow::RADROOTS_TRADE_REDUCER_VERSION, - trade_id: RadrootsTradeId::parse(hex_32('1')).expect("trade id"), - root_mutation_id: None, - buyer_pubkey: None, - seller_pubkey: None, - farm_id: None, - negotiation_state: Default::default(), - agreement_state: RadrootsTradeAgreementStateV1::Agreed, - evidence_state: RadrootsTradeEvidenceStateV1::Complete, - conflict_state: Default::default(), - private_terms_state: Default::default(), - attestation_state: RadrootsTradeAttestationStateV1::None, - fulfillment_state: Default::default(), - payment_state: Default::default(), - candidate_heads: Vec::new(), - agreement_claims: vec![radroots_trade::workflow::RadrootsTradeAgreementClaimV1 { - claim_mutation_id: claim_id.clone(), - proposal_mutation_id: RadrootsTradeMutationId::parse(hex_64('2')) - .expect("proposal id"), - candidate_id: radroots_event::ids::RadrootsTradeCandidateId::parse(hex_64('3')) - .expect("candidate id"), - candidate_author_pubkey: radroots_event::ids::RadrootsPublicKey::parse(hex_64('4')) - .expect("author"), - accepted_by_pubkey: radroots_event::ids::RadrootsPublicKey::parse(hex_64('5')) - .expect("acceptor"), - reservation_commitment: hex_64('6'), - }], - active_agreement_claim_ids: vec![claim_id], - contested_claim_ids: Vec::new(), - cancelled_claim_ids: Vec::new(), - declined_candidate_ids: Vec::new(), - missing_parent_ids: Vec::new(), - missing_proposal_ids: Vec::new(), - unsupported_mutation_ids: Vec::new(), - issues: Vec::new(), - attestations: Vec::new(), - projection_digest: hex_64('7'), - } - } - - #[test] - fn attestation_report_is_keyed_to_claim_and_projection_digest() { - let claim_id = RadrootsTradeMutationId::parse(hex_64('a')).expect("claim id"); - let report = attest_projection_claim( - &projection(claim_id.clone()), - &claim_id, - &TradeAgreementAttestationPolicy::default(), - ) - .expect("report"); - assert_eq!(report.statement.claim_mutation_id, claim_id.as_str()); - assert_eq!(report.statement.projection_digest, hex_64('7')); - assert_eq!(report.proof_system, "local_statement_hash"); - } - #[tokio::test] async fn runtime_persists_and_loads_attestation_state() { let temp = tempfile::tempdir().expect("tempdir"); diff --git a/src/host_identity.rs b/src/host_identity.rs @@ -0,0 +1,327 @@ +//! Myc-owned secret identity container. +//! +//! `radroots_identity` deliberately exposes only public, transport-neutral +//! values. This host-private type keeps service key custody and the legacy +//! Nostr-facing profile payload inside Myc. + +use std::fs; +use std::path::{Path, PathBuf}; + +use nostr::nips::nip19::ToBech32; +use nostr::nips::nip49::{EncryptedSecretKey, KeySecurity}; +use nostr::{Keys, SecretKey}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum IdentityError { + #[error("identity file missing at {0}")] + NotFound(PathBuf), + #[error("identity generation is not permitted for {0}")] + GenerationNotAllowed(PathBuf), + #[error("failed to read identity file at {0}")] + Read(PathBuf, #[source] std::io::Error), + #[error("failed to create identity directory {0}")] + CreateDir(PathBuf, #[source] std::io::Error), + #[error("failed to write identity file at {0}")] + Write(PathBuf, #[source] std::io::Error), + #[error("invalid identity JSON")] + InvalidJson(#[from] serde_json::Error), + #[error("invalid secret key")] + InvalidSecretKey(#[from] nostr::key::Error), + #[error("invalid public key")] + InvalidPublicKey, + #[error("public key does not match secret key")] + PublicKeyMismatch, + #[error("invalid encrypted secret key")] + InvalidEncryptedSecretKey, + #[error("failed to encrypt secret key")] + EncryptSecretKey, + #[error("failed to decrypt encrypted secret key")] + DecryptEncryptedSecretKey, + #[error("unsupported identity file format")] + InvalidIdentityFormat, + #[error("protected identity storage error at {path}: {message}")] + ProtectedStorage { path: PathBuf, message: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct RadrootsIdentityId(String); + +impl RadrootsIdentityId { + pub fn from_public_key(public_key: nostr::PublicKey) -> Result<Self, IdentityError> { + let key = radroots_nostr::key::public_key_from_nostr(public_key) + .map_err(|_| IdentityError::InvalidPublicKey)?; + Ok(Self( + radroots_identity::IdentityId::from_public_key(key).to_hex(), + )) + } + + pub fn parse(value: &str) -> Result<Self, IdentityError> { + radroots_identity::IdentityId::from_hex(value) + .map(|identity_id| Self(identity_id.to_hex())) + .map_err(|_| IdentityError::InvalidPublicKey) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn into_string(self) -> String { + self.0 + } + + pub fn to_final(&self) -> radroots_identity::IdentityId { + radroots_identity::IdentityId::from_hex(self.0.as_str()) + .expect("host identity ids are constructed from validated keys") + } +} + +impl std::fmt::Display for RadrootsIdentityId { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(formatter) + } +} + +impl From<radroots_identity::PublicKey> for RadrootsIdentityId { + fn from(public_key: radroots_identity::PublicKey) -> Self { + Self(radroots_identity::IdentityId::from_public_key(public_key).to_hex()) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsIdentityProfile { + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option<nostr::Event>, + #[serde(skip_serializing_if = "Option::is_none")] + pub application_handler: Option<nostr::Event>, +} + +impl RadrootsIdentityProfile { + pub fn is_empty(&self) -> bool { + self.identifier.is_none() && self.metadata.is_none() && self.application_handler.is_none() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsIdentityPublic { + pub id: RadrootsIdentityId, + pub public_key_hex: String, + pub public_key_npub: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option<RadrootsIdentityProfile>, +} + +impl PartialEq for RadrootsIdentityPublic { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.public_key_hex == other.public_key_hex + && self.profile == other.profile + } +} + +impl Eq for RadrootsIdentityPublic {} + +impl RadrootsIdentityPublic { + pub fn new(public_key: nostr::PublicKey) -> Result<Self, IdentityError> { + Ok(Self { + id: RadrootsIdentityId::from_public_key(public_key)?, + public_key_hex: public_key.to_hex(), + public_key_npub: public_key + .to_bech32() + .expect("validated Nostr public keys encode as npub"), + profile: None, + }) + } + + pub fn with_profile(mut self, profile: RadrootsIdentityProfile) -> Self { + self.profile = (!profile.is_empty()).then_some(profile); + self + } + + pub fn from_final_public_key( + public_key: radroots_identity::PublicKey, + ) -> Result<Self, IdentityError> { + let public_key = radroots_nostr::key::public_key_to_nostr(public_key) + .map_err(|_| IdentityError::InvalidPublicKey)?; + Self::new(public_key) + } + + pub fn id(&self) -> &RadrootsIdentityId { + &self.id + } + + pub fn public_key(&self) -> radroots_identity::PublicKey { + radroots_identity::PublicKey::from_hex(self.public_key_hex.as_str()) + .expect("host public identities are constructed from validated keys") + } + + pub fn to_final(&self) -> radroots_identity::PublicIdentity { + radroots_identity::PublicIdentity::new(self.public_key()) + } + + pub fn account_id(&self) -> radroots_identity::AccountId { + self.id.to_final().into() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsIdentityFile { + pub secret_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub public_key: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option<nostr::Event>, + #[serde(skip_serializing_if = "Option::is_none")] + pub application_handler: Option<nostr::Event>, +} + +#[derive(Debug, Clone)] +pub struct RadrootsIdentity { + keys: Keys, + profile: Option<RadrootsIdentityProfile>, +} + +impl RadrootsIdentity { + pub fn new(keys: Keys) -> Self { + Self { + keys, + profile: None, + } + } + + pub fn generate() -> Self { + Self::new(Keys::generate()) + } + + pub fn from_secret_key_str(value: &str) -> Result<Self, IdentityError> { + let secret = SecretKey::parse(value)?; + Ok(Self::new(Keys::new(secret))) + } + + pub fn from_encrypted_secret_key_str( + payload: &str, + password: &str, + ) -> Result<Self, IdentityError> { + use nostr::nips::nip19::FromBech32; + let encrypted = EncryptedSecretKey::from_bech32(payload) + .map_err(|_| IdentityError::InvalidEncryptedSecretKey)?; + let secret = encrypted + .decrypt(password) + .map_err(|_| IdentityError::DecryptEncryptedSecretKey)?; + Ok(Self::new(Keys::new(secret))) + } + + pub fn encrypt_secret_key_ncryptsec(&self, password: &str) -> Result<String, IdentityError> { + let encrypted = + EncryptedSecretKey::new(self.keys.secret_key(), password, 16, KeySecurity::Unknown) + .map_err(|_| IdentityError::EncryptSecretKey)?; + encrypted + .to_bech32() + .map_err(|_| IdentityError::EncryptSecretKey) + } + + pub fn keys(&self) -> &Keys { + &self.keys + } + + pub fn public_key(&self) -> nostr::PublicKey { + self.keys.public_key() + } + + pub fn final_public_key(&self) -> radroots_identity::PublicKey { + radroots_nostr::key::public_key_from_nostr(self.public_key()) + .expect("identity keys always contain a valid public key") + } + + pub fn id(&self) -> RadrootsIdentityId { + RadrootsIdentityId::from_public_key(self.public_key()) + .expect("identity keys always contain a valid public key") + } + + pub fn public_key_hex(&self) -> String { + self.public_key().to_hex() + } + + pub fn secret_key_hex(&self) -> String { + self.keys.secret_key().to_secret_hex() + } + + pub fn profile(&self) -> Option<&RadrootsIdentityProfile> { + self.profile.as_ref() + } + + pub fn set_profile(&mut self, profile: RadrootsIdentityProfile) { + self.profile = (!profile.is_empty()).then_some(profile); + } + + pub fn to_public(&self) -> RadrootsIdentityPublic { + let mut public = RadrootsIdentityPublic::new(self.public_key()) + .expect("identity keys always contain a valid public key"); + public.profile = self.profile.clone(); + public + } + + pub fn to_file(&self) -> RadrootsIdentityFile { + let profile = self.profile.clone().unwrap_or_default(); + RadrootsIdentityFile { + secret_key: self.secret_key_hex(), + public_key: Some(self.public_key_hex()), + identifier: profile.identifier, + metadata: profile.metadata, + application_handler: profile.application_handler, + } + } + + pub fn save_json(&self, path: impl AsRef<Path>) -> Result<(), IdentityError> { + let path = path.as_ref(); + if let Some(parent) = path.parent().filter(|value| !value.as_os_str().is_empty()) { + fs::create_dir_all(parent) + .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?; + } + fs::write(path, serde_json::to_vec_pretty(&self.to_file())?) + .map_err(|source| IdentityError::Write(path.to_path_buf(), source)) + } + + pub fn load_from_path_auto(path: impl AsRef<Path>) -> Result<Self, IdentityError> { + let path = path.as_ref(); + let encoded = fs::read(path).map_err(|source| { + if source.kind() == std::io::ErrorKind::NotFound { + IdentityError::NotFound(path.to_path_buf()) + } else { + IdentityError::Read(path.to_path_buf(), source) + } + })?; + let file: RadrootsIdentityFile = serde_json::from_slice(encoded.as_slice())?; + Self::try_from(file) + } +} + +impl TryFrom<RadrootsIdentityFile> for RadrootsIdentity { + type Error = IdentityError; + + fn try_from(file: RadrootsIdentityFile) -> Result<Self, Self::Error> { + let mut identity = Self::from_secret_key_str(file.secret_key.as_str())?; + if file + .public_key + .as_deref() + .is_some_and(|public| public != identity.public_key_hex()) + { + return Err(IdentityError::PublicKeyMismatch); + } + identity.set_profile(RadrootsIdentityProfile { + identifier: file.identifier, + metadata: file.metadata, + application_handler: file.application_handler, + }); + Ok(identity) + } +} diff --git a/src/host_nostr.rs b/src/host_nostr.rs @@ -0,0 +1,53 @@ +//! RHI-owned relay client over the final portable Nostr contract. + +use core::time::Duration; + +pub use nostr::{Event, Filter, Keys, Kind, Metadata, SubscriptionId, Timestamp}; +pub use nostr::{Tag, TagKind}; +pub use nostr_sdk::RelayPoolNotification; +pub use radroots_nostr::event::{ApplicationHandlerSpec, GenericBuilder, ProfileBuilder}; + +#[derive(Clone)] +pub struct Client { + inner: nostr_sdk::Client, + keys: Keys, +} + +impl Client { + pub fn new(keys: Keys) -> Self { + let inner = nostr_sdk::Client::new(keys.clone()); + inner.automatic_authentication(false); + Self { inner, keys } + } + + pub fn into_inner(self) -> nostr_sdk::Client { + self.inner + } + pub fn keys(&self) -> &Keys { + &self.keys + } + pub async fn connect(&self) { + self.inner.connect().await; + } + pub async fn wait_for_connection(&self, timeout: Duration) { + self.inner.wait_for_connection(timeout).await; + } + pub async fn add_relay(&self, url: &str) -> Result<bool, nostr_sdk::client::Error> { + self.inner.add_relay(url).await + } + pub async fn subscribe( + &self, + filter: Filter, + ) -> Result<SubscriptionId, nostr_sdk::client::Error> { + Ok(self.inner.subscribe(filter, None).await?.val) + } + pub async fn unsubscribe(&self, id: &SubscriptionId) { + self.inner.unsubscribe(id).await; + } + pub async fn send_event( + &self, + event: &Event, + ) -> Result<nostr_sdk::prelude::Output<nostr::EventId>, nostr_sdk::client::Error> { + self.inner.send_event(event).await + } +} diff --git a/src/host_paths/error.rs b/src/host_paths/error.rs @@ -0,0 +1,35 @@ +use thiserror::Error; + +use std::path::PathBuf; + +use super::{RadrootsPathProfile, RadrootsPlatform}; + +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum RadrootsRuntimePathsError { + #[error("interactive_user on {platform} requires a home directory")] + MissingHomeDir { platform: RadrootsPlatform }, + + #[error("interactive_user on windows requires APPDATA and LOCALAPPDATA roots")] + MissingWindowsUserDirs, + + #[error("service_host on windows requires a ProgramData root")] + MissingWindowsProgramDataDir, + + #[error("repo_local requires an explicit repo-local base root")] + MissingRepoLocalRoot, + + #[error("mobile_native requires explicit logical roots")] + MissingMobileRoots, + + #[error("{profile} is not supported on {platform}")] + UnsupportedProfilePlatform { + profile: RadrootsPathProfile, + platform: RadrootsPlatform, + }, + + #[error("runtime namespace `{value}` must be one non-empty path component")] + InvalidNamespaceComponent { value: String }, + + #[error("shared accounts data root `{path:?}` has no parent shared data root")] + SharedAccountsDataRootMissingParent { path: PathBuf }, +} diff --git a/src/host_paths/mod.rs b/src/host_paths/mod.rs @@ -0,0 +1,11 @@ +//! RHI-owned host path policy. + +mod error; +mod namespace; +mod platform; +mod roots; + +pub use error::RadrootsRuntimePathsError; +pub use namespace::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind}; +pub use platform::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform}; +pub use roots::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths}; diff --git a/src/host_paths/namespace.rs b/src/host_paths/namespace.rs @@ -0,0 +1,148 @@ +use std::path::PathBuf; + +use super::RadrootsRuntimePathsError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsRuntimeNamespaceKind { + App, + Service, + Worker, + Shared, +} + +impl RadrootsRuntimeNamespaceKind { + #[must_use] + pub fn path_segment(self) -> &'static str { + match self { + Self::App => "apps", + Self::Service => "services", + Self::Worker => "workers", + Self::Shared => "shared", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsRuntimeNamespace { + kind: RadrootsRuntimeNamespaceKind, + value: String, +} + +impl RadrootsRuntimeNamespace { + pub fn app(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::App, value) + } + + pub fn service(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::Service, value) + } + + pub fn worker(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::Worker, value) + } + + pub fn shared(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::Shared, value) + } + + pub fn new( + kind: RadrootsRuntimeNamespaceKind, + value: impl Into<String>, + ) -> Result<Self, RadrootsRuntimePathsError> { + let value = value.into(); + validate_component(&value)?; + Ok(Self { kind, value }) + } + + #[must_use] + pub fn kind(&self) -> RadrootsRuntimeNamespaceKind { + self.kind + } + + #[must_use] + pub fn value(&self) -> &str { + self.value.as_str() + } + + #[must_use] + pub fn relative_path(&self) -> PathBuf { + PathBuf::from(self.kind.path_segment()).join(self.value.as_str()) + } +} + +fn validate_component(value: &str) -> Result<(), RadrootsRuntimePathsError> { + let trimmed = value.trim(); + if trimmed.is_empty() + || trimmed == "." + || trimmed == ".." + || trimmed.contains('/') + || trimmed.contains('\\') + { + return Err(RadrootsRuntimePathsError::InvalidNamespaceComponent { + value: value.to_owned(), + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::RadrootsRuntimePathsError; + use super::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind}; + + #[test] + fn namespace_kind_path_segments_are_canonical() { + assert_eq!(RadrootsRuntimeNamespaceKind::App.path_segment(), "apps"); + assert_eq!( + RadrootsRuntimeNamespaceKind::Service.path_segment(), + "services" + ); + assert_eq!( + RadrootsRuntimeNamespaceKind::Worker.path_segment(), + "workers" + ); + assert_eq!( + RadrootsRuntimeNamespaceKind::Shared.path_segment(), + "shared" + ); + } + + #[test] + fn namespace_constructors_preserve_kind_and_value() { + let app = RadrootsRuntimeNamespace::app("cli").expect("app namespace"); + assert_eq!(app.kind(), RadrootsRuntimeNamespaceKind::App); + assert_eq!(app.value(), "cli"); + assert_eq!(app.relative_path(), PathBuf::from("apps/cli")); + + let service = RadrootsRuntimeNamespace::service("myc").expect("service namespace"); + assert_eq!(service.kind(), RadrootsRuntimeNamespaceKind::Service); + assert_eq!(service.value(), "myc"); + assert_eq!(service.relative_path(), PathBuf::from("services/myc")); + + let worker = RadrootsRuntimeNamespace::worker("rhi").expect("worker namespace"); + assert_eq!(worker.kind(), RadrootsRuntimeNamespaceKind::Worker); + assert_eq!(worker.value(), "rhi"); + assert_eq!(worker.relative_path(), PathBuf::from("workers/rhi")); + + let shared = RadrootsRuntimeNamespace::shared("runtime").expect("shared namespace"); + assert_eq!(shared.kind(), RadrootsRuntimeNamespaceKind::Shared); + assert_eq!(shared.value(), "runtime"); + assert_eq!(shared.relative_path(), PathBuf::from("shared/runtime")); + } + + #[test] + fn namespace_validation_rejects_invalid_components() { + for invalid in ["", " ", ".", "..", "a/b", r"a\b"] { + let err = RadrootsRuntimeNamespace::new(RadrootsRuntimeNamespaceKind::App, invalid) + .expect_err("invalid namespace component should fail"); + assert_eq!( + err, + RadrootsRuntimePathsError::InvalidNamespaceComponent { + value: invalid.to_owned(), + } + ); + } + } +} diff --git a/src/host_paths/platform.rs b/src/host_paths/platform.rs @@ -0,0 +1,179 @@ +use std::fmt; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsPlatform { + Linux, + Macos, + Windows, + Android, + Ios, +} + +impl RadrootsPlatform { + #[must_use] + #[cfg(target_os = "android")] + pub fn current() -> Self { + Self::Android + } + + #[must_use] + #[cfg(target_os = "ios")] + pub fn current() -> Self { + Self::Ios + } + + #[must_use] + #[cfg(target_os = "macos")] + pub fn current() -> Self { + Self::Macos + } + + #[must_use] + #[cfg(target_os = "windows")] + pub fn current() -> Self { + Self::Windows + } + + #[must_use] + #[cfg(all( + not(target_os = "android"), + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "windows") + ))] + pub fn current() -> Self { + Self::Linux + } + + #[must_use] + pub fn is_unix_like(self) -> bool { + matches!(self, Self::Linux | Self::Macos) + } +} + +impl fmt::Display for RadrootsPlatform { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Linux => "linux", + Self::Macos => "macos", + Self::Windows => "windows", + Self::Android => "android", + Self::Ios => "ios", + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsPathProfile { + InteractiveUser, + ServiceHost, + RepoLocal, + MobileNative, +} + +impl fmt::Display for RadrootsPathProfile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::InteractiveUser => "interactive_user", + Self::ServiceHost => "service_host", + Self::RepoLocal => "repo_local", + Self::MobileNative => "mobile_native", + }) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RadrootsHostEnvironment { + pub home_dir: Option<PathBuf>, + pub appdata_dir: Option<PathBuf>, + pub localappdata_dir: Option<PathBuf>, + pub programdata_dir: Option<PathBuf>, +} + +impl RadrootsHostEnvironment { + #[must_use] + pub fn from_current_process() -> Self { + Self { + home_dir: std::env::var_os("HOME").map(PathBuf::from), + appdata_dir: std::env::var_os("APPDATA").map(PathBuf::from), + localappdata_dir: std::env::var_os("LOCALAPPDATA").map(PathBuf::from), + programdata_dir: std::env::var_os("ProgramData").map(PathBuf::from), + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform}; + + #[test] + fn current_matches_compiled_target_platform() { + #[cfg(target_os = "android")] + let expected = RadrootsPlatform::Android; + #[cfg(target_os = "ios")] + let expected = RadrootsPlatform::Ios; + #[cfg(target_os = "macos")] + let expected = RadrootsPlatform::Macos; + #[cfg(target_os = "windows")] + let expected = RadrootsPlatform::Windows; + #[cfg(all( + not(target_os = "android"), + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "windows") + ))] + let expected = RadrootsPlatform::Linux; + + assert_eq!(RadrootsPlatform::current(), expected); + } + + #[test] + fn unix_like_classification_is_explicit() { + assert!(RadrootsPlatform::Linux.is_unix_like()); + assert!(RadrootsPlatform::Macos.is_unix_like()); + assert!(!RadrootsPlatform::Windows.is_unix_like()); + assert!(!RadrootsPlatform::Android.is_unix_like()); + assert!(!RadrootsPlatform::Ios.is_unix_like()); + } + + #[test] + fn display_uses_canonical_labels() { + assert_eq!(RadrootsPlatform::Linux.to_string(), "linux"); + assert_eq!(RadrootsPlatform::Macos.to_string(), "macos"); + assert_eq!(RadrootsPlatform::Windows.to_string(), "windows"); + assert_eq!(RadrootsPlatform::Android.to_string(), "android"); + assert_eq!(RadrootsPlatform::Ios.to_string(), "ios"); + + assert_eq!( + RadrootsPathProfile::InteractiveUser.to_string(), + "interactive_user" + ); + assert_eq!(RadrootsPathProfile::ServiceHost.to_string(), "service_host"); + assert_eq!(RadrootsPathProfile::RepoLocal.to_string(), "repo_local"); + assert_eq!( + RadrootsPathProfile::MobileNative.to_string(), + "mobile_native" + ); + } + + #[test] + fn host_environment_reads_current_process_variables() { + let env = RadrootsHostEnvironment::from_current_process(); + assert_eq!(env.home_dir, std::env::var_os("HOME").map(PathBuf::from)); + assert_eq!( + env.appdata_dir, + std::env::var_os("APPDATA").map(PathBuf::from) + ); + assert_eq!( + env.localappdata_dir, + std::env::var_os("LOCALAPPDATA").map(PathBuf::from) + ); + assert_eq!( + env.programdata_dir, + std::env::var_os("ProgramData").map(PathBuf::from) + ); + } +} diff --git a/src/host_paths/roots.rs b/src/host_paths/roots.rs @@ -0,0 +1,315 @@ +use std::path::{Path, PathBuf}; + +use super::{ + RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimeNamespace, + RadrootsRuntimePathsError, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsPaths { + pub config: PathBuf, + pub data: PathBuf, + pub cache: PathBuf, + pub logs: PathBuf, + pub run: PathBuf, + pub secrets: PathBuf, +} + +impl RadrootsPaths { + #[must_use] + pub fn from_base_root(base_root: impl AsRef<Path>) -> Self { + let base_root = base_root.as_ref(); + Self { + config: base_root.join("config"), + data: base_root.join("data"), + cache: base_root.join("cache"), + logs: base_root.join("logs"), + run: base_root.join("run"), + secrets: base_root.join("secrets"), + } + } + + #[must_use] + pub fn namespaced(&self, namespace: &RadrootsRuntimeNamespace) -> Self { + let relative = namespace.relative_path(); + Self { + config: self.config.join(&relative), + data: self.data.join(&relative), + cache: self.cache.join(&relative), + logs: self.logs.join(&relative), + run: self.run.join(&relative), + secrets: self.secrets.join(relative), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RadrootsPathOverrides { + pub repo_local_root: Option<PathBuf>, + pub mobile_roots: Option<RadrootsPaths>, +} + +impl RadrootsPathOverrides { + #[must_use] + pub fn repo_local(base_root: impl Into<PathBuf>) -> Self { + Self { + repo_local_root: Some(base_root.into()), + mobile_roots: None, + } + } + + #[must_use] + pub fn mobile(roots: RadrootsPaths) -> Self { + Self { + repo_local_root: None, + mobile_roots: Some(roots), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsPathResolver { + platform: RadrootsPlatform, + host_environment: RadrootsHostEnvironment, +} + +impl RadrootsPathResolver { + #[must_use] + pub fn new(platform: RadrootsPlatform, host_environment: RadrootsHostEnvironment) -> Self { + Self { + platform, + host_environment, + } + } + + #[must_use] + pub fn current() -> Self { + Self::new( + RadrootsPlatform::current(), + RadrootsHostEnvironment::from_current_process(), + ) + } + + #[must_use] + pub fn platform(&self) -> RadrootsPlatform { + self.platform + } + + pub fn resolve( + &self, + profile: RadrootsPathProfile, + overrides: &RadrootsPathOverrides, + ) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { + match profile { + RadrootsPathProfile::InteractiveUser => self.resolve_interactive_user(), + RadrootsPathProfile::ServiceHost => self.resolve_service_host(), + RadrootsPathProfile::RepoLocal => overrides + .repo_local_root + .as_ref() + .map(RadrootsPaths::from_base_root) + .ok_or(RadrootsRuntimePathsError::MissingRepoLocalRoot), + RadrootsPathProfile::MobileNative => match self.platform { + RadrootsPlatform::Android | RadrootsPlatform::Ios => overrides + .mobile_roots + .clone() + .ok_or(RadrootsRuntimePathsError::MissingMobileRoots), + _ => Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile, + platform: self.platform, + }), + }, + } + } + + fn resolve_interactive_user(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { + match self.platform { + RadrootsPlatform::Linux | RadrootsPlatform::Macos => self + .host_environment + .home_dir + .as_ref() + .map(|home| RadrootsPaths::from_base_root(home.join(".radroots"))) + .ok_or(RadrootsRuntimePathsError::MissingHomeDir { + platform: self.platform, + }), + RadrootsPlatform::Windows => { + let appdata = self + .host_environment + .appdata_dir + .as_ref() + .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?; + let localappdata = self + .host_environment + .localappdata_dir + .as_ref() + .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?; + let config_root = appdata.join("Radroots"); + let local_root = localappdata.join("Radroots"); + Ok(RadrootsPaths { + config: config_root.join("config"), + data: local_root.join("data"), + cache: local_root.join("cache"), + logs: local_root.join("logs"), + run: local_root.join("run"), + secrets: config_root.join("secrets"), + }) + } + RadrootsPlatform::Android | RadrootsPlatform::Ios => { + Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile: RadrootsPathProfile::InteractiveUser, + platform: self.platform, + }) + } + } + } + + fn resolve_service_host(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { + match self.platform { + RadrootsPlatform::Windows => { + let programdata = self + .host_environment + .programdata_dir + .as_ref() + .ok_or(RadrootsRuntimePathsError::MissingWindowsProgramDataDir)?; + let base = programdata.join("Radroots"); + Ok(RadrootsPaths { + config: base.join("config"), + data: base.join("data"), + cache: base.join("cache"), + logs: base.join("logs"), + run: base.join("run"), + secrets: base.join("secrets"), + }) + } + RadrootsPlatform::Linux | RadrootsPlatform::Macos => Ok(RadrootsPaths { + config: PathBuf::from("/etc/radroots"), + data: PathBuf::from("/var/lib/radroots"), + cache: PathBuf::from("/var/cache/radroots"), + logs: PathBuf::from("/var/log/radroots"), + run: PathBuf::from("/run/radroots"), + secrets: PathBuf::from("/etc/radroots/secrets"), + }), + RadrootsPlatform::Android | RadrootsPlatform::Ios => { + Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile: RadrootsPathProfile::ServiceHost, + platform: self.platform, + }) + } + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::{ + RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimePathsError, + }; + use super::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths}; + + #[test] + fn path_override_helpers_only_populate_their_owned_slot() { + let repo_local = RadrootsPathOverrides::repo_local("/repo/.local/radroots"); + assert_eq!( + repo_local.repo_local_root, + Some(PathBuf::from("/repo/.local/radroots")) + ); + assert!(repo_local.mobile_roots.is_none()); + + let mobile_roots = RadrootsPaths::from_base_root("/sandbox"); + let mobile = RadrootsPathOverrides::mobile(mobile_roots.clone()); + assert!(mobile.repo_local_root.is_none()); + assert_eq!(mobile.mobile_roots, Some(mobile_roots)); + } + + #[test] + fn resolver_current_uses_process_platform_and_environment() { + let resolver = RadrootsPathResolver::current(); + assert_eq!(resolver.platform(), RadrootsPlatform::current()); + assert_eq!( + resolver, + RadrootsPathResolver::new( + RadrootsPlatform::current(), + RadrootsHostEnvironment::from_current_process() + ) + ); + } + + #[test] + fn mobile_profile_is_rejected_on_non_mobile_platforms() { + let resolver = + RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); + + let err = resolver + .resolve( + RadrootsPathProfile::MobileNative, + &RadrootsPathOverrides::default(), + ) + .expect_err("mobile profile should be rejected on linux"); + + assert_eq!( + err, + RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile: RadrootsPathProfile::MobileNative, + platform: RadrootsPlatform::Linux, + } + ); + } + + #[test] + fn interactive_user_is_rejected_on_mobile_platforms() { + for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] { + let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default()); + let err = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect_err("interactive_user should be unsupported on mobile"); + assert_eq!( + err, + RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile: RadrootsPathProfile::InteractiveUser, + platform, + } + ); + } + } + + #[test] + fn service_host_windows_requires_programdata() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Windows, + RadrootsHostEnvironment::default(), + ); + + let err = resolver + .resolve( + RadrootsPathProfile::ServiceHost, + &RadrootsPathOverrides::default(), + ) + .expect_err("service_host on windows should require programdata"); + + assert_eq!(err, RadrootsRuntimePathsError::MissingWindowsProgramDataDir); + } + + #[test] + fn service_host_is_rejected_on_mobile_platforms() { + for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] { + let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default()); + let err = resolver + .resolve( + RadrootsPathProfile::ServiceHost, + &RadrootsPathOverrides::default(), + ) + .expect_err("service_host should be unsupported on mobile"); + assert_eq!( + err, + RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile: RadrootsPathProfile::ServiceHost, + platform, + } + ); + } + } +} diff --git a/src/host_runtime.rs b/src/host_runtime.rs @@ -0,0 +1,131 @@ +//! RHI-owned process lifecycle and retry policy. + +use core::future::Future; +use core::time::Duration; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +use clap::{ArgAction, Args, ValueHint}; +use serde::{Deserialize, Serialize}; + +#[derive(Args, Debug, Clone)] +pub struct ServiceCliArgs { + #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)] + pub config: Option<PathBuf>, + #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)] + pub identity: Option<PathBuf>, + #[arg(long, action = ArgAction::SetTrue)] + pub allow_generate_identity: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NostrServiceConfig { + pub logs_dir: String, + #[serde(default)] + pub relays: Vec<String>, + #[serde(default)] + pub nip89_identifier: Option<String>, + #[serde(default)] + pub nip89_extra_tags: Vec<Vec<String>>, +} + +const fn default_base_ms() -> u64 { + 500 +} +const fn default_max_ms() -> u64 { + 30_000 +} +const fn default_factor() -> u32 { + 2 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct BackoffConfig { + #[serde(default = "default_base_ms")] + pub base_ms: u64, + #[serde(default = "default_max_ms")] + pub max_ms: u64, + #[serde(default = "default_factor")] + pub factor: u32, + #[serde(default)] + pub jitter_ms: u64, +} + +impl Default for BackoffConfig { + fn default() -> Self { + Self { + base_ms: default_base_ms(), + max_ms: default_max_ms(), + factor: default_factor(), + jitter_ms: 0, + } + } +} + +impl BackoffConfig { + fn delay_for_attempt(&self, attempt: u32) -> Duration { + let base = self.base_ms.max(1); + let max = self.max_ms.max(base); + let factor = u64::from(self.factor.max(1)); + let mut delay = base; + for _ in 0..attempt.saturating_sub(1).min(10) { + delay = delay.saturating_mul(factor).min(max); + } + if self.jitter_ms > 0 { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .subsec_nanos(); + delay = delay + .saturating_add(u64::from(nanos) % (self.jitter_ms + 1)) + .min(max); + } + Duration::from_millis(delay) + } +} + +#[derive(Debug, Clone)] +pub struct Backoff { + config: BackoffConfig, + attempt: u32, +} + +impl Backoff { + pub fn new(config: BackoffConfig) -> Self { + Self { config, attempt: 0 } + } + pub fn reset(&mut self) { + self.attempt = 0; + } + pub fn next_delay(&mut self) -> Duration { + self.attempt = self.attempt.saturating_add(1); + self.config.delay_for_attempt(self.attempt) + } +} + +pub async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("install Ctrl+C handler") + }; + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("install termination handler") + .recv() + .await; + }; + #[cfg(not(unix))] + let terminate = core::future::pending::<()>(); + wait_for_shutdown(ctrl_c, terminate).await; +} + +async fn wait_for_shutdown<C, T>(ctrl_c: C, terminate: T) +where + C: Future<Output = ()>, + T: Future<Output = ()>, +{ + tokio::select! { _ = ctrl_c => {}, _ = terminate => {} } +} diff --git a/src/identity_storage.rs b/src/identity_storage.rs @@ -1,78 +1,364 @@ +use std::ffi::OsString; +use std::fs::{self, OpenOptions}; +use std::io::Write; use std::path::{Path, PathBuf}; -use anyhow::Result; -use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityFile}; +use chacha20poly1305::aead::{Aead, KeyInit, Payload}; +use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; +use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; +use radroots_secrets::error::Operation; +use radroots_secrets::id::{BackendKind, KeyVersion}; +use radroots_secrets::wrapping::{ + BoxFuture, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret, +}; +use radroots_secrets::{EncryptedEnvelope, KeyWrapping, SecretId, SecretRef}; +use zeroize::Zeroize; + +use crate::host_identity::{ + IdentityError, RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityPublic, +}; const RHI_IDENTITY_KEY_SLOT: &str = "rhi_identity"; +const WRAPPING_KEY_BYTES: usize = 32; +const WRAPPING_NONCE_BYTES: usize = 24; +const WRAPPED_KEY_VERSION: u8 = 1; -#[cfg(test)] pub fn encrypted_identity_key_path(path: impl AsRef<Path>) -> PathBuf { - radroots_runtime::local_wrapping_key_path(path) + encrypted_identity_wrapping_key_path(path) } pub fn load_service_identity( path: Option<&Path>, allow_generate: bool, -) -> Result<RadrootsIdentity> { - let path = resolved_identity_path(path); +) -> Result<RadrootsIdentity, IdentityError> { + let path = path.map(Path::to_path_buf).unwrap_or_else(|| { + crate::paths::default_identity_path_for_process() + .expect("resolve canonical rhi identity path") + }); if path.exists() { - return load_encrypted_identity(&path); + return load_encrypted_identity(path); } if !allow_generate { - return Err(IdentityError::GenerationNotAllowed(path).into()); + return Err(IdentityError::GenerationNotAllowed(path)); } - let identity = RadrootsIdentity::generate(); - store_encrypted_identity(&path, &identity)?; + store_encrypted_identity(path, &identity)?; Ok(identity) } -pub fn store_encrypted_identity(path: impl AsRef<Path>, identity: &RadrootsIdentity) -> Result<()> { +struct RhiFileKeyWrapping { + key_path: PathBuf, +} + +impl RhiFileKeyWrapping { + fn new(identity_path: &Path) -> Self { + Self { + key_path: encrypted_identity_wrapping_key_path(identity_path), + } + } + + fn load_or_create_key(&self) -> Result<[u8; WRAPPING_KEY_BYTES], radroots_secrets::Error> { + if let Ok(raw) = fs::read(&self.key_path) { + return key_from_bytes(raw.as_slice()); + } + if let Some(parent) = self + .key_path + .parent() + .filter(|path| !path.as_os_str().is_empty()) + { + fs::create_dir_all(parent).map_err(|_| secret_backend_failure(Operation::Provision))?; + } + let key: [u8; WRAPPING_KEY_BYTES] = rand::random(); + match OpenOptions::new() + .write(true) + .create_new(true) + .open(&self.key_path) + { + Ok(mut file) => { + file.write_all(&key) + .map_err(|_| secret_backend_failure(Operation::Write))?; + file.sync_all() + .map_err(|_| secret_backend_failure(Operation::Write))?; + set_secret_permissions(&self.key_path) + .map_err(|_| secret_backend_failure(Operation::Write))?; + Ok(key) + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + let raw = fs::read(&self.key_path) + .map_err(|_| secret_backend_failure(Operation::Read))?; + key_from_bytes(raw.as_slice()) + } + Err(_) => Err(secret_backend_failure(Operation::Provision)), + } + } + + fn load_key(&self) -> Result<[u8; WRAPPING_KEY_BYTES], radroots_secrets::Error> { + let raw = fs::read(&self.key_path).map_err(|_| secret_backend_failure(Operation::Read))?; + key_from_bytes(raw.as_slice()) + } +} + +impl KeyWrapping for RhiFileKeyWrapping { + fn wrap<'a>( + &'a self, + request: WrapRequest<'a>, + ) -> BoxFuture<'a, Result<WrappedSecret, radroots_secrets::Error>> { + Box::pin(async move { + let mut key = self.load_or_create_key()?; + let nonce: [u8; WRAPPING_NONCE_BYTES] = rand::random(); + let ciphertext = request.plaintext().expose_secret(|plaintext| { + XChaCha20Poly1305::new(Key::from_slice(&key)).encrypt( + XNonce::from_slice(&nonce), + Payload { + msg: plaintext, + aad: request.reference().id().as_str().as_bytes(), + }, + ) + }); + key.zeroize(); + let ciphertext = ciphertext.map_err(|_| secret_backend_failure(Operation::Wrap))?; + let mut wrapped = Vec::with_capacity(1 + nonce.len() + ciphertext.len()); + wrapped.push(WRAPPED_KEY_VERSION); + wrapped.extend_from_slice(&nonce); + wrapped.extend_from_slice(ciphertext.as_slice()); + WrappedSecret::from_bytes(wrapped) + }) + } + + fn unwrap<'a>( + &'a self, + request: UnwrapRequest<'a>, + ) -> BoxFuture<'a, Result<SecretMaterial, radroots_secrets::Error>> { + Box::pin(async move { + let wrapped = request.wrapped().as_bytes(); + if wrapped.len() <= 1 + WRAPPING_NONCE_BYTES || wrapped[0] != WRAPPED_KEY_VERSION { + return Err(secret_backend_failure(Operation::Unwrap)); + } + let mut key = self.load_key()?; + let plaintext = XChaCha20Poly1305::new(Key::from_slice(&key)).decrypt( + XNonce::from_slice(&wrapped[1..1 + WRAPPING_NONCE_BYTES]), + Payload { + msg: &wrapped[1 + WRAPPING_NONCE_BYTES..], + aad: request.reference().id().as_str().as_bytes(), + }, + ); + key.zeroize(); + SecretMaterial::from_slice( + &plaintext.map_err(|_| secret_backend_failure(Operation::Unwrap))?, + ) + }) + } +} + +fn identity_secret_ref() -> Result<SecretRef, radroots_secrets::Error> { + Ok(SecretRef::new( + SecretId::parse(RHI_IDENTITY_KEY_SLOT)?, + BackendKind::External, + KeyVersion::new(1)?, + )) +} + +fn secret_backend_failure(operation: Operation) -> radroots_secrets::Error { + radroots_secrets::Error::BackendFailure { + backend: BackendKind::External, + operation, + } +} + +fn key_from_bytes(raw: &[u8]) -> Result<[u8; WRAPPING_KEY_BYTES], radroots_secrets::Error> { + raw.try_into() + .map_err(|_| secret_backend_failure(Operation::Read)) +} + +fn storage_error(path: &Path, operation: &str) -> IdentityError { + IdentityError::ProtectedStorage { + path: path.to_path_buf(), + message: operation.to_owned(), + } +} + +pub fn encrypted_identity_wrapping_key_path(path: impl AsRef<Path>) -> PathBuf { + let mut value = OsString::from(path.as_ref().as_os_str()); + value.push(".key"); + PathBuf::from(value) +} + +pub fn store_encrypted_identity( + path: impl AsRef<Path>, + identity: &RadrootsIdentity, +) -> Result<(), IdentityError> { + let path = path.as_ref(); + if let Some(parent) = path.parent().filter(|value| !value.as_os_str().is_empty()) { + fs::create_dir_all(parent) + .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?; + } let payload = serde_json::to_vec(&identity.to_file())?; - radroots_runtime::seal_local_secret_file(path, RHI_IDENTITY_KEY_SLOT, &payload)?; + let plaintext = SecretMaterial::from_slice(payload.as_slice()) + .map_err(|_| storage_error(path, "validate identity secret material"))?; + let data_key = SecretMaterial::from_slice(&rand::random::<[u8; 32]>()) + .map_err(|_| storage_error(path, "validate identity data key"))?; + let wrapping = RhiFileKeyWrapping::new(path); + let envelope = futures_executor::block_on(EncryptedEnvelope::seal( + &wrapping, + SealRequest::new( + identity_secret_ref().map_err(|_| storage_error(path, "build identity reference"))?, + &plaintext, + SealMaterial::new(data_key, Nonce::new(rand::random())), + ), + )) + .map_err(|_| storage_error(path, "seal encrypted identity"))?; + let encoded = envelope + .encode() + .map_err(|_| storage_error(path, "encode encrypted identity"))?; + atomic_write(path, encoded.as_slice()) +} + +pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentity, IdentityError> { + let path = path.as_ref(); + let encoded = fs::read(path).map_err(|source| { + if source.kind() == std::io::ErrorKind::NotFound { + IdentityError::NotFound(path.to_path_buf()) + } else { + IdentityError::Read(path.to_path_buf(), source) + } + })?; + let envelope = EncryptedEnvelope::decode(encoded.as_slice()) + .map_err(|_| storage_error(path, "decode encrypted identity"))?; + let wrapping = RhiFileKeyWrapping::new(path); + let payload = futures_executor::block_on(envelope.open(&wrapping)) + .map_err(|_| storage_error(path, "open encrypted identity"))?; + let file: RadrootsIdentityFile = payload + .expose_secret(|bytes| serde_json::from_slice(bytes)) + .map_err(IdentityError::from)?; + RadrootsIdentity::try_from(file) +} + +pub fn rotate_encrypted_identity(path: impl AsRef<Path>) -> Result<(), IdentityError> { + let path = path.as_ref(); + let identity = load_encrypted_identity(path)?; + let key_path = encrypted_identity_wrapping_key_path(path); + let old_key = + fs::read(&key_path).map_err(|source| IdentityError::Read(key_path.clone(), source))?; + fs::remove_file(&key_path).map_err(|source| IdentityError::Write(key_path.clone(), source))?; + if let Err(error) = store_encrypted_identity(path, &identity) { + fs::write(&key_path, old_key) + .map_err(|source| IdentityError::Write(key_path.clone(), source))?; + set_secret_permissions(&key_path) + .map_err(|source| IdentityError::Write(key_path, source))?; + return Err(error); + } Ok(()) } -pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentity> { - let payload = radroots_runtime::open_local_secret_file(path, RHI_IDENTITY_KEY_SLOT)?; - let file: RadrootsIdentityFile = serde_json::from_slice(&payload)?; - Ok(RadrootsIdentity::try_from(file)?) +pub fn load_identity_profile( + path: impl AsRef<Path>, +) -> Result<RadrootsIdentityPublic, IdentityError> { + let path = path.as_ref(); + let encoded = fs::read(path).map_err(|source| { + if source.kind() == std::io::ErrorKind::NotFound { + IdentityError::NotFound(path.to_path_buf()) + } else { + IdentityError::Read(path.to_path_buf(), source) + } + })?; + serde_json::from_slice(encoded.as_slice()).map_err(IdentityError::from) } -fn resolved_identity_path(path: Option<&Path>) -> PathBuf { - path.map(Path::to_path_buf).unwrap_or_else(|| { - crate::paths::default_identity_path_for_process() - .expect("resolve canonical rhi identity path") - }) +pub fn store_identity_profile( + path: impl AsRef<Path>, + identity: &RadrootsIdentity, +) -> Result<(), IdentityError> { + let encoded = serde_json::to_vec_pretty(&identity.to_public())?; + atomic_write(path.as_ref(), encoded.as_slice()) +} + +fn atomic_write(path: &Path, encoded: &[u8]) -> Result<(), IdentityError> { + let parent = path + .parent() + .filter(|value| !value.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(parent) + .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?; + let mut temporary = tempfile::NamedTempFile::new_in(parent) + .map_err(|source| IdentityError::Write(path.to_path_buf(), source))?; + temporary + .write_all(encoded) + .and_then(|()| temporary.as_file().sync_all()) + .map_err(|source| IdentityError::Write(path.to_path_buf(), source))?; + set_file_permissions(temporary.as_file()) + .map_err(|source| IdentityError::Write(path.to_path_buf(), source))?; + temporary + .persist(path) + .map_err(|error| IdentityError::Write(path.to_path_buf(), error.error))?; + fs::File::open(parent) + .and_then(|directory| directory.sync_all()) + .map_err(|source| IdentityError::Write(path.to_path_buf(), source)) +} + +#[cfg(unix)] +fn set_secret_permissions(path: &Path) -> std::io::Result<()> { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o600)) +} + +#[cfg(not(unix))] +fn set_secret_permissions(_path: &Path) -> std::io::Result<()> { + Ok(()) +} + +fn set_file_permissions(file: &fs::File) -> std::io::Result<()> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + file.set_permissions(fs::Permissions::from_mode(0o600)) + } + #[cfg(not(unix))] + { + let _ = file; + Ok(()) + } } #[cfg(test)] mod tests { - use super::{encrypted_identity_key_path, load_service_identity}; + use super::*; + + fn identity() -> RadrootsIdentity { + RadrootsIdentity::from_secret_key_str( + "1111111111111111111111111111111111111111111111111111111111111111", + ) + .expect("identity") + } #[test] - fn load_service_identity_generates_encrypted_identity_artifacts() { + fn encrypted_identity_round_trips_and_rotates_wrapping_key() { let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("rhi-identity.secret.json"); - - let generated = - load_service_identity(Some(&path), true).expect("generate encrypted identity"); - let loaded = load_service_identity(Some(&path), false).expect("load encrypted identity"); - - assert_eq!(generated.id(), loaded.id()); - assert!(path.is_file()); - assert!(encrypted_identity_key_path(&path).is_file()); + let path = temp.path().join("identity.enc"); + let identity = identity(); + store_encrypted_identity(&path, &identity).expect("store"); + let key_path = encrypted_identity_wrapping_key_path(&path); + let before = fs::read(&key_path).expect("key before"); + assert_eq!( + load_encrypted_identity(&path).expect("load").id(), + identity.id() + ); + rotate_encrypted_identity(&path).expect("rotate"); + assert_ne!(before, fs::read(key_path).expect("key after")); + assert_eq!( + load_encrypted_identity(&path).expect("load").id(), + identity.id() + ); } #[test] - fn load_service_identity_fails_when_wrapping_key_is_missing() { + fn public_profile_round_trips() { let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("rhi-identity.secret.json"); - let _ = load_service_identity(Some(&path), true).expect("generate encrypted identity"); - std::fs::remove_file(encrypted_identity_key_path(&path)).expect("remove wrapping key"); - - let err = load_service_identity(Some(&path), false) - .expect_err("missing wrapping key should fail"); - assert!(err.to_string().contains("identity")); + let path = temp.path().join("identity.json"); + let identity = identity(); + store_identity_profile(&path, &identity).expect("store profile"); + assert_eq!( + load_identity_profile(path).expect("load profile").id, + identity.id() + ); } } diff --git a/src/lib.rs b/src/lib.rs @@ -4,6 +4,10 @@ pub mod adapters; pub mod cli; pub mod config; pub mod features; +pub mod host_identity; +pub mod host_nostr; +pub mod host_paths; +pub mod host_runtime; pub mod identity_storage; pub mod paths; pub mod rhi; @@ -12,8 +16,8 @@ pub use cli::Args as cli_args; use anyhow::{Context, Result, anyhow, bail}; use radroots_event::{ - kinds::TRADE_MUTATION_EVENT_KINDS, - profile::{RadrootsAuthoredProfile, RadrootsNip05Identifier}, + envelope::kind::TRADE_MUTATION_EVENT_KINDS, + profile::{AuthoredProfile, Nip05Identifier}, }; use std::time::Duration; @@ -21,12 +25,10 @@ use crate::features::trade_agreement_attestation::{ TradeAgreementAttestationRuntime, TradeAgreementAttestationRuntimeConfig, trade_mutation_subscription_kinds, }; +use crate::host_nostr::{ApplicationHandlerSpec, Metadata, ProfileBuilder}; use crate::identity_storage::load_service_identity; use crate::rhi::{Rhi, start_subscriber_with_policy}; -use radroots_nostr::prelude::{ - RadrootsNostrApplicationHandlerSpec, RadrootsNostrMetadata, RadrootsNostrProfileEventBuilder, - radroots_nostr_build_profile_event, radroots_nostr_publish_application_handler, -}; +use radroots_nostr::event::{build_application_handler, build_profile}; use tracing::{info, warn}; #[cfg(test)] @@ -74,9 +76,9 @@ fn take_bootstrap_hook_result() -> Option<Result<(), String>> { } async fn bootstrap_presence( - client: &radroots_nostr::prelude::RadrootsNostrClient, - metadata: &RadrootsNostrMetadata, - handler_spec: &RadrootsNostrApplicationHandlerSpec, + client: &crate::host_nostr::Client, + metadata: &Metadata, + handler_spec: &ApplicationHandlerSpec, ) -> Result<()> { if let Some(result) = take_bootstrap_hook_result() { return result.map_err(anyhow::Error::msg); @@ -85,26 +87,31 @@ async fn bootstrap_presence( client.connect().await; client.wait_for_connection(Duration::from_secs(5)).await; - let builder = build_authored_service_profile_event(metadata)?; + let profile_event = build_authored_service_profile_event(metadata)? + .sign_with_keys(client.keys()) + .context("sign strict RHI service Profile")?; client - .send_profile_event_builder(builder) + .send_event(&profile_event) .await .context("publish strict RHI service Profile")?; - radroots_nostr_publish_application_handler(client, handler_spec) + let handler_event = build_application_handler(handler_spec) + .context("build RHI application-handler event")? + .sign_with_keys(client.keys()) + .context("sign RHI application-handler event")?; + client + .send_event(&handler_event) .await .context("publish RHI application-handler event")?; Ok(()) } -fn build_authored_service_profile_event( - metadata: &RadrootsNostrMetadata, -) -> Result<RadrootsNostrProfileEventBuilder> { +fn build_authored_service_profile_event(metadata: &Metadata) -> Result<ProfileBuilder> { let profile = authored_service_profile(metadata)?; - radroots_nostr_build_profile_event(&profile).context("build strict RHI service Profile event") + build_profile(&profile).context("build strict RHI service Profile event") } -fn authored_service_profile(metadata: &RadrootsNostrMetadata) -> Result<RadrootsAuthoredProfile> { +fn authored_service_profile(metadata: &Metadata) -> Result<AuthoredProfile> { if metadata.picture.is_some() || metadata.banner.is_some() { bail!( "RHI service Profile media requires byte-verified Blossom descriptors and proven BUD-02 upload completion" @@ -119,7 +126,7 @@ fn authored_service_profile(metadata: &RadrootsNostrMetadata) -> Result<Radroots .clone() .ok_or_else(|| anyhow!("RHI service Profile requires metadata.name"))?; let mut profile = - RadrootsAuthoredProfile::new(name).context("validate strict RHI service Profile name")?; + AuthoredProfile::new(name).context("validate strict RHI service Profile name")?; if let Some(display_name) = metadata.display_name.as_ref() { profile = profile.with_display_name(display_name.clone()); } @@ -128,7 +135,7 @@ fn authored_service_profile(metadata: &RadrootsNostrMetadata) -> Result<Radroots } if let Some(nip05) = metadata.nip05.as_deref() { profile = profile.with_nip05( - RadrootsNip05Identifier::parse(nip05) + Nip05Identifier::parse(nip05) .context("validate strict RHI service Profile NIP-05 identifier")?, ); } @@ -160,7 +167,7 @@ async fn wait_for_shutdown_or_stopped(handle: crate::rhi::RhiHandle) -> RunRhiWa } tokio::select! { - _ = radroots_runtime::shutdown_signal() => RunRhiWaitOutcome::Shutdown, + _ = crate::host_runtime::shutdown_signal() => RunRhiWaitOutcome::Shutdown, _ = handle.stopped() => RunRhiWaitOutcome::Stopped, } } @@ -196,14 +203,13 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> if !relays.is_empty() { let handler_kinds = trade_mutation_subscription_kinds(); - let handler_spec = RadrootsNostrApplicationHandlerSpec { - kinds: handler_kinds, - identifier: service_cfg.nip89_identifier.clone(), - metadata: Some(md.clone()), - extra_tags: service_cfg.nip89_extra_tags.clone(), - relays: relays.clone(), - nostrconnect_url: None, - }; + let mut handler_spec = ApplicationHandlerSpec::new(handler_kinds) + .with_metadata(md.clone()) + .with_extra_tags(service_cfg.nip89_extra_tags.clone()) + .with_relays(relays.clone()); + if let Some(identifier) = service_cfg.nip89_identifier.clone() { + handler_spec = handler_spec.with_identifier(identifier); + } if let Err(e) = bootstrap_presence(&client, &md, &handler_spec).await { warn!("Failed to publish service presence on startup: {e}"); } else { @@ -260,17 +266,15 @@ mod tests { run_rhi, run_rhi_bootstrap_hook, run_rhi_wait_hook, }; use crate::{cli_args, config}; - use radroots_event::kinds::TRADE_MUTATION_EVENT_KINDS; + use radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS; use std::path::PathBuf; use std::sync::atomic::Ordering; - use std::sync::{Mutex, MutexGuard}; + use tokio::sync::{Mutex, MutexGuard}; - static TEST_LOCK: Mutex<()> = Mutex::new(()); + static TEST_LOCK: Mutex<()> = Mutex::const_new(()); - fn test_guard() -> MutexGuard<'static, ()> { - let guard = TEST_LOCK - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); + async fn test_guard() -> MutexGuard<'static, ()> { + let guard = TEST_LOCK.lock().await; RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed); RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed); *run_rhi_bootstrap_hook() @@ -286,7 +290,7 @@ mod tests { config::Settings { metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"), config: config::Configuration { - service: radroots_runtime::RadrootsNostrServiceConfig { + service: crate::host_runtime::NostrServiceConfig { logs_dir: std::env::temp_dir() .join("rhi-test-logs") .display() @@ -301,7 +305,7 @@ mod tests { stdout: true, }, subscriber: config::SubscriberConfig { - backoff: radroots_runtime::BackoffConfig { + backoff: crate::host_runtime::BackoffConfig { base_ms: 1, max_ms: 2, factor: 1, @@ -321,7 +325,7 @@ mod tests { fn args_for_identity(path: PathBuf) -> cli_args { cli_args { command: None, - service: radroots_runtime::RadrootsServiceCliArgs { + service: crate::host_runtime::ServiceCliArgs { config: Some(PathBuf::from("config.toml")), identity: Some(path), allow_generate_identity: true, @@ -349,7 +353,7 @@ mod tests { #[tokio::test] async fn run_rhi_starts_and_stops_without_relays() { - let _guard = test_guard(); + let _guard = test_guard().await; RUN_RHI_AUTO_STOP.store(true, Ordering::Relaxed); let identity_path = unique_identity_path("no-relays"); let args = args_for_identity(identity_path); @@ -359,7 +363,7 @@ mod tests { #[tokio::test] async fn run_rhi_bootstraps_release_product_handler_kinds_when_relays_exist() { - let _guard = test_guard(); + let _guard = test_guard().await; RUN_RHI_SKIP_SUBSCRIBER.store(true, Ordering::Relaxed); *run_rhi_bootstrap_hook() .lock() @@ -373,7 +377,7 @@ mod tests { #[tokio::test] async fn run_rhi_stops_on_wait_hook() { - let _guard = test_guard(); + let _guard = test_guard().await; *run_rhi_wait_hook() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunRhiWaitOutcome::Stopped); @@ -385,23 +389,19 @@ mod tests { #[tokio::test] async fn bootstrap_presence_reports_hook_error() { - let _guard = test_guard(); + let _guard = test_guard().await; *run_rhi_bootstrap_hook() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Err("forced bootstrap failure".to_string())); - let keys = radroots_nostr::prelude::RadrootsNostrKeys::generate(); - let client = radroots_nostr::prelude::RadrootsNostrClient::new(keys.clone()); - let metadata: radroots_nostr::prelude::RadrootsNostrMetadata = + let keys = crate::host_nostr::Keys::generate(); + let client = crate::host_nostr::Client::new(keys.clone()); + let metadata: crate::host_nostr::Metadata = serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"); - let spec = radroots_nostr::prelude::RadrootsNostrApplicationHandlerSpec { - kinds: TRADE_MUTATION_EVENT_KINDS.to_vec(), - identifier: Some("rhi".to_string()), - metadata: Some(metadata.clone()), - extra_tags: Vec::new(), - relays: Vec::new(), - nostrconnect_url: None, - }; + let spec = + crate::host_nostr::ApplicationHandlerSpec::new(TRADE_MUTATION_EVENT_KINDS.to_vec()) + .with_identifier("rhi") + .with_metadata(metadata.clone()); let err = bootstrap_presence(&client, &metadata, &spec) .await .expect_err("forced error"); @@ -410,7 +410,7 @@ mod tests { #[test] fn service_profile_uses_only_strict_authored_fields() { - let metadata: radroots_nostr::prelude::RadrootsNostrMetadata = serde_json::from_str( + let metadata: crate::host_nostr::Metadata = serde_json::from_str( r#"{ "name":"rhi", "display_name":"Radroots agreement attestation", @@ -437,7 +437,7 @@ mod tests { assert!(profile.picture().is_none()); assert!(profile.banner().is_none()); - let keys = radroots_nostr::prelude::RadrootsNostrKeys::generate(); + let keys = crate::host_nostr::Keys::generate(); let event = build_authored_service_profile_event(&metadata) .expect("strict Profile event") .sign_with_keys(&keys) diff --git a/src/main.rs b/src/main.rs @@ -6,8 +6,6 @@ use anyhow::Result; #[cfg(not(test))] use clap::Parser; #[cfg(not(test))] -use radroots_log::{LogFileLayout, LoggingOptions}; -#[cfg(not(test))] use rhi::cli::Command; #[cfg(not(test))] use rhi::features::trade_agreement_attestation::run_smoke_cli_command; @@ -39,12 +37,14 @@ fn exit_code_from_run(result: Result<()>) -> ExitCode { } #[cfg(test)] -static RUN_LOAD_HOOK: std::sync::OnceLock< - std::sync::Mutex<Option<Result<(cli_args, config::Settings)>>>, -> = std::sync::OnceLock::new(); +type RunLoadHookValue = Option<Result<(cli_args, config::Settings)>>; +#[cfg(test)] +type RunLoadHook = std::sync::Mutex<RunLoadHookValue>; +#[cfg(test)] +static RUN_LOAD_HOOK: std::sync::OnceLock<RunLoadHook> = std::sync::OnceLock::new(); #[cfg(test)] -fn run_load_hook() -> &'static std::sync::Mutex<Option<Result<(cli_args, config::Settings)>>> { +fn run_load_hook() -> &'static RunLoadHook { RUN_LOAD_HOOK.get_or_init(|| std::sync::Mutex::new(None)) } @@ -78,12 +78,12 @@ fn load_args_and_settings() -> Result<(cli_args, config::Settings)> { { return result; } - return Err(anyhow::anyhow!("run loader hook not set")); + Err(anyhow::anyhow!("run loader hook not set")) } #[cfg(not(test))] { - let args = cli_args::try_parse().map_err(radroots_runtime::RuntimeCliError::from)?; + let args = cli_args::try_parse()?; let config_path = args .service .config @@ -99,15 +99,31 @@ fn load_args_and_settings() -> Result<(cli_args, config::Settings)> { #[cfg(not(test))] fn init_rhi_logging(settings: &config::Settings) -> Result<()> { - radroots_log::init_logging(LoggingOptions { - dir: Some(settings.config.logging.output_dir.clone()), - file_name: "rhi.log".to_owned(), - stdout: settings.config.logging.stdout, - default_level: Some(settings.config.logging.filter.clone()), - file_layout: LogFileLayout::PrefixedDate, - ..LoggingOptions::default() - }) - .context("initialize logging") + use tracing_subscriber::fmt::writer::MakeWriterExt as _; + + std::fs::create_dir_all(&settings.config.logging.output_dir) + .context("create RHI log directory")?; + let appender = tracing_appender::rolling::daily(&settings.config.logging.output_dir, "rhi.log"); + let (writer, guard) = tracing_appender::non_blocking(appender); + static LOG_GUARD: std::sync::OnceLock<tracing_appender::non_blocking::WorkerGuard> = + std::sync::OnceLock::new(); + let filter = tracing_subscriber::EnvFilter::new(&settings.config.logging.filter); + if settings.config.logging.stdout { + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(writer.and(std::io::stdout)) + .try_init() + .map_err(|error| anyhow::anyhow!("initialize RHI logging: {error}"))?; + } else { + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(writer) + .try_init() + .map_err(|error| anyhow::anyhow!("initialize RHI logging: {error}"))?; + } + LOG_GUARD + .set(guard) + .map_err(|_| anyhow::anyhow!("RHI logging is already initialized")) } fn runtime_startup_report( @@ -215,7 +231,7 @@ fn log_runtime_startup_report(report: &RhiRuntimeStartupReport) { async fn run() -> Result<()> { #[cfg(not(test))] { - let args = cli_args::try_parse().map_err(radroots_runtime::RuntimeCliError::from)?; + let args = cli_args::try_parse()?; if let Some(command) = args.command { return match command { Command::AttestationSmoke { .. } => run_smoke_cli_command(command).await, @@ -244,24 +260,23 @@ mod tests { RhiRuntimeStartupReport, exit_code_from_run, main, run, run_load_hook, run_rhi, runtime_startup_report, }; - use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys}; use rhi::features::trade_agreement_attestation::TradeAgreementAttestationRuntime; + use rhi::host_nostr::{Client, Keys}; use rhi::{cli_args, config, paths}; use std::path::PathBuf; use std::process::ExitCode; - static RUN_HOOK_TEST_LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = - std::sync::OnceLock::new(); + static RUN_HOOK_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); - fn run_hook_test_lock() -> &'static std::sync::Mutex<()> { - RUN_HOOK_TEST_LOCK.get_or_init(|| std::sync::Mutex::new(())) + async fn run_hook_test_guard() -> tokio::sync::MutexGuard<'static, ()> { + RUN_HOOK_TEST_LOCK.lock().await } fn minimal_settings() -> config::Settings { config::Settings { metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"), config: config::Configuration { - service: radroots_runtime::RadrootsNostrServiceConfig { + service: rhi::host_runtime::NostrServiceConfig { logs_dir: std::env::temp_dir() .join("rhi-test-logs") .display() @@ -329,7 +344,7 @@ mod tests { async fn run_rhi_returns_error_when_identity_is_missing() { let args = cli_args { command: None, - service: radroots_runtime::RadrootsServiceCliArgs { + service: rhi::host_runtime::ServiceCliArgs { config: Some(PathBuf::from("config.toml")), identity: Some(PathBuf::from("/tmp/rhi-missing-identity.secret.json")), allow_generate_identity: false, @@ -350,12 +365,10 @@ mod tests { #[tokio::test] async fn run_uses_injected_config_loader_result() { - let _guard = run_hook_test_lock() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); + let _guard = run_hook_test_guard().await; let args = cli_args { command: None, - service: radroots_runtime::RadrootsServiceCliArgs { + service: rhi::host_runtime::ServiceCliArgs { config: Some(PathBuf::from("config.toml")), identity: Some(PathBuf::from("/tmp/rhi-run-hook-missing.secret.json")), allow_generate_identity: false, @@ -372,9 +385,7 @@ mod tests { #[tokio::test] async fn run_returns_error_when_loader_hook_is_absent() { - let _guard = run_hook_test_lock() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); + let _guard = run_hook_test_guard().await; *run_load_hook() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = None; @@ -387,13 +398,13 @@ mod tests { #[tokio::test] async fn non_test_start_subscriber_path_can_start_and_stop() { - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); + let keys = Keys::generate(); + let client = Client::new(keys.clone()); let handle = rhi::rhi::start_subscriber( client, keys, TradeAgreementAttestationRuntime::new(), - radroots_runtime::BackoffConfig { + rhi::host_runtime::BackoffConfig { base_ms: 1, max_ms: 2, factor: 1, @@ -409,7 +420,7 @@ mod tests { #[test] fn runtime_startup_report_prefers_explicit_cli_paths() { let args = cli_args { - service: radroots_runtime::RadrootsServiceCliArgs { + service: rhi::host_runtime::ServiceCliArgs { config: Some(PathBuf::from("/tmp/rhi/config.toml")), identity: Some(PathBuf::from("/tmp/rhi/identity.secret.json")), allow_generate_identity: false, @@ -457,7 +468,7 @@ mod tests { fn runtime_startup_report_falls_back_to_canonical_contract_paths() { let args = cli_args { command: None, - service: radroots_runtime::RadrootsServiceCliArgs { + service: rhi::host_runtime::ServiceCliArgs { config: None, identity: None, allow_generate_identity: false, diff --git a/src/paths.rs b/src/paths.rs @@ -1,13 +1,14 @@ use std::path::{Path, PathBuf}; -use anyhow::{Context, Result, bail}; -use radroots_runtime_paths::{ - DEFAULT_CONFIG_FILE_NAME, DEFAULT_SERVICE_IDENTITY_FILE_NAME, RadrootsPathOverrides, - RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace, +use crate::host_paths::{ + RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace, }; +use anyhow::{Context, Result, bail}; use serde::Serialize; const RHI_RUNTIME_ID: &str = "rhi"; +const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; +const DEFAULT_SERVICE_IDENTITY_FILE_NAME: &str = "identity.secret.json"; const SUBSCRIBER_STATE_DIR_NAME: &str = "trade-agreement-attestation"; const SUBSCRIBER_STATE_FILE_NAME: &str = "state.json"; const RHI_PATHS_PROFILE_ENV: &str = "RHI_PATHS_PROFILE"; diff --git a/src/rhi.rs b/src/rhi.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use std::time::{Duration, Instant}; -use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys}; -use radroots_runtime::{Backoff, BackoffConfig}; +use crate::host_nostr::{Client, Keys}; +use crate::host_runtime::{Backoff, BackoffConfig}; use tokio::sync::Mutex; use crate::features::trade_agreement_attestation::{ @@ -33,8 +33,8 @@ fn subscriber_result_hook() } async fn run_subscriber_once( - client: RadrootsNostrClient, - keys: RadrootsNostrKeys, + client: Client, + keys: Keys, runtime: TradeAgreementAttestationRuntime, policy: TradeAgreementAttestationPolicy, stop_rx: tokio::sync::watch::Receiver<bool>, @@ -53,7 +53,7 @@ async fn run_subscriber_once( } async fn wait_for_connection_or_stop( - client: &RadrootsNostrClient, + client: &Client, stop_rx: &mut tokio::sync::watch::Receiver<bool>, ) -> bool { if *stop_rx.borrow() { @@ -67,18 +67,18 @@ async fn wait_for_connection_or_stop( pub struct Rhi { pub(crate) _started: Instant, - pub client: RadrootsNostrClient, + pub client: Client, pub(crate) agreement_attestation_runtime: TradeAgreementAttestationRuntime, pub(crate) agreement_attestation_policy: TradeAgreementAttestationPolicy, } impl Rhi { - pub fn new(keys: RadrootsNostrKeys) -> Self { + pub fn new(keys: Keys) -> Self { Self::with_agreement_attestation_runtime(keys, TradeAgreementAttestationRuntime::new()) } pub fn with_agreement_attestation_runtime( - keys: RadrootsNostrKeys, + keys: Keys, agreement_attestation_runtime: TradeAgreementAttestationRuntime, ) -> Self { Self::with_agreement_attestation_runtime_and_policy( @@ -89,11 +89,11 @@ impl Rhi { } pub fn with_agreement_attestation_runtime_and_policy( - keys: RadrootsNostrKeys, + keys: Keys, agreement_attestation_runtime: TradeAgreementAttestationRuntime, agreement_attestation_policy: TradeAgreementAttestationPolicy, ) -> Self { - let client = RadrootsNostrClient::new(keys); + let client = Client::new(keys); Self { _started: Instant::now(), client, @@ -132,8 +132,8 @@ impl RhiHandle { } pub async fn start_subscriber( - client: RadrootsNostrClient, - keys: RadrootsNostrKeys, + client: Client, + keys: Keys, runtime: TradeAgreementAttestationRuntime, backoff_cfg: BackoffConfig, ) -> RhiHandle { @@ -148,8 +148,8 @@ pub async fn start_subscriber( } pub async fn start_subscriber_with_policy( - client: RadrootsNostrClient, - keys: RadrootsNostrKeys, + client: Client, + keys: Keys, runtime: TradeAgreementAttestationRuntime, policy: TradeAgreementAttestationPolicy, backoff_cfg: BackoffConfig, @@ -212,15 +212,15 @@ mod tests { Rhi, RhiHandle, start_subscriber, subscriber_result_hook, wait_for_connection_or_stop, }; use crate::features::trade_agreement_attestation::TradeAgreementAttestationRuntime; + use crate::host_nostr::{Client, Keys}; + use crate::host_runtime::BackoffConfig; use anyhow::anyhow; - use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys}; - use radroots_runtime::BackoffConfig; use std::sync::Arc; use tokio::sync::Mutex; #[tokio::test] async fn rhi_new_initializes_client_and_runtime() { - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); let rhi = Rhi::new(keys); let _ = rhi.client.clone(); assert!(rhi.agreement_attestation_runtime.reports().await.is_empty()); @@ -242,7 +242,7 @@ mod tests { #[tokio::test] async fn start_subscriber_runs_with_and_without_relay() { - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); let cfg = BackoffConfig { base_ms: 1, max_ms: 2, @@ -250,7 +250,7 @@ mod tests { jitter_ms: 0, }; - let client_err = RadrootsNostrClient::new(keys.clone()); + let client_err = Client::new(keys.clone()); let handle_err = start_subscriber( client_err, keys.clone(), @@ -262,7 +262,7 @@ mod tests { handle_err.stop(); handle_err.stopped().await; - let client_ok = RadrootsNostrClient::new(keys.clone()); + let client_ok = Client::new(keys.clone()); let _ = client_ok.add_relay("wss://relay.example.com").await; subscriber_result_hook() .lock() @@ -282,8 +282,8 @@ mod tests { #[tokio::test] async fn start_subscriber_stops_during_connection_wait_branch() { - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); + let keys = Keys::generate(); + let client = Client::new(keys.clone()); let handle = start_subscriber( client, keys, @@ -303,8 +303,8 @@ mod tests { #[tokio::test] async fn start_subscriber_stops_during_backoff_wait_branch() { - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); + let keys = Keys::generate(); + let client = Client::new(keys.clone()); let _ = client.add_relay("wss://relay.example.com").await; subscriber_result_hook() .lock() @@ -329,15 +329,15 @@ mod tests { #[tokio::test] async fn wait_for_connection_or_stop_covers_both_outcomes() { - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); - let client_stop = RadrootsNostrClient::new(keys.clone()); + let client_stop = Client::new(keys.clone()); let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false); let _ = stop_tx.send(true); let stop_branch = wait_for_connection_or_stop(&client_stop, &mut stop_rx).await; assert!(!stop_branch); - let client_wait = RadrootsNostrClient::new(keys); + let client_wait = Client::new(keys); let (_tx, mut rx) = tokio::sync::watch::channel(false); let wait_branch = wait_for_connection_or_stop(&client_wait, &mut rx).await; assert!(wait_branch); diff --git a/tests/source_guards.rs b/tests/source_guards.rs @@ -34,9 +34,12 @@ fn rhi_manifest_exact_pins_radroots_contract() { if !name.starts_with("radroots_") { continue; } + let version = dependency + .as_str() + .or_else(|| dependency.get("version").and_then(toml::Value::as_str)); assert_eq!( - dependency["version"].as_str(), - Some("=1.0.0-alpha.1"), + version, + Some("=0.1.0-alpha"), "RHI must exact-pin {name} to the governed event contract release" ); } @@ -118,15 +121,13 @@ fn rhi_agreement_attestation_is_release_product_optional_infrastructure() { assert!( lib.contains("trade_mutation_subscription_kinds()") - && lib.contains("&TRADE_MUTATION_EVENT_KINDS") - && lib.contains("RadrootsAuthoredProfile") - && lib.contains("RadrootsNostrProfileEventBuilder") - && lib.contains("radroots_nostr_build_profile_event") - && lib.contains("send_profile_event_builder") - && !lib.contains("radroots_nostr_build_event") - && lib.contains("radroots_nostr_publish_application_handler") - && !lib.contains("radroots_nostr_bootstrap_service_presence") - && !lib.contains("RadrootsProfileType") + && lib.contains("TRADE_MUTATION_EVENT_KINDS") + && lib.contains("AuthoredProfile") + && lib.contains("ProfileBuilder") + && lib.contains("build_profile") + && lib.contains("build_application_handler") + && lib.contains(".send_event(") + && !lib.contains("radroots_nostr::prelude") && lib.contains("client.into_inner()"), "RHI service presence must advertise canonical release-product trade mutation kinds" );