rhi

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

commit ded5c32f19d3f8e4ece5c6111bdb3d5238bbe0b3
parent ddfc4b1719b7dbc0bbd5eec5d6697d10b44a570e
Author: triesap <tyson@radroots.org>
Date:   Sun, 23 Aug 2026 20:22:40 +0000

refactor(rhi): remove prototype runtime

Diffstat:
MAGENTS.md | 5+++--
MCargo.lock | 526++-----------------------------------------------------------------------------
MCargo.toml | 8+-------
MREADME | 9++++++---
Dconfig.toml | 31-------------------------------
Mradroots.service.source-lock.v2.toml | 2+-
Msrc/adapters/nostr/event.rs | 5+++--
Dsrc/config.rs | 314-------------------------------------------------------------------------------
Msrc/features/trade_agreement_attestation.rs | 605+------------------------------------------------------------------------------
Dsrc/host_nostr.rs | 53-----------------------------------------------------
Dsrc/host_runtime.rs | 119-------------------------------------------------------------------------------
Msrc/lib.rs | 481-------------------------------------------------------------------------------
Msrc/main.rs | 87+++++++++++++++++++++++++++----------------------------------------------------
Dsrc/rhi.rs | 345-------------------------------------------------------------------------------
Mtests/services_hardening_runtime_context.rs | 9++++++---
Atests/services_hardening_wave_100_a.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/source_guards.rs | 90+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
17 files changed, 198 insertions(+), 2575 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -32,8 +32,9 @@ ## 2. Authority and preflight - Before editing, read this file, `README`, `Cargo.toml`, - `radroots.service.source-lock.v2.toml`, the relevant implementation and tests, - and `config.toml` when it is in scope. + `radroots.service.source-lock.v2.toml`, and the relevant implementation and + tests. The removed prototype root `config.toml` is not configuration + authority. - `.radroots-consumer-root` is the standalone source-lock identity and must remain exactly `rhi`. The reserved pre-implementation evidence authority is `contracts/services_hardening/evidence_policy.v1.json`, and the reserved diff --git a/Cargo.lock b/Cargo.lock @@ -88,7 +88,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -99,7 +99,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -115,37 +115,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "async-utility" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" -dependencies = [ - "futures-util", - "gloo-timers", - "tokio", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-wsocket" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" -dependencies = [ - "async-utility", - "futures", - "futures-util", - "js-sys", - "tokio", - "tokio-rustls", - "tokio-socks", - "tokio-tungstenite", - "url", - "wasm-bindgen", - "web-sys", -] - -[[package]] name = "atoi" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -155,12 +124,6 @@ dependencies = [ ] [[package]] -name = "atomic-destructor" -version = "0.3.0" -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" @@ -424,15 +387,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] name = "crossbeam-queue" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -487,15 +441,6 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -571,7 +516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -831,18 +776,6 @@ dependencies = [ ] [[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1302,21 +1235,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] name = "mediatype" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1342,16 +1260,10 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] -name = "negentropy" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" - -[[package]] name = "nostr" version = "0.44.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1378,68 +1290,6 @@ dependencies = [ ] [[package]] -name = "nostr-database" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" -dependencies = [ - "lru", - "nostr", - "tokio", -] - -[[package]] -name = "nostr-gossip" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" -dependencies = [ - "nostr", -] - -[[package]] -name = "nostr-relay-pool" -version = "0.44.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85c54d6ca9aae4ae2bf19a7663ba9db5f45f783f1d24aff55f006386b8b99a1" -dependencies = [ - "async-utility", - "async-wsocket", - "atomic-destructor", - "hex", - "lru", - "negentropy", - "nostr", - "nostr-database", - "tokio", - "tracing", -] - -[[package]] -name = "nostr-sdk" -version = "0.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" -dependencies = [ - "async-utility", - "nostr", - "nostr-database", - "nostr-gossip", - "nostr-relay-pool", - "tokio", - "tracing", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1479,12 +1329,6 @@ dependencies = [ ] [[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - -[[package]] name = "num-integer" version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1636,12 +1480,6 @@ dependencies = [ ] [[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2014,13 +1852,11 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" name = "rhi" version = "0.1.0" dependencies = [ - "anyhow", "chacha20poly1305", "clap", "futures-executor", "jsonschema", "nostr", - "nostr-sdk", "radroots_event", "radroots_event_codec", "radroots_identity", @@ -2037,30 +1873,12 @@ dependencies = [ "sha2", "tempfile", "thiserror 2.0.18", - "tokio", "toml", - "tracing", - "tracing-appender", - "tracing-subscriber", "url", "zeroize", ] [[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] name = "rust_decimal" version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2081,41 +1899,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "windows-sys", ] [[package]] @@ -2243,17 +2027,6 @@ dependencies = [ ] [[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2265,31 +2038,12 @@ dependencies = [ ] [[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2308,7 +2062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2506,7 +2260,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2550,46 +2304,6 @@ dependencies = [ ] [[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2623,12 +2337,10 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2643,28 +2355,6 @@ dependencies = [ ] [[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-socks" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" -dependencies = [ - "either", - "futures-util", - "thiserror 1.0.69", - "tokio", -] - -[[package]] name = "tokio-stream" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2676,22 +2366,6 @@ dependencies = [ ] [[package]] -name = "tokio-tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots 0.26.11", -] - -[[package]] name = "tokio-util" version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2759,18 +2433,6 @@ dependencies = [ ] [[package]] -name = "tracing-appender" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" -dependencies = [ - "crossbeam-channel", - "thiserror 2.0.18", - "time", - "tracing-subscriber", -] - -[[package]] name = "tracing-attributes" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2788,36 +2450,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -2827,25 +2459,6 @@ 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" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.2", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - -[[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2895,12 +2508,6 @@ dependencies = [ ] [[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] name = "url" version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2926,12 +2533,6 @@ dependencies = [ ] [[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2954,12 +2555,6 @@ dependencies = [ ] [[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3024,20 +2619,6 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] name = "wasm-bindgen-macro" version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3114,24 +2695,6 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - -[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3161,15 +2724,6 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" @@ -3178,70 +2732,6 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "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]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[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_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[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_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" authors = ["Radroots Authors"] rust-version = "1.97.1" license = "AGPL-3.0-or-later" -description = "Radroots trade agreement attestation worker" +description = "Radroots evidence reconciliation and attestation service" repository = "https://github.com/radrootslabs/rhi" readme = "README" publish = false @@ -54,24 +54,18 @@ radroots_service_sqlite = { git = "https://github.com/radrootslabs/lib", rev = " radroots_secrets = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" } radroots_trade = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" } -anyhow = { version = "1" } chacha20poly1305 = { version = "0.10" } clap = { version = "4", features = ["derive"] } futures-executor = { version = "0.3" } jsonschema = { version = "0.48.1", default-features = false } nostr = { version = "0.44.7", 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"] } url = "2" zeroize = { version = "1" } diff --git a/README b/README @@ -20,9 +20,12 @@ reviewed bounded operational leaves have defaults. Bootstrap profile, instance, repo-local root, and config-path selection are CLI concerns and are not document fields. -The current runtime loader and root `config.toml` remain transitional until -their ordered replacement steps are complete. Prototype environment and worker -selectors are removed and are not compatibility authority. +The retired root `config.toml`, transitional runtime loader, JSON state +adapter, environment and worker selectors, and prototype smoke/runtime paths +are removed. The executable admits one strict CLI invocation and one sealed +runtime context, then fails closed until later ordered checkpoints bind each +command to its governed state and runtime authority. It never falls back to a +prototype execution path. `parse_rhi_config_v1` is the strict in-memory admission boundary for the target document. It bounds original bytes before parsing, rejects malformed duplicate diff --git a/config.toml b/config.toml @@ -1,31 +0,0 @@ -[metadata] -name = "rhi" - -[logging] -filter = "info" -stdout = true - -[relays] -urls = [ - "ws://127.0.0.1:8080" -] - -[nostr.nip89] -identifier = "rhi" -extra_tags = [] - -[subscriber.backoff] -base_ms = 500 -max_ms = 30000 -factor = 2 -jitter_ms = 0 - -[subscriber.state] -replay_window_secs = 86400 -replay_overlap_secs = 300 - -[trade_validation_receipt] -backend = "local_execute" -proof_mode = "none" -validator_set_addr = "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" -validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" diff --git a/radroots.service.source-lock.v2.toml b/radroots.service.source-lock.v2.toml @@ -7,7 +7,7 @@ architecture = "radroots.crates.release.v2" workspace_catalog_sha256 = "deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4" version = "0.1.0-alpha" source_archive_sha256 = "b425371c134be96cce46b37f7035d6212f1efe8cff50bef366631ba5632991b0" -cargo_lock_sha256 = "ba1cca624b0b2fbc49e82bc392b4cc1bf80a9ffcb5bd86e15ffed2818075f565" +cargo_lock_sha256 = "407af5f68ddb487db8867bb84e20c8a4645351c96c3695ae41973c4ea4b44a12" rust_version = "1.97.1" host_feature_profile = "service-host" diff --git a/src/adapters/nostr/event.rs b/src/adapters/nostr/event.rs @@ -1,4 +1,4 @@ -use crate::host_nostr::{Event, Kind}; +use nostr::{Event, Kind}; use radroots_event_codec::decode::job::{JobEventBorrow, JobEventLike}; #[derive(Clone, Debug)] @@ -81,8 +81,9 @@ 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 nostr::{Event, Keys, Kind, Tag, TagKind}; use radroots_event_codec::decode::job::{JobEventBorrow, JobEventLike}; + use radroots_nostr::event::GenericBuilder; fn build_event(keys: &Keys, kind: Kind, tags: Vec<Tag>) -> Event { GenericBuilder::new(kind, "content") diff --git a/src/config.rs b/src/config.rs @@ -1,314 +0,0 @@ -//! Transitional runtime settings materialization under the sealed path context. - -use std::path::{Path, PathBuf}; - -use anyhow::{Context, Result, bail}; -use serde::{Deserialize, Serialize}; - -use crate::RhiRuntimeContext; -use crate::features::trade_agreement_attestation::TradeAgreementAttestationPolicy; -use crate::host_nostr::Metadata; -use crate::host_runtime::{BackoffConfig, NostrServiceConfig}; - -fn default_replay_window_secs() -> u64 { - 24 * 60 * 60 -} - -fn default_replay_overlap_secs() -> u64 { - 5 * 60 -} - -fn default_logging_filter() -> String { - "info".to_owned() -} - -const fn default_logging_stdout() -> bool { - true -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LoggingConfig { - pub output_dir: PathBuf, - pub filter: String, - pub stdout: bool, -} - -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default, deny_unknown_fields)] -struct RawLoggingConfig { - filter: Option<String>, - stdout: Option<bool>, -} - -impl RawLoggingConfig { - fn into_logging_config(self, context: &RhiRuntimeContext) -> Result<LoggingConfig> { - let filter = self.filter.unwrap_or_else(default_logging_filter); - let filter = filter.trim(); - if filter.is_empty() { - bail!("logging.filter must not be empty"); - } - Ok(LoggingConfig { - output_dir: context.context().paths().logs().to_path_buf(), - filter: filter.to_owned(), - stdout: self.stdout.unwrap_or_else(default_logging_stdout), - }) - } -} - -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default, deny_unknown_fields)] -struct RawRelaysConfig { - urls: Vec<String>, -} - -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default, deny_unknown_fields)] -struct RawNostrConfig { - nip89: RawNip89Config, -} - -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default, deny_unknown_fields)] -struct RawNip89Config { - identifier: Option<String>, - extra_tags: Vec<Vec<String>>, -} - -#[derive(Debug, Clone)] -struct RawServiceConfig { - logging: LoggingConfig, - relays: RawRelaysConfig, - nostr: RawNostrConfig, -} - -impl RawServiceConfig { - 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, - nip89_extra_tags: self.nostr.nip89.extra_tags, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Configuration { - #[serde(flatten)] - pub service: NostrServiceConfig, - pub logging: LoggingConfig, - pub subscriber: SubscriberConfig, - #[serde(default)] - pub trade_agreement_attestation: TradeAgreementAttestationPolicy, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubscriberConfig { - pub backoff: BackoffConfig, - pub state: SubscriberStateConfig, -} - -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default, deny_unknown_fields)] -struct RawSubscriberConfig { - backoff: BackoffConfig, - state: RawSubscriberStateConfig, -} - -impl RawSubscriberConfig { - fn into_subscriber_config(self, context: &RhiRuntimeContext) -> SubscriberConfig { - SubscriberConfig { - backoff: self.backoff, - state: self.state.into_subscriber_state_config(context), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubscriberStateConfig { - pub path: PathBuf, - pub replay_window_secs: u64, - pub replay_overlap_secs: u64, -} - -#[derive(Debug, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -struct RawSubscriberStateConfig { - #[serde(default = "default_replay_window_secs")] - replay_window_secs: u64, - #[serde(default = "default_replay_overlap_secs")] - replay_overlap_secs: u64, -} - -impl Default for RawSubscriberStateConfig { - fn default() -> Self { - Self { - replay_window_secs: default_replay_window_secs(), - replay_overlap_secs: default_replay_overlap_secs(), - } - } -} - -impl RawSubscriberStateConfig { - fn into_subscriber_state_config(self, context: &RhiRuntimeContext) -> SubscriberStateConfig { - SubscriberStateConfig { - path: context - .context() - .paths() - .state() - .join("trade-agreement-attestation") - .join("state.json"), - replay_window_secs: self.replay_window_secs, - replay_overlap_secs: self.replay_overlap_secs, - } - } -} - -#[derive(Debug, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -struct RawSettings { - metadata: Metadata, - #[serde(default)] - logging: RawLoggingConfig, - #[serde(default)] - relays: RawRelaysConfig, - #[serde(default)] - nostr: RawNostrConfig, - #[serde(default)] - subscriber: RawSubscriberConfig, - #[serde(default)] - trade_agreement_attestation: TradeAgreementAttestationPolicy, -} - -impl RawSettings { - fn into_settings(self, context: &RhiRuntimeContext) -> Result<Settings> { - let logging = self.logging.into_logging_config(context)?; - let service = RawServiceConfig { - logging: logging.clone(), - relays: self.relays, - nostr: self.nostr, - } - .into_service_config(); - Ok(Settings { - metadata: self.metadata, - config: Configuration { - service, - logging, - subscriber: self.subscriber.into_subscriber_config(context), - trade_agreement_attestation: self.trade_agreement_attestation, - }, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Settings { - pub metadata: Metadata, - pub config: Configuration, -} - -/// Loads transitional runtime settings with all paths supplied by one sealed context. -/// -/// The final versioned configuration parser is [`crate::parse_rhi_config_v1`]. -/// This adapter remains only until the legacy runtime is removed in Step 168; -/// it accepts no path overrides and performs no ambient environment selection. -pub fn load_settings_from_path(path: &Path, context: &RhiRuntimeContext) -> Result<Settings> { - let raw = std::fs::read_to_string(path) - .with_context(|| format!("read configuration from {}", path.display()))?; - let settings: RawSettings = - toml::from_str(&raw).with_context(|| format!("parse configuration {}", path.display()))?; - let settings = settings.into_settings(context)?; - settings.config.trade_agreement_attestation.validate()?; - Ok(settings) -} - -#[cfg(test)] -mod tests { - use std::path::{Path, PathBuf}; - - use crate::{ - RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, parse_rhi_cli_v1_from, - resolve_rhi_runtime_context, - }; - - use super::load_settings_from_path; - - fn context() -> crate::RhiRuntimeContext { - let invocation = parse_rhi_cli_v1_from([ - "rhi", - "--profile", - "interactive", - "--instance", - "default", - "run", - ]) - .expect("invocation"); - resolve_rhi_runtime_context( - &RadrootsPathResolver::new( - RadrootsPlatform::Linux, - RadrootsHostEnvironment { - home_dir: Some(PathBuf::from("/home/operator")), - xdg_config_home: Some(PathBuf::from("/xdg/config")), - xdg_data_home: Some(PathBuf::from("/xdg/data")), - xdg_state_home: Some(PathBuf::from("/xdg/state")), - xdg_cache_home: Some(PathBuf::from("/xdg/cache")), - xdg_runtime_dir: Some(PathBuf::from("/xdg/run")), - ..RadrootsHostEnvironment::default() - }, - ), - &invocation, - ) - .expect("context") - } - - #[test] - fn materializes_only_context_derived_paths() { - let temp = tempfile::tempdir().expect("tempdir"); - let config_path = temp.path().join("config.toml"); - std::fs::write( - &config_path, - r#" -[metadata] -name = "rhi-test" - -[relays] -urls = ["wss://relay.example.com"] - -[subscriber.state] -replay_window_secs = 123 -replay_overlap_secs = 45 -"#, - ) - .expect("config"); - let settings = load_settings_from_path(&config_path, &context()).expect("settings"); - assert_eq!( - settings.config.logging.output_dir, - Path::new("/xdg/state/radroots/logs/services/rhi/default") - ); - assert_eq!( - settings.config.subscriber.state.path, - Path::new( - "/xdg/data/radroots/services/rhi/default/trade-agreement-attestation/state.json" - ) - ); - assert_eq!(settings.config.subscriber.state.replay_window_secs, 123); - assert_eq!(settings.config.subscriber.state.replay_overlap_secs, 45); - } - - #[test] - fn path_leaf_overrides_are_rejected() { - let temp = tempfile::tempdir().expect("tempdir"); - for (name, extra) in [ - ("logging", "[logging]\noutput_dir = \"/tmp/logs\"\n"), - ("state", "[subscriber.state]\npath = \"/tmp/state.json\"\n"), - ] { - let config_path = temp.path().join(format!("{name}.toml")); - std::fs::write( - &config_path, - format!("[metadata]\nname = \"rhi-test\"\n\n{extra}"), - ) - .expect("config"); - assert!(load_settings_from_path(&config_path, &context()).is_err()); - } - } -} diff --git a/src/features/trade_agreement_attestation.rs b/src/features/trade_agreement_attestation.rs @@ -1,41 +1,19 @@ #![forbid(unsafe_code)] #![cfg_attr(coverage_nightly, coverage(off))] -use std::collections::{BTreeMap, BTreeSet}; -use std::convert::TryFrom; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::Duration; - -use crate::host_nostr::{ - Client, Event, Filter, Keys, Kind, RelayPoolNotification, SubscriptionId, Timestamp, -}; -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::{ - TradeMutationEnvelopeV1, canonical_jcs_value, trade_mutation_from_canonical_content, -}; -use radroots_identity::PublicKey; -use radroots_trade::evidence::{ - RadrootsTradeAttestationResultV1, RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1, -}; +use radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS; +use radroots_event::id::{AddressableCoordinate, EventId, MutationId}; +use radroots_event::trade::canonical_jcs_value; +use radroots_trade::evidence::{RadrootsTradeAttestationResultV1, RadrootsTradeEvidenceStateV1}; use radroots_trade::model::{ RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, RadrootsTradeProjectionV1, }; -use radroots_trade::reducer::{ - RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION, - RadrootsTradeReductionInputV1, reduce_trade_records, -}; +use radroots_trade::reducer::{RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; -use tokio::sync::{Mutex, watch}; -use tokio::time::sleep; -use tracing::{info, warn}; pub const RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID: &str = "radroots.rhi.agreement_attestation.v1"; -pub const RHI_AGREEMENT_ATTESTATION_STATE_VERSION: u32 = 1; pub const RHI_AGREEMENT_ATTESTATION_REPORT_VERSION: u16 = 1; pub const RHI_AGREEMENT_ATTESTATION_PROOF_SYSTEM_LOCAL_STATEMENT_HASH: &str = "local_statement_hash"; @@ -155,63 +133,8 @@ pub struct TradeAgreementAttestationReportV1 { pub proof_identity_hash: String, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TradeMutationObservationV1 { - pub event_id: String, - pub event_kind: u32, - pub event_pubkey: String, - pub trade_id: String, - pub mutation_id: String, - pub content: String, - pub observed_at_unix_s: u64, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TradeAgreementAttestationState { - #[serde(default)] - seen_event_ids: BTreeSet<String>, - #[serde(default)] - mutation_events: BTreeMap<String, TradeMutationObservationV1>, - #[serde(default)] - reports_by_claim: BTreeMap<String, TradeAgreementAttestationReportV1>, - last_event_created_at: Option<u32>, -} - -#[derive(Clone, Debug)] -pub struct TradeAgreementAttestationRuntime { - state: Arc<Mutex<TradeAgreementAttestationState>>, - config: TradeAgreementAttestationRuntimeConfig, - persistence: Option<Arc<TradeAgreementAttestationStatePersistence>>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TradeAgreementAttestationRuntimeConfig { - pub state_path: PathBuf, - pub replay_window_secs: u64, - pub replay_overlap_secs: u64, -} - -#[derive(Clone, Debug)] -struct TradeAgreementAttestationStatePersistence { - path: PathBuf, -} - -#[derive(Debug, Serialize, Deserialize)] -struct PersistedTradeAgreementAttestationState { - version: u32, - state: TradeAgreementAttestationState, -} - #[derive(Debug, Error)] pub enum TradeAgreementAttestationError { - #[error("event kind not supported")] - UnsupportedKind, - #[error("invalid event author")] - InvalidEventAuthor, - #[error("trade mutation is missing mutation_id")] - MissingMutationId, #[error("agreement claim is missing")] MissingAgreementClaim, #[error("attestation policy is missing {0}")] @@ -220,309 +143,10 @@ pub enum TradeAgreementAttestationError { InvalidValidatorSetBinding(&'static str), #[error("invalid configured hash field")] InvalidHashField, - #[error("invalid signed event")] - InvalidSignedEvent, #[error("trade protocol error: {0}")] TradeProtocol(#[from] radroots_event::trade::TradeProtocolError), #[error("serde error: {0}")] Serde(#[from] serde_json::Error), - #[error("state error: {0}")] - State(#[from] TradeAgreementAttestationRuntimeError), -} - -#[derive(Debug, Error)] -pub enum TradeAgreementAttestationRuntimeError { - #[error("state path is invalid: {0}")] - InvalidStatePath(PathBuf), - #[error("state version {0} is unsupported")] - UnsupportedStateVersion(u32), - #[error("io error: {0}")] - Io(#[from] std::io::Error), - #[error("serde error: {0}")] - Serde(#[from] serde_json::Error), -} - -impl Default for TradeAgreementAttestationRuntimeConfig { - fn default() -> Self { - Self { - // In-memory runtimes never construct persistence from this value. - // Persistent runtimes must receive their context-derived path. - state_path: PathBuf::new(), - replay_window_secs: 24 * 60 * 60, - replay_overlap_secs: 5 * 60, - } - } -} - -impl Default for TradeAgreementAttestationRuntime { - fn default() -> Self { - Self { - state: Arc::new(Mutex::new(TradeAgreementAttestationState::default())), - config: TradeAgreementAttestationRuntimeConfig::default(), - persistence: None, - } - } -} - -impl TradeAgreementAttestationRuntime { - pub fn new() -> Self { - Self::default() - } - - pub async fn load( - config: TradeAgreementAttestationRuntimeConfig, - ) -> Result<Self, TradeAgreementAttestationRuntimeError> { - let persistence = Arc::new(TradeAgreementAttestationStatePersistence::new( - config.state_path.clone(), - )); - let state = persistence.load().await?; - Ok(Self { - state: Arc::new(Mutex::new(state)), - config, - persistence: Some(persistence), - }) - } - - pub fn state(&self) -> Arc<Mutex<TradeAgreementAttestationState>> { - Arc::clone(&self.state) - } - - pub async fn persist(&self) -> Result<(), TradeAgreementAttestationRuntimeError> { - let Some(persistence) = &self.persistence else { - return Ok(()); - }; - let snapshot = self.state.lock().await.clone(); - persistence.persist(&snapshot).await - } - - pub async fn mark_processed_event( - &self, - created_at: u32, - ) -> Result<(), TradeAgreementAttestationRuntimeError> { - { - let mut state = self.state.lock().await; - state.observe_event_created_at(created_at); - } - self.persist().await - } - - pub async fn recovery_filter(&self, kinds: Vec<Kind>) -> Filter { - let since = { - let state = self.state.lock().await; - state.replay_since( - Timestamp::now().as_secs(), - self.config.replay_window_secs, - self.config.replay_overlap_secs, - ) - }; - Filter::new().kinds(kinds).since(Timestamp::from(since)) - } - - pub async fn reports(&self) -> Vec<TradeAgreementAttestationReportV1> { - self.state - .lock() - .await - .reports_by_claim - .values() - .cloned() - .collect() - } - - async fn observe_mutation_event( - &self, - 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.to_hex(), - mutation_id: mutation_id.to_hex(), - content: event.content.clone(), - observed_at_unix_s: event.created_at.as_secs(), - }; - let mut state = self.state.lock().await; - if !state.seen_event_ids.insert(observation.event_id.clone()) { - return Ok(false); - } - state - .mutation_events - .insert(observation.mutation_id.clone(), observation); - Ok(true) - } - - async fn reduce_trade( - &self, - 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.to_hex().as_str()) - .cloned() - .collect::<Vec<_>>() - }; - let mutations = observations - .iter() - .map(|observation| { - let mutation = trade_mutation_from_canonical_content(observation.content.as_str())?; - let transport_event_id = EventId::parse(observation.event_id.as_str()) - .map(Some) - .map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?; - 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)) - } - - async fn store_report( - &self, - report: TradeAgreementAttestationReportV1, - ) -> Result<(), TradeAgreementAttestationRuntimeError> { - { - let mut state = self.state.lock().await; - state - .reports_by_claim - .insert(report.statement.claim_mutation_id.clone(), report); - } - self.persist().await - } -} - -impl TradeAgreementAttestationState { - pub fn report_for_claim( - &self, - claim_mutation_id: &str, - ) -> Option<&TradeAgreementAttestationReportV1> { - self.reports_by_claim.get(claim_mutation_id) - } - - pub fn observed_mutation_count(&self) -> usize { - self.mutation_events.len() - } - - fn observe_event_created_at(&mut self, created_at: u32) { - self.last_event_created_at = Some( - self.last_event_created_at - .map_or(created_at, |current| current.max(created_at)), - ); - } - - fn replay_since(&self, now: u64, replay_window_secs: u64, replay_overlap_secs: u64) -> u64 { - let window_start = now.saturating_sub(replay_window_secs); - match self.last_event_created_at { - Some(last) => u64::from(last).saturating_sub(replay_overlap_secs), - None => window_start, - } - } -} - -impl TradeAgreementAttestationStatePersistence { - fn new(path: PathBuf) -> Self { - Self { path } - } - - async fn load( - &self, - ) -> Result<TradeAgreementAttestationState, TradeAgreementAttestationRuntimeError> { - if !self.path.exists() { - return Ok(TradeAgreementAttestationState::default()); - } - let payload = tokio::fs::read_to_string(&self.path).await?; - let snapshot: PersistedTradeAgreementAttestationState = serde_json::from_str(&payload)?; - if snapshot.version != RHI_AGREEMENT_ATTESTATION_STATE_VERSION { - return Err( - TradeAgreementAttestationRuntimeError::UnsupportedStateVersion(snapshot.version), - ); - } - Ok(snapshot.state) - } - - async fn persist( - &self, - state: &TradeAgreementAttestationState, - ) -> Result<(), TradeAgreementAttestationRuntimeError> { - let parent = self.path.parent().ok_or_else(|| { - TradeAgreementAttestationRuntimeError::InvalidStatePath(self.path.clone()) - })?; - tokio::fs::create_dir_all(parent).await?; - let snapshot = PersistedTradeAgreementAttestationState { - version: RHI_AGREEMENT_ATTESTATION_STATE_VERSION, - state: state.clone(), - }; - let payload = serde_json::to_vec_pretty(&snapshot)?; - let temp = temp_state_path(&self.path)?; - tokio::fs::write(&temp, payload).await?; - tokio::fs::rename(temp, &self.path).await?; - Ok(()) - } -} - -fn temp_state_path(path: &Path) -> Result<PathBuf, TradeAgreementAttestationRuntimeError> { - let parent = path.parent().ok_or_else(|| { - TradeAgreementAttestationRuntimeError::InvalidStatePath(path.to_path_buf()) - })?; - let file_name = path - .file_name() - .and_then(|value| value.to_str()) - .ok_or_else(|| { - TradeAgreementAttestationRuntimeError::InvalidStatePath(path.to_path_buf()) - })?; - Ok(parent.join(format!(".{file_name}.tmp"))) -} - -pub async fn handle_trade_mutation_event( - event: Event, - runtime: TradeAgreementAttestationRuntime, - policy: &TradeAgreementAttestationPolicy, -) -> Result<Option<TradeAgreementAttestationReportV1>, TradeAgreementAttestationError> { - policy.validate()?; - let kind = event_kind_u32(&event)?; - if !is_trade_mutation_event_kind(kind) { - return Err(TradeAgreementAttestationError::UnsupportedKind); - } - let mutation = trade_mutation_from_canonical_content(event.content.as_str())?; - if mutation.mutation_kind().nostr_kind() != kind { - return Err(TradeAgreementAttestationError::UnsupportedKind); - } - 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 - .ok_or(TradeAgreementAttestationError::MissingMutationId)?; - if !runtime - .observe_mutation_event(&event, &mutation, &mutation_id, kind) - .await? - { - return Ok(None); - } - let projection = runtime.reduce_trade(&mutation.trade_id).await?; - let Some(claim_id) = projection - .active_agreement_claim_ids() - .iter() - .find(|claim_id| **claim_id == mutation_id) - .or_else(|| projection.active_agreement_claim_ids().first()) - .cloned() - else { - return Ok(None); - }; - let report = attest_projection_claim(&projection, &claim_id, policy)?; - runtime.store_report(report.clone()).await?; - Ok(Some(report)) } pub fn attest_projection_claim( @@ -614,13 +238,6 @@ fn mutation_ids_to_strings(values: &[MutationId]) -> Vec<String> { values.iter().map(MutationId::to_hex).collect() } -fn event_kind_u32(event: &Event) -> Result<u32, TradeAgreementAttestationError> { - match event.kind { - Kind::Custom(value) => Ok(u32::from(value)), - _ => Err(TradeAgreementAttestationError::UnsupportedKind), - } -} - fn validate_optional_hash32(value: &Option<String>) -> Result<(), TradeAgreementAttestationError> { if let Some(value) = value { validate_hash32(value)?; @@ -647,215 +264,3 @@ fn hash_canonical_value( hasher.update(canonical.as_bytes()); Ok(format!("{:x}", hasher.finalize())) } - -fn map_notification_recv_result( - result: Result<RelayPoolNotification, tokio::sync::broadcast::error::RecvError>, -) -> Result<RelayPoolNotification, ()> { - result.map_err(|_| ()) -} - -async fn subscribe_io(client: &Client, filter: Filter) -> Result<SubscriptionId> { - client.subscribe(filter).await.map_err(Into::into) -} - -async fn unsubscribe_io(client: &Client, subscription_id: &SubscriptionId) { - client.unsubscribe(subscription_id).await; -} - -fn should_delay_before_event_handle() -> bool { - cfg!(all(debug_assertions, not(test))) -} - -async fn process_event_notification( - event: Event, - runtime: TradeAgreementAttestationRuntime, - policy: TradeAgreementAttestationPolicy, -) -> Result<()> { - let created_at = u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX); - if should_delay_before_event_handle() { - sleep(Duration::from_millis(200)).await; - } - match handle_trade_mutation_event(event, runtime.clone(), &policy).await { - Ok(_) | Err(TradeAgreementAttestationError::UnsupportedKind) => { - runtime.mark_processed_event(created_at).await?; - Ok(()) - } - Err(error) => { - warn!("rhi agreement attestation rejected event: {error}"); - runtime.mark_processed_event(created_at).await?; - Ok(()) - } - } -} - -pub async fn subscriber( - client: Client, - _keys: Keys, - runtime: TradeAgreementAttestationRuntime, - policy: TradeAgreementAttestationPolicy, - mut stop_rx: watch::Receiver<bool>, -) -> Result<()> { - let subscribed_kinds = trade_mutation_subscription_kinds(); - info!( - "Starting subscriber for release-product trade mutation kinds: {:?}", - 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(Kind::Custom) - .collect(); - let filter = runtime.recovery_filter(kinds).await; - - if *stop_rx.borrow() { - return Ok(()); - } - - let subscription_id = subscribe_io(&client, filter).await?; - let sdk_client = client.clone().into_inner(); - let mut notifications = sdk_client.notifications(); - let mut notifications_closed = false; - - loop { - tokio::select! { - _ = stop_rx.changed() => { - break; - } - msg = async { - map_notification_recv_result(notifications.recv().await) - } => { - let n = match msg { - Ok(n) => n, - Err(_) => { - notifications_closed = true; - break; - } - }; - - if let RelayPoolNotification::Event { event, .. } = n { - let event = (*event).clone(); - process_event_notification(event, runtime.clone(), policy.clone()).await?; - } - } - } - } - - unsubscribe_io(&client, &subscription_id).await; - if notifications_closed { - return Err(anyhow!( - "rhi agreement attestation subscriber notifications closed" - )); - } - Ok(()) -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TradeAgreementAttestationSmokeRequest { - pub protocol_id: String, - pub operation: TradeAgreementAttestationSmokeOperation, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TradeAgreementAttestationSmokeOperation { - Health, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TradeAgreementAttestationSmokeResponse { - pub ok: bool, - pub protocol_id: String, - pub operation: TradeAgreementAttestationSmokeOperation, - pub worker_name: String, - pub worker_version: String, - pub capabilities: Vec<String>, - pub error: Option<String>, -} - -pub async fn handle_smoke_request_bytes(bytes: &[u8]) -> TradeAgreementAttestationSmokeResponse { - match serde_json::from_slice::<TradeAgreementAttestationSmokeRequest>(bytes) { - Ok(request) if request.protocol_id == RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID => { - TradeAgreementAttestationSmokeResponse { - ok: true, - protocol_id: request.protocol_id, - operation: request.operation, - worker_name: "rhi".to_owned(), - worker_version: env!("CARGO_PKG_VERSION").to_owned(), - capabilities: vec![ - "release_product_trade_mutation_observation".to_owned(), - "agreement_claim_attestation_report".to_owned(), - "no_agreement_authority".to_owned(), - ], - error: None, - } - } - Ok(request) => TradeAgreementAttestationSmokeResponse { - ok: false, - protocol_id: request.protocol_id, - operation: request.operation, - worker_name: "rhi".to_owned(), - worker_version: env!("CARGO_PKG_VERSION").to_owned(), - capabilities: Vec::new(), - error: Some("invalid protocol id".to_owned()), - }, - Err(error) => TradeAgreementAttestationSmokeResponse { - ok: false, - protocol_id: RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID.to_owned(), - operation: TradeAgreementAttestationSmokeOperation::Health, - worker_name: "rhi".to_owned(), - worker_version: env!("CARGO_PKG_VERSION").to_owned(), - capabilities: Vec::new(), - error: Some(error.to_string()), - }, - } -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, TradeAgreementAttestationRuntime, - TradeAgreementAttestationRuntimeConfig, TradeAgreementAttestationSmokeOperation, - TradeAgreementAttestationSmokeRequest, handle_smoke_request_bytes, - }; - #[tokio::test] - async fn runtime_persists_and_loads_attestation_state() { - let temp = tempfile::tempdir().expect("tempdir"); - let config = TradeAgreementAttestationRuntimeConfig { - state_path: temp.path().join("state.json"), - replay_window_secs: 100, - replay_overlap_secs: 10, - }; - let runtime = TradeAgreementAttestationRuntime::load(config.clone()) - .await - .expect("load"); - { - let state = runtime.state(); - state.lock().await.observe_event_created_at(42); - } - runtime.persist().await.expect("persist"); - let loaded = TradeAgreementAttestationRuntime::load(config) - .await - .expect("load persisted"); - assert_eq!(loaded.state().lock().await.replay_since(100, 100, 10), 32); - } - - #[tokio::test] - async fn smoke_request_reports_optional_capabilities() { - let request = TradeAgreementAttestationSmokeRequest { - protocol_id: RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID.to_owned(), - operation: TradeAgreementAttestationSmokeOperation::Health, - }; - let response = - handle_smoke_request_bytes(&serde_json::to_vec(&request).expect("json")).await; - assert!(response.ok); - assert!( - response - .capabilities - .contains(&"no_agreement_authority".to_owned()) - ); - } -} diff --git a/src/host_nostr.rs b/src/host_nostr.rs @@ -1,53 +0,0 @@ -//! 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_runtime.rs b/src/host_runtime.rs @@ -1,119 +0,0 @@ -//! RHI-owned process lifecycle and retry policy. - -use core::future::Future; -use core::time::Duration; -use std::time::{SystemTime, UNIX_EPOCH}; - -use serde::{Deserialize, Serialize}; - -#[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/lib.rs b/src/lib.rs @@ -2,14 +2,10 @@ pub mod adapters; mod cli_v1; -pub mod config; mod config_v1; pub mod features; pub mod host_identity; -pub mod host_nostr; -pub mod host_runtime; pub mod identity_storage; -pub mod rhi; mod runtime_context; pub use cli_v1::{ @@ -33,480 +29,3 @@ pub use runtime_context::{ RhiRuntimeContext, RhiRuntimeContextError, RhiRuntimeContextErrorKind, resolve_rhi_runtime_context, }; - -use anyhow::{Context, Result, anyhow, bail}; -use radroots_event::{ - envelope::kind::TRADE_MUTATION_EVENT_KINDS, - profile::{AuthoredProfile, Nip05Identifier}, -}; -use std::time::Duration; - -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::event::{build_application_handler, build_profile}; -use tracing::{info, warn}; - -#[cfg(test)] -static RUN_RHI_AUTO_STOP: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); -#[cfg(test)] -static RUN_RHI_SKIP_SUBSCRIBER: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -#[cfg(test)] -static RUN_RHI_BOOTSTRAP_HOOK: std::sync::OnceLock<std::sync::Mutex<Option<Result<(), String>>>> = - std::sync::OnceLock::new(); - -#[derive(Clone, Copy)] -enum RunRhiWaitOutcome { - Shutdown, - Stopped, -} - -#[cfg(test)] -static RUN_RHI_WAIT_HOOK: std::sync::OnceLock<std::sync::Mutex<Option<RunRhiWaitOutcome>>> = - std::sync::OnceLock::new(); - -#[cfg(test)] -fn run_rhi_bootstrap_hook() -> &'static std::sync::Mutex<Option<Result<(), String>>> { - RUN_RHI_BOOTSTRAP_HOOK.get_or_init(|| std::sync::Mutex::new(None)) -} - -#[cfg(test)] -fn run_rhi_wait_hook() -> &'static std::sync::Mutex<Option<RunRhiWaitOutcome>> { - RUN_RHI_WAIT_HOOK.get_or_init(|| std::sync::Mutex::new(None)) -} - -#[cfg(test)] -fn take_bootstrap_hook_result() -> Option<Result<(), String>> { - run_rhi_bootstrap_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .take() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_bootstrap_hook_result() -> Option<Result<(), String>> { - None -} - -async fn bootstrap_presence( - 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); - } - - client.connect().await; - client.wait_for_connection(Duration::from_secs(5)).await; - - let profile_event = build_authored_service_profile_event(metadata)? - .sign_with_keys(client.keys()) - .context("sign strict RHI service Profile")?; - client - .send_event(&profile_event) - .await - .context("publish strict RHI service Profile")?; - - 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: &Metadata) -> Result<ProfileBuilder> { - let profile = authored_service_profile(metadata)?; - build_profile(&profile).context("build strict RHI service Profile event") -} - -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" - ); - } - if metadata.website.is_some() || metadata.lud06.is_some() || metadata.lud16.is_some() { - bail!("RHI service Profile contains fields outside the strict authored contract"); - } - - let name = metadata - .name - .clone() - .ok_or_else(|| anyhow!("RHI service Profile requires metadata.name"))?; - let mut profile = - 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()); - } - if let Some(about) = metadata.about.as_ref() { - profile = profile.with_about(about.clone()); - } - if let Some(nip05) = metadata.nip05.as_deref() { - profile = profile.with_nip05( - Nip05Identifier::parse(nip05) - .context("validate strict RHI service Profile NIP-05 identifier")?, - ); - } - - for key in metadata.custom.keys() { - if key != "bot" { - bail!("RHI service Profile contains unsupported metadata field `{key}`"); - } - } - if let Some(bot) = metadata.custom.get("bot") { - profile = profile.with_bot( - bot.as_bool() - .ok_or_else(|| anyhow!("RHI service Profile `bot` field must be a Boolean"))?, - ); - } - - Ok(profile) -} - -#[cfg_attr(coverage_nightly, coverage(off))] -async fn wait_for_shutdown_or_stopped(handle: crate::rhi::RhiHandle) -> RunRhiWaitOutcome { - #[cfg(test)] - if let Some(outcome) = run_rhi_wait_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .take() - { - return outcome; - } - - tokio::select! { - _ = crate::host_runtime::shutdown_signal() => RunRhiWaitOutcome::Shutdown, - _ = handle.stopped() => RunRhiWaitOutcome::Stopped, - } -} - -pub async fn run_rhi(settings: &config::Settings, context: &RhiRuntimeContext) -> Result<()> { - let identity = load_service_identity(context.identity_path())?; - let keys = identity.keys().clone(); - let agreement_attestation_runtime = - TradeAgreementAttestationRuntime::load(TradeAgreementAttestationRuntimeConfig { - state_path: settings.config.subscriber.state.path.clone(), - replay_window_secs: settings.config.subscriber.state.replay_window_secs, - replay_overlap_secs: settings.config.subscriber.state.replay_overlap_secs, - }) - .await?; - - let rhi = Rhi::with_agreement_attestation_runtime_and_policy( - keys.clone(), - agreement_attestation_runtime, - settings.config.trade_agreement_attestation.clone(), - ); - let client = rhi.client.clone(); - let service_cfg = settings.config.service.clone(); - let relays = service_cfg.relays.clone(); - - for relay in &relays { - client.add_relay(relay).await?; - } - - let md = settings.metadata.clone(); - - if !relays.is_empty() { - let handler_kinds = trade_mutation_subscription_kinds(); - 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 { - info!("Published service presence on startup"); - } - } - - #[cfg(test)] - if RUN_RHI_SKIP_SUBSCRIBER.load(std::sync::atomic::Ordering::Relaxed) { - return Ok(()); - } - - let handle = start_subscriber_with_policy( - client.clone(), - keys.clone(), - rhi.agreement_attestation_runtime.clone(), - rhi.agreement_attestation_policy.clone(), - settings.config.subscriber.backoff.clone(), - ) - .await; - - let stop_handle = handle.clone(); - - #[cfg(test)] - if RUN_RHI_AUTO_STOP.load(std::sync::atomic::Ordering::Relaxed) { - stop_handle.stop(); - } - - match wait_for_shutdown_or_stopped(handle).await { - RunRhiWaitOutcome::Shutdown => { - info!("Shutting down"); - stop_handle.stop(); - } - RunRhiWaitOutcome::Stopped => {} - } - - let sdk_client = client.into_inner(); - sdk_client.unsubscribe_all().await; - sdk_client.disconnect().await; - - Ok(()) -} - -pub fn release_product_handler_kinds() -> &'static [u32] { - &TRADE_MUTATION_EVENT_KINDS -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - RUN_RHI_AUTO_STOP, RUN_RHI_SKIP_SUBSCRIBER, RunRhiWaitOutcome, authored_service_profile, - bootstrap_presence, build_authored_service_profile_event, release_product_handler_kinds, - run_rhi, run_rhi_bootstrap_hook, run_rhi_wait_hook, - }; - use crate::{config, parse_rhi_cli_v1_from, resolve_rhi_runtime_context}; - use radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS; - use std::sync::atomic::Ordering; - use tokio::sync::{Mutex, MutexGuard}; - - static TEST_LOCK: Mutex<()> = Mutex::const_new(()); - - 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() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = None; - *run_rhi_wait_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = None; - guard - } - - fn settings_with_relays( - relays: Vec<String>, - context: &crate::RhiRuntimeContext, - ) -> config::Settings { - config::Settings { - metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"), - config: config::Configuration { - service: crate::host_runtime::NostrServiceConfig { - logs_dir: std::env::temp_dir() - .join("rhi-test-logs") - .display() - .to_string(), - relays, - nip89_identifier: Some("rhi".to_string()), - nip89_extra_tags: Vec::new(), - }, - logging: config::LoggingConfig { - output_dir: std::env::temp_dir().join("rhi-test-logs"), - filter: "info".to_string(), - stdout: true, - }, - subscriber: config::SubscriberConfig { - backoff: crate::host_runtime::BackoffConfig { - base_ms: 1, - max_ms: 2, - factor: 1, - jitter_ms: 0, - }, - state: config::SubscriberStateConfig { - path: context - .context() - .paths() - .state() - .join("trade-agreement-attestation/state.json"), - replay_window_secs: 24 * 60 * 60, - replay_overlap_secs: 5 * 60, - }, - }, - trade_agreement_attestation: - crate::features::trade_agreement_attestation::TradeAgreementAttestationPolicy::default(), - }, - } - } - - fn context_for_root(root: &std::path::Path) -> crate::RhiRuntimeContext { - let root = root.to_str().expect("UTF-8 temp root"); - let invocation = parse_rhi_cli_v1_from([ - "rhi", - "--profile", - "repo-local", - "--instance", - "default", - "--repo-local-root", - root, - "run", - ]) - .expect("invocation"); - resolve_rhi_runtime_context( - &crate::RadrootsPathResolver::new( - crate::RadrootsPlatform::Linux, - crate::RadrootsHostEnvironment::default(), - ), - &invocation, - ) - .expect("context") - } - - fn provision_identity(context: &crate::RhiRuntimeContext) { - crate::identity_storage::store_encrypted_identity( - context.identity_path(), - &crate::host_identity::RadrootsIdentity::generate(), - ) - .expect("identity"); - } - - #[tokio::test] - async fn run_rhi_starts_and_stops_without_relays() { - let _guard = test_guard().await; - RUN_RHI_AUTO_STOP.store(true, Ordering::Relaxed); - let temp = tempfile::tempdir().expect("tempdir"); - let context = context_for_root(temp.path()); - provision_identity(&context); - let settings = settings_with_relays(Vec::new(), &context); - run_rhi(&settings, &context).await.expect("run rhi"); - } - - #[tokio::test] - async fn run_rhi_bootstraps_release_product_handler_kinds_when_relays_exist() { - let _guard = test_guard().await; - RUN_RHI_SKIP_SUBSCRIBER.store(true, Ordering::Relaxed); - *run_rhi_bootstrap_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Ok(())); - let temp = tempfile::tempdir().expect("tempdir"); - let context = context_for_root(temp.path()); - provision_identity(&context); - let settings = settings_with_relays(vec!["wss://relay.example.com".to_string()], &context); - run_rhi(&settings, &context).await.expect("run rhi"); - assert_eq!(release_product_handler_kinds(), TRADE_MUTATION_EVENT_KINDS); - } - - #[tokio::test] - async fn run_rhi_stops_on_wait_hook() { - let _guard = test_guard().await; - *run_rhi_wait_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunRhiWaitOutcome::Stopped); - let temp = tempfile::tempdir().expect("tempdir"); - let context = context_for_root(temp.path()); - provision_identity(&context); - let settings = settings_with_relays(Vec::new(), &context); - run_rhi(&settings, &context).await.expect("run rhi"); - } - - #[tokio::test] - async fn bootstrap_presence_reports_hook_error() { - 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 = 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 = - 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"); - assert!(format!("{err:#}").contains("forced bootstrap failure")); - } - - #[test] - fn service_profile_uses_only_strict_authored_fields() { - let metadata: crate::host_nostr::Metadata = serde_json::from_str( - r#"{ - "name":"rhi", - "display_name":"Radroots agreement attestation", - "about":"Attests trade agreement projections", - "nip05":"rhi@EXAMPLE.COM", - "bot":true - }"#, - ) - .expect("metadata"); - - let profile = authored_service_profile(&metadata).expect("strict profile"); - - assert_eq!(profile.name(), "rhi"); - assert_eq!( - profile.display_name(), - Some("Radroots agreement attestation") - ); - assert_eq!(profile.about(), Some("Attests trade agreement projections")); - assert_eq!( - profile.nip05().map(|identifier| identifier.as_str()), - Some("rhi@example.com") - ); - assert_eq!(profile.bot(), Some(true)); - assert!(profile.picture().is_none()); - assert!(profile.banner().is_none()); - - let keys = crate::host_nostr::Keys::generate(); - let event = build_authored_service_profile_event(&metadata) - .expect("strict Profile event") - .sign_with_keys(&keys) - .expect("sign strict Profile event"); - assert_eq!(event.kind.as_u16(), 0); - assert!(event.tags.is_empty()); - assert_eq!( - event.content, - r#"{"name":"rhi","display_name":"Radroots agreement attestation","about":"Attests trade agreement projections","nip05":"rhi@example.com","bot":true}"# - ); - } - - #[test] - fn service_profile_rejects_unverified_media_and_unsupported_fields() { - for (metadata, expected) in [ - ( - r#"{"name":"rhi","picture":"https://cdn.example/rhi.png"}"#, - "byte-verified Blossom descriptors", - ), - ( - r#"{"name":"rhi","website":"https://radroots.org"}"#, - "outside the strict authored contract", - ), - ( - r#"{"name":"rhi","bot":"yes"}"#, - "`bot` field must be a Boolean", - ), - ( - r#"{"name":"rhi","legacy_role":"worker"}"#, - "unsupported metadata field `legacy_role`", - ), - (r#"{}"#, "requires metadata.name"), - ( - r#"{"name":"rhi","nip05":"RHI@example.com"}"#, - "validate strict RHI service Profile NIP-05 identifier", - ), - ] { - let metadata = serde_json::from_str(metadata).expect("metadata"); - let error = authored_service_profile(&metadata).expect_err("must fail closed"); - assert!(format!("{error:#}").contains(expected), "{error:#}"); - } - } -} diff --git a/src/main.rs b/src/main.rs @@ -3,10 +3,9 @@ use std::path::PathBuf; use std::process::ExitCode; -use anyhow::{Context, Result, bail}; use rhi::{ - RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiCommandV1, - RhiRuntimeContext, parse_rhi_cli_v1_from, resolve_rhi_runtime_context, run_rhi, + RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, parse_rhi_cli_v1_from, + resolve_rhi_runtime_context, }; fn main() -> ExitCode { @@ -14,17 +13,10 @@ fn main() -> ExitCode { Ok(invocation) => invocation, Err(_) => return ExitCode::FAILURE, }; - let runtime = match tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - { - Ok(runtime) => runtime, - Err(_) => return ExitCode::FAILURE, - }; - exit_code_from_run(runtime.block_on(execute(invocation))) + exit_code_from_run(execute(invocation)) } -fn exit_code_from_run(result: Result<()>) -> ExitCode { +fn exit_code_from_run(result: Result<(), ()>) -> ExitCode { match result { Ok(()) => ExitCode::SUCCESS, Err(_) => { @@ -34,49 +26,10 @@ fn exit_code_from_run(result: Result<()>) -> ExitCode { } } -async fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<()> { +fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<(), ()> { let resolver = RadrootsPathResolver::new(RadrootsPlatform::current(), host_environment()); - let context = resolve_rhi_runtime_context(&resolver, &invocation) - .map_err(|_| anyhow::anyhow!("RHI runtime context is invalid"))?; - match invocation.command() { - RhiCommandV1::Run => execute_run(&context).await, - _ => bail!("RHI command execution is not available in this checkpoint"), - } -} - -async fn execute_run(context: &RhiRuntimeContext) -> Result<()> { - let settings = rhi::config::load_settings_from_path(context.selected_config_path(), context) - .context("load RHI configuration")?; - init_rhi_logging(&settings)?; - run_rhi(&settings, context).await -} - -fn init_rhi_logging(settings: &rhi::config::Settings) -> Result<()> { - 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(|_| anyhow::anyhow!("initialize RHI logging"))?; - } else { - tracing_subscriber::fmt() - .with_env_filter(filter) - .with_writer(writer) - .try_init() - .map_err(|_| anyhow::anyhow!("initialize RHI logging"))?; - } - LOG_GUARD - .set(guard) - .map_err(|_| anyhow::anyhow!("RHI logging is already initialized")) + let _context = resolve_rhi_runtime_context(&resolver, &invocation).map_err(|_| ())?; + Err(()) } fn host_environment() -> RadrootsHostEnvironment { @@ -99,15 +52,35 @@ fn host_environment() -> RadrootsHostEnvironment { #[cfg(test)] mod tests { - use super::exit_code_from_run; + use super::{execute, exit_code_from_run}; + use rhi::parse_rhi_cli_v1_from; use std::process::ExitCode; #[test] fn process_result_is_stable() { assert_eq!(exit_code_from_run(Ok(())), ExitCode::SUCCESS); + assert_eq!(exit_code_from_run(Err(())), ExitCode::FAILURE); + } + + #[test] + fn admitted_command_fails_closed_without_creating_runtime_state() { + let root = tempfile::tempdir().expect("temporary repo-local root"); + let invocation = parse_rhi_cli_v1_from([ + "rhi", + "--profile", + "repo-local", + "--instance", + "default", + "--repo-local-root", + root.path().to_str().expect("UTF-8 test root"), + "run", + ]) + .expect("valid invocation"); + + assert_eq!(execute(invocation), Err(())); assert_eq!( - exit_code_from_run(Err(anyhow::anyhow!("secret path"))), - ExitCode::FAILURE + std::fs::read_dir(root.path()).expect("read root").count(), + 0 ); } } diff --git a/src/rhi.rs b/src/rhi.rs @@ -1,345 +0,0 @@ -#![cfg_attr(coverage_nightly, coverage(off))] - -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use crate::host_nostr::{Client, Keys}; -use crate::host_runtime::{Backoff, BackoffConfig}; -use tokio::sync::Mutex; - -use crate::features::trade_agreement_attestation::{ - TradeAgreementAttestationPolicy, TradeAgreementAttestationRuntime, -}; - -#[cfg(not(test))] -fn connection_wait_timeout() -> Duration { - Duration::from_secs(5) -} - -#[cfg(test)] -fn connection_wait_timeout() -> Duration { - Duration::from_millis(10) -} - -#[cfg(test)] -static SUBSCRIBER_RESULT_HOOK: std::sync::OnceLock< - std::sync::Mutex<std::collections::VecDeque<Result<(), anyhow::Error>>>, -> = std::sync::OnceLock::new(); - -#[cfg(test)] -fn subscriber_result_hook() --> &'static std::sync::Mutex<std::collections::VecDeque<Result<(), anyhow::Error>>> { - SUBSCRIBER_RESULT_HOOK.get_or_init(|| std::sync::Mutex::new(std::collections::VecDeque::new())) -} - -async fn run_subscriber_once( - client: Client, - keys: Keys, - runtime: TradeAgreementAttestationRuntime, - policy: TradeAgreementAttestationPolicy, - stop_rx: tokio::sync::watch::Receiver<bool>, -) -> Result<(), anyhow::Error> { - #[cfg(test)] - if let Some(result) = subscriber_result_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .pop_front() - { - return result; - } - - crate::features::trade_agreement_attestation::subscriber(client, keys, runtime, policy, stop_rx) - .await -} - -async fn wait_for_connection_or_stop( - client: &Client, - stop_rx: &mut tokio::sync::watch::Receiver<bool>, -) -> bool { - if *stop_rx.borrow() { - return false; - } - tokio::select! { - _ = client.wait_for_connection(connection_wait_timeout()) => true, - _ = stop_rx.changed() => false, - } -} - -pub struct Rhi { - pub(crate) _started: Instant, - pub client: Client, - pub(crate) agreement_attestation_runtime: TradeAgreementAttestationRuntime, - pub(crate) agreement_attestation_policy: TradeAgreementAttestationPolicy, -} - -impl Rhi { - pub fn new(keys: Keys) -> Self { - Self::with_agreement_attestation_runtime(keys, TradeAgreementAttestationRuntime::new()) - } - - pub fn with_agreement_attestation_runtime( - keys: Keys, - agreement_attestation_runtime: TradeAgreementAttestationRuntime, - ) -> Self { - Self::with_agreement_attestation_runtime_and_policy( - keys, - agreement_attestation_runtime, - TradeAgreementAttestationPolicy::default(), - ) - } - - pub fn with_agreement_attestation_runtime_and_policy( - keys: Keys, - agreement_attestation_runtime: TradeAgreementAttestationRuntime, - agreement_attestation_policy: TradeAgreementAttestationPolicy, - ) -> Self { - let client = Client::new(keys); - Self { - _started: Instant::now(), - client, - agreement_attestation_runtime, - agreement_attestation_policy, - } - } -} - -pub struct RhiHandle { - stop_tx: Arc<Mutex<Option<tokio::sync::watch::Sender<bool>>>>, - join: Option<tokio::task::JoinHandle<()>>, -} - -impl Clone for RhiHandle { - fn clone(&self) -> Self { - Self { - stop_tx: Arc::clone(&self.stop_tx), - join: None, - } - } -} - -impl RhiHandle { - pub fn stop(&self) { - if let Some(tx) = self.stop_tx.try_lock().ok().and_then(|mut opt| opt.take()) { - let _ = tx.send(true); - } - } - - pub async fn stopped(mut self) { - if let Some(join) = self.join.take() { - let _ = join.await; - } - } -} - -pub async fn start_subscriber( - client: Client, - keys: Keys, - runtime: TradeAgreementAttestationRuntime, - backoff_cfg: BackoffConfig, -) -> RhiHandle { - start_subscriber_with_policy( - client, - keys, - runtime, - TradeAgreementAttestationPolicy::default(), - backoff_cfg, - ) - .await -} - -pub async fn start_subscriber_with_policy( - client: Client, - keys: Keys, - runtime: TradeAgreementAttestationRuntime, - policy: TradeAgreementAttestationPolicy, - backoff_cfg: BackoffConfig, -) -> RhiHandle { - let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false); - - let join = tokio::spawn(async move { - let mut backoff = Backoff::new(backoff_cfg); - loop { - if *stop_rx.borrow() { - break; - } - - client.connect().await; - if !wait_for_connection_or_stop(&client, &mut stop_rx).await { - break; - } - - let res = run_subscriber_once( - client.clone(), - keys.clone(), - runtime.clone(), - policy.clone(), - stop_rx.clone(), - ) - .await; - - let failed = res.is_err(); - - if let Err(e) = res { - tracing::error!("Error on agreement attestation subscription: {e}"); - } else { - backoff.reset(); - } - - if *stop_rx.borrow() { - break; - } - - if failed { - let delay = backoff.next_delay(); - tokio::select! { - _ = tokio::time::sleep(delay) => {} - _ = stop_rx.changed() => break, - } - } - } - }); - - RhiHandle { - stop_tx: Arc::new(Mutex::new(Some(stop_tx))), - join: Some(join), - } -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - 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 std::sync::Arc; - use tokio::sync::Mutex; - - #[tokio::test] - async fn rhi_new_initializes_client_and_runtime() { - let keys = Keys::generate(); - let rhi = Rhi::new(keys); - let _ = rhi.client.clone(); - assert!(rhi.agreement_attestation_runtime.reports().await.is_empty()); - } - - #[tokio::test] - async fn rhi_handle_stop_and_stopped_cover_paths() { - let (tx, _rx) = tokio::sync::watch::channel(false); - let join = tokio::spawn(async {}); - let handle = RhiHandle { - stop_tx: Arc::new(Mutex::new(Some(tx))), - join: Some(join), - }; - handle.stop(); - handle.stop(); - handle.clone().stopped().await; - handle.stopped().await; - } - - #[tokio::test] - async fn start_subscriber_runs_with_and_without_relay() { - let keys = Keys::generate(); - let cfg = BackoffConfig { - base_ms: 1, - max_ms: 2, - factor: 1, - jitter_ms: 0, - }; - - let client_err = Client::new(keys.clone()); - let handle_err = start_subscriber( - client_err, - keys.clone(), - TradeAgreementAttestationRuntime::new(), - cfg.clone(), - ) - .await; - tokio::time::sleep(std::time::Duration::from_millis(30)).await; - handle_err.stop(); - handle_err.stopped().await; - - let client_ok = Client::new(keys.clone()); - let _ = client_ok.add_relay("wss://relay.example.com").await; - subscriber_result_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .push_back(Ok(())); - let handle_ok = start_subscriber( - client_ok, - keys, - TradeAgreementAttestationRuntime::new(), - cfg, - ) - .await; - tokio::time::sleep(std::time::Duration::from_millis(30)).await; - handle_ok.stop(); - handle_ok.stopped().await; - } - - #[tokio::test] - async fn start_subscriber_stops_during_connection_wait_branch() { - let keys = Keys::generate(); - let client = Client::new(keys.clone()); - let handle = start_subscriber( - client, - keys, - TradeAgreementAttestationRuntime::new(), - BackoffConfig { - base_ms: 25, - max_ms: 50, - factor: 1, - jitter_ms: 0, - }, - ) - .await; - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - handle.stop(); - handle.stopped().await; - } - - #[tokio::test] - async fn start_subscriber_stops_during_backoff_wait_branch() { - let keys = Keys::generate(); - let client = Client::new(keys.clone()); - let _ = client.add_relay("wss://relay.example.com").await; - subscriber_result_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .push_back(Err(anyhow!("forced subscriber failure"))); - let handle = start_subscriber( - client, - keys, - TradeAgreementAttestationRuntime::new(), - BackoffConfig { - base_ms: 200, - max_ms: 200, - factor: 1, - jitter_ms: 0, - }, - ) - .await; - tokio::time::sleep(std::time::Duration::from_millis(25)).await; - handle.stop(); - handle.stopped().await; - } - - #[tokio::test] - async fn wait_for_connection_or_stop_covers_both_outcomes() { - let keys = Keys::generate(); - - 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 = 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/services_hardening_runtime_context.rs b/tests/services_hardening_runtime_context.rs @@ -263,7 +263,6 @@ fn source_contains_no_legacy_path_authority() { let lib = include_str!("../src/lib.rs"); let context = include_str!("../src/runtime_context.rs"); let main = include_str!("../src/main.rs"); - let config = include_str!("../src/config.rs"); for forbidden in [ "pub mod host_paths", "pub mod paths", @@ -279,9 +278,13 @@ fn source_contains_no_legacy_path_authority() { assert!(!lib.contains(forbidden)); assert!(!context.contains(forbidden)); assert!(!main.contains(forbidden)); - assert!(!config.contains(forbidden)); } - for removed in ["src/paths.rs", "src/host_paths/mod.rs", "src/cli.rs"] { + for removed in [ + "src/paths.rs", + "src/host_paths/mod.rs", + "src/cli.rs", + "src/config.rs", + ] { assert!( !Path::new(env!("CARGO_MANIFEST_DIR")).join(removed).exists(), "legacy path authority remains at {removed}" diff --git a/tests/services_hardening_wave_100_a.rs b/tests/services_hardening_wave_100_a.rs @@ -0,0 +1,84 @@ +#![forbid(unsafe_code)] + +use std::path::Path; + +use rhi::{RHI_CONFIG_SCHEMA, RhiConfigProfile, parse_rhi_config_v1}; +use serde_json::Value; + +const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); +const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json"); +const SOURCE_LOCK: &str = include_str!("../radroots.service.source-lock.v2.toml"); + +#[test] +fn canonical_example_agrees_with_the_exact_schema_and_parser() { + let schema: Value = serde_json::from_str(CONFIG_SCHEMA).expect("configuration schema"); + let example: toml::Value = toml::from_str(CONFIG_EXAMPLE).expect("canonical example TOML"); + let example = serde_json::to_value(example).expect("canonical example JSON projection"); + let errors = jsonschema::validator_for(&schema) + .expect("configuration schema compiles") + .iter_errors(&example) + .map(|error| error.to_string()) + .collect::<Vec<_>>(); + assert!(errors.is_empty(), "schema/parser example drift: {errors:?}"); + + let document = parse_rhi_config_v1(CONFIG_EXAMPLE.as_bytes(), RhiConfigProfile::Production) + .expect("the canonical example must pass the production parser"); + assert_eq!(document.schema(), RHI_CONFIG_SCHEMA); + assert_eq!(document.profile(), RhiConfigProfile::Production); + assert!(document.effective().field_count() > 0); +} + +#[test] +fn wave_one_removed_files_and_predecessor_lock_are_absent() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + for removed in [ + "config.toml", + "flake.lock", + "flake.nix", + "radroots.lib.source-lock.v1.toml", + "src/config.rs", + "src/host_nostr.rs", + "src/host_runtime.rs", + "src/rhi.rs", + ] { + assert!( + !root.join(removed).exists(), + "removed path remains: {removed}" + ); + } + assert!(root.join("radroots.service.source-lock.v2.toml").is_file()); + assert!(SOURCE_LOCK.starts_with( + "schema = \"radroots.service.source-lock.v2\"\ncontract_version = 2\nservice = \"rhi\"\n" + )); + for forbidden in [ + "lib_revision =", + "flake_lock_sha256", + "material = \"present\"", + ] { + assert!( + !SOURCE_LOCK.contains(forbidden), + "source lock retains predecessor behavior: {forbidden}" + ); + } +} + +#[test] +fn executable_has_no_prototype_runtime_fallback() { + let main = include_str!("../src/main.rs"); + for forbidden in [ + "load_settings_from_path", + "run_rhi", + "init_rhi_logging", + "tokio::runtime", + "tracing_subscriber", + "RHI_", + ] { + assert!( + !main.contains(forbidden), + "executable retains prototype fallback: {forbidden}" + ); + } + assert!(main.contains("parse_rhi_cli_v1_from")); + assert!(main.contains("resolve_rhi_runtime_context")); + assert!(main.contains("Err(())")); +} diff --git a/tests/source_guards.rs b/tests/source_guards.rs @@ -100,73 +100,40 @@ fn rhi_release_product_surface_has_no_order_or_receipt_modules() { } #[test] -fn rhi_agreement_attestation_is_release_product_optional_infrastructure() { - let worker = read_repo_file("src/features/trade_agreement_attestation.rs"); - let lib = read_repo_file("src/lib.rs"); +fn rhi_agreement_attestation_retains_only_the_pure_foundation() { + let attestation = read_repo_file("src/features/trade_agreement_attestation.rs"); let cli = read_repo_file("src/cli_v1.rs"); - let config = read_repo_file("src/config.rs"); for required in [ "RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID", "TradeAgreementAttestationPolicy", "LocalStatementHash", - "TradeAgreementAttestationRuntime", - "handle_trade_mutation_event", "attest_projection_claim", - "claim_mutation_id", "projection_digest", "RadrootsTradeAttestationResultV1::Valid", "RadrootsTradeAttestationResultV1::Invalid", "TRADE_MUTATION_EVENT_KINDS", - "is_trade_mutation_event_kind", - "no_agreement_authority", "expected_statement_contract_hash", ] { assert!( - worker.contains(required), - "agreement attestation worker must retain release-product requirement `{required}`" + attestation.contains(required), + "agreement attestation foundation must retain release-product requirement `{required}`" ); } assert!( - lib.contains("trade_mutation_subscription_kinds()") - && 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" - ); - assert!( !cli.contains("AttestationSmoke") && !cli.contains("ProofSmoke") && !cli.contains("remote-prove"), "RHI CLI must not retain prototype smoke commands" ); - assert!( - config.contains("settings.config.trade_agreement_attestation.validate()?"), - "RHI config loading must validate the canonical attestation policy" - ); } #[test] -fn rhi_state_paths_are_named_for_agreement_attestation() { - let config = read_repo_file("src/config.rs"); +fn rhi_runtime_context_retains_only_governed_artifacts() { let context = read_repo_file("src/runtime_context.rs"); - assert!( - config.contains("trade-agreement-attestation"), - "transitional RHI state paths must use agreement-attestation naming" - ); - for source in [config.as_str(), context.as_str()] { - assert!( - !source.contains("trade-listing"), - "RHI runtime state paths must not retain trade-listing naming" - ); - } + assert!(!context.contains("trade-listing")); assert!( context.contains("default_service_instance_artifacts") && context.contains("service.identity.ncrypt"), @@ -174,6 +141,51 @@ fn rhi_state_paths_are_named_for_agreement_attestation() { ); } +#[test] +fn rhi_wave_one_removes_prototype_runtime_and_selection_authority() { + for forbidden_path in [ + "config.toml", + "flake.lock", + "flake.nix", + "radroots.lib.source-lock.v1.toml", + "src/config.rs", + "src/host_nostr.rs", + "src/host_runtime.rs", + "src/rhi.rs", + ] { + assert!( + !Path::new(env!("CARGO_MANIFEST_DIR")) + .join(forbidden_path) + .exists(), + "RHI must not retain removed wave-one path `{forbidden_path}`" + ); + } + + for (path, source) in rust_sources_under("src") { + for forbidden in [ + "load_settings_from_path", + "TradeAgreementAttestationRuntime", + "TradeAgreementAttestationStatePersistence", + "TradeAgreementAttestationSmoke", + "handle_smoke_request_bytes", + "worker_name", + "state.json", + "std::env::var(\"RHI_", + "std::env::var_os(\"RHI_", + "worker_root", + "nostr_sdk::Client", + "tokio::signal", + "tracing_appender", + "tracing_subscriber", + ] { + assert!( + !source.contains(forbidden), + "{path} retains removed wave-one authority `{forbidden}`" + ); + } + } +} + fn read_repo_file(relative_path: &str) -> String { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path); fs::read_to_string(path.as_path())