rhi

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

commit b4e083f3beb8dd59f6506a129980133cc3a12233
parent 1038fc9b5b564fbf54bab3bb7c810231b6e8a53e
Author: triesap <tyson@radroots.org>
Date:   Thu, 16 Jul 2026 17:40:24 +0000

release-product: align RHI agreement attestation

- remove order validation receipt and remote proof worker paths
- add release-product mutation observation and local attestation reports
- publish only canonical trade mutation handler kinds for service presence
- tighten config, CLI, state paths, source guards, and dependency graph

Diffstat:
MCargo.lock | 5781++++++++++++++-----------------------------------------------------------------
MCargo.toml | 16++--------------
Msrc/cli.rs | 18+++---------------
Msrc/config.rs | 103++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/features/mod.rs | 3+--
Asrc/features/trade_agreement_attestation.rs | 980+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/features/trade_listing/handlers/events.rs | 947-------------------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/mod.rs | 1-
Dsrc/features/trade_listing/mod.rs | 4----
Dsrc/features/trade_listing/processed_jobs.rs | 1762-------------------------------------------------------------------------------
Dsrc/features/trade_listing/state.rs | 583-------------------------------------------------------------------------------
Dsrc/features/trade_listing/subscriber.rs | 835-------------------------------------------------------------------------------
Dsrc/features/trade_validation_receipt.rs | 848-------------------------------------------------------------------------------
Msrc/lib.rs | 186++++++++++++++++++++++++++++---------------------------------------------------
Msrc/main.rs | 19+++++++++----------
Msrc/paths.rs | 2+-
Dsrc/proof_smoke.rs | 638-------------------------------------------------------------------------------
Dsrc/remote_prove.rs | 476-------------------------------------------------------------------------------
Msrc/rhi.rs | 93+++++++++++++++++++++++++++++++++++--------------------------------------------
Mtests/source_guards.rs | 216++++++++++++++++++++++++++++++++-----------------------------------------------
20 files changed, 2274 insertions(+), 11237 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3,39 +3,13 @@ version = 4 [[package]] -name = "addchain" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" -dependencies = [ - "num-bigint 0.3.3", - "num-integer", - "num-traits", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -46,7 +20,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -86,15 +60,6 @@ dependencies = [ ] [[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -157,51 +122,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "async-scoped" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740" -dependencies = [ - "futures", - "pin-project", - "tokio", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -209,7 +135,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -244,24 +170,6 @@ dependencies = [ ] [[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] name = "atomic-destructor" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -280,75 +188,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "serde", - "windows-link", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -373,15 +212,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] name = "bip39" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -419,38 +249,12 @@ dependencies = [ ] [[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake3" -version = "1.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures 0.3.0", -] - -[[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -459,7 +263,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -469,76 +273,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -577,7 +317,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -648,7 +388,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -664,15 +404,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] name = "config" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -692,25 +423,6 @@ dependencies = [ ] [[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -731,33 +443,6 @@ dependencies = [ ] [[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] name = "convert_case" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -782,4730 +467,1733 @@ dependencies = [ ] [[package]] -name = "cpufeatures" -version = "0.3.0" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "libc", + "crossbeam-utils", ] [[package]] -name = "crash-context" -version = "0.6.3" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" -dependencies = [ - "cfg-if", - "libc", - "mach2", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crash-handler" -version = "0.7.0" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df5c9639f4942eb7702b964b3f9adf03a55724a57558cc177407388a8b936e2" -dependencies = [ - "cfg-if", - "crash-context", - "libc", - "mach2", - "parking_lot", -] +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] -name = "crc" -version = "3.4.0" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "crc-catalog", + "generic-array", + "rand_core 0.6.4", + "typenum", ] [[package]] -name = "crc-catalog" -version = "2.5.0" +name = "data-encoding" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] -name = "crc32fast" -version = "1.5.0" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "cfg-if", + "powerfmt", ] [[package]] -name = "crossbeam" -version = "0.8.4" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", + "block-buffer", + "crypto-common", + "subtle", ] [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "crossbeam-utils", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "dlv-list" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "const-random", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "crossbeam-queue" -version = "0.3.12" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "crossbeam-utils", + "cfg-if", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "crypto-bigint" -version = "0.5.5" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "subtle", - "zeroize", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "typenum", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "dashu" -version = "0.4.2" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b3e5ac1e23ff1995ef05b912e2b012a8784506987a2651552db2c73fb3d7e0" -dependencies = [ - "dashu-base", - "dashu-float", - "dashu-int", - "dashu-macros", - "dashu-ratio", - "rustversion", -] +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "dashu-base" -version = "0.4.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "dashu-float" -version = "0.4.3" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85078445a8dbd2e1bd21f04a816f352db8d333643f0c9b78ca7c3d1df71063e7" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "dashu-base", - "dashu-int", - "num-modular", - "num-order", - "rustversion", - "static_assertions", + "percent-encoding", ] [[package]] -name = "dashu-int" -version = "0.4.1" +name = "futures" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ - "cfg-if", - "dashu-base", - "num-modular", - "num-order", - "rustversion", - "static_assertions", + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "dashu-macros" -version = "0.4.1" +name = "futures-channel" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93381c3ef6366766f6e9ed9cf09e4ef9dec69499baf04f0c60e70d653cf0ab10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ - "dashu-base", - "dashu-float", - "dashu-int", - "dashu-ratio", - "paste", - "proc-macro2", - "quote", - "rustversion", + "futures-core", + "futures-sink", ] [[package]] -name = "dashu-ratio" -version = "0.4.1" +name = "futures-core" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e33b04dd7ce1ccf8a02a69d3419e354f2bbfdf4eb911a0b7465487248764c9" -dependencies = [ - "dashu-base", - "dashu-float", - "dashu-int", - "num-modular", - "num-order", - "rustversion", -] +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] -name = "data-encoding" -version = "2.10.0" +name = "futures-io" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] -name = "deepsize2" -version = "0.1.0" +name = "futures-sink" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b5184084af9beed35eecbf4c36baf6e26b9dc47b61b74e02f930c72a58e71b" -dependencies = [ - "deepsize_derive2", - "hashbrown 0.14.5", -] +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] -name = "deepsize_derive2" -version = "0.1.0" +name = "futures-task" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f8817865cacf3b93b943ca06b0fc5fd8e99eabfdb7ea5d296efcbc4afc4f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] -name = "der" -version = "0.7.10" +name = "futures-util" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", ] [[package]] -name = "deranged" -version = "0.5.8" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "powerfmt", + "typenum", + "version_check", ] [[package]] -name = "derive-where" -version = "1.6.1" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] -name = "derive_more" -version = "1.0.0" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "derive_more-impl", + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] -name = "derive_more-impl" -version = "1.0.0" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", ] [[package]] -name = "digest" -version = "0.10.7" +name = "gloo-timers" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "dirs" -version = "5.0.1" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "dirs-sys", + "ahash", + "allocator-api2", ] [[package]] -name = "dirs-sys" -version = "0.4.1" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", + "foldhash", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "dlv-list" -version = "0.5.2" +name = "hashlink" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "const-random", + "hashbrown 0.14.5", ] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "downcast-rs" -version = "1.2.1" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "dynasm" -version = "3.2.1" +name = "hex-conservative" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ - "bitflags", - "byteorder", - "lazy_static", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", + "arrayvec", ] [[package]] -name = "dynasmrt" -version = "3.2.1" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "byteorder", - "dynasm", - "fnv", - "memmap2", + "digest", ] [[package]] -name = "ecdsa" -version = "0.16.9" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "serdect", - "signature", - "spki", + "bytes", + "itoa", ] [[package]] -name = "either" -version = "1.15.0" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "serde", + "bytes", + "http", ] [[package]] -name = "elf" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array 0.14.7", - "group", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "serdect", - "subtle", - "zeroize", + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", ] [[package]] -name = "encode_unicode" -version = "1.0.0" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "hyper" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ - "cfg-if", + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", ] [[package]] -name = "enum-map" -version = "2.7.3" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "enum-map-derive", - "serde", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", ] [[package]] -name = "enum-map-derive" -version = "0.17.0" +name = "hyper-util" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "iana-time-zone" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] [[package]] -name = "errno" -version = "0.3.14" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "libc", - "windows-sys 0.61.2", + "cc", ] [[package]] -name = "event-listener" -version = "5.4.1" +name = "icu_collections" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "eventsource-stream" -version = "0.2.3" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ - "futures-core", - "nom", - "pin-project-lite", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "eyre" -version = "0.6.12" +name = "icu_normalizer" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "indenter", - "once_cell", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "icu_normalizer_data" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] -name = "ff" -version = "0.13.1" +name = "icu_properties" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "bitvec", - "byteorder", - "ff_derive", - "rand_core 0.6.4", - "subtle", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", ] [[package]] -name = "ff_derive" -version = "0.13.1" +name = "icu_properties_data" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60" -dependencies = [ - "addchain", - "num-bigint 0.3.3", - "num-integer", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "icu_provider" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] [[package]] -name = "fixedbitset" -version = "0.5.7" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "flate2" -version = "1.1.9" +name = "idna" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "crc32fast", - "miniz_oxide", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "flume" -version = "0.12.0" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ - "futures-core", - "futures-sink", - "spin", + "icu_normalizer", + "icu_properties", ] [[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" +name = "indexmap" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] [[package]] -name = "foldhash" -version = "0.2.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "instant" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "percent-encoding", + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "funty" -version = "2.0.0" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "futures" -version = "0.3.32" +name = "iri-string" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "memchr", + "serde", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "futures-core" -version = "0.3.32" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "futures-executor" -version = "0.3.32" +name = "js-sys" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "futures-intrusive" -version = "0.5.0" +name = "json5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" dependencies = [ - "futures-core", - "lock_api", - "parking_lot", + "pest", + "pest_derive", + "serde", ] [[package]] -name = "futures-io" -version = "0.3.32" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gcd" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" - -[[package]] -name = "gen_ops" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304de19db7028420975a296ab0fcbbc8e69438c4ed254a1e41e2a7f37d5f0e0a" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "generic-array" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96512db27971c2c3eece70a1e106fbe6c87760234e31e8f7e5634912fe52794a" -dependencies = [ - "serde", - "typenum", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[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" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", - "serde", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "hashlink" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex-conservative" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.6", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.3", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console", - "number_prefix", - "portable-atomic", - "unicode-width", - "web-time", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array 0.14.7", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "serdect", - "sha2", - "signature", -] - -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libredox" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "libc", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.37.0" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memfd" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" -dependencies = [ - "rustix", -] - -[[package]] -name = "memmap2" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -dependencies = [ - "libc", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "mti" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9563a7d5556636e74bbd8773241fbcbc5c89b9f6bfdc97b29b56e740c2c74b9" -dependencies = [ - "typeid_prefix", - "typeid_suffix", -] - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "negentropy" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nostr" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" -dependencies = [ - "aes", - "base64 0.22.1", - "bech32", - "bip39", - "bitcoin_hashes", - "cbc", - "chacha20", - "chacha20poly1305", - "getrandom 0.2.17", - "hex", - "instant", - "scrypt", - "secp256k1", - "serde", - "serde_json", - "unicode-normalization", - "url", -] - -[[package]] -name = "nostr-database" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" -dependencies = [ - "lru 0.16.3", - "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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" -dependencies = [ - "async-utility", - "async-wsocket", - "atomic-destructor", - "hex", - "lru 0.16.3", - "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 = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint 0.4.6", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[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.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-modular" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" - -[[package]] -name = "num-order" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" -dependencies = [ - "num-modular", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint 0.4.6", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "crc32fast", - "flate2", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "memchr", - "ruzstd", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "opentelemetry" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "once_cell", - "pin-project-lite", - "thiserror 1.0.69", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p3-air" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a5de20a2301bf2530de1ceb13768ec3a80f729fbf8b72f813e30bc54c5bce2" -dependencies = [ - "p3-field", - "p3-matrix", - "serde", -] - -[[package]] -name = "p3-baby-bear" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69e6e9af4eaaaa60f7bb9f0e0f73ebcbaefe7e00974d97ad0fa542d6a4f0890" -dependencies = [ - "cfg-if", - "num-bigint 0.4.6", - "p3-field", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "rand 0.8.5", - "rustc_version", - "serde", -] - -[[package]] -name = "p3-bn254-fr" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2077757c7cb514202ccb5368f521f23f5709c720599e6545c683c66e0a52d2d8" -dependencies = [ - "ff", - "num-bigint 0.4.6", - "p3-field", - "p3-poseidon2", - "p3-symmetric", - "rand 0.8.5", - "serde", -] - -[[package]] -name = "p3-challenger" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a908924d43e4cfb93fb41c8346cac211b70314385a9037e9241f5b7f3eaf77" -dependencies = [ - "p3-field", - "p3-maybe-rayon", - "p3-symmetric", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-commit" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50acacc7219fce6c01db938f82c1b21b5e7133990b7fff861f91534aeb569419" -dependencies = [ - "itertools 0.12.1", - "p3-challenger", - "p3-field", - "p3-matrix", - "p3-util", - "serde", -] - -[[package]] -name = "p3-dft" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6408b10a2c27eb13a7d5580c546c2179a8dc7dbc10a990657311891f9b41c0" -dependencies = [ - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "tracing", -] - -[[package]] -name = "p3-field" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc75969ca3ac847f43e632ab979d59ff7a68f9eac8dbf8edcbba47fc2e1d3aa" -dependencies = [ - "itertools 0.12.1", - "num-bigint 0.4.6", - "num-traits", - "p3-util", - "rand 0.8.5", - "serde", -] - -[[package]] -name = "p3-fri" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc4965ee488f3247867b7ec4bb005b8afa72cb0d461a4dcb1387ecab6426d5" -dependencies = [ - "itertools 0.12.1", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-interpolation", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-interpolation" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ad7e9f08c336d7ea39d12e11951188473542565323bac2a6535e536b58487d" -dependencies = [ - "p3-field", - "p3-matrix", - "p3-util", -] - -[[package]] -name = "p3-keccak-air" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f5bf177d56740078b5a5a842fa2393427796283dc8174b4a1a325c2d0b042de" -dependencies = [ - "p3-air", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "tracing", -] - -[[package]] -name = "p3-koala-bear" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9683cd0ef68100df7c62490533047bcf19c04c4a0fa1efc9d7c1e03e31f6b3" -dependencies = [ - "cfg-if", - "num-bigint 0.4.6", - "p3-field", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "rand 0.8.5", - "rustc_version", - "serde", -] - -[[package]] -name = "p3-matrix" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c3f150ceb90e09539413bf481e618d05ee19210b4e467d2902eb82d2e15281" -dependencies = [ - "itertools 0.12.1", - "p3-field", - "p3-maybe-rayon", - "p3-util", - "rand 0.8.5", - "serde", - "tracing", -] - -[[package]] -name = "p3-maybe-rayon" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0641952b42da45e1dfa2d4a2a3163e330f944ad9740942f35026c0a71a605f1" -dependencies = [ - "rayon", -] - -[[package]] -name = "p3-mds" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4a5f250e174dcfca5cbeac6ad75713924e7e7320e0a335e3c50b8b1f4fe8ec" -dependencies = [ - "itertools 0.12.1", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-symmetric", - "p3-util", - "rand 0.8.5", -] - -[[package]] -name = "p3-merkle-tree" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5703d9229d52a8c09970e4d722c3a8b4d37e688c306c3a1c03b872efcd204e6" -dependencies = [ - "itertools 0.12.1", - "p3-commit", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-symmetric", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-poseidon2" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522986377b2164c5f94f2dae88e0e0a3d169cc6239202ef4aeb4322d60feffd0" -dependencies = [ - "gcd", - "p3-field", - "p3-mds", - "p3-symmetric", - "rand 0.8.5", - "serde", -] - -[[package]] -name = "p3-symmetric" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9047ce85c086a9b3f118e10078f10636f7bfeed5da871a04da0b61400af8793a" -dependencies = [ - "itertools 0.12.1", - "p3-field", - "serde", -] - -[[package]] -name = "p3-uni-stark" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3dfdeba14d8db621c4e52dd63973384ff35f353fd750154ff88397f4ea5adf" -dependencies = [ - "itertools 0.12.1", - "p3-air", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-util" -version = "0.4.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff962f8eaa5f36e0447cee7c241f6b4b475fadf3ee61f154327a26bb4e009ba" -dependencies = [ - "serde", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.13.0", -] - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.11+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck", - "itertools 0.14.0", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.117", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.6.3", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.3", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "radroots_authority" -version = "0.1.0-alpha.2" -dependencies = [ - "radroots_event", -] - -[[package]] -name = "radroots_core" -version = "0.1.0-alpha.2" -dependencies = [ - "rust_decimal", - "rust_decimal_macros", - "serde", -] - -[[package]] -name = "radroots_event" -version = "0.1.0-alpha.2" -dependencies = [ - "hex", - "radroots_core", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "radroots_event_codec" -version = "0.1.0-alpha.2" -dependencies = [ - "nostr", - "radroots_core", - "radroots_event", - "serde", - "serde_json", -] - -[[package]] -name = "radroots_identity" -version = "0.1.0-alpha.2" -dependencies = [ - "nostr", - "radroots_event", - "radroots_protected_store", - "radroots_runtime", - "radroots_runtime_paths", - "radroots_secret_vault", - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", -] - -[[package]] -name = "radroots_log" -version = "0.1.0-alpha.2" -dependencies = [ - "chrono", - "thiserror 1.0.69", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - -[[package]] -name = "radroots_nostr" -version = "0.1.0-alpha.2" -dependencies = [ - "nostr", - "nostr-sdk", - "radroots_event", - "radroots_event_codec", - "radroots_identity", - "reqwest", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "radroots_protected_store" -version = "0.1.0-alpha.2" -dependencies = [ - "chacha20poly1305", - "getrandom 0.2.17", - "radroots_secret_vault", - "serde", - "serde_json", - "zeroize", -] - -[[package]] -name = "radroots_runtime" -version = "0.1.0-alpha.2" -dependencies = [ - "anyhow", - "chacha20poly1305", - "clap", - "config", - "getrandom 0.2.17", - "radroots_log", - "radroots_protected_store", - "radroots_runtime_paths", - "radroots_secret_vault", - "serde", - "serde_json", - "tempfile", - "thiserror 1.0.69", - "tokio", - "toml", - "tracing", - "zeroize", -] - -[[package]] -name = "radroots_runtime_paths" -version = "0.1.0-alpha.2" -dependencies = [ - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "radroots_secret_vault" -version = "0.1.0-alpha.2" - -[[package]] -name = "radroots_trade" -version = "0.1.0-alpha.2" -dependencies = [ - "base64 0.22.1", - "hex", - "radroots_authority", - "radroots_core", - "radroots_event", - "radroots_event_codec", - "serde", - "serde_json", - "sha2", - "thiserror 1.0.69", -] - -[[package]] -name = "radroots_trade_sp1_guest" -version = "0.1.0-alpha.2" -dependencies = [ - "serde", - "serde_json", - "sha2", - "thiserror 1.0.69", -] - -[[package]] -name = "radroots_trade_sp1_host" -version = "0.1.0-alpha.2" -dependencies = [ - "base64 0.22.1", - "bincode", - "futures", - "radroots_trade", - "radroots_trade_sp1_guest", - "serde", - "serde_json", - "sha2", - "sp1-build", - "sp1-sdk", - "thiserror 1.0.69", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "range-set-blaze" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8421b5d459262eabbe49048d362897ff3e3830b44eac6cfe341d6acb2f0f13d2" -dependencies = [ - "gen_ops", - "itertools 0.12.1", - "num-integer", - "num-traits", -] - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "rayon-scan" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f87cc11a0140b4b0da0ffc889885760c61b13672d80a908920b2c0df078fa14" -dependencies = [ - "rayon", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower 0.5.3", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 1.0.6", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "rhi" +name = "leb128fmt" version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "radroots_core", - "radroots_event", - "radroots_event_codec", - "radroots_identity", - "radroots_log", - "radroots_nostr", - "radroots_runtime", - "radroots_runtime_paths", - "radroots_trade", - "radroots_trade_sp1_guest", - "radroots_trade_sp1_host", - "reqwest", - "serde", - "serde_json", - "sha2", - "sqlx", - "tempfile", - "thiserror 2.0.18", - "tokio", - "toml", - "tracing", -] - -[[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 = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags", - "serde", - "serde_derive", -] - -[[package]] -name = "rrs-succinct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd079cd303257a4cb4e5aadfa79a7fe23f3c8301aa4740ccc3a99673485a352" -dependencies = [ - "downcast-rs", - "num_enum", - "paste", -] - -[[package]] -name = "rust-ini" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rust_decimal" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" -dependencies = [ - "arrayvec", - "num-traits", - "serde", -] - -[[package]] -name = "rust_decimal_macros" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "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 = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "rustls-pki-types" -version = "1.14.0" +name = "libc" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] -name = "rustls-webpki" -version = "0.103.9" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "rustversion" -version = "1.0.22" +[[package]] +name = "litemap" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] -name = "ruzstd" -version = "0.8.3" +name = "lock_api" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "twox-hash", + "scopeguard", ] [[package]] -name = "ryu" -version = "1.0.23" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "salsa20" -version = "0.10.2" +name = "lru" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] -name = "scale-info" -version = "2.11.6" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" -dependencies = [ - "cfg-if", - "derive_more", - "parity-scale-codec", - "scale-info-derive", -] +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "scale-info-derive" -version = "2.11.6" +name = "matchers" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", + "regex-automata", ] [[package]] -name = "scc" -version = "2.4.0" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "scopeguard" -version = "1.2.0" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "scrypt" -version = "0.11.0" +name = "mio" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "password-hash", - "pbkdf2", - "salsa20", - "sha2", + "libc", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "sdd" -version = "3.0.10" +name = "negentropy" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" [[package]] -name = "sec1" -version = "0.7.3" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "base16ct", - "der", - "generic-array 0.14.7", - "pkcs8", - "serdect", - "subtle", - "zeroize", + "memchr", + "minimal-lexical", ] [[package]] -name = "secp256k1" -version = "0.29.1" +name = "nostr" +version = "0.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" dependencies = [ - "rand 0.8.5", - "secp256k1-sys", + "aes", + "base64 0.22.1", + "bech32", + "bip39", + "bitcoin_hashes", + "cbc", + "chacha20", + "chacha20poly1305", + "getrandom 0.2.17", + "hex", + "instant", + "scrypt", + "secp256k1", "serde", + "serde_json", + "unicode-normalization", + "url", ] [[package]] -name = "secp256k1-sys" -version = "0.10.1" +name = "nostr-database" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" dependencies = [ - "cc", + "lru", + "nostr", + "tokio", ] [[package]] -name = "semver" -version = "1.0.27" +name = "nostr-gossip" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" dependencies = [ - "serde", - "serde_core", + "nostr", ] [[package]] -name = "serde" -version = "1.0.228" +name = "nostr-relay-pool" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" dependencies = [ - "serde_core", - "serde_derive", + "async-utility", + "async-wsocket", + "atomic-destructor", + "hex", + "lru", + "negentropy", + "nostr", + "nostr-database", + "tokio", + "tracing", ] [[package]] -name = "serde_arrays" -version = "0.2.0" +name = "nostr-sdk" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" dependencies = [ - "serde", + "async-utility", + "nostr", + "nostr-database", + "nostr-gossip", + "nostr-relay-pool", + "tokio", + "tracing", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "serde_derive", + "windows-sys 0.61.2", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "num-conv" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] -name = "serde_json" -version = "1.0.149" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "autocfg", ] [[package]] -name = "serde_spanned" -version = "0.6.9" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "serdect" -version = "0.2.0" +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ - "base16ct", - "serde", + "dlv-list", + "hashbrown 0.14.5", ] [[package]] -name = "serial_test" -version = "3.4.0" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", + "lock_api", + "parking_lot_core", ] [[package]] -name = "serial_test_derive" -version = "3.4.0" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", ] [[package]] -name = "sha1" -version = "0.10.6" +name = "password-hash" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", + "base64ct", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "sha1_smol" -version = "1.0.1" +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] -name = "sha2" -version = "0.10.9" +name = "pbkdf2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", "digest", + "hmac", ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ - "lazy_static", + "memchr", + "ucd-trie", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "pest_derive" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "pest_generator" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ - "errno", - "libc", + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "signature" -version = "2.2.0" +name = "pest_meta" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ - "digest", - "rand_core 0.6.4", + "pest", + "sha2", ] [[package]] -name = "simd-adler32" -version = "0.3.9" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "slab" -version = "0.4.12" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "slop-air" -version = "6.2.3" +name = "poly1305" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15597dcaa23dbf30bdbf9709938f34182736792300bb4623fe3459ebe33e775a" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "p3-air", + "cpufeatures", + "opaque-debug", + "universal-hash", ] [[package]] -name = "slop-algebra" -version = "6.2.3" +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7e25239ac2fe1cbc2ed5114844209d70361c8d55ef812cb935c90d6f771b5b" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "itertools 0.14.0", - "p3-field", - "serde", + "zerovec", ] [[package]] -name = "slop-alloc" -version = "6.2.3" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aadbdfa7badeb1c7576621d1eeb04f305ed6c93c1accd5da5012ad1ddaea2aa1" -dependencies = [ - "serde", - "slop-algebra", - "thiserror 1.0.69", -] +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "slop-baby-bear" -version = "6.2.3" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da8ad7e63b774a2f20704bfc963ebeb4f891e23754a3cb6df47a4a465a85492" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "lazy_static", - "p3-baby-bear", - "serde", - "slop-algebra", - "slop-challenger", - "slop-poseidon2", - "slop-symmetric", + "zerocopy", ] [[package]] -name = "slop-basefold" -version = "6.2.3" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "012eb95fec1133adbf989e6e819943ce3cc2f12b817d063c13885a4b338581ae" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "derive-where", - "itertools 0.14.0", - "serde", - "slop-algebra", - "slop-alloc", - "slop-baby-bear", - "slop-bn254", - "slop-challenger", - "slop-koala-bear", - "slop-merkle-tree", - "slop-multilinear", - "slop-primitives", - "slop-tensor", - "slop-utils", - "thiserror 1.0.69", + "proc-macro2", + "syn", ] [[package]] -name = "slop-basefold-prover" -version = "6.2.3" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16dfb91c773ba557a735efe3ffbd29bff54b314440d0623ad9756ffc2dfe3fd" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "derive-where", - "itertools 0.14.0", - "rand 0.8.5", - "serde", - "slop-algebra", - "slop-alloc", - "slop-baby-bear", - "slop-basefold", - "slop-bn254", - "slop-challenger", - "slop-commit", - "slop-dft", - "slop-fri", - "slop-futures", - "slop-koala-bear", - "slop-merkle-tree", - "slop-multilinear", - "slop-tensor", - "thiserror 1.0.69", + "unicode-ident", ] [[package]] -name = "slop-bn254" -version = "6.2.3" +name = "quinn" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1243ca619a3f4c07f536a060d1fd2c6f779d5b0e4cfeee3acee4d1f76ae35cb4" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "ff", - "p3-bn254-fr", - "serde", - "slop-algebra", - "slop-challenger", - "slop-poseidon2", - "slop-symmetric", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "slop-challenger" -version = "6.2.3" +name = "quinn-proto" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "594a3251b7fbbabf7e0ba1e5c2f563c2797fc2d51982c0208ce70b2f52b8fbe4" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ - "futures", - "p3-challenger", - "serde", - "slop-algebra", - "slop-symmetric", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", ] [[package]] -name = "slop-commit" -version = "6.2.3" +name = "quinn-udp" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8fd26ee268213b9c36d208a322f6d43ca46347d82cf07a5b83a6b95701890b" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "p3-commit", - "serde", - "slop-alloc", + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", ] [[package]] -name = "slop-dft" -version = "6.2.3" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79c2c28e4155679cc2b2ff17a1e258cbffea580182158a8e4d71dc7cd5dc9fe" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "p3-dft", - "serde", - "slop-algebra", - "slop-alloc", - "slop-matrix", - "slop-tensor", + "proc-macro2", ] [[package]] -name = "slop-fri" -version = "6.2.3" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3645dbec4f2ddfc598d0250e880d422ecae2e70fbb2d7f434f0d5c21df011c1e" -dependencies = [ - "p3-fri", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "slop-futures" -version = "6.2.3" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058c54147a291a867e4253356adf491a29e9d999f45b26103bfc02452d8daa8e" -dependencies = [ - "crossbeam", - "futures", - "pin-project", - "rayon", - "thiserror 1.0.69", - "tokio", - "tracing", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "slop-jagged" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60222309dec4d4bffae090b160847267a23bc5b9ecd4412ba4494b486d21c776" +name = "radroots_authority" +version = "0.1.0-alpha.2" dependencies = [ - "derive-where", - "futures", - "itertools 0.14.0", - "num_cpus", - "rand 0.8.5", - "rayon", - "serde", - "slop-algebra", - "slop-alloc", - "slop-baby-bear", - "slop-basefold", - "slop-basefold-prover", - "slop-bn254", - "slop-challenger", - "slop-commit", - "slop-futures", - "slop-koala-bear", - "slop-merkle-tree", - "slop-multilinear", - "slop-stacked", - "slop-sumcheck", - "slop-symmetric", - "slop-tensor", - "slop-utils", - "thiserror 1.0.69", - "tracing", + "radroots_event", ] [[package]] -name = "slop-keccak-air" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d7511944cafbc44ce208dc768b591fa6fc55a87d3f7238f8888e2536db607c" +name = "radroots_core" +version = "0.1.0-alpha.2" dependencies = [ - "p3-keccak-air", + "rust_decimal", + "rust_decimal_macros", + "serde", ] [[package]] -name = "slop-koala-bear" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ccc675284794435dd053d5e7415089bbd08fccf92cd91e044ea3213821dfb5c" +name = "radroots_event" +version = "0.1.0-alpha.2" dependencies = [ - "lazy_static", - "p3-koala-bear", + "hex", + "radroots_core", "serde", - "slop-algebra", - "slop-challenger", - "slop-poseidon2", - "slop-symmetric", + "serde_json", + "sha2", ] [[package]] -name = "slop-matrix" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3d316b50bf50f8e75d388c829a499ec2f2db77c87c4183d3884d75b99df244" +name = "radroots_event_codec" +version = "0.1.0-alpha.2" dependencies = [ - "p3-matrix", + "nostr", + "radroots_core", + "radroots_event", + "serde", + "serde_json", ] [[package]] -name = "slop-maybe-rayon" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabdac3bd6efd75739924e4162340caa59b32b24e02d3f84ab8f56d3f3386ee2" +name = "radroots_identity" +version = "0.1.0-alpha.2" dependencies = [ - "p3-maybe-rayon", + "nostr", + "radroots_event", + "radroots_protected_store", + "radroots_runtime", + "radroots_runtime_paths", + "radroots_secret_vault", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", ] [[package]] -name = "slop-merkle-tree" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b49522dc0a3b87966ba354d8cfcb762baadf9d2641f565aebca39e3032fe2a" +name = "radroots_log" +version = "0.1.0-alpha.2" dependencies = [ - "derive-where", - "itertools 0.14.0", - "p3-merkle-tree", - "serde", - "slop-algebra", - "slop-alloc", - "slop-baby-bear", - "slop-bn254", - "slop-challenger", - "slop-commit", - "slop-futures", - "slop-koala-bear", - "slop-matrix", - "slop-poseidon2", - "slop-symmetric", - "slop-tensor", - "slop-utils", + "chrono", "thiserror 1.0.69", + "tracing", + "tracing-appender", + "tracing-subscriber", ] [[package]] -name = "slop-multilinear" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d8ec813376aaa38b94c3e12a07dd6dd76d3e7240708f51a8daef48b6c2f8a" +name = "radroots_nostr" +version = "0.1.0-alpha.2" dependencies = [ - "derive-where", - "futures", - "num_cpus", - "rand 0.8.5", - "rayon", + "nostr", + "nostr-sdk", + "radroots_event", + "radroots_event_codec", + "radroots_identity", + "reqwest", "serde", - "slop-algebra", - "slop-alloc", - "slop-challenger", - "slop-commit", - "slop-futures", - "slop-matrix", - "slop-tensor", + "serde_json", + "thiserror 1.0.69", ] [[package]] -name = "slop-poseidon2" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f77602b918f667f970d017293189f9e60ba056d1933c9f717634877838297bc" +name = "radroots_protected_store" +version = "0.1.0-alpha.2" dependencies = [ - "p3-poseidon2", + "chacha20poly1305", + "getrandom 0.2.17", + "radroots_secret_vault", + "serde", + "serde_json", + "zeroize", ] [[package]] -name = "slop-primitives" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592ded42eb74fd87d2ce13592906cf86f201128dd18e2aa5a35d298bc3534dcb" +name = "radroots_runtime" +version = "0.1.0-alpha.2" dependencies = [ - "slop-algebra", + "anyhow", + "chacha20poly1305", + "clap", + "config", + "getrandom 0.2.17", + "radroots_log", + "radroots_protected_store", + "radroots_runtime_paths", + "radroots_secret_vault", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "tokio", + "toml", + "tracing", + "zeroize", ] [[package]] -name = "slop-stacked" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd59d332620d6da35a7b4f8837906ec7365761bc0bfda344bb232f8c0b395db7" +name = "radroots_runtime_paths" +version = "0.1.0-alpha.2" dependencies = [ - "derive-where", - "futures", - "itertools 0.14.0", "serde", - "slop-algebra", - "slop-alloc", - "slop-basefold", - "slop-basefold-prover", - "slop-challenger", - "slop-commit", - "slop-futures", - "slop-merkle-tree", - "slop-multilinear", - "slop-tensor", "thiserror 1.0.69", ] [[package]] -name = "slop-sumcheck" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1176c38a586911076061f419101bdc6491b7b9e2039553f2d3a8237158cfb3" +name = "radroots_secret_vault" +version = "0.1.0-alpha.2" + +[[package]] +name = "radroots_trade" +version = "0.1.0-alpha.2" dependencies = [ - "futures", - "itertools 0.14.0", - "rayon", + "base64 0.22.1", + "hex", + "radroots_authority", + "radroots_core", + "radroots_event", + "radroots_event_codec", "serde", - "slop-algebra", - "slop-alloc", - "slop-baby-bear", - "slop-challenger", - "slop-multilinear", + "serde_json", + "sha2", "thiserror 1.0.69", ] [[package]] -name = "slop-symmetric" -version = "6.2.3" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "108bf1769132b782218e3606b8e2f310741fb89d1745234b59ff17d9e7ab8700" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "p3-symmetric", + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", ] [[package]] -name = "slop-tensor" -version = "6.2.3" +name = "rand" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb040f7f2dc9be449a25184d2aee0321a748058d81d646a4a96ffcf32e53963" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "arrayvec", - "derive-where", - "itertools 0.14.0", - "rand 0.8.5", - "rayon", - "serde", - "slop-algebra", - "slop-alloc", - "slop-futures", - "slop-matrix", - "thiserror 1.0.69", - "transpose", + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] -name = "slop-uni-stark" -version = "6.2.3" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8a279d09e78b0ea5c5e97daa1b6440d47db8118bf3f5014667661c169c4590" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "p3-uni-stark", + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] -name = "slop-utils" -version = "6.2.3" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9910567ffcfdd03aec3e5d220853f01bdbdfe1c1752f7e34cd0a4e20b226bd" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "p3-util", - "tracing-forest", - "tracing-subscriber", + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] -name = "slop-whir" -version = "6.2.3" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99519ca11d444567b54d786f71687f63612a75c58404abde353185f53f38dd5e" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "derive-where", - "futures", - "itertools 0.14.0", - "rand 0.8.5", - "rayon", - "serde", - "slop-algebra", - "slop-alloc", - "slop-baby-bear", - "slop-basefold", - "slop-challenger", - "slop-commit", - "slop-dft", - "slop-jagged", - "slop-koala-bear", - "slop-matrix", - "slop-merkle-tree", - "slop-multilinear", - "slop-stacked", - "slop-tensor", - "slop-utils", - "thiserror 1.0.69", + "getrandom 0.2.17", ] [[package]] -name = "smallvec" -version = "1.15.1" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "serde", + "getrandom 0.3.4", ] [[package]] -name = "snowbridge-amcl" -version = "1.0.2" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460a9ed63cdf03c1b9847e8a12a5f5ba19c4efd5869e4a737e05be25d7c427e5" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "parity-scale-codec", - "scale-info", + "bitflags", ] [[package]] -name = "socket2" -version = "0.5.10" +name = "regex-automata" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ - "libc", - "windows-sys 0.52.0", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] -name = "socket2" -version = "0.6.3" +name = "regex-syntax" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "sp1-build" -version = "6.2.3" +name = "reqwest" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb06355982a703c0839b6ca1dc69a50d56a3c85e55d0badc8c3b18ebe8c9fa6" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "anyhow", - "cargo_metadata", - "chrono", - "clap", - "dirs", - "sp1-primitives", + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", ] [[package]] -name = "sp1-core-executor" -version = "6.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58d3af539d746d5312346d33899d297232351ca5aed1668f16d5a5a0ee0a3ab3" +name = "rhi" +version = "0.1.0" dependencies = [ - "bincode", - "bytemuck", - "cfg-if", + "anyhow", "clap", - "deepsize2", - "elf", - "enum-map", - "eyre", - "hashbrown 0.14.5", - "hex", - "itertools 0.14.0", - "memmap2", - "num", - "object", - "rrs-succinct", + "radroots_core", + "radroots_event", + "radroots_event_codec", + "radroots_identity", + "radroots_log", + "radroots_nostr", + "radroots_runtime", + "radroots_runtime_paths", + "radroots_trade", "serde", - "serde_arrays", "serde_json", - "slop-air", - "slop-algebra", - "slop-maybe-rayon", - "slop-symmetric", - "sp1-curves", - "sp1-hypercube", - "sp1-jit", - "sp1-primitives", - "strum", - "subenum", - "thiserror 1.0.69", - "tiny-keccak", + "sha2", + "tempfile", + "thiserror 2.0.18", + "tokio", + "toml", "tracing", - "typenum", - "vec_map", ] [[package]] -name = "sp1-core-executor-runner" -version = "6.2.3" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b3b61a7c5c891a8c18ac31deccca3914d59132f8656d6d7db09c453ee5ed6f" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "base64 0.22.1", - "bincode", - "cargo_metadata", - "hashbrown 0.14.5", - "hex", + "cc", + "cfg-if", + "getrandom 0.2.17", "libc", - "sha2", - "sp1-core-executor", - "sp1-core-executor-runner-binary", - "sp1-jit", - "sp1-primitives", - "sysinfo", - "tracing", - "uuid", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "sp1-core-executor-runner-binary" -version = "6.2.3" +name = "ron" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0c810b99e4fca0b0d0e791a2a7d6b080aad6ffb6cf950e633409eb2723cba3" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "bincode", - "crash-handler", - "libc", + "base64 0.21.7", + "bitflags", "serde", - "sp1-core-executor", - "sp1-jit", - "tracing-subscriber", + "serde_derive", ] [[package]] -name = "sp1-core-machine" -version = "6.2.3" +name = "rust-ini" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b3851384225aae5588b1fca3c9c8f67001187d53abff21709a7cf8f6098f08" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" dependencies = [ - "bincode", "cfg-if", - "enum-map", - "futures", - "generic-array 1.1.0", - "hashbrown 0.14.5", - "itertools 0.14.0", - "num", - "num_cpus", - "rayon", - "rayon-scan", - "rrs-succinct", - "serde", - "serde_json", - "slop-air", - "slop-algebra", - "slop-challenger", - "slop-futures", - "slop-keccak-air", - "slop-matrix", - "slop-maybe-rayon", - "slop-uni-stark", - "snowbridge-amcl", - "sp1-core-executor", - "sp1-core-executor-runner", - "sp1-curves", - "sp1-derive", - "sp1-hypercube", - "sp1-jit", - "sp1-primitives", - "static_assertions", - "struct-reflection", - "strum", - "sysinfo", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-forest", - "tracing-subscriber", - "typenum", + "ordered-multimap", ] [[package]] -name = "sp1-cuda" -version = "6.2.3" +name = "rust_decimal" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "468c0e82c980535eacdc57d2969f087e56002cd764809596e2f8e513c74abff9" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" dependencies = [ - "bincode", - "bytes", - "reqwest", + "arrayvec", + "num-traits", "serde", - "serde_json", - "sp1-core-executor", - "sp1-core-machine", - "sp1-hypercube", - "sp1-primitives", - "sp1-prover", - "sp1-prover-types", - "thiserror 1.0.69", - "tokio", - "tracing", ] [[package]] -name = "sp1-curves" -version = "6.2.3" +name = "rust_decimal_macros" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd90718c62dc06a42b9a47308ca914bb6d3f04f9265c77cb76bc135929cd59d" +checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" dependencies = [ - "cfg-if", - "dashu", - "elliptic-curve", - "generic-array 1.1.0", - "itertools 0.14.0", - "k256", - "num", - "p256", - "serde", - "slop-algebra", - "snowbridge-amcl", - "sp1-primitives", - "typenum", + "quote", + "syn", ] [[package]] -name = "sp1-derive" -version = "6.2.3" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecac7ec9b0d2a12f486486a62b2805257e7ffcced51dd39d25fcefaa133adaf6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "sp1-hypercube" -version = "6.2.3" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8449d118a6fe1532899da955c30247c413c90e8c78b06a452f3a40ff6432299e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "arrayref", - "deepsize2", - "derive-where", - "futures", - "hashbrown 0.14.5", - "itertools 0.14.0", - "num-bigint 0.4.6", - "num-traits", - "num_cpus", - "rayon", - "rayon-scan", - "serde", - "slop-air", - "slop-algebra", - "slop-alloc", - "slop-basefold", - "slop-basefold-prover", - "slop-bn254", - "slop-challenger", - "slop-commit", - "slop-futures", - "slop-jagged", - "slop-koala-bear", - "slop-matrix", - "slop-merkle-tree", - "slop-multilinear", - "slop-poseidon2", - "slop-stacked", - "slop-sumcheck", - "slop-symmetric", - "slop-tensor", - "slop-uni-stark", - "slop-whir", - "sp1-derive", - "sp1-primitives", - "struct-reflection", - "strum", - "thiserror 1.0.69", - "thousands", - "tokio", - "tracing", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] -name = "sp1-jit" -version = "6.2.3" +name = "rustls" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9355cbdc9d7183d8ab244b695ae7630afb559c6b2bd466bc89a2f3e334b2047a" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ - "dynasmrt", - "hashbrown 0.14.5", - "libc", - "memfd", - "memmap2", - "serde", - "sp1-primitives", - "tracing", - "uuid", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "sp1-primitives" -version = "6.2.3" +name = "rustls-pki-types" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f20c3dd3131b1285420ca25a37f507915ab609887d6dda73973396a0faa719c1" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "bincode", - "blake3", - "elf", - "hex", - "itertools 0.14.0", - "lazy_static", - "num-bigint 0.4.6", - "serde", - "sha2", - "slop-algebra", - "slop-bn254", - "slop-challenger", - "slop-koala-bear", - "slop-poseidon2", - "slop-primitives", - "slop-symmetric", + "web-time", + "zeroize", ] [[package]] -name = "sp1-prover" -version = "6.2.3" +name = "rustls-webpki" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c3cb0201a41e9da5372b2b437af07ee3ccd6dca99a69ece4213af40d9436b7" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ - "anyhow", - "bincode", - "clap", - "dirs", - "either", - "enum-map", - "eyre", - "futures", - "hashbrown 0.14.5", - "hex", - "indicatif", - "itertools 0.14.0", - "lru 0.12.5", - "mti", - "num-bigint 0.4.6", - "opentelemetry", - "pin-project", - "rand 0.8.5", - "reqwest", - "serde", - "serde_json", - "serial_test", - "sha2", - "slop-air", - "slop-algebra", - "slop-basefold", - "slop-bn254", - "slop-challenger", - "slop-futures", - "slop-jagged", - "slop-multilinear", - "slop-stacked", - "slop-symmetric", - "sp1-core-executor", - "sp1-core-executor-runner", - "sp1-core-machine", - "sp1-derive", - "sp1-hypercube", - "sp1-jit", - "sp1-primitives", - "sp1-prover-types", - "sp1-recursion-circuit", - "sp1-recursion-compiler", - "sp1-recursion-executor", - "sp1-recursion-gnark-ffi", - "sp1-recursion-machine", - "sp1-verifier", - "static_assertions", - "sysinfo", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tonic", - "tracing", - "tracing-appender", - "tracing-subscriber", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "sp1-prover-types" -version = "6.2.3" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b289303f43e7ae1d07b528f34e78ca088c61dea13a3458f63a6570da11e32e5" -dependencies = [ - "anyhow", - "async-scoped", - "bincode", - "chrono", - "futures-util", - "hashbrown 0.14.5", - "mti", - "prost", - "serde", - "sp1-core-machine", - "sp1-hypercube", - "sp1-primitives", - "tokio", - "tonic", - "tonic-build", - "tracing", -] +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "sp1-recursion-circuit" -version = "6.2.3" +name = "ryu" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d9ce031605bc111474c11e7841536f3469311253791ef326ee16427e10178e" -dependencies = [ - "bincode", - "itertools 0.14.0", - "rand 0.8.5", - "rayon", - "serde", - "slop-air", - "slop-algebra", - "slop-alloc", - "slop-basefold", - "slop-basefold-prover", - "slop-bn254", - "slop-challenger", - "slop-commit", - "slop-jagged", - "slop-koala-bear", - "slop-matrix", - "slop-merkle-tree", - "slop-multilinear", - "slop-stacked", - "slop-sumcheck", - "slop-symmetric", - "slop-tensor", - "slop-whir", - "sp1-core-executor", - "sp1-core-machine", - "sp1-derive", - "sp1-hypercube", - "sp1-primitives", - "sp1-recursion-compiler", - "sp1-recursion-executor", - "sp1-recursion-machine", - "tracing", -] +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] -name = "sp1-recursion-compiler" -version = "6.2.3" +name = "salsa20" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993b5dcde57622009857883637177946254f3cf00290cc4b998b54a76ffab7c0" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "backtrace", - "cfg-if", - "itertools 0.14.0", - "serde", - "slop-algebra", - "slop-bn254", - "slop-symmetric", - "sp1-core-machine", - "sp1-hypercube", - "sp1-primitives", - "sp1-recursion-executor", - "tracing", - "vec_map", + "cipher", ] [[package]] -name = "sp1-recursion-executor" -version = "6.2.3" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983d18b6e0e641c8173b250c234fe355c3a9ba5b7485d3af5a54ddb7ab51e814" -dependencies = [ - "backtrace", - "cfg-if", - "hashbrown 0.14.5", - "itertools 0.14.0", - "range-set-blaze", - "serde", - "slop-algebra", - "slop-maybe-rayon", - "slop-poseidon2", - "slop-symmetric", - "smallvec", - "sp1-derive", - "sp1-hypercube", - "static_assertions", - "thiserror 1.0.69", - "tracing", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "sp1-recursion-gnark-ffi" -version = "6.2.3" +name = "scrypt" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cb9411bdf57706fa6b342c60534c47c6a066aeb5129e89d8d39f6fb37d12e5" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "anyhow", - "bincode", - "cfg-if", - "hex", - "num-bigint 0.4.6", - "serde", - "serde_json", + "password-hash", + "pbkdf2", + "salsa20", "sha2", - "slop-algebra", - "slop-symmetric", - "sp1-hypercube", - "sp1-primitives", - "sp1-recursion-compiler", - "sp1-verifier", - "tempfile", - "tracing", ] [[package]] -name = "sp1-recursion-machine" -version = "6.2.3" +name = "secp256k1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ab820757fa3783a0230efd48a509fb64b8e7fbaf1d257211dae0c66df79002" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "itertools 0.14.0", "rand 0.8.5", - "slop-air", - "slop-algebra", - "slop-basefold", - "slop-matrix", - "slop-maybe-rayon", - "slop-symmetric", - "sp1-derive", - "sp1-hypercube", - "sp1-primitives", - "sp1-recursion-executor", - "strum", - "tracing", + "secp256k1-sys", + "serde", ] [[package]] -name = "sp1-sdk" -version = "6.2.3" +name = "secp256k1-sys" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321a52e7b41aef0eb04e880bc2625f9d150553a04be7607756ac5d494850d037" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" dependencies = [ - "anyhow", - "async-trait", - "bincode", - "cfg-if", - "dirs", - "eventsource-stream", - "futures", - "hex", - "indicatif", - "itertools 0.14.0", - "k256", - "num-bigint 0.4.6", - "serde", - "sha2", - "sp1-build", - "sp1-core-executor", - "sp1-core-executor-runner", - "sp1-core-machine", - "sp1-cuda", - "sp1-hypercube", - "sp1-primitives", - "sp1-prover", - "sp1-prover-types", - "sp1-recursion-executor", - "sp1-verifier", - "strum", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tracing", + "cc", ] [[package]] -name = "sp1-verifier" -version = "6.2.3" +name = "semver" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e7a8627fcbfd89ec21932ae33e3a5b9adc4ef844c0e295ef36ee43f0fbf442" -dependencies = [ - "bincode", - "blake3", - "cfg-if", - "dirs", - "hex", - "lazy_static", - "serde", - "sha2", - "slop-algebra", - "slop-challenger", - "slop-primitives", - "slop-symmetric", - "sp1-hypercube", - "sp1-primitives", - "sp1-recursion-executor", - "sp1-recursion-machine", - "strum", - "substrate-bn-succinct-rs", - "thiserror 2.0.18", -] +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] -name = "spin" -version = "0.9.8" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "lock_api", + "serde_core", + "serde_derive", ] [[package]] -name = "spki" -version = "0.7.3" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "base64ct", - "der", + "serde_derive", ] [[package]] -name = "sqlx" -version = "0.9.0" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-sqlite", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "sqlx-core" -version = "0.9.0" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "base64 0.22.1", - "bytes", - "cfg-if", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.16.1", - "hashlink 0.11.1", - "indexmap 2.13.0", - "log", + "itoa", "memchr", - "percent-encoding", "serde", - "sha2", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", - "url", -] - -[[package]] -name = "sqlx-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.117", + "serde_core", + "zmij", ] [[package]] -name = "sqlx-macros-core" -version = "0.9.0" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "cfg-if", - "dotenvy", - "either", - "heck", - "hex", - "proc-macro2", - "quote", "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-sqlite", - "syn 2.0.117", - "tokio", - "url", ] [[package]] -name = "sqlx-sqlite" -version = "0.9.0" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "atoi", - "flume", "form_urlencoded", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", + "itoa", + "ryu", "serde", - "sqlx-core", - "thiserror 2.0.18", - "tracing", - "url", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] [[package]] -name = "strength_reduce" -version = "0.2.4" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] [[package]] -name = "strsim" -version = "0.11.1" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "struct-reflection" -version = "0.1.0" +name = "signal-hook-registry" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701b671d1ad68e250e05718f95dae3014a17f4e69cbe51842531c30495ff3301" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "struct-reflection-derive", + "errno", + "libc", ] [[package]] -name = "struct-reflection-derive" -version = "0.1.0" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ab74230a0592602e361bd63c645413fa8cbe4500d10274e849179e5c72548f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "strum" -version = "0.27.2" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "strum_macros" -version = "0.27.2" +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "subenum" -version = "1.2.0" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee3fb942ed39f3971438fcc7e05e20717e599e14c5c7cb50edd0df2a44b274" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "substrate-bn-succinct-rs" -version = "0.6.0" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a241fd7c1016fb8ad30fcf5a20986c0c4538e8f15a1b41a1761516299e377ec1" -dependencies = [ - "bytemuck", - "byteorder", - "cfg-if", - "crunchy", - "lazy_static", - "num-bigint 0.4.6", - "rand 0.8.5", - "rustc-hex", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" @@ -5515,17 +2203,6 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" @@ -5540,41 +2217,20 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "ntapi", - "once_cell", - "rayon", - "windows", +dependencies = [ + "futures-core", ] [[package]] -name = "tap" -version = "1.0.1" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "tempfile" @@ -5615,7 +2271,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5626,16 +2282,10 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] -name = "thousands" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" - -[[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5721,7 +2371,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -5734,7 +2384,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5760,17 +2410,6 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] name = "tokio-tungstenite" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5787,19 +2426,6 @@ dependencies = [ ] [[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5807,8 +2433,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", + "toml_datetime", + "toml_edit", ] [[package]] @@ -5821,58 +2447,17 @@ dependencies = [ ] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.0", + "indexmap", "serde", "serde_spanned", - "toml_datetime 0.6.11", + "toml_datetime", "toml_write", - "winnow 0.7.15", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.3", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", + "winnow", ] [[package]] @@ -5882,72 +2467,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum", - "base64 0.22.1", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "rustls-pemfile", - "socket2 0.5.10", - "tokio", - "tokio-rustls", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-build" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] name = "tower" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5975,7 +2494,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", ] @@ -5998,7 +2517,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6024,7 +2542,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -6038,19 +2556,6 @@ dependencies = [ ] [[package]] -name = "tracing-forest" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee40835db14ddd1e3ba414292272eddde9dad04d3d4b65509656414d1c42592f" -dependencies = [ - "ansi_term", - "smallvec", - "thiserror 1.0.69", - "tracing", - "tracing-subscriber", -] - -[[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6080,16 +2585,6 @@ dependencies = [ ] [[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - -[[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6115,27 +2610,6 @@ dependencies = [ ] [[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - -[[package]] -name = "typeid_prefix" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9da1387307fdee46aa441e4f08a1b491e659fcac1aca9cd71f2c624a0de5d1b" - -[[package]] -name = "typeid_suffix" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b55e96f110c6db5d1a2f24072552537f0091dc90cebeaa679540bac93e7405" -dependencies = [ - "uuid", -] - -[[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6169,12 +2643,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6228,41 +2696,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "atomic", - "getrandom 0.4.2", - "js-sys", - "md-5", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -dependencies = [ - "serde", -] - -[[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6347,7 +2786,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -6377,25 +2816,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap", "wasm-encoder", "wasmparser", ] [[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6403,7 +2829,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap", "semver", ] @@ -6446,47 +2872,6 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6507,7 +2892,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -6518,7 +2903,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -6547,15 +2932,6 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" @@ -6565,15 +2941,6 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" @@ -6592,21 +2959,6 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" @@ -6640,12 +2992,6 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" @@ -6658,12 +3004,6 @@ checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" @@ -6676,12 +3016,6 @@ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" @@ -6706,12 +3040,6 @@ checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" @@ -6724,12 +3052,6 @@ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" @@ -6742,12 +3064,6 @@ checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" @@ -6760,12 +3076,6 @@ checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" @@ -6778,15 +3088,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" @@ -6795,15 +3096,6 @@ dependencies = [ ] [[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - -[[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6831,9 +3123,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -6849,7 +3141,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -6862,7 +3154,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.13.0", + "indexmap", "log", "serde", "serde_derive", @@ -6881,7 +3173,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap", "log", "semver", "serde", @@ -6898,15 +3190,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] name = "yaml-rust2" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6914,7 +3197,7 @@ checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" dependencies = [ "arraydeque", "encoding_rs", - "hashlink 0.8.4", + "hashlink", ] [[package]] @@ -6936,7 +3219,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -6957,7 +3240,7 @@ checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -6977,7 +3260,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -7017,7 +3300,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" authors = ["Radroots Authors"] rust-version = "1.97.0" license = "AGPL-3.0-or-later" -description = "Radroots trade validation and receipt worker" +description = "Radroots trade agreement attestation worker" [workspace] resolver = "2" @@ -19,21 +19,16 @@ radroots_log = { path = "../lib/crates/log" } radroots_nostr = { path = "../lib/crates/nostr" } radroots_runtime = { path = "../lib/crates/runtime" } radroots_runtime_paths = { path = "../lib/crates/runtime_paths" } -radroots_trade_sp1_guest = { path = "../lib/crates/trade_sp1_guest" } -radroots_trade_sp1_host = { path = "../lib/crates/trade_sp1_host" } radroots_trade = { path = "../lib/crates/trade" } [features] default = [] -sp1_verify = ["radroots_trade_sp1_host/sp1_verify"] -sp1_proving = ["sp1_verify", "radroots_trade_sp1_host/sp1_proving"] -sp1_cuda_proving = ["sp1_proving", "radroots_trade_sp1_host/sp1_cuda"] [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] -radroots_core = { workspace = true, features = ["std", "serde"] } +radroots_core = { workspace = true, features = ["std", "serde"] } radroots_event = { workspace = true, features = ["serde"] } radroots_event_codec = { workspace = true, features = ["nostr"] } radroots_identity = { workspace = true } @@ -41,24 +36,17 @@ radroots_log = { workspace = true } radroots_nostr = { workspace = true, features = ["client", "codec", "events", "http"] } radroots_runtime = { workspace = true, features = ["cli"] } radroots_runtime_paths = { workspace = true } -radroots_trade_sp1_guest = { workspace = true } -radroots_trade_sp1_host = { workspace = true } radroots_trade = { workspace = true } anyhow = { version = "1" } clap = { version = "4", features = ["derive"] } serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } sha2 = { version = "0.10" } -sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "sqlite-bundled"] } tokio = { version = "1", features = ["full"] } thiserror = { version = "2" } toml = { version = "0.8" } tracing = { version = "0.1" } -[patch.crates-io] -libsqlite3-sys = { path = "../lib/crates/libsqlite3_sys_3_53_3" } - [dev-dependencies] tempfile = { version = "3" } diff --git a/src/cli.rs b/src/cli.rs @@ -18,25 +18,13 @@ pub struct Args { #[derive(clap::Subcommand, Debug, Clone)] pub enum Command { #[command( - name = "proof-smoke", - about = "Run a provider-neutral proof smoke request" + name = "attestation-smoke", + about = "Run a release-product agreement attestation smoke request" )] - ProofSmoke { + AttestationSmoke { #[arg(long)] input: Option<PathBuf>, #[arg(long)] output: Option<PathBuf>, }, - #[command( - name = "remote-prove", - about = "Run a provider-neutral remote proof request" - )] - RemoteProve { - #[arg(long)] - input: Option<PathBuf>, - #[arg(long)] - output: Option<PathBuf>, - #[arg(long, default_value = "cpu", value_parser = ["cpu", "cuda"])] - proof_engine: String, - }, } diff --git a/src/config.rs b/src/config.rs @@ -4,7 +4,7 @@ use radroots_runtime::{BackoffConfig, RadrootsNostrServiceConfig}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; -use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; +use crate::features::trade_agreement_attestation::TradeAgreementAttestationPolicy; use crate::paths::{ RhiRuntimePaths, default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver, }; @@ -101,7 +101,7 @@ pub struct Configuration { #[serde(default)] pub subscriber: SubscriberConfig, #[serde(default)] - pub trade_validation_receipt: TradeValidationReceiptProverPolicy, + pub trade_agreement_attestation: TradeAgreementAttestationPolicy, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -194,7 +194,7 @@ struct RawSettings { #[serde(default)] pub subscriber: RawSubscriberConfig, #[serde(default)] - pub trade_validation_receipt: TradeValidationReceiptProverPolicy, + pub trade_agreement_attestation: TradeAgreementAttestationPolicy, } impl RawSettings { @@ -213,7 +213,7 @@ impl RawSettings { service, logging, subscriber: self.subscriber.into_subscriber_config(paths), - trade_validation_receipt: self.trade_validation_receipt, + trade_agreement_attestation: self.trade_agreement_attestation, }, }) } @@ -237,7 +237,7 @@ fn load_settings_from_path_with_resolver( let settings: RawSettings = toml::from_str(&raw).with_context(|| format!("parse configuration {}", path.display()))?; let settings = settings.into_settings(&paths)?; - settings.config.trade_validation_receipt.validate()?; + settings.config.trade_agreement_attestation.validate()?; Ok(settings) } @@ -254,7 +254,7 @@ pub fn load_settings_from_path(path: &Path) -> Result<Settings> { #[cfg(test)] mod tests { use super::load_settings_from_path_with_resolver; - use crate::features::trade_validation_receipt::TradeValidationReceiptProverBackend; + use crate::features::trade_agreement_attestation::TradeAgreementAttestationBackend; use crate::paths::{ default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver, runtime_contract_with_resolver, @@ -263,7 +263,6 @@ mod tests { RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, RadrootsRuntimeNamespace, }; - use radroots_trade_sp1_host::RadrootsSp1TradeProofMode; use std::path::PathBuf; fn linux_resolver() -> RadrootsPathResolver { @@ -328,7 +327,9 @@ mod tests { ); assert_eq!( paths.subscriber_state_path, - PathBuf::from("/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json") + PathBuf::from( + "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json" + ) ); } @@ -354,7 +355,7 @@ mod tests { ); assert_eq!( paths.subscriber_state_path, - PathBuf::from("/var/lib/radroots/workers/rhi/trade-listing/state.json") + PathBuf::from("/var/lib/radroots/workers/rhi/trade-agreement-attestation/state.json") ); } @@ -379,7 +380,7 @@ mod tests { ); assert_eq!( paths.subscriber_state_path, - repo_local_root.join("data/workers/rhi/trade-listing/state.json") + repo_local_root.join("data/workers/rhi/trade-agreement-attestation/state.json") ); } @@ -434,22 +435,20 @@ replay_overlap_secs = 45 ); assert_eq!( settings.config.subscriber.state.path, - PathBuf::from("/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json") + PathBuf::from( + "/home/treesap/.radroots/data/workers/rhi/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); assert_eq!( - settings.config.trade_validation_receipt.backend, - TradeValidationReceiptProverBackend::LocalExecute - ); - assert_eq!( - settings.config.trade_validation_receipt.proof_mode, - RadrootsSp1TradeProofMode::None + settings.config.trade_agreement_attestation.backend, + TradeAgreementAttestationBackend::LocalStatementHash ); } #[test] - fn load_settings_parses_trade_validation_receipt_policy() { + fn load_settings_parses_trade_agreement_attestation_policy() { let temp = tempfile::tempdir().expect("tempdir"); let config_path = temp.path().join("config.toml"); std::fs::write( @@ -477,11 +476,11 @@ factor = 3 jitter_ms = 5 [subscriber.state] -path = "state/trade-listing.json" +path = "state/trade-agreement-attestation.json" -[trade_validation_receipt] -backend = "local_execute" -proof_mode = "none" +[trade_agreement_attestation] +backend = "local_statement_hash" +expected_statement_contract_hash = "0x1111111111111111111111111111111111111111111111111111111111111111" validator_set_addr = "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "#, @@ -521,20 +520,24 @@ validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee assert_eq!(settings.config.subscriber.backoff.jitter_ms, 5); assert_eq!( settings.config.subscriber.state.path, - PathBuf::from("state/trade-listing.json") + PathBuf::from("state/trade-agreement-attestation.json") ); assert_eq!( - settings.config.trade_validation_receipt.backend, - TradeValidationReceiptProverBackend::LocalExecute + settings.config.trade_agreement_attestation.backend, + TradeAgreementAttestationBackend::LocalStatementHash ); assert_eq!( - settings.config.trade_validation_receipt.proof_mode, - RadrootsSp1TradeProofMode::None + settings + .config + .trade_agreement_attestation + .expected_statement_contract_hash + .as_deref(), + Some("0x1111111111111111111111111111111111111111111111111111111111111111") ); assert_eq!( settings .config - .trade_validation_receipt + .trade_agreement_attestation .validator_set_addr .as_deref(), Some( @@ -544,7 +547,7 @@ validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee assert_eq!( settings .config - .trade_validation_receipt + .trade_agreement_attestation .validator_set_event_id .as_deref(), Some("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") @@ -552,7 +555,7 @@ validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee } #[test] - fn load_settings_rejects_sp1_identity_hash_without_sp1_mode() { + fn load_settings_rejects_invalid_statement_contract_hash() { let temp = tempfile::tempdir().expect("tempdir"); let config_path = temp.path().join("config.toml"); std::fs::write( @@ -561,10 +564,9 @@ validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee [metadata] name = "rhi-test" -[trade_validation_receipt] -backend = "local_execute" -proof_mode = "none" -expected_sp1_program_hash = "0x1111111111111111111111111111111111111111111111111111111111111111" +[trade_agreement_attestation] +backend = "local_statement_hash" +expected_statement_contract_hash = "not-a-hash" "#, ) .expect("write config"); @@ -575,16 +577,16 @@ expected_sp1_program_hash = "0x1111111111111111111111111111111111111111111111111 RadrootsPathProfile::InteractiveUser, None, ) - .expect_err("sp1 identity without sp1 proof mode must fail"); + .expect_err("invalid statement contract hash must fail"); let message = format!("{error:#}"); assert!( - message.contains("SP1 identity constraints require an SP1 proof mode"), + message.contains("invalid configured hash field"), "{message}" ); } #[test] - fn load_settings_rejects_sp1_backend_without_identity_hashes() { + fn load_settings_rejects_partial_validator_set_binding() { let temp = tempfile::tempdir().expect("tempdir"); let config_path = temp.path().join("config.toml"); std::fs::write( @@ -593,9 +595,9 @@ expected_sp1_program_hash = "0x1111111111111111111111111111111111111111111111111 [metadata] name = "rhi-test" -[trade_validation_receipt] -backend = "local_cpu_prove" -proof_mode = "core" +[trade_agreement_attestation] +backend = "local_statement_hash" +validator_set_addr = "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" "#, ) .expect("write config"); @@ -606,12 +608,10 @@ proof_mode = "core" RadrootsPathProfile::InteractiveUser, None, ) - .expect_err("sp1 backend without identity hashes must fail"); + .expect_err("partial validator-set binding must fail"); let message = format!("{error:#}"); assert!( - message.contains( - "trade validation receipt policy requires SP1 program and verifying-key hashes" - ), + message.contains("attestation policy is missing validator_set_event_id"), "{message}" ); } @@ -654,14 +654,13 @@ replay_window_secs = 10 "unknown field `config`", ), ( - "config-trade-validation-receipt", + "config-trade-agreement-attestation", r#" [metadata] name = "rhi-test" -[config.trade_validation_receipt] -backend = "local_execute" -proof_mode = "none" +[config.trade_agreement_attestation] +backend = "local_statement_hash" "#, "unknown field `config`", ), @@ -685,7 +684,7 @@ proof_mode = "none" fn default_subscriber_state_path_is_canonical_for_current_process() { let path = default_subscriber_state_path_for_process().expect("resolve current process defaults"); - assert!(path.ends_with("trade-listing/state.json")); + assert!(path.ends_with("trade-agreement-attestation/state.json")); } #[test] @@ -740,7 +739,9 @@ proof_mode = "none" ); assert_eq!( contract.canonical_subscriber_state_path, - PathBuf::from("/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json") + PathBuf::from( + "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json" + ) ); } @@ -767,7 +768,7 @@ proof_mode = "none" ); assert_eq!( contract.canonical_subscriber_state_path, - PathBuf::from("/var/lib/radroots/workers/rhi/trade-listing/state.json") + PathBuf::from("/var/lib/radroots/workers/rhi/trade-agreement-attestation/state.json") ); } } diff --git a/src/features/mod.rs b/src/features/mod.rs @@ -1,2 +1 @@ -pub mod trade_listing; -pub mod trade_validation_receipt; +pub mod trade_agreement_attestation; diff --git a/src/features/trade_agreement_attestation.rs b/src/features/trade_agreement_attestation.rs @@ -0,0 +1,980 @@ +#![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 anyhow::{Result, anyhow}; +use radroots_event::ids::{ + RadrootsAddressableCoordinate, RadrootsEventId, RadrootsPublicKey, RadrootsTradeId, + RadrootsTradeMutationId, +}; +use radroots_event::kinds::{TRADE_MUTATION_EVENT_KINDS, is_trade_mutation_event_kind}; +use radroots_event::trade::{ + RadrootsTradeMutationEnvelopeV1, canonical_jcs_value, trade_mutation_from_canonical_content, +}; +use radroots_nostr::prelude::{ + RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKeys, + RadrootsNostrKind, RadrootsNostrRelayPoolNotification, RadrootsNostrSubscriptionId, + RadrootsNostrTimestamp, +}; +use radroots_trade::workflow::{ + RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION, + RadrootsTradeAgreementStateV1, RadrootsTradeAttestationResultV1, + RadrootsTradeAttestationStateV1, RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1, + RadrootsTradeProjectionV1, RadrootsTradeReductionInputV1, reduce_trade_records, +}; +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"; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TradeAgreementAttestationBackend { + #[default] + LocalStatementHash, +} + +impl TradeAgreementAttestationBackend { + pub const fn as_str(self) -> &'static str { + match self { + Self::LocalStatementHash => "local_statement_hash", + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TradeAgreementAttestationPolicy { + #[serde(default)] + pub backend: TradeAgreementAttestationBackend, + #[serde(default)] + pub validator_set_addr: Option<String>, + #[serde(default)] + pub validator_set_event_id: Option<String>, + #[serde(default)] + pub expected_statement_contract_hash: Option<String>, +} + +impl TradeAgreementAttestationPolicy { + pub fn validate(&self) -> Result<(), TradeAgreementAttestationError> { + validate_optional_hash32(&self.expected_statement_contract_hash)?; + match ( + self.validator_set_addr.as_deref(), + self.validator_set_event_id.as_deref(), + ) { + (Some(addr), Some(event_id)) => { + RadrootsAddressableCoordinate::parse(addr).map_err(|_| { + TradeAgreementAttestationError::InvalidValidatorSetBinding("validator_set_addr") + })?; + RadrootsEventId::parse(event_id).map_err(|_| { + TradeAgreementAttestationError::InvalidValidatorSetBinding( + "validator_set_event_id", + ) + })?; + Ok(()) + } + (None, None) => Ok(()), + (Some(_), None) => Err(TradeAgreementAttestationError::MissingValidatorSetBinding( + "validator_set_event_id", + )), + (None, Some(_)) => Err(TradeAgreementAttestationError::MissingValidatorSetBinding( + "validator_set_addr", + )), + } + } + + fn validator_set_binding( + &self, + ) -> Result<Option<TradeAgreementAttestationValidatorSetBinding>, TradeAgreementAttestationError> + { + self.validate()?; + match ( + self.validator_set_addr.as_deref(), + self.validator_set_event_id.as_deref(), + ) { + (Some(addr), Some(event_id)) => { + Ok(Some(TradeAgreementAttestationValidatorSetBinding { + validator_set_addr: addr.to_owned(), + validator_set_event_id: event_id.to_owned(), + })) + } + _ => Ok(None), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TradeAgreementAttestationValidatorSetBinding { + pub validator_set_addr: String, + pub validator_set_event_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TradeAgreementAttestationStatementV1 { + pub protocol_id: String, + pub schema_version: u16, + pub reducer_contract_id: String, + pub reducer_version: u16, + pub trade_id: String, + pub claim_mutation_id: String, + pub projection_digest: String, + pub agreement_state: RadrootsTradeAgreementStateV1, + pub attestation_state_before_report: RadrootsTradeAttestationStateV1, + pub active_agreement_claim_ids: Vec<String>, + pub contested_claim_ids: Vec<String>, + pub cancelled_claim_ids: Vec<String>, + pub evidence_state: RadrootsTradeEvidenceStateV1, + #[serde(skip_serializing_if = "Option::is_none")] + pub validator_set: Option<TradeAgreementAttestationValidatorSetBinding>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TradeAgreementAttestationReportV1 { + pub report_version: u16, + pub attestation_id: String, + pub result: RadrootsTradeAttestationResultV1, + pub statement: TradeAgreementAttestationStatementV1, + pub statement_hash: String, + pub proof_system: String, + 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}")] + MissingValidatorSetBinding(&'static str), + #[error("attestation policy has invalid {0}")] + InvalidValidatorSetBinding(&'static str), + #[error("invalid configured hash field")] + InvalidHashField, + #[error("invalid signed event")] + InvalidSignedEvent, + #[error("trade protocol error: {0}")] + TradeProtocol(#[from] radroots_event::trade::RadrootsTradeProtocolError), + #[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 { + state_path: crate::paths::default_subscriber_state_path_for_process() + .expect("resolve canonical rhi agreement-attestation state path"), + 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<RadrootsNostrKind>) -> RadrootsNostrFilter { + let since = { + let state = self.state.lock().await; + state.replay_since( + RadrootsNostrTimestamp::now().as_secs(), + self.config.replay_window_secs, + self.config.replay_overlap_secs, + ) + }; + RadrootsNostrFilter::new() + .kinds(kinds) + .since(RadrootsNostrTimestamp::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: &RadrootsNostrEvent, + mutation: &RadrootsTradeMutationEnvelopeV1, + mutation_id: &RadrootsTradeMutationId, + kind: u32, + ) -> Result<bool, TradeAgreementAttestationRuntimeError> { + let observation = TradeMutationObservationV1 { + event_id: event.id.to_hex(), + event_kind: kind, + event_pubkey: event.pubkey.to_hex(), + trade_id: mutation.trade_id.as_str().to_owned(), + mutation_id: mutation_id.as_str().to_owned(), + 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: &RadrootsTradeId, + ) -> Result<RadrootsTradeProjectionV1, TradeAgreementAttestationError> { + let observations = { + let state = self.state.lock().await; + state + .mutation_events + .values() + .filter(|observation| observation.trade_id == trade_id.as_str()) + .cloned() + .collect::<Vec<_>>() + }; + let mut input = RadrootsTradeReductionInputV1::new(trade_id.clone()); + input.evidence_state = RadrootsTradeEvidenceStateV1::Complete; + input.mutations = observations + .iter() + .map(|observation| { + let mutation = trade_mutation_from_canonical_content(observation.content.as_str())?; + let transport_event_id = RadrootsEventId::parse(observation.event_id.as_str()) + .map(Some) + .map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?; + Ok(RadrootsTradeMutationRecordV1 { + transport_event_id, + mutation, + }) + }) + .collect::<Result<Vec<_>, TradeAgreementAttestationError>>()?; + 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: RadrootsNostrEvent, + 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 = RadrootsPublicKey::parse(event.pubkey.to_hex()) + .map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?; + if event_author != mutation.author_pubkey { + return Err(TradeAgreementAttestationError::InvalidEventAuthor); + } + let mutation_id = mutation + .mutation_id + .clone() + .ok_or(TradeAgreementAttestationError::MissingMutationId)?; + if !runtime + .observe_mutation_event(&event, &mutation, &mutation_id, kind) + .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( + projection: &RadrootsTradeProjectionV1, + claim_mutation_id: &RadrootsTradeMutationId, + policy: &TradeAgreementAttestationPolicy, +) -> Result<TradeAgreementAttestationReportV1, TradeAgreementAttestationError> { + policy.validate()?; + if !projection + .agreement_claims + .iter() + .any(|claim| claim.claim_mutation_id == *claim_mutation_id) + { + return Err(TradeAgreementAttestationError::MissingAgreementClaim); + } + let statement = TradeAgreementAttestationStatementV1 { + protocol_id: RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID.to_owned(), + schema_version: RHI_AGREEMENT_ATTESTATION_REPORT_VERSION, + reducer_contract_id: RADROOTS_TRADE_REDUCER_CONTRACT_ID.to_owned(), + reducer_version: RADROOTS_TRADE_REDUCER_VERSION, + trade_id: projection.trade_id.as_str().to_owned(), + claim_mutation_id: claim_mutation_id.as_str().to_owned(), + projection_digest: projection.projection_digest.clone(), + agreement_state: projection.agreement_state, + attestation_state_before_report: projection.attestation_state, + active_agreement_claim_ids: mutation_ids_to_strings(&projection.active_agreement_claim_ids), + contested_claim_ids: mutation_ids_to_strings(&projection.contested_claim_ids), + cancelled_claim_ids: mutation_ids_to_strings(&projection.cancelled_claim_ids), + evidence_state: projection.evidence_state, + validator_set: policy.validator_set_binding()?, + }; + let statement_hash = hash_canonical_value( + b"radroots:rhi-agreement-attestation-statement:v1\0", + &statement, + )?; + let result = if projection.agreement_state == RadrootsTradeAgreementStateV1::Agreed + && projection + .active_agreement_claim_ids + .iter() + .any(|claim| claim == claim_mutation_id) + && !projection + .contested_claim_ids + .iter() + .any(|claim| claim == claim_mutation_id) + && !projection + .cancelled_claim_ids + .iter() + .any(|claim| claim == claim_mutation_id) + { + RadrootsTradeAttestationResultV1::Valid + } else { + RadrootsTradeAttestationResultV1::Invalid + }; + let proof_identity_hash = hash_canonical_value( + b"radroots:rhi-agreement-attestation-proof-identity:v1\0", + &serde_json::json!({ + "backend": policy.backend.as_str(), + "proof_system": RHI_AGREEMENT_ATTESTATION_PROOF_SYSTEM_LOCAL_STATEMENT_HASH, + "statement_hash": statement_hash, + "validator_set": statement.validator_set.clone(), + }), + )?; + let attestation_id = hash_canonical_value( + b"radroots:rhi-agreement-attestation-report:v1\0", + &serde_json::json!({ + "proof_identity_hash": proof_identity_hash, + "result": result, + "statement_hash": statement_hash, + }), + )?; + Ok(TradeAgreementAttestationReportV1 { + report_version: RHI_AGREEMENT_ATTESTATION_REPORT_VERSION, + attestation_id, + result, + statement, + statement_hash, + proof_system: RHI_AGREEMENT_ATTESTATION_PROOF_SYSTEM_LOCAL_STATEMENT_HASH.to_owned(), + proof_identity_hash, + }) +} + +pub fn trade_mutation_subscription_kinds() -> Vec<u32> { + TRADE_MUTATION_EVENT_KINDS.to_vec() +} + +fn mutation_ids_to_strings(values: &[RadrootsTradeMutationId]) -> Vec<String> { + values + .iter() + .map(|value| value.as_str().to_owned()) + .collect() +} + +fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeAgreementAttestationError> { + match event.kind { + RadrootsNostrKind::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)?; + } + Ok(()) +} + +fn validate_hash32(value: &str) -> Result<(), TradeAgreementAttestationError> { + let stripped = value.strip_prefix("0x").unwrap_or(value); + if stripped.len() != 64 || !stripped.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(TradeAgreementAttestationError::InvalidHashField); + } + Ok(()) +} + +fn hash_canonical_value( + domain: &[u8], + value: &impl Serialize, +) -> Result<String, TradeAgreementAttestationError> { + let value = serde_json::to_value(value)?; + let canonical = canonical_jcs_value(&value)?; + let mut hasher = Sha256::new(); + hasher.update(domain); + hasher.update(canonical.as_bytes()); + Ok(format!("{:x}", hasher.finalize())) +} + +fn map_notification_recv_result( + result: Result<RadrootsNostrRelayPoolNotification, tokio::sync::broadcast::error::RecvError>, +) -> Result<RadrootsNostrRelayPoolNotification, ()> { + result.map_err(|_| ()) +} + +async fn subscribe_io( + client: &RadrootsNostrClient, + filter: RadrootsNostrFilter, +) -> Result<RadrootsNostrSubscriptionId> { + let subscription = client.subscribe(filter, None).await?; + Ok(subscription.val) +} + +async fn unsubscribe_io( + client: &RadrootsNostrClient, + subscription_id: &RadrootsNostrSubscriptionId, +) { + client.unsubscribe(subscription_id).await; +} + +fn should_delay_before_event_handle() -> bool { + cfg!(all(debug_assertions, not(test))) +} + +async fn process_event_notification( + event: RadrootsNostrEvent, + 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: RadrootsNostrClient, + _keys: RadrootsNostrKeys, + 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<RadrootsNostrKind> = subscribed_kinds + .iter() + .map(|kind| u16::try_from(*kind).expect("trade mutation kinds fit in nostr custom range")) + .map(RadrootsNostrKind::Custom) + .collect(); + let filter = runtime.recovery_filter(kinds).await; + + if *stop_rx.borrow() { + return Ok(()); + } + + let subscription_id = subscribe_io(&client, filter).await?; + let mut notifications = 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 RadrootsNostrRelayPoolNotification::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 run_smoke_cli_command(command: crate::cli::Command) -> anyhow::Result<()> { + let crate::cli::Command::AttestationSmoke { input, output } = command; + let request_bytes = read_input(input.as_deref())?; + let response = handle_smoke_request_bytes(&request_bytes).await; + let response_bytes = serde_json::to_vec_pretty(&response)?; + write_output(output.as_deref(), &response_bytes)?; + if response.ok { + Ok(()) + } else { + Err(anyhow!( + "{}", + response + .error + .as_deref() + .unwrap_or("attestation smoke request failed") + )) + } +} + +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()), + }, + } +} + +fn read_input(path: Option<&Path>) -> anyhow::Result<Vec<u8>> { + match path { + Some(path) => std::fs::read(path).map_err(anyhow::Error::from), + None => std::io::read_to_string(std::io::stdin()) + .map(|value| value.into_bytes()) + .map_err(anyhow::Error::from), + } +} + +fn write_output(path: Option<&Path>, bytes: &[u8]) -> anyhow::Result<()> { + match path { + Some(path) => std::fs::write(path, bytes).map_err(anyhow::Error::from), + None => { + println!("{}", String::from_utf8_lossy(bytes)); + Ok(()) + } + } +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{ + RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, TradeAgreementAttestationPolicy, + TradeAgreementAttestationRuntime, TradeAgreementAttestationRuntimeConfig, + TradeAgreementAttestationSmokeOperation, TradeAgreementAttestationSmokeRequest, + attest_projection_claim, handle_smoke_request_bytes, + }; + use radroots_event::ids::{RadrootsTradeId, RadrootsTradeMutationId}; + use radroots_trade::workflow::{ + RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, + RadrootsTradeEvidenceStateV1, RadrootsTradeProjectionV1, + }; + + fn hex_64(ch: char) -> String { + std::iter::repeat_n(ch, 64).collect() + } + + fn hex_32(ch: char) -> String { + std::iter::repeat_n(ch, 32).collect() + } + + fn projection(claim_id: RadrootsTradeMutationId) -> RadrootsTradeProjectionV1 { + RadrootsTradeProjectionV1 { + reducer_contract_id: radroots_trade::workflow::RADROOTS_TRADE_REDUCER_CONTRACT_ID + .to_owned(), + reducer_version: radroots_trade::workflow::RADROOTS_TRADE_REDUCER_VERSION, + trade_id: RadrootsTradeId::parse(hex_32('1')).expect("trade id"), + root_mutation_id: None, + buyer_pubkey: None, + seller_pubkey: None, + farm_id: None, + negotiation_state: Default::default(), + agreement_state: RadrootsTradeAgreementStateV1::Agreed, + evidence_state: RadrootsTradeEvidenceStateV1::Complete, + conflict_state: Default::default(), + private_terms_state: Default::default(), + attestation_state: RadrootsTradeAttestationStateV1::None, + fulfillment_state: Default::default(), + payment_state: Default::default(), + candidate_heads: Vec::new(), + agreement_claims: vec![radroots_trade::workflow::RadrootsTradeAgreementClaimV1 { + claim_mutation_id: claim_id.clone(), + proposal_mutation_id: RadrootsTradeMutationId::parse(hex_64('2')) + .expect("proposal id"), + candidate_id: radroots_event::ids::RadrootsTradeCandidateId::parse(hex_64('3')) + .expect("candidate id"), + candidate_author_pubkey: radroots_event::ids::RadrootsPublicKey::parse(hex_64('4')) + .expect("author"), + accepted_by_pubkey: radroots_event::ids::RadrootsPublicKey::parse(hex_64('5')) + .expect("acceptor"), + reservation_commitment: hex_64('6'), + }], + active_agreement_claim_ids: vec![claim_id], + contested_claim_ids: Vec::new(), + cancelled_claim_ids: Vec::new(), + declined_candidate_ids: Vec::new(), + missing_parent_ids: Vec::new(), + missing_proposal_ids: Vec::new(), + unsupported_mutation_ids: Vec::new(), + issues: Vec::new(), + attestations: Vec::new(), + projection_digest: hex_64('7'), + } + } + + #[test] + fn attestation_report_is_keyed_to_claim_and_projection_digest() { + let claim_id = RadrootsTradeMutationId::parse(hex_64('a')).expect("claim id"); + let report = attest_projection_claim( + &projection(claim_id.clone()), + &claim_id, + &TradeAgreementAttestationPolicy::default(), + ) + .expect("report"); + assert_eq!(report.statement.claim_mutation_id, claim_id.as_str()); + assert_eq!(report.statement.projection_digest, hex_64('7')); + assert_eq!(report.proof_system, "local_statement_hash"); + } + + #[tokio::test] + async fn runtime_persists_and_loads_attestation_state() { + let temp = tempfile::tempdir().expect("tempdir"); + 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/features/trade_listing/handlers/events.rs b/src/features/trade_listing/handlers/events.rs @@ -1,947 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr(coverage_nightly, coverage(off))] - -use std::sync::Arc; - -use radroots_event::farm::RadrootsFarmRef; -use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey}; -use radroots_event::kinds::{ - KIND_JOB_FEEDBACK, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, - KIND_TRADE_VALIDATION_RECEIPT, is_listing_kind, is_order_event_kind, - is_trade_validation_service_event_kind, -}; -use radroots_event::trade_validation::RadrootsTradeValidationListingError as TradeListingValidationError; -use radroots_event_codec::order::{RadrootsOrderEnvelopeParseError, parse_order_listing_event_tag}; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys, - RadrootsNostrKind, RadrootsNostrTag, radroots_event_from_nostr, radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, radroots_nostr_send_event, -}; -use radroots_trade::listing::{parse_public_listing_address, validation::validate_listing_event}; -use radroots_trade::order::{ - RadrootsOrderEventRecord, RadrootsOrderProjection, order_event_record_from_event, - reduce_order_event_records, -}; -use radroots_trade::workflow::RadrootsTradeWorkflowState; -use thiserror::Error; - -use crate::features::trade_listing::state::{ - TradeListingRuntime, TradeListingState, TradeListingStateError, TradeOrderState, -}; -use crate::features::trade_validation_receipt::{ - TradeValidationReceiptJobError, TradeValidationReceiptProverPolicy, publish_validation_receipt, -}; - -#[derive(Debug, Error)] -pub enum TradeListingEventError { - #[error("event kind not supported")] - UnsupportedKind, - #[error("missing recipient tag")] - MissingRecipient, - #[error("missing required tag: {0}")] - MissingTag(&'static str), - #[error("tag mismatch: {0}")] - TagMismatch(&'static str), - #[error("invalid envelope: {0}")] - InvalidEnvelope(String), - #[error("invalid envelope payload: {0}")] - InvalidPayload(String), - #[error("invalid listing address")] - InvalidListingAddr, - #[error("invalid order request payload")] - InvalidOrder, - #[error("shared workflow rejected trade transition: {0}")] - Workflow(String), - #[error("state error: {0}")] - State(#[from] TradeListingStateError), - #[error("nostr error: {0}")] - Nostr(#[from] radroots_nostr::error::RadrootsNostrError), - #[error("serde error: {0}")] - Serde(#[from] serde_json::Error), - #[error("unauthorized sender")] - Unauthorized, -} - -#[cfg(test)] -#[derive(Default)] -struct TradeListingEventTestHooks { - fetch_event_by_id_results: - std::collections::VecDeque<Result<RadrootsNostrEvent, TradeListingEventError>>, - send_event_results: std::collections::VecDeque<Result<(), TradeListingEventError>>, - validate_listing_results: - std::collections::VecDeque<Result<(String, RadrootsFarmRef), TradeListingValidationError>>, -} - -#[cfg(test)] -static TRADE_LISTING_EVENT_TEST_HOOKS: std::sync::OnceLock< - std::sync::Mutex<TradeListingEventTestHooks>, -> = std::sync::OnceLock::new(); - -#[cfg(test)] -fn trade_listing_event_test_hooks() -> &'static std::sync::Mutex<TradeListingEventTestHooks> { - TRADE_LISTING_EVENT_TEST_HOOKS - .get_or_init(|| std::sync::Mutex::new(TradeListingEventTestHooks::default())) -} - -#[cfg(test)] -fn pop_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeListingEventError>> { - trade_listing_event_test_hooks() - .lock() - .expect("trade listing event test hooks lock") - .fetch_event_by_id_results - .pop_front() -} - -#[cfg(test)] -fn pop_send_event_hook() -> Option<Result<(), TradeListingEventError>> { - trade_listing_event_test_hooks() - .lock() - .expect("trade listing event test hooks lock") - .send_event_results - .pop_front() -} - -#[cfg(test)] -fn pop_validate_listing_hook() --> Option<Result<(String, RadrootsFarmRef), TradeListingValidationError>> { - trade_listing_event_test_hooks() - .lock() - .expect("trade listing event test hooks lock") - .validate_listing_results - .pop_front() -} - -#[cfg(test)] -fn take_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeListingEventError>> { - pop_fetch_event_by_id_hook() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeListingEventError>> { - None -} - -#[cfg(test)] -fn take_send_event_hook() -> Option<Result<(), TradeListingEventError>> { - pop_send_event_hook() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_send_event_hook() -> Option<Result<(), TradeListingEventError>> { - None -} - -#[cfg(test)] -fn take_validate_listing_hook() --> Option<Result<(String, RadrootsFarmRef), TradeListingValidationError>> { - pop_validate_listing_hook() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_validate_listing_hook() --> Option<Result<(String, RadrootsFarmRef), TradeListingValidationError>> { - None -} - -async fn fetch_event_by_id_io( - client: &RadrootsNostrClient, - id: &str, -) -> Result<RadrootsNostrEvent, TradeListingEventError> { - match take_fetch_event_by_id_hook() { - Some(result) => result, - None => radroots_nostr_fetch_event_by_id(client, id) - .await - .map_err(TradeListingEventError::from), - } -} - -#[cfg_attr(all(not(test), coverage_nightly), coverage(off))] -async fn send_event_io( - client: &RadrootsNostrClient, - builder: RadrootsNostrEventBuilder, -) -> Result<(), TradeListingEventError> { - match take_send_event_hook() { - Some(result) => result, - None => radroots_nostr_send_event(client, builder) - .await - .map(|_| ()) - .map_err(TradeListingEventError::from), - } -} - -#[cfg_attr(all(not(test), coverage_nightly), coverage(off))] -fn validate_listing_event_io( - event: &RadrootsNostrEvent, -) -> Result<(String, RadrootsFarmRef), TradeListingValidationError> { - match take_validate_listing_hook() { - Some(result) => result, - None => validate_listing_event(&radroots_event_from_nostr(event)) - .map(|listing| (listing.listing_addr, listing.listing.farm)), - } -} - -pub async fn handle_event_with_policy( - event: RadrootsNostrEvent, - _tags: Vec<RadrootsNostrTag>, - keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - runtime: TradeListingRuntime, - proof_policy: &TradeValidationReceiptProverPolicy, -) -> Result<(), TradeListingEventError> { - let kind = event_kind_u32(&event)?; - let state = runtime.state(); - if is_listing_kind(kind) { - return handle_listing_event(&event, &state).await; - } - if event.pubkey == keys.public_key() { - return Ok(()); - } - if kind == KIND_TRADE_VALIDATION_RECEIPT { - state - .lock() - .await - .mark_non_order_event_seen(&event.id.to_string()); - return Ok(()); - } - if is_order_event_kind(kind) { - return handle_order_event(&event, kind, &keys, &client, &runtime, proof_policy).await; - } - if is_trade_validation_service_event_kind(kind) { - return Err(TradeListingEventError::UnsupportedKind); - } - Err(TradeListingEventError::UnsupportedKind) -} - -#[cfg(test)] -pub async fn handle_event( - event: RadrootsNostrEvent, - tags: Vec<RadrootsNostrTag>, - keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - runtime: TradeListingRuntime, -) -> Result<(), TradeListingEventError> { - handle_event_with_policy( - event, - tags, - keys, - client, - runtime, - &TradeValidationReceiptProverPolicy::default(), - ) - .await -} - -fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeListingEventError> { - match event.kind { - RadrootsNostrKind::Custom(value) => Ok(u32::from(value)), - _ => Err(TradeListingEventError::UnsupportedKind), - } -} - -fn map_trade_validation_receipt_job_error( - error: TradeValidationReceiptJobError, -) -> TradeListingEventError { - match error { - TradeValidationReceiptJobError::UnsupportedKind => TradeListingEventError::UnsupportedKind, - TradeValidationReceiptJobError::MissingRecipient => { - TradeListingEventError::MissingRecipient - } - TradeValidationReceiptJobError::Nostr(error) => TradeListingEventError::Nostr(error), - other => TradeListingEventError::InvalidPayload(other.to_string()), - } -} - -fn map_order_parse_error(error: RadrootsOrderEnvelopeParseError) -> TradeListingEventError { - match error { - RadrootsOrderEnvelopeParseError::InvalidKind(_) => TradeListingEventError::UnsupportedKind, - RadrootsOrderEnvelopeParseError::MissingTag(tag) => TradeListingEventError::MissingTag(tag), - RadrootsOrderEnvelopeParseError::ListingAddrTagMismatch => { - TradeListingEventError::TagMismatch("a") - } - RadrootsOrderEnvelopeParseError::OrderIdTagMismatch => { - TradeListingEventError::TagMismatch("d") - } - RadrootsOrderEnvelopeParseError::InvalidListingAddr(_) => { - TradeListingEventError::InvalidListingAddr - } - RadrootsOrderEnvelopeParseError::InvalidEnvelope(error) => { - TradeListingEventError::InvalidEnvelope(error.to_string()) - } - other => TradeListingEventError::InvalidPayload(other.to_string()), - } -} - -async fn handle_listing_event( - event: &RadrootsNostrEvent, - state: &Arc<tokio::sync::Mutex<TradeListingState>>, -) -> Result<(), TradeListingEventError> { - let event_id = event.id.to_string(); - { - let state = state.lock().await; - if state.is_non_order_event_seen(&event_id) { - return Ok(()); - } - } - let validated = validate_listing_event(&radroots_event_from_nostr(event)) - .map_err(|error| TradeListingEventError::InvalidPayload(error.to_string()))?; - let kind = event_kind_u32(event)?; - let mut state = state.lock().await; - state.upsert_listing_event(&validated.listing_addr, &event_id, kind); - state.mark_non_order_event_seen(&event_id); - Ok(()) -} - -async fn handle_order_event( - event: &RadrootsNostrEvent, - kind: u32, - keys: &RadrootsNostrKeys, - client: &RadrootsNostrClient, - runtime: &TradeListingRuntime, - proof_policy: &TradeValidationReceiptProverPolicy, -) -> Result<(), TradeListingEventError> { - let state = runtime.state(); - match kind { - KIND_ORDER_REQUEST => handle_order_request(event, client, &state).await, - KIND_ORDER_DECISION | KIND_ORDER_CANCELLATION => { - handle_order_workflow_event(event, client, runtime, keys, proof_policy).await - } - _ => Err(TradeListingEventError::UnsupportedKind), - } -} - -async fn handle_order_request( - event: &RadrootsNostrEvent, - client: &RadrootsNostrClient, - state: &Arc<tokio::sync::Mutex<TradeListingState>>, -) -> Result<(), TradeListingEventError> { - let rr_event = radroots_event_from_nostr(event); - let record = order_event_record_from_event(&rr_event).map_err(map_order_decode_error)?; - let order_id = record.order_id().clone(); - let projection = reduce_order_event_records(&order_id, [record.clone()]); - if projection.status != RadrootsTradeWorkflowState::Requested || !projection.issues.is_empty() { - return Err(TradeListingEventError::Workflow( - workflow_rejection_message(&projection), - )); - } - let RadrootsOrderEventRecord::Request(request) = record else { - return Err(TradeListingEventError::UnsupportedKind); - }; - let listing_addr = parse_public_listing_address(&request.payload.listing_addr) - .map_err(|_| TradeListingEventError::InvalidListingAddr)?; - if request.payload.seller_pubkey != listing_addr.seller_pubkey { - return Err(TradeListingEventError::InvalidListingAddr); - } - let rr_tags = rr_event.tags_as_vec(); - let listing_event = parse_order_listing_event_tag(&rr_tags) - .map_err(|error| TradeListingEventError::InvalidPayload(error.to_string()))? - .ok_or(TradeListingEventError::MissingTag("listing_event"))?; - let listing_snapshot_event_id = - ensure_listing_snapshot(&request.payload.listing_addr, &listing_event, client, state) - .await?; - let event_id = event.id.to_string(); - let mut state = state.lock().await; - if state.order_exists(order_id.as_str()) { - return Ok(()); - } - let mut seen = std::collections::HashSet::new(); - seen.insert(event_id.clone()); - state.insert_order(TradeOrderState { - order_id: order_id.to_string(), - listing_addr: request.payload.listing_addr.to_string(), - buyer_pubkey: request.payload.buyer_pubkey.to_string(), - seller_pubkey: request.payload.seller_pubkey.to_string(), - status: projection.status, - listing_snapshot_event_id: Some(listing_snapshot_event_id), - root_event_id: Some(event_id.clone()), - last_event_id: Some(event_id), - seen_event_ids: seen, - }); - Ok(()) -} - -async fn ensure_listing_snapshot( - listing_addr: &str, - listing_event: &radroots_event::RadrootsEventPtr, - client: &RadrootsNostrClient, - state: &Arc<tokio::sync::Mutex<TradeListingState>>, -) -> Result<String, TradeListingEventError> { - { - let state = state.lock().await; - if state.listing_event_id(listing_addr) == Some(listing_event.id.as_str()) { - return Ok(listing_event.id.clone()); - } - } - let event = fetch_event_by_id_io(client, &listing_event.id).await?; - let (validated_listing_addr, _) = validate_listing_event_io(&event) - .map_err(|error| TradeListingEventError::InvalidPayload(error.to_string()))?; - if validated_listing_addr != listing_addr { - return Err(TradeListingEventError::InvalidOrder); - } - let kind = event_kind_u32(&event)?; - let mut state = state.lock().await; - state.upsert_listing_event(listing_addr, &listing_event.id, kind); - Ok(listing_event.id.clone()) -} - -async fn handle_order_workflow_event( - event: &RadrootsNostrEvent, - client: &RadrootsNostrClient, - runtime: &TradeListingRuntime, - keys: &RadrootsNostrKeys, - proof_policy: &TradeValidationReceiptProverPolicy, -) -> Result<(), TradeListingEventError> { - let state = runtime.state(); - let rr_event = radroots_event_from_nostr(event); - let current_record = - order_event_record_from_event(&rr_event).map_err(map_order_decode_error)?; - let order_id = current_record.order_id().clone(); - let event_id = event.id.to_string(); - let order_snapshot = { - let state = state.lock().await; - if state.is_event_seen(order_id.as_str(), &event_id) { - return Ok(()); - } - state - .get_order(order_id.as_str()) - .cloned() - .ok_or(TradeListingStateError::MissingOrder)? - }; - let mut records = - fetch_seen_order_records(client, &order_snapshot, event, current_record).await?; - let projection = reduce_order_event_records(&order_id, records.drain(..)); - if projection.status != RadrootsTradeWorkflowState::Invalid && !projection.issues.is_empty() { - return Err(TradeListingEventError::Workflow( - workflow_rejection_message(&projection), - )); - } - let mut state = state.lock().await; - if state.is_event_seen(order_id.as_str(), &event_id) { - return Ok(()); - } - let order = state - .get_order_mut(order_id.as_str()) - .ok_or(TradeListingStateError::MissingOrder)?; - ensure_projection_binding(order, &projection)?; - let projected_status = projection.status.clone(); - order.status = projected_status.clone(); - order.last_event_id = projection - .last_event_id - .map(|last_event_id| last_event_id.to_string()) - .or_else(|| Some(event_id.clone())); - order.seen_event_ids.insert(event_id); - drop(state); - if projected_status == RadrootsTradeWorkflowState::AgreedPendingValidation { - publish_validation_receipt(event, client, runtime, keys, proof_policy) - .await - .map_err(map_trade_validation_receipt_job_error)?; - } - Ok(()) -} - -async fn fetch_seen_order_records( - client: &RadrootsNostrClient, - order: &TradeOrderState, - current_event: &RadrootsNostrEvent, - current_record: RadrootsOrderEventRecord, -) -> Result<Vec<RadrootsOrderEventRecord>, TradeListingEventError> { - let current_event_id = current_event.id.to_string(); - let mut event_ids = order - .seen_event_ids - .iter() - .filter(|event_id| event_id.as_str() != current_event_id) - .cloned() - .collect::<Vec<_>>(); - event_ids.sort(); - let mut records = Vec::with_capacity(event_ids.len() + 1); - for event_id in event_ids { - let event = fetch_event_by_id_io(client, &event_id).await?; - let rr_event = radroots_event_from_nostr(&event); - let record = order_event_record_from_event(&rr_event).map_err(map_order_decode_error)?; - if record.order_id().as_str() != order.order_id { - return Err(TradeListingEventError::InvalidOrder); - } - records.push(record); - } - records.push(current_record); - Ok(records) -} - -fn ensure_projection_binding( - order: &TradeOrderState, - projection: &RadrootsOrderProjection, -) -> Result<(), TradeListingEventError> { - if projection - .listing_addr - .as_ref() - .is_some_and(|listing_addr| listing_addr.to_string() != order.listing_addr) - || projection - .buyer_pubkey - .as_ref() - .is_some_and(|buyer_pubkey| buyer_pubkey.to_string() != order.buyer_pubkey) - || projection - .seller_pubkey - .as_ref() - .is_some_and(|seller_pubkey| seller_pubkey.to_string() != order.seller_pubkey) - { - return Err(TradeListingEventError::InvalidOrder); - } - Ok(()) -} - -fn workflow_rejection_message(projection: &RadrootsOrderProjection) -> String { - format!("{:?}:{:?}", projection.status, projection.issues) -} - -fn map_order_decode_error( - error: radroots_trade::order::RadrootsOrderEventDecodeError, -) -> TradeListingEventError { - match error { - radroots_trade::order::RadrootsOrderEventDecodeError::Envelope(error) => { - map_order_parse_error(error) - } - radroots_trade::order::RadrootsOrderEventDecodeError::UnsupportedKind { .. } => { - TradeListingEventError::UnsupportedKind - } - other => TradeListingEventError::InvalidPayload(other.to_string()), - } -} - -#[cfg(test)] -fn tag_has_value(tags: &[Vec<String>], key: &str, value: &str) -> bool { - tags.iter().any(|tag| { - tag.first().map(String::as_str) == Some(key) - && tag.get(1).map(String::as_str) == Some(value) - }) -} - -pub async fn handle_error( - error: TradeListingEventError, - event: &RadrootsNostrEvent, - client: &RadrootsNostrClient, -) -> Result<(), TradeListingEventError> { - let request_event_id = RadrootsEventId::parse(event.id.to_hex()) - .map_err(|err| TradeListingEventError::InvalidPayload(err.to_string()))?; - let customer_pubkey = RadrootsPublicKey::parse(event.pubkey.to_hex()) - .map_err(|err| TradeListingEventError::InvalidPayload(err.to_string()))?; - let tags = vec![ - vec!["e".to_string(), request_event_id.into_string()], - vec!["p".to_string(), customer_pubkey.into_string()], - vec!["status".to_string(), "error".to_string()], - ]; - let builder = radroots_nostr_build_event(KIND_JOB_FEEDBACK, error.to_string(), tags)?; - send_event_io(client, builder).await -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - TradeListingEventError, TradeListingEventTestHooks, handle_error, handle_event, - tag_has_value, trade_listing_event_test_hooks, - }; - use crate::features::trade_listing::state::TradeListingRuntime; - use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, - }; - use radroots_event::RadrootsEventPtr; - use radroots_event::ids::{ - RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, - RadrootsOrderQuoteId, RadrootsPublicKey, - }; - use radroots_event::kinds::{KIND_LISTING, KIND_ORDER_REQUEST}; - use radroots_event::order::{ - RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, - RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, - RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis, - RadrootsOrderRequest, - }; - use radroots_event_codec::order::{ - order_cancellation_event_build, order_decision_event_build, order_request_event_build, - }; - use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys, - RadrootsNostrKind, radroots_nostr_build_event, - }; - use radroots_trade::workflow::RadrootsTradeWorkflowState; - use tokio::sync::{Mutex, MutexGuard}; - - static TEST_LOCK: Mutex<()> = Mutex::const_new(()); - - async fn test_guard() -> MutexGuard<'static, ()> { - let guard = TEST_LOCK.lock().await; - *trade_listing_event_test_hooks().lock().expect("hooks") = - TradeListingEventTestHooks::default(); - guard - } - - fn listing_id() -> &'static str { - "AAAAAAAAAAAAAAAAAAAAAg" - } - - fn listing_addr(seller: &RadrootsNostrKeys) -> String { - format!("{}:{}:{}", KIND_LISTING, seller.public_key(), listing_id()) - } - - fn listing_event_id() -> &'static str { - "0000000000000000000000000000000000000000000000000000000000000001" - } - - fn typed_listing_addr(seller: &RadrootsNostrKeys) -> RadrootsListingAddress { - RadrootsListingAddress::parse(listing_addr(seller)).expect("listing address") - } - - fn typed_order_id(order_id: &str) -> RadrootsOrderId { - RadrootsOrderId::parse(order_id).expect("order id") - } - - fn typed_quote_id(order_id: &str) -> RadrootsOrderQuoteId { - RadrootsOrderQuoteId::parse(format!("{order_id}-quote")).expect("quote id") - } - - fn typed_bin_id() -> RadrootsInventoryBinId { - RadrootsInventoryBinId::parse("bin-1").expect("bin id") - } - - fn typed_pubkey(keys: &RadrootsNostrKeys) -> RadrootsPublicKey { - RadrootsPublicKey::parse(keys.public_key().to_string()).expect("public key") - } - - fn typed_event_id(event: &RadrootsNostrEvent) -> RadrootsEventId { - RadrootsEventId::parse(event.id.to_string()).expect("event id") - } - - fn listing_event_ptr() -> RadrootsEventPtr { - RadrootsEventPtr { - id: listing_event_id().to_string(), - relays: None, - } - } - - fn order_economics(order_id: &str) -> RadrootsOrderEconomics { - RadrootsOrderEconomics { - quote_id: typed_quote_id(order_id), - quote_version: 1, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: typed_bin_id(), - bin_count: 2, - quantity_amount: RadrootsCoreDecimal::from(1u32), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: RadrootsCoreDecimal::from(5u32), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(10u32), - RadrootsCoreCurrency::USD, - ), - }], - discounts: Vec::<RadrootsOrderEconomicLine>::new(), - adjustments: Vec::<RadrootsOrderEconomicLine>::new(), - subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(10u32), - RadrootsCoreCurrency::USD, - ), - discount_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - adjustment_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(10u32), - RadrootsCoreCurrency::USD, - ), - } - } - - fn order_request( - order_id: &str, - buyer: &RadrootsNostrKeys, - seller: &RadrootsNostrKeys, - ) -> RadrootsOrderRequest { - RadrootsOrderRequest { - order_id: typed_order_id(order_id), - listing_addr: typed_listing_addr(seller), - buyer_pubkey: typed_pubkey(buyer), - seller_pubkey: typed_pubkey(seller), - items: vec![RadrootsOrderItem { - bin_id: typed_bin_id(), - bin_count: 2, - }], - economics: order_economics(order_id), - } - } - - fn order_decision( - order_id: &str, - buyer: &RadrootsNostrKeys, - seller: &RadrootsNostrKeys, - ) -> RadrootsOrderDecision { - RadrootsOrderDecision { - order_id: typed_order_id(order_id), - listing_addr: typed_listing_addr(seller), - buyer_pubkey: typed_pubkey(buyer), - seller_pubkey: typed_pubkey(seller), - decision: RadrootsOrderDecisionOutcome::Accepted { - inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: typed_bin_id(), - bin_count: 2, - }], - }, - } - } - - fn order_cancellation( - order_id: &str, - buyer: &RadrootsNostrKeys, - seller: &RadrootsNostrKeys, - ) -> RadrootsOrderCancellation { - RadrootsOrderCancellation { - order_id: typed_order_id(order_id), - listing_addr: typed_listing_addr(seller), - buyer_pubkey: typed_pubkey(buyer), - seller_pubkey: typed_pubkey(seller), - reason: "cancel after agreement".to_string(), - } - } - - fn signed_order_request_event( - buyer: &RadrootsNostrKeys, - seller: &RadrootsNostrKeys, - ) -> RadrootsNostrEvent { - let payload = order_request("order-1", buyer, seller); - let wire = order_request_event_build(&listing_event_ptr(), &payload).expect("wire"); - radroots_nostr_build_event(wire.kind, wire.content, wire.tags) - .expect("builder") - .sign_with_keys(buyer) - .expect("event") - } - - fn signed_order_decision_event( - buyer: &RadrootsNostrKeys, - seller: &RadrootsNostrKeys, - request_event: &RadrootsNostrEvent, - ) -> RadrootsNostrEvent { - let payload = order_decision("order-1", buyer, seller); - let root_event_id = typed_event_id(request_event); - let wire = - order_decision_event_build(&root_event_id, &root_event_id, &payload).expect("wire"); - radroots_nostr_build_event(wire.kind, wire.content, wire.tags) - .expect("builder") - .sign_with_keys(seller) - .expect("event") - } - - fn signed_order_cancellation_event( - buyer: &RadrootsNostrKeys, - seller: &RadrootsNostrKeys, - request_event: &RadrootsNostrEvent, - decision_event: &RadrootsNostrEvent, - ) -> RadrootsNostrEvent { - let payload = order_cancellation("order-1", buyer, seller); - let root_event_id = typed_event_id(request_event); - let prev_event_id = typed_event_id(decision_event); - let wire = - order_cancellation_event_build(&root_event_id, &prev_event_id, &payload).expect("wire"); - radroots_nostr_build_event(wire.kind, wire.content, wire.tags) - .expect("builder") - .sign_with_keys(buyer) - .expect("event") - } - - #[tokio::test] - async fn order_request_inserts_canonical_order_state() { - let _guard = test_guard().await; - let worker = RadrootsNostrKeys::generate(); - let buyer = RadrootsNostrKeys::generate(); - let seller = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(worker.clone()); - let runtime = TradeListingRuntime::new(); - let state = runtime.state(); - state.lock().await.upsert_listing_event( - &listing_addr(&seller), - listing_event_id(), - KIND_LISTING, - ); - - let request_event = signed_order_request_event(&buyer, &seller); - handle_event(request_event, Vec::new(), worker, client, runtime.clone()) - .await - .expect("order request"); - - let mut state = state.lock().await; - let order = state.get_order_mut("order-1").expect("order"); - assert_eq!(order.status, RadrootsTradeWorkflowState::Requested); - assert_eq!(order.buyer_pubkey, buyer.public_key().to_string()); - assert_eq!(order.seller_pubkey, seller.public_key().to_string()); - } - - #[tokio::test] - async fn order_decision_uses_shared_workflow_pending_validation_state() { - let _guard = test_guard().await; - let worker = RadrootsNostrKeys::generate(); - let buyer = RadrootsNostrKeys::generate(); - let seller = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(worker.clone()); - let runtime = TradeListingRuntime::new(); - let state = runtime.state(); - state.lock().await.upsert_listing_event( - &listing_addr(&seller), - listing_event_id(), - KIND_LISTING, - ); - let request_event = signed_order_request_event(&buyer, &seller); - handle_event( - request_event.clone(), - Vec::new(), - worker.clone(), - client.clone(), - runtime.clone(), - ) - .await - .expect("order request"); - let decision_event = signed_order_decision_event(&buyer, &seller, &request_event); - trade_listing_event_test_hooks() - .lock() - .expect("hooks") - .fetch_event_by_id_results - .push_back(Ok(request_event)); - - let error = handle_event(decision_event, Vec::new(), worker, client, runtime.clone()) - .await - .expect_err("missing validator-set policy must fail closed"); - assert!(matches!(error, TradeListingEventError::InvalidPayload(_))); - - let mut state = state.lock().await; - let order = state.get_order_mut("order-1").expect("order"); - assert_eq!( - order.status, - RadrootsTradeWorkflowState::AgreedPendingValidation - ); - } - - #[tokio::test] - async fn cancellation_after_pending_agreement_uses_shared_workflow_invalid_state() { - let _guard = test_guard().await; - let worker = RadrootsNostrKeys::generate(); - let buyer = RadrootsNostrKeys::generate(); - let seller = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(worker.clone()); - let runtime = TradeListingRuntime::new(); - let state = runtime.state(); - state.lock().await.upsert_listing_event( - &listing_addr(&seller), - listing_event_id(), - KIND_LISTING, - ); - let request_event = signed_order_request_event(&buyer, &seller); - handle_event( - request_event.clone(), - Vec::new(), - worker.clone(), - client.clone(), - runtime.clone(), - ) - .await - .expect("order request"); - let decision_event = signed_order_decision_event(&buyer, &seller, &request_event); - trade_listing_event_test_hooks() - .lock() - .expect("hooks") - .fetch_event_by_id_results - .push_back(Ok(request_event.clone())); - let decision_error = handle_event( - decision_event.clone(), - Vec::new(), - worker.clone(), - client.clone(), - runtime.clone(), - ) - .await - .expect_err("missing validator-set policy must fail closed"); - assert!(matches!( - decision_error, - TradeListingEventError::InvalidPayload(_) - )); - let cancellation_event = - signed_order_cancellation_event(&buyer, &seller, &request_event, &decision_event); - { - let mut hooks = trade_listing_event_test_hooks().lock().expect("hooks"); - hooks.fetch_event_by_id_results.push_back(Ok(request_event)); - hooks - .fetch_event_by_id_results - .push_back(Ok(decision_event)); - } - - handle_event( - cancellation_event, - Vec::new(), - worker, - client, - runtime.clone(), - ) - .await - .expect("order cancellation"); - - let mut state = state.lock().await; - let order = state.get_order_mut("order-1").expect("order"); - assert_eq!(order.status, RadrootsTradeWorkflowState::Invalid); - } - - #[tokio::test] - async fn unsupported_kind_is_rejected() { - let _guard = test_guard().await; - let worker = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(worker.clone()); - let runtime = TradeListingRuntime::new(); - let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(4999), "test") - .sign_with_keys(&RadrootsNostrKeys::generate()) - .expect("event"); - assert!(matches!( - handle_event(event, Vec::new(), worker, client, runtime).await, - Err(TradeListingEventError::UnsupportedKind) - )); - } - - #[test] - fn tag_helpers_cover_core_paths() { - assert!(tag_has_value( - &[vec!["p".to_string(), "pubkey".to_string()]], - "p", - "pubkey" - )); - assert!(!tag_has_value( - &[vec!["p".to_string(), "pubkey".to_string()]], - "p", - "other" - )); - } - - #[tokio::test] - async fn handle_error_uses_send_hook() { - let _guard = test_guard().await; - trade_listing_event_test_hooks() - .lock() - .expect("hooks") - .send_event_results - .push_back(Ok(())); - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let event = RadrootsNostrEventBuilder::new( - RadrootsNostrKind::Custom(KIND_ORDER_REQUEST as u16), - "bad", - ) - .sign_with_keys(&keys) - .expect("event"); - assert!( - handle_error(TradeListingEventError::InvalidOrder, &event, &client) - .await - .is_ok() - ); - } -} diff --git a/src/features/trade_listing/handlers/mod.rs b/src/features/trade_listing/handlers/mod.rs @@ -1 +0,0 @@ -pub mod events; diff --git a/src/features/trade_listing/mod.rs b/src/features/trade_listing/mod.rs @@ -1,4 +0,0 @@ -pub mod handlers; -pub mod processed_jobs; -pub mod state; -pub mod subscriber; diff --git a/src/features/trade_listing/processed_jobs.rs b/src/features/trade_listing/processed_jobs.rs @@ -1,1762 +0,0 @@ -#![forbid(unsafe_code)] - -use serde::{Deserialize, Serialize}; -use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow}; -use sqlx::{Row, SqlitePool}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::Arc; -use thiserror::Error; -use tokio::sync::OnceCell; - -const RHI_PROCESSED_JOB_SCHEMA_VERSION: i64 = 3; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RhiProcessedJobStatus { - Processing, - ReceiptPublishing, - ReceiptPublished, - ResultPublishing, - Completed, - Failed, -} - -impl RhiProcessedJobStatus { - pub const fn as_str(self) -> &'static str { - match self { - Self::Processing => "processing", - Self::ReceiptPublishing => "receipt_publishing", - Self::ReceiptPublished => "receipt_published", - Self::ResultPublishing => "result_publishing", - Self::Completed => "completed", - Self::Failed => "failed", - } - } - - fn parse(value: &str) -> Result<Self, RhiProcessedJobStoreError> { - match value { - "processing" => Ok(Self::Processing), - "receipt_publishing" => Ok(Self::ReceiptPublishing), - "receipt_published" => Ok(Self::ReceiptPublished), - "result_publishing" => Ok(Self::ResultPublishing), - "completed" => Ok(Self::Completed), - "failed" => Ok(Self::Failed), - _ => Err(RhiProcessedJobStoreError::InvalidStatus(value.to_owned())), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct RhiProcessedJobState { - pub request_id: String, - pub request_kind: u32, - pub request_hash: String, - pub customer_pubkey: String, - pub status: RhiProcessedJobStatus, - #[serde(default)] - pub receipt_event_id: Option<String>, - #[serde(default)] - pub receipt_event_json: Option<String>, - #[serde(default)] - pub result_event_id: Option<String>, - #[serde(default)] - pub result_event_json: Option<String>, - #[serde(default)] - pub proof_metadata_json: Option<String>, - #[serde(default)] - pub error_code: Option<String>, - pub created_timestamp: u32, - #[serde(default)] - pub completed_timestamp: Option<u32>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RhiProcessedJobClaim { - Execute, - InProgress, - Failed { - error_code: String, - }, - RecoverReceipt { - receipt_event_id: String, - receipt_event_json: String, - }, - RecoverResult { - receipt_event_id: String, - receipt_event_json: String, - result_event_id: Option<String>, - result_event_json: Option<String>, - proof_metadata_json: Option<String>, - }, - Completed, -} - -#[derive(Clone, Debug)] -pub struct RhiProcessedJobStore { - pool: SqlitePool, - file_backed: bool, - schema_ready: Arc<OnceCell<()>>, -} - -#[derive(Debug, Error)] -pub enum RhiProcessedJobStoreError { - #[error("invalid rhi processed-job store path: {0}")] - InvalidPath(PathBuf), - #[error("unsupported rhi processed-job store schema version: {0}")] - UnsupportedSchemaVersion(i64), - #[error("rhi processed-job store io error: {0}")] - Io(#[from] std::io::Error), - #[error("rhi processed-job sqlite error: {0}")] - Sqlite(#[from] sqlx::Error), - #[error("duplicate conflicting processed job")] - DuplicateConflictingJob, - #[error("duplicate conflicting receipt")] - DuplicateConflictingReceipt, - #[error("duplicate conflicting result")] - DuplicateConflictingResult, - #[error("receipt publication was not claimed")] - ReceiptPublicationNotClaimed, - #[error("result publication was not claimed")] - ResultPublicationNotClaimed, - #[error("result recovery is missing stored receipt event json")] - MissingReceiptEventJson, - #[error("missing processed-job claim: {0}")] - MissingProcessedJobClaim(String), - #[error("invalid rhi processed-job status: {0}")] - InvalidStatus(String), - #[error("invalid rhi processed-job stored value: {0}")] - InvalidStoredValue(&'static str), -} - -impl RhiProcessedJobStore { - pub fn open_memory() -> Result<Self, RhiProcessedJobStoreError> { - let options = SqliteConnectOptions::from_str("sqlite::memory:")?; - Ok(Self::from_options(options, false, 1)) - } - - pub async fn open_file(path: impl AsRef<Path>) -> Result<Self, RhiProcessedJobStoreError> { - let path = path.as_ref(); - if path.file_name().is_none() { - return Err(RhiProcessedJobStoreError::InvalidPath(path.to_path_buf())); - } - if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() - { - tokio::fs::create_dir_all(parent).await?; - } - let options = SqliteConnectOptions::new() - .filename(path) - .create_if_missing(true); - let store = Self::from_options(options, true, 5); - store.ensure_schema().await?; - Ok(store) - } - - pub async fn claim_job( - &self, - job: &RhiProcessedJobState, - now_ms: i64, - lease_ms: i64, - ) -> Result<RhiProcessedJobClaim, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - let claim_expires_at_ms = now_ms.saturating_add(lease_ms.max(1)); - let mut tx = self.pool.begin().await?; - let inserted = insert_claimed_job(&mut tx, job, now_ms, claim_expires_at_ms).await?; - if inserted { - tx.commit().await?; - return Ok(RhiProcessedJobClaim::Execute); - } - - let Some(existing) = select_job(&mut tx, job.request_id.as_str()).await? else { - return Err(RhiProcessedJobStoreError::MissingProcessedJobClaim( - job.request_id.clone(), - )); - }; - ensure_processed_job_matches(&existing, job)?; - let claim = claim_for_existing_job(&mut tx, existing, now_ms, claim_expires_at_ms).await?; - tx.commit().await?; - Ok(claim) - } - - pub async fn mark_receipt_publishing( - &self, - job: &RhiProcessedJobState, - receipt_event_id: &str, - receipt_event_json: &str, - proof_metadata_json: Option<&str>, - now_ms: i64, - ) -> Result<RhiProcessedJobState, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - let mut tx = self.pool.begin().await?; - let Some(mut existing) = select_job(&mut tx, job.request_id.as_str()).await? else { - return Err(RhiProcessedJobStoreError::MissingProcessedJobClaim( - job.request_id.clone(), - )); - }; - ensure_processed_job_matches(&existing, job)?; - ensure_receipt_matches(&existing, receipt_event_id)?; - ensure_receipt_event_json_matches(&existing, receipt_event_json)?; - ensure_proof_metadata_matches(&existing, proof_metadata_json)?; - existing.status = RhiProcessedJobStatus::ReceiptPublishing; - existing.receipt_event_id = Some(receipt_event_id.to_owned()); - existing.receipt_event_json = Some(receipt_event_json.to_owned()); - existing.proof_metadata_json = proof_metadata_json.map(ToOwned::to_owned); - update_job_without_claim_change(&mut tx, &existing, now_ms).await?; - tx.commit().await?; - Ok(existing) - } - - pub async fn mark_receipt_published( - &self, - job: &RhiProcessedJobState, - receipt_event_id: &str, - now_ms: i64, - ) -> Result<RhiProcessedJobState, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - let mut tx = self.pool.begin().await?; - let Some(mut existing) = select_job(&mut tx, job.request_id.as_str()).await? else { - return Err(RhiProcessedJobStoreError::MissingProcessedJobClaim( - job.request_id.clone(), - )); - }; - ensure_processed_job_matches(&existing, job)?; - ensure_receipt_matches(&existing, receipt_event_id)?; - if existing.status != RhiProcessedJobStatus::ReceiptPublishing - || existing.receipt_event_json.is_none() - { - return Err(RhiProcessedJobStoreError::ReceiptPublicationNotClaimed); - } - existing.status = RhiProcessedJobStatus::ReceiptPublished; - existing.receipt_event_id = Some(receipt_event_id.to_owned()); - update_job(&mut tx, &existing, now_ms, None).await?; - tx.commit().await?; - Ok(existing) - } - - pub async fn mark_receipt_completed( - &self, - job: &RhiProcessedJobState, - receipt_event_id: &str, - receipt_event_json: &str, - completed_timestamp: u32, - now_ms: i64, - ) -> Result<RhiProcessedJobState, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - let mut tx = self.pool.begin().await?; - let Some(mut existing) = select_job(&mut tx, job.request_id.as_str()).await? else { - return Err(RhiProcessedJobStoreError::MissingProcessedJobClaim( - job.request_id.clone(), - )); - }; - ensure_processed_job_matches(&existing, job)?; - ensure_receipt_matches(&existing, receipt_event_id)?; - ensure_receipt_event_json_matches(&existing, receipt_event_json)?; - ensure_result_matches(&existing, receipt_event_id)?; - ensure_result_event_json_matches(&existing, receipt_event_json)?; - if existing.status == RhiProcessedJobStatus::Completed { - tx.commit().await?; - return Ok(existing); - } - if !matches!( - existing.status, - RhiProcessedJobStatus::ReceiptPublished | RhiProcessedJobStatus::ResultPublishing - ) || existing.receipt_event_id.is_none() - { - return Err(RhiProcessedJobStoreError::ReceiptPublicationNotClaimed); - } - existing.status = RhiProcessedJobStatus::Completed; - existing.receipt_event_id = Some(receipt_event_id.to_owned()); - existing.receipt_event_json = Some(receipt_event_json.to_owned()); - existing.result_event_id = Some(receipt_event_id.to_owned()); - existing.result_event_json = Some(receipt_event_json.to_owned()); - existing.completed_timestamp = Some(completed_timestamp); - update_job(&mut tx, &existing, now_ms, None).await?; - tx.commit().await?; - Ok(existing) - } - - pub async fn mark_completed( - &self, - job: &RhiProcessedJobState, - receipt_event_id: &str, - result_event_id: &str, - completed_timestamp: u32, - now_ms: i64, - ) -> Result<RhiProcessedJobState, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - let mut tx = self.pool.begin().await?; - let Some(mut existing) = select_job(&mut tx, job.request_id.as_str()).await? else { - return Err(RhiProcessedJobStoreError::MissingProcessedJobClaim( - job.request_id.clone(), - )); - }; - ensure_processed_job_matches(&existing, job)?; - ensure_receipt_matches(&existing, receipt_event_id)?; - ensure_result_matches(&existing, result_event_id)?; - if existing.status == RhiProcessedJobStatus::Completed { - tx.commit().await?; - return Ok(existing); - } - if existing.status != RhiProcessedJobStatus::ResultPublishing - || existing.result_event_id.is_none() - { - return Err(RhiProcessedJobStoreError::ResultPublicationNotClaimed); - } - existing.status = RhiProcessedJobStatus::Completed; - existing.receipt_event_id = Some(receipt_event_id.to_owned()); - existing.result_event_id = Some(result_event_id.to_owned()); - existing.completed_timestamp = Some(completed_timestamp); - update_job(&mut tx, &existing, now_ms, None).await?; - tx.commit().await?; - Ok(existing) - } - - pub async fn mark_result_publishing( - &self, - job: &RhiProcessedJobState, - receipt_event_id: &str, - result_event_id: &str, - result_event_json: &str, - now_ms: i64, - ) -> Result<RhiProcessedJobState, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - let mut tx = self.pool.begin().await?; - let Some(mut existing) = select_job(&mut tx, job.request_id.as_str()).await? else { - return Err(RhiProcessedJobStoreError::MissingProcessedJobClaim( - job.request_id.clone(), - )); - }; - ensure_processed_job_matches(&existing, job)?; - ensure_receipt_matches(&existing, receipt_event_id)?; - ensure_result_matches(&existing, result_event_id)?; - ensure_result_event_json_matches(&existing, result_event_json)?; - if existing.status == RhiProcessedJobStatus::Completed { - tx.commit().await?; - return Ok(existing); - } - if existing.status != RhiProcessedJobStatus::ResultPublishing { - return Err(RhiProcessedJobStoreError::ResultPublicationNotClaimed); - } - existing.receipt_event_id = Some(receipt_event_id.to_owned()); - existing.result_event_id = Some(result_event_id.to_owned()); - existing.result_event_json = Some(result_event_json.to_owned()); - sqlx::query( - "UPDATE rhi_processed_jobs - SET receipt_event_id = ?, - result_event_id = ?, - result_event_json = ?, - updated_at_ms = ? - WHERE request_id = ?", - ) - .bind(existing.receipt_event_id.as_deref()) - .bind(existing.result_event_id.as_deref()) - .bind(existing.result_event_json.as_deref()) - .bind(now_ms) - .bind(existing.request_id.as_str()) - .execute(&mut *tx) - .await?; - tx.commit().await?; - Ok(existing) - } - - pub async fn get_job( - &self, - request_id: &str, - ) -> Result<Option<RhiProcessedJobState>, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - let mut tx = self.pool.begin().await?; - let job = select_job(&mut tx, request_id).await?; - tx.commit().await?; - Ok(job) - } - - pub async fn pragma_busy_timeout(&self) -> Result<i64, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - query_i64(&self.pool, "PRAGMA busy_timeout").await - } - - pub async fn pragma_journal_mode(&self) -> Result<String, RhiProcessedJobStoreError> { - self.ensure_schema().await?; - query_string(&self.pool, "PRAGMA journal_mode").await - } - - fn from_options( - options: SqliteConnectOptions, - file_backed: bool, - max_connections: u32, - ) -> Self { - let pool = SqlitePoolOptions::new() - .max_connections(max_connections) - .connect_lazy_with(options); - Self { - pool, - file_backed, - schema_ready: Arc::new(OnceCell::new()), - } - } - - async fn ensure_schema(&self) -> Result<(), RhiProcessedJobStoreError> { - self.schema_ready - .get_or_try_init(|| async { - configure_connection(&self.pool, self.file_backed).await?; - apply_schema(&self.pool).await - }) - .await?; - Ok(()) - } -} - -async fn configure_connection( - pool: &SqlitePool, - file_backed: bool, -) -> Result<(), RhiProcessedJobStoreError> { - sqlx::query("PRAGMA foreign_keys = ON") - .execute(pool) - .await?; - sqlx::query("PRAGMA busy_timeout = 5000") - .execute(pool) - .await?; - if file_backed { - sqlx::query("PRAGMA journal_mode = WAL") - .execute(pool) - .await?; - } - Ok(()) -} - -async fn apply_schema(pool: &SqlitePool) -> Result<(), RhiProcessedJobStoreError> { - sqlx::query( - "CREATE TABLE IF NOT EXISTS rhi_processed_job_schema( - schema_id INTEGER PRIMARY KEY CHECK(schema_id = 1), - version INTEGER NOT NULL - )", - ) - .execute(pool) - .await?; - let existing_version: Option<i64> = - sqlx::query("SELECT version FROM rhi_processed_job_schema WHERE schema_id = 1") - .fetch_optional(pool) - .await? - .map(|row| row.try_get("version")) - .transpose()?; - match existing_version { - Some(version) if version == RHI_PROCESSED_JOB_SCHEMA_VERSION => {} - Some(version) => return Err(RhiProcessedJobStoreError::UnsupportedSchemaVersion(version)), - None => { - sqlx::query("INSERT INTO rhi_processed_job_schema(schema_id, version) VALUES (1, ?)") - .bind(RHI_PROCESSED_JOB_SCHEMA_VERSION) - .execute(pool) - .await?; - } - } - sqlx::query( - "CREATE TABLE IF NOT EXISTS rhi_processed_jobs( - request_id TEXT PRIMARY KEY, - request_kind INTEGER NOT NULL, - request_hash TEXT NOT NULL, - customer_pubkey TEXT NOT NULL, - status TEXT NOT NULL, - receipt_event_id TEXT, - receipt_event_json TEXT, - result_event_id TEXT, - result_event_json TEXT, - proof_metadata_json TEXT, - error_code TEXT, - created_timestamp INTEGER NOT NULL, - completed_timestamp INTEGER, - claim_expires_at_ms INTEGER, - inserted_at_ms INTEGER NOT NULL, - updated_at_ms INTEGER NOT NULL, - CHECK ( - (receipt_event_id IS NULL AND receipt_event_json IS NULL) - OR (receipt_event_id IS NOT NULL AND receipt_event_json IS NOT NULL) - ), - CHECK ( - (result_event_id IS NULL AND result_event_json IS NULL) - OR (result_event_id IS NOT NULL AND result_event_json IS NOT NULL) - ), - CHECK ( - proof_metadata_json IS NULL - OR receipt_event_id IS NOT NULL - ), - CHECK ( - ( - status = 'processing' - AND receipt_event_id IS NULL - AND receipt_event_json IS NULL - AND result_event_id IS NULL - AND result_event_json IS NULL - AND proof_metadata_json IS NULL - AND error_code IS NULL - AND completed_timestamp IS NULL - AND claim_expires_at_ms IS NOT NULL - ) - OR ( - status = 'receipt_publishing' - AND receipt_event_id IS NOT NULL - AND receipt_event_json IS NOT NULL - AND result_event_id IS NULL - AND result_event_json IS NULL - AND error_code IS NULL - AND completed_timestamp IS NULL - AND claim_expires_at_ms IS NOT NULL - ) - OR ( - status = 'receipt_published' - AND receipt_event_id IS NOT NULL - AND receipt_event_json IS NOT NULL - AND result_event_id IS NULL - AND result_event_json IS NULL - AND error_code IS NULL - AND completed_timestamp IS NULL - AND claim_expires_at_ms IS NULL - ) - OR ( - status = 'result_publishing' - AND receipt_event_id IS NOT NULL - AND receipt_event_json IS NOT NULL - AND error_code IS NULL - AND completed_timestamp IS NULL - AND claim_expires_at_ms IS NOT NULL - ) - OR ( - status = 'completed' - AND receipt_event_id IS NOT NULL - AND receipt_event_json IS NOT NULL - AND result_event_id IS NOT NULL - AND result_event_json IS NOT NULL - AND error_code IS NULL - AND completed_timestamp IS NOT NULL - AND claim_expires_at_ms IS NULL - ) - OR ( - status = 'failed' - AND receipt_event_id IS NULL - AND receipt_event_json IS NULL - AND result_event_id IS NULL - AND result_event_json IS NULL - AND proof_metadata_json IS NULL - AND error_code IS NOT NULL - AND completed_timestamp IS NOT NULL - AND claim_expires_at_ms IS NULL - ) - ) - )", - ) - .execute(pool) - .await?; - sqlx::query( - "CREATE INDEX IF NOT EXISTS rhi_processed_jobs_status_idx - ON rhi_processed_jobs(status)", - ) - .execute(pool) - .await?; - sqlx::query( - "CREATE UNIQUE INDEX IF NOT EXISTS rhi_processed_jobs_receipt_event_idx - ON rhi_processed_jobs(receipt_event_id) - WHERE receipt_event_id IS NOT NULL", - ) - .execute(pool) - .await?; - sqlx::query( - "CREATE UNIQUE INDEX IF NOT EXISTS rhi_processed_jobs_result_event_idx - ON rhi_processed_jobs(result_event_id) - WHERE result_event_id IS NOT NULL", - ) - .execute(pool) - .await?; - Ok(()) -} - -async fn insert_claimed_job( - tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - job: &RhiProcessedJobState, - now_ms: i64, - claim_expires_at_ms: i64, -) -> Result<bool, RhiProcessedJobStoreError> { - let changed = sqlx::query( - "INSERT INTO rhi_processed_jobs( - request_id, - request_kind, - request_hash, - customer_pubkey, - status, - receipt_event_id, - receipt_event_json, - result_event_id, - result_event_json, - proof_metadata_json, - error_code, - created_timestamp, - completed_timestamp, - claim_expires_at_ms, - inserted_at_ms, - updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(request_id) DO NOTHING", - ) - .bind(job.request_id.as_str()) - .bind(i64::from(job.request_kind)) - .bind(job.request_hash.as_str()) - .bind(job.customer_pubkey.as_str()) - .bind(RhiProcessedJobStatus::Processing.as_str()) - .bind(job.receipt_event_id.as_deref()) - .bind(job.receipt_event_json.as_deref()) - .bind(job.result_event_id.as_deref()) - .bind(job.result_event_json.as_deref()) - .bind(job.proof_metadata_json.as_deref()) - .bind(job.error_code.as_deref()) - .bind(i64::from(job.created_timestamp)) - .bind(job.completed_timestamp.map(i64::from)) - .bind(claim_expires_at_ms) - .bind(now_ms) - .bind(now_ms) - .execute(&mut **tx) - .await? - .rows_affected(); - Ok(changed == 1) -} - -async fn select_job( - tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - request_id: &str, -) -> Result<Option<RhiProcessedJobState>, RhiProcessedJobStoreError> { - let job = sqlx::query( - "SELECT - request_id, - request_kind, - request_hash, - customer_pubkey, - status, - receipt_event_id, - receipt_event_json, - result_event_id, - result_event_json, - proof_metadata_json, - error_code, - created_timestamp, - completed_timestamp - FROM rhi_processed_jobs - WHERE request_id = ?", - ) - .bind(request_id) - .fetch_optional(&mut **tx) - .await? - .map(job_from_row) - .transpose()?; - if let Some(job) = job.as_ref() { - validate_stored_job_state(job)?; - } - Ok(job) -} - -async fn claim_for_existing_job( - tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - existing: RhiProcessedJobState, - now_ms: i64, - claim_expires_at_ms: i64, -) -> Result<RhiProcessedJobClaim, RhiProcessedJobStoreError> { - if existing.status == RhiProcessedJobStatus::Completed && existing.result_event_id.is_some() { - return Ok(RhiProcessedJobClaim::Completed); - } - if existing.status == RhiProcessedJobStatus::Failed { - return Ok(RhiProcessedJobClaim::Failed { - error_code: existing.error_code.clone().ok_or( - RhiProcessedJobStoreError::InvalidStoredValue("failed_error_code"), - )?, - }); - } - if let Some(receipt_event_id) = existing.receipt_event_id.clone() { - let current_claim_expires_at_ms = - select_claim_expires_at_ms(tx, existing.request_id.as_str()).await?; - if matches!( - existing.status, - RhiProcessedJobStatus::ReceiptPublishing | RhiProcessedJobStatus::ResultPublishing - ) && current_claim_expires_at_ms.is_some_and(|expires_at_ms| expires_at_ms > now_ms) - { - return Ok(RhiProcessedJobClaim::InProgress); - } - if existing.status == RhiProcessedJobStatus::ReceiptPublishing { - let receipt_event_json = existing - .receipt_event_json - .clone() - .ok_or(RhiProcessedJobStoreError::ReceiptPublicationNotClaimed)?; - let changed = sqlx::query( - "UPDATE rhi_processed_jobs - SET claim_expires_at_ms = ?, - updated_at_ms = ? - WHERE request_id = ? - AND receipt_event_id = ? - AND status = ? - AND ( - claim_expires_at_ms IS NULL - OR claim_expires_at_ms <= ? - )", - ) - .bind(claim_expires_at_ms) - .bind(now_ms) - .bind(existing.request_id.as_str()) - .bind(receipt_event_id.as_str()) - .bind(RhiProcessedJobStatus::ReceiptPublishing.as_str()) - .bind(now_ms) - .execute(&mut **tx) - .await? - .rows_affected(); - if changed == 1 { - return Ok(RhiProcessedJobClaim::RecoverReceipt { - receipt_event_id, - receipt_event_json, - }); - } - return Ok(RhiProcessedJobClaim::InProgress); - } - let receipt_event_json = existing - .receipt_event_json - .clone() - .ok_or(RhiProcessedJobStoreError::MissingReceiptEventJson)?; - let changed = sqlx::query( - "UPDATE rhi_processed_jobs - SET status = ?, - claim_expires_at_ms = ?, - updated_at_ms = ? - WHERE request_id = ? - AND receipt_event_id = ? - AND status != ? - AND ( - status != ? - OR claim_expires_at_ms IS NULL - OR claim_expires_at_ms <= ? - )", - ) - .bind(RhiProcessedJobStatus::ResultPublishing.as_str()) - .bind(claim_expires_at_ms) - .bind(now_ms) - .bind(existing.request_id.as_str()) - .bind(receipt_event_id.as_str()) - .bind(RhiProcessedJobStatus::Completed.as_str()) - .bind(RhiProcessedJobStatus::ResultPublishing.as_str()) - .bind(now_ms) - .execute(&mut **tx) - .await? - .rows_affected(); - if changed == 1 { - return Ok(RhiProcessedJobClaim::RecoverResult { - receipt_event_id, - receipt_event_json, - result_event_id: existing.result_event_id, - result_event_json: existing.result_event_json, - proof_metadata_json: existing.proof_metadata_json, - }); - } - return Ok(RhiProcessedJobClaim::InProgress); - } - - let current_claim_expires_at_ms: Option<i64> = - sqlx::query("SELECT claim_expires_at_ms FROM rhi_processed_jobs WHERE request_id = ?") - .bind(existing.request_id.as_str()) - .fetch_one(&mut **tx) - .await? - .try_get("claim_expires_at_ms")?; - if existing.status == RhiProcessedJobStatus::Processing - && current_claim_expires_at_ms.is_some_and(|expires_at_ms| expires_at_ms > now_ms) - { - return Ok(RhiProcessedJobClaim::InProgress); - } - - let changed = sqlx::query( - "UPDATE rhi_processed_jobs - SET status = ?, - claim_expires_at_ms = ?, - updated_at_ms = ? - WHERE request_id = ? - AND ( - status != ? - OR claim_expires_at_ms IS NULL - OR claim_expires_at_ms <= ? - )", - ) - .bind(RhiProcessedJobStatus::Processing.as_str()) - .bind(claim_expires_at_ms) - .bind(now_ms) - .bind(existing.request_id.as_str()) - .bind(RhiProcessedJobStatus::Processing.as_str()) - .bind(now_ms) - .execute(&mut **tx) - .await? - .rows_affected(); - if changed == 1 { - Ok(RhiProcessedJobClaim::Execute) - } else { - Ok(RhiProcessedJobClaim::InProgress) - } -} - -async fn update_job( - tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - job: &RhiProcessedJobState, - now_ms: i64, - claim_expires_at_ms: Option<i64>, -) -> Result<(), RhiProcessedJobStoreError> { - sqlx::query( - "UPDATE rhi_processed_jobs - SET request_kind = ?, - request_hash = ?, - customer_pubkey = ?, - status = ?, - receipt_event_id = ?, - receipt_event_json = ?, - result_event_id = ?, - result_event_json = ?, - proof_metadata_json = ?, - error_code = ?, - created_timestamp = ?, - completed_timestamp = ?, - claim_expires_at_ms = ?, - updated_at_ms = ? - WHERE request_id = ?", - ) - .bind(i64::from(job.request_kind)) - .bind(job.request_hash.as_str()) - .bind(job.customer_pubkey.as_str()) - .bind(job.status.as_str()) - .bind(job.receipt_event_id.as_deref()) - .bind(job.receipt_event_json.as_deref()) - .bind(job.result_event_id.as_deref()) - .bind(job.result_event_json.as_deref()) - .bind(job.proof_metadata_json.as_deref()) - .bind(job.error_code.as_deref()) - .bind(i64::from(job.created_timestamp)) - .bind(job.completed_timestamp.map(i64::from)) - .bind(claim_expires_at_ms) - .bind(now_ms) - .bind(job.request_id.as_str()) - .execute(&mut **tx) - .await?; - Ok(()) -} - -async fn update_job_without_claim_change( - tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - job: &RhiProcessedJobState, - now_ms: i64, -) -> Result<(), RhiProcessedJobStoreError> { - sqlx::query( - "UPDATE rhi_processed_jobs - SET request_kind = ?, - request_hash = ?, - customer_pubkey = ?, - status = ?, - receipt_event_id = ?, - receipt_event_json = ?, - result_event_id = ?, - result_event_json = ?, - proof_metadata_json = ?, - error_code = ?, - created_timestamp = ?, - completed_timestamp = ?, - updated_at_ms = ? - WHERE request_id = ?", - ) - .bind(i64::from(job.request_kind)) - .bind(job.request_hash.as_str()) - .bind(job.customer_pubkey.as_str()) - .bind(job.status.as_str()) - .bind(job.receipt_event_id.as_deref()) - .bind(job.receipt_event_json.as_deref()) - .bind(job.result_event_id.as_deref()) - .bind(job.result_event_json.as_deref()) - .bind(job.proof_metadata_json.as_deref()) - .bind(job.error_code.as_deref()) - .bind(i64::from(job.created_timestamp)) - .bind(job.completed_timestamp.map(i64::from)) - .bind(now_ms) - .bind(job.request_id.as_str()) - .execute(&mut **tx) - .await?; - Ok(()) -} - -fn ensure_processed_job_matches( - existing: &RhiProcessedJobState, - incoming: &RhiProcessedJobState, -) -> Result<(), RhiProcessedJobStoreError> { - if existing.request_kind != incoming.request_kind - || existing.request_hash != incoming.request_hash - || existing.customer_pubkey != incoming.customer_pubkey - { - return Err(RhiProcessedJobStoreError::DuplicateConflictingJob); - } - Ok(()) -} - -fn validate_stored_job_state(job: &RhiProcessedJobState) -> Result<(), RhiProcessedJobStoreError> { - validate_stored_event_pair( - job.receipt_event_id.as_ref(), - job.receipt_event_json.as_ref(), - "receipt_event", - )?; - validate_stored_event_pair( - job.result_event_id.as_ref(), - job.result_event_json.as_ref(), - "result_event", - )?; - if job.proof_metadata_json.is_some() && job.receipt_event_id.is_none() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "proof_metadata_without_receipt", - )); - } - match job.status { - RhiProcessedJobStatus::Processing => { - validate_no_receipt(job)?; - validate_no_result(job)?; - validate_no_terminal_fields(job)?; - if job.proof_metadata_json.is_some() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "processing_proof_metadata", - )); - } - } - RhiProcessedJobStatus::ReceiptPublishing | RhiProcessedJobStatus::ReceiptPublished => { - validate_receipt(job)?; - validate_no_result(job)?; - validate_no_terminal_fields(job)?; - } - RhiProcessedJobStatus::ResultPublishing => { - validate_receipt(job)?; - validate_no_terminal_fields(job)?; - } - RhiProcessedJobStatus::Completed => { - validate_receipt(job)?; - validate_result(job)?; - if job.error_code.is_some() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "completed_error_code", - )); - } - if job.completed_timestamp.is_none() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "completed_timestamp", - )); - } - } - RhiProcessedJobStatus::Failed => { - validate_no_receipt(job)?; - validate_no_result(job)?; - if job.proof_metadata_json.is_some() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "failed_proof_metadata", - )); - } - if job.error_code.is_none() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "failed_error_code", - )); - } - if job.completed_timestamp.is_none() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "failed_completed_timestamp", - )); - } - } - } - Ok(()) -} - -fn validate_stored_event_pair( - event_id: Option<&String>, - event_json: Option<&String>, - field: &'static str, -) -> Result<(), RhiProcessedJobStoreError> { - if event_id.is_some() == event_json.is_some() { - Ok(()) - } else { - Err(RhiProcessedJobStoreError::InvalidStoredValue(field)) - } -} - -fn validate_receipt(job: &RhiProcessedJobState) -> Result<(), RhiProcessedJobStoreError> { - if job.receipt_event_id.is_some() && job.receipt_event_json.is_some() { - Ok(()) - } else { - Err(RhiProcessedJobStoreError::InvalidStoredValue( - "receipt_event", - )) - } -} - -fn validate_result(job: &RhiProcessedJobState) -> Result<(), RhiProcessedJobStoreError> { - if job.result_event_id.is_some() && job.result_event_json.is_some() { - Ok(()) - } else { - Err(RhiProcessedJobStoreError::InvalidStoredValue( - "result_event", - )) - } -} - -fn validate_no_receipt(job: &RhiProcessedJobState) -> Result<(), RhiProcessedJobStoreError> { - if job.receipt_event_id.is_none() && job.receipt_event_json.is_none() { - Ok(()) - } else { - Err(RhiProcessedJobStoreError::InvalidStoredValue( - "unexpected_receipt_event", - )) - } -} - -fn validate_no_result(job: &RhiProcessedJobState) -> Result<(), RhiProcessedJobStoreError> { - if job.result_event_id.is_none() && job.result_event_json.is_none() { - Ok(()) - } else { - Err(RhiProcessedJobStoreError::InvalidStoredValue( - "unexpected_result_event", - )) - } -} - -fn validate_no_terminal_fields( - job: &RhiProcessedJobState, -) -> Result<(), RhiProcessedJobStoreError> { - if job.error_code.is_some() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "active_error_code", - )); - } - if job.completed_timestamp.is_some() { - return Err(RhiProcessedJobStoreError::InvalidStoredValue( - "active_completed_timestamp", - )); - } - Ok(()) -} - -fn ensure_receipt_matches( - existing: &RhiProcessedJobState, - receipt_event_id: &str, -) -> Result<(), RhiProcessedJobStoreError> { - if existing - .receipt_event_id - .as_ref() - .is_some_and(|existing| existing != receipt_event_id) - { - return Err(RhiProcessedJobStoreError::DuplicateConflictingReceipt); - } - Ok(()) -} - -fn ensure_receipt_event_json_matches( - existing: &RhiProcessedJobState, - receipt_event_json: &str, -) -> Result<(), RhiProcessedJobStoreError> { - if existing - .receipt_event_json - .as_ref() - .is_some_and(|existing| existing != receipt_event_json) - { - return Err(RhiProcessedJobStoreError::DuplicateConflictingReceipt); - } - Ok(()) -} - -fn ensure_result_matches( - existing: &RhiProcessedJobState, - result_event_id: &str, -) -> Result<(), RhiProcessedJobStoreError> { - if existing - .result_event_id - .as_ref() - .is_some_and(|existing| existing != result_event_id) - { - return Err(RhiProcessedJobStoreError::DuplicateConflictingResult); - } - Ok(()) -} - -fn ensure_result_event_json_matches( - existing: &RhiProcessedJobState, - result_event_json: &str, -) -> Result<(), RhiProcessedJobStoreError> { - if existing - .result_event_json - .as_ref() - .is_some_and(|existing| existing != result_event_json) - { - return Err(RhiProcessedJobStoreError::DuplicateConflictingResult); - } - Ok(()) -} - -fn ensure_proof_metadata_matches( - existing: &RhiProcessedJobState, - proof_metadata_json: Option<&str>, -) -> Result<(), RhiProcessedJobStoreError> { - if existing - .proof_metadata_json - .as_deref() - .is_some_and(|existing| Some(existing) != proof_metadata_json) - { - return Err(RhiProcessedJobStoreError::DuplicateConflictingReceipt); - } - Ok(()) -} - -async fn select_claim_expires_at_ms( - tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - request_id: &str, -) -> Result<Option<i64>, RhiProcessedJobStoreError> { - sqlx::query("SELECT claim_expires_at_ms FROM rhi_processed_jobs WHERE request_id = ?") - .bind(request_id) - .fetch_one(&mut **tx) - .await? - .try_get("claim_expires_at_ms") - .map_err(Into::into) -} - -fn job_from_row(row: SqliteRow) -> Result<RhiProcessedJobState, RhiProcessedJobStoreError> { - Ok(RhiProcessedJobState { - request_id: row.try_get("request_id")?, - request_kind: u32_from_i64(row.try_get("request_kind")?, "request_kind")?, - request_hash: row.try_get("request_hash")?, - customer_pubkey: row.try_get("customer_pubkey")?, - status: RhiProcessedJobStatus::parse(row.try_get::<String, _>("status")?.as_str())?, - receipt_event_id: row.try_get("receipt_event_id")?, - receipt_event_json: row.try_get("receipt_event_json")?, - result_event_id: row.try_get("result_event_id")?, - result_event_json: row.try_get("result_event_json")?, - proof_metadata_json: row.try_get("proof_metadata_json")?, - error_code: row.try_get("error_code")?, - created_timestamp: u32_from_i64(row.try_get("created_timestamp")?, "created_timestamp")?, - completed_timestamp: row - .try_get::<Option<i64>, _>("completed_timestamp")? - .map(|value| u32_from_i64(value, "completed_timestamp")) - .transpose()?, - }) -} - -fn u32_from_i64(value: i64, field: &'static str) -> Result<u32, RhiProcessedJobStoreError> { - u32::try_from(value).map_err(|_| RhiProcessedJobStoreError::InvalidStoredValue(field)) -} - -async fn query_i64(pool: &SqlitePool, sql: &'static str) -> Result<i64, RhiProcessedJobStoreError> { - let row = sqlx::query(sql).fetch_one(pool).await?; - Ok(row.try_get(0)?) -} - -async fn query_string( - pool: &SqlitePool, - sql: &'static str, -) -> Result<String, RhiProcessedJobStoreError> { - let row = sqlx::query(sql).fetch_one(pool).await?; - Ok(row.try_get(0)?) -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - RhiProcessedJobClaim, RhiProcessedJobState, RhiProcessedJobStatus, RhiProcessedJobStore, - RhiProcessedJobStoreError, - }; - - fn job(request_id: &str) -> RhiProcessedJobState { - RhiProcessedJobState { - request_id: request_id.to_owned(), - request_kind: 5322, - request_hash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_owned(), - customer_pubkey: "customer".to_owned(), - status: RhiProcessedJobStatus::Processing, - receipt_event_id: None, - receipt_event_json: None, - result_event_id: None, - result_event_json: None, - proof_metadata_json: None, - error_code: None, - created_timestamp: 1_700_000_000, - completed_timestamp: None, - } - } - - fn receipt_json(value: &str) -> String { - format!(r#"{{"kind":"receipt","value":"{value}"}}"#) - } - - fn result_json(value: &str) -> String { - format!(r#"{{"kind":"result","value":"{value}"}}"#) - } - - fn proof_json(value: &str) -> String { - format!(r#"{{"proof":"{value}"}}"#) - } - - async fn disable_store_constraints(store: &RhiProcessedJobStore) { - sqlx::query("PRAGMA ignore_check_constraints = ON") - .execute(&store.pool) - .await - .expect("disable check constraints"); - } - - async fn enable_store_constraints(store: &RhiProcessedJobStore) { - sqlx::query("PRAGMA ignore_check_constraints = OFF") - .execute(&store.pool) - .await - .expect("enable check constraints"); - } - - async fn corrupt_job( - store: &RhiProcessedJobStore, - job: &RhiProcessedJobState, - sql: &'static str, - ) { - disable_store_constraints(store).await; - sqlx::query(sql) - .bind(job.request_id.as_str()) - .execute(&store.pool) - .await - .expect("corrupt job"); - enable_store_constraints(store).await; - } - - async fn complete_job(store: &RhiProcessedJobStore, job: &RhiProcessedJobState) { - store.claim_job(job, 10, 100).await.expect("claim"); - store - .mark_receipt_publishing( - job, - "receipt-complete", - receipt_json("complete").as_str(), - None, - 20, - ) - .await - .expect("receipt intent"); - store - .mark_receipt_published(job, "receipt-complete", 30) - .await - .expect("receipt published"); - store.claim_job(job, 131, 100).await.expect("result claim"); - store - .mark_result_publishing( - job, - "receipt-complete", - "result-complete", - result_json("complete").as_str(), - 140, - ) - .await - .expect("result intent"); - store - .mark_completed( - job, - "receipt-complete", - "result-complete", - 1_700_000_001, - 150, - ) - .await - .expect("complete"); - } - - #[tokio::test] - async fn processed_job_store_claims_updates_and_reopens_completed_jobs() { - let tempdir = tempfile::tempdir().expect("tempdir"); - let path = tempdir.path().join("processed_jobs.sqlite"); - let store = RhiProcessedJobStore::open_file(path.as_path()) - .await - .expect("store"); - assert_eq!(store.pragma_busy_timeout().await.expect("timeout"), 5000); - assert_eq!( - store.pragma_journal_mode().await.expect("journal"), - "wal".to_owned() - ); - let job = job("request-1"); - - assert_eq!( - store.claim_job(&job, 1_000, 10_000).await.expect("claim"), - RhiProcessedJobClaim::Execute - ); - let receipt_intent = store - .mark_receipt_publishing( - &job, - "receipt-1", - receipt_json("one").as_str(), - Some(proof_json("one").as_str()), - 1_050, - ) - .await - .expect("receipt intent"); - assert_eq!( - receipt_intent.status, - RhiProcessedJobStatus::ReceiptPublishing - ); - assert_eq!( - receipt_intent.receipt_event_json.as_deref(), - Some(receipt_json("one").as_str()) - ); - let published = store - .mark_receipt_published(&job, "receipt-1", 1_100) - .await - .expect("receipt"); - assert_eq!(published.status, RhiProcessedJobStatus::ReceiptPublished); - let error = store - .mark_completed(&job, "receipt-1", "result-1", 1_700_000_001, 1_150) - .await - .expect_err("unclaimed result completion"); - assert!(matches!( - error, - RhiProcessedJobStoreError::ResultPublicationNotClaimed - )); - assert_eq!( - store - .claim_job(&job, 1_160, 10_000) - .await - .expect("result claim"), - RhiProcessedJobClaim::RecoverResult { - receipt_event_id: "receipt-1".to_owned(), - receipt_event_json: receipt_json("one"), - result_event_id: None, - result_event_json: None, - proof_metadata_json: Some(proof_json("one")), - } - ); - let publishing = store - .mark_result_publishing( - &job, - "receipt-1", - "result-1", - result_json("one").as_str(), - 1_170, - ) - .await - .expect("result intent"); - assert_eq!(publishing.status, RhiProcessedJobStatus::ResultPublishing); - assert_eq!(publishing.result_event_id.as_deref(), Some("result-1")); - assert_eq!( - publishing.result_event_json.as_deref(), - Some(result_json("one").as_str()) - ); - let completed = store - .mark_completed(&job, "receipt-1", "result-1", 1_700_000_001, 1_200) - .await - .expect("complete"); - assert_eq!(completed.status, RhiProcessedJobStatus::Completed); - - let reopened = RhiProcessedJobStore::open_file(path.as_path()) - .await - .expect("reopen"); - let stored = reopened - .get_job("request-1") - .await - .expect("stored") - .expect("job"); - assert_eq!(stored.status, RhiProcessedJobStatus::Completed); - assert_eq!(stored.receipt_event_id.as_deref(), Some("receipt-1")); - assert_eq!( - stored.receipt_event_json.as_deref(), - Some(receipt_json("one").as_str()) - ); - assert_eq!(stored.result_event_id.as_deref(), Some("result-1")); - assert_eq!( - stored.result_event_json.as_deref(), - Some(result_json("one").as_str()) - ); - } - - #[tokio::test] - async fn processed_job_store_prevent_unexpired_duplicate_claims_and_reclaims_expired_claims() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("request-2"); - - assert_eq!( - store.claim_job(&job, 10, 100).await.expect("first claim"), - RhiProcessedJobClaim::Execute - ); - assert_eq!( - store - .claim_job(&job, 20, 100) - .await - .expect("duplicate claim"), - RhiProcessedJobClaim::InProgress - ); - assert_eq!( - store - .claim_job(&job, 111, 100) - .await - .expect("expired claim"), - RhiProcessedJobClaim::Execute - ); - } - - #[tokio::test] - async fn processed_job_store_recovers_expired_receipt_publication_intent() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("request-receipt-recover"); - store.claim_job(&job, 10, 100).await.expect("claim"); - store - .mark_receipt_publishing( - &job, - "receipt-recover", - receipt_json("recover").as_str(), - Some(proof_json("recover").as_str()), - 20, - ) - .await - .expect("receipt intent"); - - assert_eq!( - store - .claim_job(&job, 30, 100) - .await - .expect("unexpired receipt intent"), - RhiProcessedJobClaim::InProgress - ); - assert_eq!( - store - .claim_job(&job, 111, 100) - .await - .expect("expired receipt intent"), - RhiProcessedJobClaim::RecoverReceipt { - receipt_event_id: "receipt-recover".to_owned(), - receipt_event_json: receipt_json("recover"), - } - ); - } - - #[tokio::test] - async fn processed_job_store_claims_result_publication_and_rejects_conflicting_result_ids() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("request-2-result"); - store.claim_job(&job, 10, 100).await.expect("claim"); - store - .mark_receipt_publishing(&job, "receipt-1", receipt_json("two").as_str(), None, 15) - .await - .expect("receipt intent"); - store - .mark_receipt_published(&job, "receipt-1", 20) - .await - .expect("receipt"); - assert_eq!( - store.claim_job(&job, 30, 100).await.expect("result claim"), - RhiProcessedJobClaim::RecoverResult { - receipt_event_id: "receipt-1".to_owned(), - receipt_event_json: receipt_json("two"), - result_event_id: None, - result_event_json: None, - proof_metadata_json: None, - } - ); - store - .mark_result_publishing( - &job, - "receipt-1", - "result-1", - result_json("two").as_str(), - 40, - ) - .await - .expect("result intent"); - assert_eq!( - store - .claim_job(&job, 50, 100) - .await - .expect("duplicate result claim"), - RhiProcessedJobClaim::InProgress - ); - assert_eq!( - store - .claim_job(&job, 131, 100) - .await - .expect("expired result claim"), - RhiProcessedJobClaim::RecoverResult { - receipt_event_id: "receipt-1".to_owned(), - receipt_event_json: receipt_json("two"), - result_event_id: Some("result-1".to_owned()), - result_event_json: Some(result_json("two")), - proof_metadata_json: None, - } - ); - let error = store - .mark_result_publishing( - &job, - "receipt-1", - "result-2", - result_json("other").as_str(), - 140, - ) - .await - .expect_err("conflicting result"); - assert!(matches!( - error, - RhiProcessedJobStoreError::DuplicateConflictingResult - )); - let error = store - .mark_completed(&job, "receipt-1", "result-2", 1_700_000_001, 150) - .await - .expect_err("conflicting completion"); - assert!(matches!( - error, - RhiProcessedJobStoreError::DuplicateConflictingResult - )); - } - - #[tokio::test] - async fn processed_job_store_rejects_result_recovery_without_receipt_event_json() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("request-missing-receipt-json"); - store.claim_job(&job, 10, 100).await.expect("claim"); - store - .mark_receipt_publishing( - &job, - "receipt-1", - receipt_json("missing").as_str(), - None, - 15, - ) - .await - .expect("receipt intent"); - store - .mark_receipt_published(&job, "receipt-1", 20) - .await - .expect("receipt"); - - corrupt_job( - &store, - &job, - "UPDATE rhi_processed_jobs SET receipt_event_json = NULL WHERE request_id = ?", - ) - .await; - - let error = store - .claim_job(&job, 131, 100) - .await - .expect_err("invalid receipt event json"); - assert!(matches!( - error, - RhiProcessedJobStoreError::InvalidStoredValue("receipt_event") - )); - } - - #[tokio::test] - async fn processed_job_store_rejects_malformed_completed_rows_before_claim() { - for (name, sql) in [ - ( - "completed without result id", - "UPDATE rhi_processed_jobs SET result_event_id = NULL WHERE request_id = ?", - ), - ( - "completed without result json", - "UPDATE rhi_processed_jobs SET result_event_json = NULL WHERE request_id = ?", - ), - ( - "completed with receipt only", - "UPDATE rhi_processed_jobs SET result_event_id = NULL, result_event_json = NULL WHERE request_id = ?", - ), - ] { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job(name); - complete_job(&store, &job).await; - corrupt_job(&store, &job, sql).await; - - let error = store - .claim_job(&job, 200, 100) - .await - .expect_err("invalid completed row"); - assert!( - matches!( - error, - RhiProcessedJobStoreError::InvalidStoredValue("result_event") - ), - "{name}: {error:?}" - ); - } - } - - #[tokio::test] - async fn processed_job_store_rejects_malformed_failed_rows_before_claim() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let missing_error = job("failed-without-error"); - store - .claim_job(&missing_error, 10, 100) - .await - .expect("claim"); - corrupt_job( - &store, - &missing_error, - "UPDATE rhi_processed_jobs - SET status = 'failed', - completed_timestamp = 1700000001, - claim_expires_at_ms = NULL - WHERE request_id = ?", - ) - .await; - let error = store - .claim_job(&missing_error, 200, 100) - .await - .expect_err("failed row without error"); - assert!(matches!( - error, - RhiProcessedJobStoreError::InvalidStoredValue("failed_error_code") - )); - - let partial = job("failed-with-partial-evidence"); - store.claim_job(&partial, 10, 100).await.expect("claim"); - store - .mark_receipt_publishing( - &partial, - "receipt-partial", - receipt_json("partial").as_str(), - None, - 20, - ) - .await - .expect("receipt intent"); - store - .mark_receipt_published(&partial, "receipt-partial", 30) - .await - .expect("receipt published"); - corrupt_job( - &store, - &partial, - "UPDATE rhi_processed_jobs - SET status = 'failed', - error_code = 'proof_failed', - completed_timestamp = 1700000001, - claim_expires_at_ms = NULL - WHERE request_id = ?", - ) - .await; - let error = store - .claim_job(&partial, 200, 100) - .await - .expect_err("failed row with partial evidence"); - assert!(matches!( - error, - RhiProcessedJobStoreError::InvalidStoredValue("unexpected_receipt_event") - )); - } - - #[tokio::test] - async fn processed_job_store_treats_failed_rows_as_terminal() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("failed-terminal"); - store.claim_job(&job, 10, 100).await.expect("claim"); - sqlx::query( - "UPDATE rhi_processed_jobs - SET status = 'failed', - error_code = 'proof_failed', - completed_timestamp = 1700000001, - claim_expires_at_ms = NULL - WHERE request_id = ?", - ) - .bind(job.request_id.as_str()) - .execute(&store.pool) - .await - .expect("valid failed row"); - - assert_eq!( - store - .claim_job(&job, 200, 100) - .await - .expect("terminal failed claim"), - RhiProcessedJobClaim::Failed { - error_code: "proof_failed".to_owned() - } - ); - } - - #[tokio::test] - async fn processed_job_store_schema_rejects_malformed_completed_rows() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("completed-schema-check"); - complete_job(&store, &job).await; - - let error = sqlx::query( - "UPDATE rhi_processed_jobs SET result_event_json = NULL WHERE request_id = ?", - ) - .bind(job.request_id.as_str()) - .execute(&store.pool) - .await - .expect_err("schema check rejects malformed completed row"); - - assert!(matches!(error, sqlx::Error::Database(_))); - } - - #[tokio::test] - async fn processed_job_store_rejects_conflicting_duplicate_jobs() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("request-3"); - store.claim_job(&job, 10, 100).await.expect("claim"); - let mut conflicting = job.clone(); - conflicting.request_hash = - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_owned(); - - let error = store - .claim_job(&conflicting, 111, 100) - .await - .expect_err("conflicting job"); - assert!(matches!( - error, - RhiProcessedJobStoreError::DuplicateConflictingJob - )); - } - - #[tokio::test] - async fn processed_job_store_rejects_conflicting_receipt_updates() { - let store = RhiProcessedJobStore::open_memory().expect("store"); - let job = job("request-4"); - store.claim_job(&job, 10, 100).await.expect("claim"); - store - .mark_receipt_publishing(&job, "receipt-1", receipt_json("four").as_str(), None, 15) - .await - .expect("receipt intent"); - store - .mark_receipt_published(&job, "receipt-1", 20) - .await - .expect("receipt"); - - let error = store - .mark_receipt_publishing(&job, "receipt-2", receipt_json("other").as_str(), None, 30) - .await - .expect_err("conflicting receipt"); - assert!(matches!( - error, - RhiProcessedJobStoreError::DuplicateConflictingReceipt - )); - } - - #[tokio::test] - async fn processed_job_store_rejects_unsupported_old_schema_versions() { - let tempdir = tempfile::tempdir().expect("tempdir"); - let path = tempdir.path().join("processed_jobs_old.sqlite"); - let options = sqlx::sqlite::SqliteConnectOptions::new() - .filename(path.as_path()) - .create_if_missing(true); - let pool = sqlx::sqlite::SqlitePoolOptions::new() - .max_connections(1) - .connect_with(options) - .await - .expect("open sqlite"); - sqlx::query( - "CREATE TABLE rhi_processed_job_schema( - schema_id INTEGER PRIMARY KEY CHECK(schema_id = 1), - version INTEGER NOT NULL - )", - ) - .execute(&pool) - .await - .expect("schema table"); - sqlx::query("INSERT INTO rhi_processed_job_schema(schema_id, version) VALUES (1, 2)") - .execute(&pool) - .await - .expect("schema version"); - pool.close().await; - - let error = RhiProcessedJobStore::open_file(path.as_path()) - .await - .expect_err("old schema rejected"); - assert!(matches!( - error, - RhiProcessedJobStoreError::UnsupportedSchemaVersion(2) - )); - } -} diff --git a/src/features/trade_listing/state.rs b/src/features/trade_listing/state.rs @@ -1,583 +0,0 @@ -#![forbid(unsafe_code)] - -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrTimestamp}; -use radroots_trade::workflow::RadrootsTradeWorkflowState; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use tokio::sync::Mutex; - -use crate::features::trade_listing::processed_jobs::{ - RhiProcessedJobStore, RhiProcessedJobStoreError, -}; - -pub type SharedTradeListingState = Arc<Mutex<TradeListingState>>; - -const TRADE_LISTING_STATE_VERSION: u32 = 3; -const PROCESSED_JOB_STORE_FILE_NAME: &str = "processed_jobs.sqlite"; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TradeOrderState { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub status: RadrootsTradeWorkflowState, - #[serde(default)] - pub listing_snapshot_event_id: Option<String>, - #[serde(default)] - pub root_event_id: Option<String>, - #[serde(default)] - pub last_event_id: Option<String>, - pub seen_event_ids: HashSet<String>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ValidatedListingState { - pub event_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ListingEventState { - pub event_id: String, - pub kind: u32, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TradeListingState { - #[serde(default)] - validated_listing_events: HashMap<String, ValidatedListingState>, - #[serde(default)] - listing_events: HashMap<String, ListingEventState>, - #[serde(default)] - seen_non_order_event_ids: HashSet<String>, - orders: HashMap<String, TradeOrderState>, - last_event_created_at: Option<u32>, -} - -#[derive(Clone, Debug)] -pub struct TradeListingRuntime { - state: SharedTradeListingState, - config: TradeListingRuntimeConfig, - persistence: Option<Arc<TradeListingStatePersistence>>, - processed_jobs: Arc<RhiProcessedJobStore>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TradeListingRuntimeConfig { - pub state_path: PathBuf, - pub replay_window_secs: u64, - pub replay_overlap_secs: u64, -} - -#[derive(Clone, Debug)] -struct TradeListingStatePersistence { - path: PathBuf, -} - -#[derive(Debug, Serialize, Deserialize)] -struct PersistedTradeListingState { - version: u32, - state: TradeListingState, -} - -impl Default for TradeListingRuntimeConfig { - fn default() -> Self { - Self { - state_path: crate::paths::default_subscriber_state_path_for_process() - .expect("resolve canonical rhi trade-listing state path"), - replay_window_secs: 24 * 60 * 60, - replay_overlap_secs: 5 * 60, - } - } -} - -impl Default for TradeListingRuntime { - fn default() -> Self { - Self { - state: Arc::new(Mutex::new(TradeListingState::default())), - config: TradeListingRuntimeConfig::default(), - persistence: None, - processed_jobs: Arc::new( - RhiProcessedJobStore::open_memory() - .expect("open in-memory rhi processed-job store"), - ), - } - } -} - -impl TradeListingRuntime { - pub fn new() -> Self { - Self::default() - } - - pub async fn load(config: TradeListingRuntimeConfig) -> Result<Self, TradeListingRuntimeError> { - let persistence = Arc::new(TradeListingStatePersistence::new(config.state_path.clone())); - let processed_jobs = Arc::new( - RhiProcessedJobStore::open_file(processed_job_store_path_for_state_path( - &config.state_path, - )?) - .await?, - ); - let state = persistence.load().await?; - Ok(Self { - state: Arc::new(Mutex::new(state)), - config, - persistence: Some(persistence), - processed_jobs, - }) - } - - pub fn state(&self) -> SharedTradeListingState { - Arc::clone(&self.state) - } - - pub fn processed_jobs(&self) -> Arc<RhiProcessedJobStore> { - Arc::clone(&self.processed_jobs) - } - - pub async fn persist(&self) -> Result<(), TradeListingRuntimeError> { - 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<(), TradeListingRuntimeError> { - { - 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<RadrootsNostrKind>) -> RadrootsNostrFilter { - let since = { - let state = self.state.lock().await; - state.replay_since( - RadrootsNostrTimestamp::now().as_secs(), - self.config.replay_window_secs, - self.config.replay_overlap_secs, - ) - }; - RadrootsNostrFilter::new() - .kinds(kinds) - .since(RadrootsNostrTimestamp::from(since)) - } -} - -impl TradeListingState { - pub fn upsert_listing_event(&mut self, listing_addr: &str, event_id: &str, kind: u32) { - self.listing_events.insert( - listing_addr.to_string(), - ListingEventState { - event_id: event_id.to_string(), - kind, - }, - ); - } - - pub fn listing_event_id(&self, listing_addr: &str) -> Option<&str> { - self.listing_events - .get(listing_addr) - .map(|listing| listing.event_id.as_str()) - } - - pub fn mark_listing_validated(&mut self, listing_addr: &str, event_id: &str) { - self.validated_listing_events.insert( - listing_addr.to_string(), - ValidatedListingState { - event_id: event_id.to_string(), - }, - ); - } - - pub fn clear_listing_validation(&mut self, listing_addr: &str) { - self.validated_listing_events.remove(listing_addr); - } - - pub fn validated_listing_event_id(&self, listing_addr: &str) -> Option<&str> { - self.validated_listing_events - .get(listing_addr) - .map(|validated| validated.event_id.as_str()) - } - - pub fn is_listing_validated(&self, listing_addr: &str) -> bool { - self.validated_listing_event_id(listing_addr).is_some() - } - - pub fn order_exists(&self, order_id: &str) -> bool { - self.orders.contains_key(order_id) - } - - pub fn get_order_mut(&mut self, order_id: &str) -> Option<&mut TradeOrderState> { - self.orders.get_mut(order_id) - } - - pub fn get_order(&self, order_id: &str) -> Option<&TradeOrderState> { - self.orders.get(order_id) - } - - pub fn insert_order(&mut self, order: TradeOrderState) { - self.orders.insert(order.order_id.clone(), order); - } - - pub fn mark_event_seen(&mut self, order_id: &str, event_id: &str) -> bool { - if let Some(state) = self.orders.get_mut(order_id) { - state.seen_event_ids.insert(event_id.to_string()) - } else { - false - } - } - - pub fn is_event_seen(&self, order_id: &str, event_id: &str) -> bool { - self.orders - .get(order_id) - .map(|state| state.seen_event_ids.contains(event_id)) - .unwrap_or(false) - } - - pub fn mark_non_order_event_seen(&mut self, event_id: &str) -> bool { - self.seen_non_order_event_ids.insert(event_id.to_string()) - } - - pub fn is_non_order_event_seen(&self, event_id: &str) -> bool { - self.seen_non_order_event_ids.contains(event_id) - } - - pub 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)), - ); - } - - pub fn last_event_created_at(&self) -> Option<u32> { - self.last_event_created_at - } - - pub fn replay_since( - &self, - now_secs: u64, - replay_window_secs: u64, - replay_overlap_secs: u64, - ) -> u64 { - match self.last_event_created_at { - Some(last) => u64::from(last).saturating_sub(replay_overlap_secs), - None => now_secs.saturating_sub(replay_window_secs), - } - } -} - -impl TradeListingStatePersistence { - fn new(path: PathBuf) -> Self { - Self { path } - } - - async fn load(&self) -> Result<TradeListingState, TradeListingRuntimeError> { - if !tokio::fs::try_exists(&self.path).await? { - return Ok(TradeListingState::default()); - } - - let payload = tokio::fs::read_to_string(&self.path).await?; - let snapshot: PersistedTradeListingState = serde_json::from_str(&payload)?; - if snapshot.version != TRADE_LISTING_STATE_VERSION { - return Err(TradeListingRuntimeError::UnsupportedStateVersion( - snapshot.version, - )); - } - Ok(snapshot.state) - } - - async fn persist(&self, state: &TradeListingState) -> Result<(), TradeListingRuntimeError> { - if let Some(parent) = self.path.parent() { - if !parent.as_os_str().is_empty() { - tokio::fs::create_dir_all(parent).await?; - } - } - - let snapshot = PersistedTradeListingState { - version: TRADE_LISTING_STATE_VERSION, - state: state.clone(), - }; - let payload = serde_json::to_vec_pretty(&snapshot)?; - let temp_path = temp_state_path(&self.path)?; - tokio::fs::write(&temp_path, payload).await?; - tokio::fs::rename(&temp_path, &self.path).await?; - Ok(()) - } -} - -fn temp_state_path(path: &Path) -> Result<PathBuf, TradeListingRuntimeError> { - let file_name = path - .file_name() - .ok_or_else(|| TradeListingRuntimeError::InvalidStatePath(path.to_path_buf()))?; - Ok(path.with_file_name(format!("{}.tmp", file_name.to_string_lossy()))) -} - -fn processed_job_store_path_for_state_path( - path: &Path, -) -> Result<PathBuf, TradeListingRuntimeError> { - path.file_name() - .ok_or_else(|| TradeListingRuntimeError::InvalidStatePath(path.to_path_buf()))?; - Ok(path.with_file_name(PROCESSED_JOB_STORE_FILE_NAME)) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TradeListingStateError { - MissingOrder, -} - -impl core::fmt::Display for TradeListingStateError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - TradeListingStateError::MissingOrder => write!(f, "missing order state"), - } - } -} - -impl std::error::Error for TradeListingStateError {} - -#[derive(Debug, Error)] -pub enum TradeListingRuntimeError { - #[error("invalid trade listing state path: {0}")] - InvalidStatePath(PathBuf), - #[error("unsupported trade listing state version: {0}")] - UnsupportedStateVersion(u32), - #[error("trade listing state io error: {0}")] - Io(#[from] std::io::Error), - #[error("trade listing state json error: {0}")] - Json(#[from] serde_json::Error), - #[error("rhi processed-job store error: {0}")] - ProcessedJobStore(#[from] RhiProcessedJobStoreError), -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - ListingEventState, PersistedTradeListingState, TradeListingRuntime, - TradeListingRuntimeConfig, TradeListingRuntimeError, TradeListingState, - TradeListingStateError, TradeOrderState, ValidatedListingState, - processed_job_store_path_for_state_path, - }; - use radroots_trade::workflow::RadrootsTradeWorkflowState; - use std::collections::{HashMap, HashSet}; - use std::path::Path; - - fn unique_state_path(suffix: &str) -> std::path::PathBuf { - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("time") - .as_nanos(); - let path = std::env::temp_dir() - .join(format!("rhi-trade-state-{suffix}-{nanos}")) - .join("state.json"); - std::fs::create_dir_all(path.parent().expect("state parent")).expect("state parent dir"); - path - } - - #[test] - fn state_tracks_listings_events_and_replay_anchor() { - let mut state = TradeListingState::default(); - assert!(!state.is_listing_validated("addr")); - state.mark_listing_validated("addr", "evt-listing-1"); - assert!(state.is_listing_validated("addr")); - assert_eq!( - state.validated_listing_event_id("addr"), - Some("evt-listing-1") - ); - - let order = TradeOrderState { - order_id: "order-1".into(), - listing_addr: "addr".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - status: RadrootsTradeWorkflowState::Requested, - listing_snapshot_event_id: Some("evt-listing-1".into()), - root_event_id: Some("evt-root-1".into()), - last_event_id: Some("evt-root-1".into()), - seen_event_ids: Default::default(), - }; - state.insert_order(order); - assert!(!state.is_event_seen("order-1", "evt")); - assert!(state.mark_event_seen("order-1", "evt")); - assert!(state.is_event_seen("order-1", "evt")); - assert!(!state.is_non_order_event_seen("evt-non-order")); - assert!(state.mark_non_order_event_seen("evt-non-order")); - assert!(state.is_non_order_event_seen("evt-non-order")); - state.upsert_listing_event("addr", "evt-listing-1", 30402); - assert_eq!(state.listing_event_id("addr"), Some("evt-listing-1")); - assert_eq!(state.replay_since(1_000, 300, 60), 700); - - state.observe_event_created_at(900); - assert_eq!(state.last_event_created_at(), Some(900)); - assert_eq!(state.replay_since(1_000, 300, 60), 840); - } - - #[test] - fn state_covers_missing_order_paths_and_error_display() { - let mut state = TradeListingState::default(); - assert!(!state.order_exists("missing")); - assert!(state.get_order_mut("missing").is_none()); - assert!(!state.mark_event_seen("missing", "evt-1")); - assert!(!state.is_event_seen("missing", "evt-1")); - assert!(!state.is_non_order_event_seen("evt-2")); - - assert_eq!( - TradeListingStateError::MissingOrder.to_string(), - "missing order state" - ); - - assert!(state.get_order("missing").is_none()); - } - - #[tokio::test] - async fn runtime_reuses_shared_trade_listing_state() { - let runtime = TradeListingRuntime::new(); - let state = runtime.state(); - state - .lock() - .await - .mark_listing_validated("addr", "evt-listing-1"); - - assert!(runtime.state().lock().await.is_listing_validated("addr")); - } - - #[tokio::test] - async fn runtime_persists_and_loads_trade_listing_state() { - let path = unique_state_path("roundtrip"); - let processed_jobs_path = processed_job_store_path_for_state_path(&path).expect("job path"); - let config = TradeListingRuntimeConfig { - state_path: path.clone(), - replay_window_secs: 600, - replay_overlap_secs: 30, - }; - let runtime = TradeListingRuntime::load(config.clone()) - .await - .expect("runtime"); - - { - let state_handle = runtime.state(); - let mut state = state_handle.lock().await; - state.mark_listing_validated("addr", "evt-listing-1"); - state.mark_non_order_event_seen("evt-validate-1"); - state.observe_event_created_at(456); - } - runtime.persist().await.expect("persist"); - - let loaded = TradeListingRuntime::load(config).await.expect("load"); - let loaded_state_handle = loaded.state(); - let loaded_state = loaded_state_handle.lock().await; - assert!(loaded_state.is_listing_validated("addr")); - assert_eq!( - loaded_state.validated_listing_event_id("addr"), - Some("evt-listing-1") - ); - assert!(loaded_state.is_non_order_event_seen("evt-validate-1")); - assert_eq!(loaded_state.last_event_created_at(), Some(456)); - assert!( - tokio::fs::try_exists(processed_jobs_path.as_path()) - .await - .expect("processed job store exists check") - ); - - let _ = tokio::fs::remove_file(path).await; - let _ = tokio::fs::remove_file(processed_jobs_path).await; - } - - #[tokio::test] - async fn runtime_load_rejects_unsupported_snapshot_version() { - let path = unique_state_path("version"); - let payload = PersistedTradeListingState { - version: 99, - state: TradeListingState::default(), - }; - tokio::fs::write(&path, serde_json::to_vec(&payload).expect("payload")) - .await - .expect("write"); - - let err = TradeListingRuntime::load(TradeListingRuntimeConfig { - state_path: path.clone(), - replay_window_secs: 600, - replay_overlap_secs: 30, - }) - .await - .expect_err("unsupported snapshot should fail"); - assert!(matches!( - err, - TradeListingRuntimeError::UnsupportedStateVersion(99) - )); - - let _ = tokio::fs::remove_file(path).await; - } - - #[tokio::test] - async fn runtime_rejects_previous_state_versions() { - let path = unique_state_path("previous-version"); - let payload = PersistedTradeListingState { - version: 1, - state: TradeListingState::default(), - }; - tokio::fs::write(&path, serde_json::to_vec(&payload).expect("payload")) - .await - .expect("write"); - - let err = TradeListingRuntime::load(TradeListingRuntimeConfig { - state_path: path.clone(), - replay_window_secs: 600, - replay_overlap_secs: 30, - }) - .await - .expect_err("previous snapshot version should fail"); - assert!(matches!( - err, - TradeListingRuntimeError::UnsupportedStateVersion(1) - )); - - let _ = tokio::fs::remove_file(path).await; - } - - #[test] - fn state_can_clear_listing_validation() { - let mut state = TradeListingState { - validated_listing_events: HashMap::from([( - "addr".to_string(), - ValidatedListingState { - event_id: "evt-listing-1".to_string(), - }, - )]), - listing_events: HashMap::from([( - "addr".to_string(), - ListingEventState { - event_id: "evt-listing-1".to_string(), - kind: 30402, - }, - )]), - seen_non_order_event_ids: HashSet::new(), - orders: HashMap::new(), - last_event_created_at: None, - }; - assert!(state.is_listing_validated("addr")); - state.clear_listing_validation("addr"); - assert!(!state.is_listing_validated("addr")); - assert_eq!(state.validated_listing_event_id("addr"), None); - } - - #[test] - fn processed_job_store_path_lives_next_to_configured_state_snapshot() { - assert_eq!( - processed_job_store_path_for_state_path(Path::new("state/trade-listing-state.json")) - .expect("processed job path"), - Path::new("state/processed_jobs.sqlite") - ); - } -} diff --git a/src/features/trade_listing/subscriber.rs b/src/features/trade_listing/subscriber.rs @@ -1,835 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr(coverage_nightly, coverage(off))] - -use std::convert::TryFrom; -use std::time::Duration; - -use anyhow::{Result, anyhow}; -use radroots_event::kinds::{ - KIND_LISTING, ORDER_EVENT_KINDS, TRADE_VALIDATION_EVENT_KINDS, - is_trade_validation_service_event_kind, -}; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKeys, - RadrootsNostrKind, RadrootsNostrRelayPoolNotification, RadrootsNostrSubscriptionId, - RadrootsNostrTag, radroots_nostr_tags_resolve, -}; -use tokio::sync::watch; -use tokio::time::sleep; -use tracing::{info, warn}; - -use crate::features::trade_listing::{ - handlers::events::{TradeListingEventError, handle_error, handle_event_with_policy}, - state::TradeListingRuntime, -}; -use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; - -#[cfg(test)] -#[derive(Default)] -struct SubscriberTestHooks { - subscribe_results: std::collections::VecDeque<Result<RadrootsNostrSubscriptionId, ()>>, - unsubscribe_results: std::collections::VecDeque<()>, - notifications: std::collections::VecDeque<Result<RadrootsNostrRelayPoolNotification, ()>>, - delay_before_event_handle: std::collections::VecDeque<bool>, - resolve_tags_results: std::collections::VecDeque< - Result<Vec<RadrootsNostrTag>, radroots_nostr::error::RadrootsNostrTagsResolveError>, - >, - handle_event_results: std::collections::VecDeque<Result<(), TradeListingEventError>>, - handle_error_results: std::collections::VecDeque<Result<(), TradeListingEventError>>, -} - -#[cfg(test)] -static SUBSCRIBER_TEST_HOOKS: std::sync::OnceLock<std::sync::Mutex<SubscriberTestHooks>> = - std::sync::OnceLock::new(); - -#[cfg(test)] -fn subscriber_test_hooks() -> &'static std::sync::Mutex<SubscriberTestHooks> { - SUBSCRIBER_TEST_HOOKS.get_or_init(|| std::sync::Mutex::new(SubscriberTestHooks::default())) -} - -#[cfg(test)] -fn pop_subscribe_hook() -> Option<Result<RadrootsNostrSubscriptionId, ()>> { - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .subscribe_results - .pop_front() -} - -#[cfg(test)] -fn pop_unsubscribe_hook() -> Option<()> { - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .unsubscribe_results - .pop_front() -} - -#[cfg(test)] -fn pop_notification_hook() -> Option<Result<RadrootsNostrRelayPoolNotification, ()>> { - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .notifications - .pop_front() -} - -#[cfg(test)] -fn pop_delay_hook() -> Option<bool> { - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .delay_before_event_handle - .pop_front() -} - -#[cfg(test)] -fn take_delay_hook() -> Option<bool> { - pop_delay_hook() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_delay_hook() -> Option<bool> { - None -} - -#[cfg(test)] -fn pop_resolve_tags_hook() --> Option<Result<Vec<RadrootsNostrTag>, radroots_nostr::error::RadrootsNostrTagsResolveError>> { - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .resolve_tags_results - .pop_front() -} - -#[cfg(test)] -fn take_resolve_tags_hook() --> Option<Result<Vec<RadrootsNostrTag>, radroots_nostr::error::RadrootsNostrTagsResolveError>> { - pop_resolve_tags_hook() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_resolve_tags_hook() --> Option<Result<Vec<RadrootsNostrTag>, radroots_nostr::error::RadrootsNostrTagsResolveError>> { - None -} - -#[cfg(test)] -fn pop_handle_event_hook() -> Option<Result<(), TradeListingEventError>> { - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .handle_event_results - .pop_front() -} - -#[cfg(test)] -fn take_handle_event_hook() -> Option<Result<(), TradeListingEventError>> { - pop_handle_event_hook() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_handle_event_hook() -> Option<Result<(), TradeListingEventError>> { - None -} - -#[cfg(test)] -fn pop_handle_error_hook() -> Option<Result<(), TradeListingEventError>> { - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .handle_error_results - .pop_front() -} - -#[cfg(test)] -fn take_handle_error_hook() -> Option<Result<(), TradeListingEventError>> { - pop_handle_error_hook() -} - -#[cfg(not(test))] -#[cfg_attr(coverage_nightly, coverage(off))] -fn take_handle_error_hook() -> Option<Result<(), TradeListingEventError>> { - None -} - -fn resolve_tags_io( - event: &RadrootsNostrEvent, - keys: &RadrootsNostrKeys, -) -> Result<Vec<RadrootsNostrTag>, radroots_nostr::error::RadrootsNostrTagsResolveError> { - let resolved = match take_resolve_tags_hook() { - Some(result) => result?, - None => return radroots_nostr_tags_resolve(event, keys), - }; - Ok(resolved) -} - -fn map_notification_recv_result( - result: Result<RadrootsNostrRelayPoolNotification, tokio::sync::broadcast::error::RecvError>, -) -> Result<RadrootsNostrRelayPoolNotification, ()> { - result.map_err(|_| ()) -} - -async fn subscribe_io( - client: &RadrootsNostrClient, - filter: RadrootsNostrFilter, -) -> Result<RadrootsNostrSubscriptionId> { - #[cfg(test)] - if let Some(result) = pop_subscribe_hook() { - return result.map_err(|_| anyhow!("trade_listing subscriber subscribe failed")); - } - let subscription = client.subscribe(filter, None).await?; - Ok(subscription.val) -} - -async fn unsubscribe_io( - client: &RadrootsNostrClient, - subscription_id: &RadrootsNostrSubscriptionId, -) { - #[cfg(test)] - if pop_unsubscribe_hook().is_some() { - return; - } - client.unsubscribe(subscription_id).await; -} - -async fn handle_event_io( - event: RadrootsNostrEvent, - resolved_tags: Vec<RadrootsNostrTag>, - keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - runtime: TradeListingRuntime, - proof_policy: TradeValidationReceiptProverPolicy, -) -> Result<(), TradeListingEventError> { - let result = match take_handle_event_hook() { - Some(result) => result, - None => { - handle_event_with_policy(event, resolved_tags, keys, client, runtime, &proof_policy) - .await - } - }; - result?; - Ok(()) -} - -async fn handle_error_io( - err: TradeListingEventError, - event: &RadrootsNostrEvent, - client: &RadrootsNostrClient, -) -> Result<(), TradeListingEventError> { - let result = match take_handle_error_hook() { - Some(result) => result, - None => handle_error(err, event, client).await, - }; - result?; - Ok(()) -} - -fn should_delay_before_event_handle() -> bool { - if let Some(delay) = take_delay_hook() { - return delay; - } - cfg!(all(debug_assertions, not(test))) -} - -#[cfg_attr(all(not(test), coverage_nightly), coverage(off))] -async fn process_event_notification( - event: RadrootsNostrEvent, - keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - runtime: TradeListingRuntime, - proof_policy: TradeValidationReceiptProverPolicy, -) -> 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; - } - - let resolved_tags = match resolve_tags_io(&event, &keys) { - Ok(tags) => tags, - Err(err) => { - warn!("trade_listing: failed to resolve tags: {err}"); - return Ok(()); - } - }; - - let event_kind = match event.kind { - RadrootsNostrKind::Custom(v) => Some(u32::from(v)), - _ => None, - }; - if let Err(err) = handle_event_io( - event.clone(), - resolved_tags, - keys, - client.clone(), - runtime.clone(), - proof_policy, - ) - .await - { - match err { - TradeListingEventError::MissingRecipient | TradeListingEventError::UnsupportedKind => {} - other => { - if event_kind.is_some_and(is_trade_validation_service_event_kind) { - if let Err(err) = handle_error_io(other, &event, &client).await { - warn!("trade_listing: failed to send error feedback: {err}"); - } - } else { - warn!("trade_listing: rejected public trade event: {other}"); - } - runtime.mark_processed_event(created_at).await?; - } - } - return Ok(()); - } - - runtime.mark_processed_event(created_at).await?; - Ok(()) -} - -async fn dispatch_event_processing( - event: RadrootsNostrEvent, - keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - runtime: TradeListingRuntime, - proof_policy: TradeValidationReceiptProverPolicy, -) -> Result<()> { - process_event_notification(event, keys, client, runtime, proof_policy).await -} - -pub async fn subscriber( - client: RadrootsNostrClient, - keys: RadrootsNostrKeys, - runtime: TradeListingRuntime, - proof_policy: TradeValidationReceiptProverPolicy, - mut stop_rx: watch::Receiver<bool>, -) -> Result<()> { - let subscribed_kinds = [KIND_LISTING] - .into_iter() - .chain(ORDER_EVENT_KINDS) - .chain(TRADE_VALIDATION_EVENT_KINDS) - .collect::<Vec<_>>(); - info!( - "Starting subscriber for trade listing, order, and trade validation kinds: {:?}", - subscribed_kinds - ); - - let kinds: Vec<RadrootsNostrKind> = subscribed_kinds - .iter() - .map(|kind| u16::try_from(*kind).expect("trade listing kinds fit in nostr custom range")) - .map(RadrootsNostrKind::Custom) - .collect(); - let filter: RadrootsNostrFilter = runtime.recovery_filter(kinds).await; - - if *stop_rx.borrow() { - return Ok(()); - } - - let subscription_id = subscribe_io(&client, filter).await?; - let mut notifications = client.notifications(); - - let mut notifications_closed = false; - - loop { - tokio::select! { - _ = stop_rx.changed() => { - break; - } - msg = async { - #[cfg(test)] - if let Some(result) = pop_notification_hook() { - return result; - } - map_notification_recv_result(notifications.recv().await) - } => { - let n = match msg { - Ok(n) => n, - Err(_) => { - notifications_closed = true; - break; - } - }; - - if let RadrootsNostrRelayPoolNotification::Event { event, .. } = n { - let event = (*event).clone(); - let keys = keys.clone(); - let client = client.clone(); - let runtime = runtime.clone(); - let proof_policy = proof_policy.clone(); - dispatch_event_processing(event, keys, client, runtime, proof_policy).await?; - } - } - } - } - - unsubscribe_io(&client, &subscription_id).await; - if notifications_closed { - return Err(anyhow!("trade_listing subscriber notifications closed")); - } - Ok(()) -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - SubscriberTestHooks, handle_error_io, handle_event_io, map_notification_recv_result, - process_event_notification, resolve_tags_io, subscriber, subscriber_test_hooks, - }; - use crate::features::trade_listing::handlers::events::TradeListingEventError; - use crate::features::trade_listing::state::TradeListingRuntime; - use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; - use radroots_nostr::error::RadrootsNostrTagsResolveError; - use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEventBuilder, RadrootsNostrKeys, RadrootsNostrKind, - RadrootsNostrRelayPoolNotification, RadrootsNostrRelayUrl, RadrootsNostrSubscriptionId, - RadrootsNostrTag, - }; - use tokio::sync::{Mutex, MutexGuard, watch}; - - static TEST_LOCK: Mutex<()> = Mutex::const_new(()); - - async fn test_guard() -> MutexGuard<'static, ()> { - let guard = TEST_LOCK.lock().await; - *subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = SubscriberTestHooks::default(); - guard - } - - fn scripted_event_notification(keys: &RadrootsNostrKeys) -> RadrootsNostrRelayPoolNotification { - let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(6000), "test") - .sign_with_keys(keys) - .expect("event"); - RadrootsNostrRelayPoolNotification::Event { - relay_url: RadrootsNostrRelayUrl::parse("wss://relay.example.com").expect("relay"), - subscription_id: RadrootsNostrSubscriptionId::new("sub-1"), - event: Box::new(event), - } - } - - fn scripted_shutdown_notification() -> RadrootsNostrRelayPoolNotification { - RadrootsNostrRelayPoolNotification::Shutdown - } - - fn prime_subscription_hooks() { - let mut hooks = subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - hooks - .subscribe_results - .push_back(Ok(RadrootsNostrSubscriptionId::new("sub-hook"))); - hooks.unsubscribe_results.push_back(()); - } - - fn shared_runtime() -> TradeListingRuntime { - TradeListingRuntime::new() - } - - fn proof_policy() -> TradeValidationReceiptProverPolicy { - TradeValidationReceiptProverPolicy::default() - } - - #[test] - fn notification_recv_result_mapping_covers_ok_and_err() { - let keys = RadrootsNostrKeys::generate(); - assert!(map_notification_recv_result(Ok(scripted_event_notification(&keys))).is_ok()); - assert!( - map_notification_recv_result(Err(tokio::sync::broadcast::error::RecvError::Closed)) - .is_err() - ); - } - - #[tokio::test] - async fn subscriber_io_wrappers_cover_direct_and_hook_paths() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::TextNote, "test") - .sign_with_keys(&keys) - .expect("event"); - - let _ = resolve_tags_io(&event, &keys); - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .resolve_tags_results - .push_back(Ok(Vec::<RadrootsNostrTag>::new())); - assert!(resolve_tags_io(&event, &keys).is_ok()); - - let runtime = shared_runtime(); - assert!(matches!( - handle_event_io( - event.clone(), - Vec::new(), - keys.clone(), - client.clone(), - runtime.clone(), - proof_policy() - ) - .await, - Err(TradeListingEventError::UnsupportedKind) - )); - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .handle_event_results - .push_back(Ok(())); - assert!( - handle_event_io( - event.clone(), - Vec::new(), - keys.clone(), - client.clone(), - runtime, - proof_policy() - ) - .await - .is_ok() - ); - - let _ = handle_error_io(TradeListingEventError::InvalidOrder, &event, &client).await; - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .handle_error_results - .push_back(Ok(())); - assert!( - handle_error_io(TradeListingEventError::InvalidOrder, &event, &client) - .await - .is_ok() - ); - } - - #[tokio::test] - async fn subscriber_returns_ok_when_stop_is_pre_requested() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let (_tx, rx) = watch::channel(true); - assert!( - subscriber(client, keys, shared_runtime(), proof_policy(), rx) - .await - .is_ok() - ); - } - - #[tokio::test] - async fn subscriber_reuses_runtime_owned_state_across_runs() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let runtime = shared_runtime(); - let state = runtime.state(); - state - .lock() - .await - .mark_listing_validated("addr", "evt-listing-1"); - - let (_tx_first, rx_first) = watch::channel(true); - assert!( - subscriber( - client.clone(), - keys.clone(), - runtime.clone(), - proof_policy(), - rx_first - ) - .await - .is_ok() - ); - assert!(state.lock().await.is_listing_validated("addr")); - - let (_tx_second, rx_second) = watch::channel(true); - assert!( - subscriber(client, keys, runtime.clone(), proof_policy(), rx_second) - .await - .is_ok() - ); - assert!(state.lock().await.is_listing_validated("addr")); - } - - #[tokio::test] - async fn subscriber_returns_err_when_no_relays_are_configured() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let (_tx, rx) = watch::channel(false); - let err = subscriber(client, keys, shared_runtime(), proof_policy(), rx) - .await - .expect_err("expected relay error"); - let msg = format!("{err:#}"); - assert!(msg.contains("relay")); - } - - #[tokio::test] - async fn subscriber_can_stop_after_start_when_subscription_is_ready() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - prime_subscription_hooks(); - let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber( - client, - keys, - shared_runtime(), - proof_policy(), - rx, - )); - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - let _ = tx.send(true); - let _ = join.await; - } - - #[tokio::test] - async fn subscriber_covers_notification_closed_path() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - prime_subscription_hooks(); - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .notifications - .push_back(Err(())); - let (_tx, rx) = watch::channel(false); - let err = subscriber(client, keys, shared_runtime(), proof_policy(), rx) - .await - .expect_err("closed notifications"); - let msg = format!("{err:#}"); - assert!(msg.contains("notifications closed")); - } - - #[tokio::test] - async fn subscriber_covers_non_event_notification_and_stop_ok_path() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - prime_subscription_hooks(); - - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .notifications - .push_back(Ok(scripted_shutdown_notification())); - - let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber( - client, - keys, - shared_runtime(), - proof_policy(), - rx, - )); - tokio::time::sleep(std::time::Duration::from_millis(30)).await; - let _ = tx.send(true); - let result = join.await.expect("subscriber join"); - assert!(result.is_ok()); - } - - #[tokio::test] - async fn subscriber_covers_event_processing_paths() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - prime_subscription_hooks(); - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .notifications - .push_back(Ok(scripted_event_notification(&keys))); - subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .resolve_tags_results - .push_back(Err(RadrootsNostrTagsResolveError::DecryptionError( - "resolve-failed".to_string(), - ))); - let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber( - client, - keys, - shared_runtime(), - proof_policy(), - rx, - )); - tokio::time::sleep(std::time::Duration::from_millis(30)).await; - let _ = tx.send(true); - let _ = join.await; - } - - #[tokio::test] - async fn subscriber_covers_handle_event_and_error_paths() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - prime_subscription_hooks(); - - let mut hooks = subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - hooks - .notifications - .push_back(Ok(scripted_event_notification(&keys))); - hooks - .notifications - .push_back(Ok(scripted_event_notification(&keys))); - hooks.resolve_tags_results.push_back(Ok(Vec::new())); - hooks.resolve_tags_results.push_back(Ok(Vec::new())); - hooks - .handle_event_results - .push_back(Err(TradeListingEventError::MissingRecipient)); - hooks - .handle_event_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - hooks - .handle_error_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - drop(hooks); - - let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber( - client, - keys, - shared_runtime(), - proof_policy(), - rx, - )); - tokio::time::sleep(std::time::Duration::from_millis(40)).await; - let _ = tx.send(true); - let _ = join.await; - } - - #[tokio::test] - async fn subscriber_covers_delay_and_error_feedback_warn_path() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - prime_subscription_hooks(); - - let mut hooks = subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - hooks - .notifications - .push_back(Ok(scripted_event_notification(&keys))); - hooks.notifications.push_back(Err(())); - hooks.delay_before_event_handle.push_back(true); - hooks.resolve_tags_results.push_back(Ok(Vec::new())); - hooks - .handle_event_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - hooks - .handle_error_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - drop(hooks); - - let (_tx, rx) = watch::channel(false); - let err = subscriber(client, keys, shared_runtime(), proof_policy(), rx) - .await - .expect_err("notifications closed"); - let msg = format!("{err:#}"); - assert!(msg.contains("notifications closed")); - } - - #[tokio::test] - async fn handled_domain_errors_advance_replay_anchor() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let runtime = shared_runtime(); - let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(6000), "test") - .custom_created_at(1_234_u64.into()) - .sign_with_keys(&keys) - .expect("event"); - - let mut hooks = subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - hooks.resolve_tags_results.push_back(Ok(Vec::new())); - hooks - .handle_event_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - hooks.handle_error_results.push_back(Ok(())); - drop(hooks); - - process_event_notification(event, keys, client, runtime.clone(), proof_policy()) - .await - .expect("notification"); - - assert_eq!( - runtime.state().lock().await.last_event_created_at(), - Some(1_234) - ); - } - - #[tokio::test] - async fn subscriber_process_event_feedback_error_branches_are_covered() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(6000), "event") - .sign_with_keys(&keys) - .expect("event"); - let runtime = shared_runtime(); - - let mut hooks = subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - hooks.resolve_tags_results.push_back(Ok(Vec::new())); - hooks - .handle_event_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - hooks - .handle_error_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - drop(hooks); - - process_event_notification(event, keys, client, runtime, proof_policy()) - .await - .expect("processing"); - } - - #[tokio::test] - async fn subscriber_process_event_feedback_non_error_branches_are_covered() { - let _guard = test_guard().await; - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); - let event_ok = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(6000), "ok") - .sign_with_keys(&keys) - .expect("event ok"); - let event_err = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(6000), "err") - .sign_with_keys(&keys) - .expect("event err"); - let runtime = shared_runtime(); - - let mut hooks = subscriber_test_hooks() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - hooks.resolve_tags_results.push_back(Ok(Vec::new())); - hooks.handle_event_results.push_back(Ok(())); - hooks.resolve_tags_results.push_back(Ok(Vec::new())); - hooks - .handle_event_results - .push_back(Err(TradeListingEventError::InvalidOrder)); - hooks.handle_error_results.push_back(Ok(())); - drop(hooks); - - process_event_notification( - event_ok, - keys.clone(), - client.clone(), - runtime.clone(), - proof_policy(), - ) - .await - .expect("ok path"); - process_event_notification(event_err, keys, client, runtime, proof_policy()) - .await - .expect("error path"); - } -} diff --git a/src/features/trade_validation_receipt.rs b/src/features/trade_validation_receipt.rs @@ -1,848 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr(coverage_nightly, coverage(off))] - -use radroots_event::ids::{RadrootsAddressableCoordinate, RadrootsEventId}; -use radroots_event::kinds::KIND_ORDER_DECISION; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrKeys, RadrootsNostrKind, - RadrootsNostrTimestamp, radroots_event_from_nostr, radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, -}; -use radroots_trade::order::{RadrootsOrderEventRecord, order_event_record_from_event}; -use radroots_trade::validation_receipt::{ - RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, - RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof, - RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, - RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, - VALIDATION_RECEIPT_VERSION, validation_receipt_event_build, - validation_receipt_public_values_hash_hex, validator_set_address_from_str, - verify_validation_receipt_event, -}; -use radroots_trade::workflow::RadrootsTradeWorkflowState; -use radroots_trade_sp1_host::RadrootsSp1TradeProofMode; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::time::{SystemTime, UNIX_EPOCH}; -use thiserror::Error; - -use crate::features::trade_listing::processed_jobs::{ - RhiProcessedJobClaim, RhiProcessedJobState, RhiProcessedJobStatus, RhiProcessedJobStoreError, -}; -use crate::features::trade_listing::state::{ - TradeListingRuntime, TradeListingRuntimeError, TradeOrderState, -}; - -const RHI_PROCESSED_JOB_CLAIM_LEASE_MS: i64 = 10 * 60 * 1000; -const ZERO_ERROR_BITMAP: &str = "0x00000000000000000000000000000000"; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TradeValidationReceiptProverBackend { - #[default] - LocalExecute, - LocalCpuProve, - LocalCudaProve, - RemoteHttpProve, -} - -impl TradeValidationReceiptProverBackend { - pub const fn as_str(self) -> &'static str { - match self { - Self::LocalExecute => "local_execute", - Self::LocalCpuProve => "local_cpu_prove", - Self::LocalCudaProve => "local_cuda_prove", - Self::RemoteHttpProve => "remote_http_prove", - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TradeValidationReceiptProverPolicy { - #[serde(default)] - pub backend: TradeValidationReceiptProverBackend, - #[serde(default = "default_proof_mode")] - pub proof_mode: RadrootsSp1TradeProofMode, - #[serde(default)] - pub validator_set_addr: Option<String>, - #[serde(default)] - pub validator_set_event_id: Option<String>, - #[serde(default)] - pub expected_sp1_program_hash: Option<String>, - #[serde(default)] - pub expected_sp1_verifying_key_hash: Option<String>, - #[serde(default)] - pub remote_http: Option<TradeValidationReceiptRemoteHttpProverConfig>, -} - -fn default_proof_mode() -> RadrootsSp1TradeProofMode { - RadrootsSp1TradeProofMode::None -} - -impl Default for TradeValidationReceiptProverPolicy { - fn default() -> Self { - Self { - backend: TradeValidationReceiptProverBackend::LocalExecute, - proof_mode: RadrootsSp1TradeProofMode::None, - validator_set_addr: None, - validator_set_event_id: None, - expected_sp1_program_hash: None, - expected_sp1_verifying_key_hash: None, - remote_http: None, - } - } -} - -impl TradeValidationReceiptProverPolicy { - pub fn validate(&self) -> Result<(), TradeValidationReceiptJobError> { - validate_optional_hash32(&self.expected_sp1_program_hash)?; - validate_optional_hash32(&self.expected_sp1_verifying_key_hash)?; - match self.backend { - TradeValidationReceiptProverBackend::LocalExecute => { - if self.proof_mode != RadrootsSp1TradeProofMode::None { - return Err(TradeValidationReceiptJobError::ProverBackendRequiresNone); - } - if self.expected_sp1_program_hash.is_some() - || self.expected_sp1_verifying_key_hash.is_some() - { - return Err( - TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof, - ); - } - Ok(()) - } - TradeValidationReceiptProverBackend::LocalCpuProve - | TradeValidationReceiptProverBackend::LocalCudaProve - | TradeValidationReceiptProverBackend::RemoteHttpProve => { - if self.proof_mode == RadrootsSp1TradeProofMode::None { - return Err(TradeValidationReceiptJobError::ProverBackendRequiresSp1Proof); - } - if self.expected_sp1_program_hash.is_none() - || self.expected_sp1_verifying_key_hash.is_none() - { - return Err(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired); - } - match self.backend { - TradeValidationReceiptProverBackend::LocalCpuProve - | TradeValidationReceiptProverBackend::RemoteHttpProve => { - if !cfg!(feature = "sp1_verify") { - return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( - self.backend.as_str(), - )); - } - } - TradeValidationReceiptProverBackend::LocalCudaProve => { - return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( - self.backend.as_str(), - )); - } - TradeValidationReceiptProverBackend::LocalExecute => {} - } - if self.backend == TradeValidationReceiptProverBackend::RemoteHttpProve - && self.remote_http.is_none() - { - return Err(TradeValidationReceiptJobError::RemoteHttpConfigRequired); - } - Ok(()) - } - } - } - - pub fn validator_binding( - &self, - ) -> Result<(RadrootsAddressableCoordinate, String), TradeValidationReceiptJobError> { - let validator_set_addr = self.validator_set_addr.as_deref().ok_or( - TradeValidationReceiptJobError::MissingValidatorSetBinding("validator_set_addr"), - )?; - let validator_set_event_id = self.validator_set_event_id.as_deref().ok_or( - TradeValidationReceiptJobError::MissingValidatorSetBinding("validator_set_event_id"), - )?; - let validator_set_addr = - validator_set_address_from_str(validator_set_addr).map_err(|_| { - TradeValidationReceiptJobError::InvalidValidatorSetBinding("validator_set_addr") - })?; - let validator_set_event_id = RadrootsEventId::parse(validator_set_event_id) - .map_err(|_| { - TradeValidationReceiptJobError::InvalidValidatorSetBinding("validator_set_event_id") - })? - .into_string(); - Ok((validator_set_addr, validator_set_event_id)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TradeValidationReceiptRemoteHttpProverConfig { - pub endpoint_url: String, - #[serde(default)] - pub auth: Option<TradeValidationReceiptRemoteHttpAuth>, - #[serde(default)] - pub max_response_bytes: Option<usize>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case", deny_unknown_fields)] -pub enum TradeValidationReceiptRemoteHttpAuth { - BearerEnv { env_var: String }, -} - -#[derive(Debug, Error)] -pub enum TradeValidationReceiptJobError { - #[error("event kind not supported")] - UnsupportedKind, - #[error("missing recipient tag")] - MissingRecipient, - #[error("trade validation receipt policy is missing {0}")] - MissingValidatorSetBinding(&'static str), - #[error("trade validation receipt policy has invalid {0}")] - InvalidValidatorSetBinding(&'static str), - #[error("trade validation receipt policy backend requires proof_mode none")] - ProverBackendRequiresNone, - #[error("trade validation receipt policy backend requires SP1 proof mode")] - ProverBackendRequiresSp1Proof, - #[error("trade validation receipt policy backend is unavailable: {0}")] - ProverBackendUnavailable(&'static str), - #[error("trade validation receipt policy requires SP1 program and verifying-key hashes")] - Sp1IdentityPolicyRequired, - #[error("SP1 identity constraints require an SP1 proof mode")] - Sp1IdentityConstraintsRequireSp1Proof, - #[error("remote HTTP prover config is required")] - RemoteHttpConfigRequired, - #[error("invalid configured hash field")] - InvalidHashField, - #[error("invalid active trade event: {0}")] - InvalidActiveTradeEvent(String), - #[error("invalid signed event")] - InvalidSignedEvent, - #[error("duplicate conflicting processed job")] - DuplicateConflictingJob, - #[error("duplicate conflicting receipt")] - DuplicateConflictingReceipt, - #[error("duplicate conflicting result")] - DuplicateConflictingResult, - #[error("stored processed job failed: {0}")] - StoredProcessedJobFailed(String), - #[error("nostr error: {0}")] - Nostr(#[from] radroots_nostr::error::RadrootsNostrError), - #[error("serde error: {0}")] - Serde(#[from] serde_json::Error), - #[error("validation receipt error: {0}")] - ValidationReceipt(#[from] RadrootsValidationReceiptError), - #[error("order decode error: {0}")] - OrderDecode(#[from] radroots_trade::order::RadrootsOrderEventDecodeError), - #[error("runtime state error: {0}")] - Runtime(#[from] TradeListingRuntimeError), - #[error("processed-job store error: {0}")] - ProcessedJobStore(#[from] RhiProcessedJobStoreError), -} - -#[derive(Clone, Debug, Serialize)] -struct DeterministicReceiptPublicValues { - changed_records_root: String, - event_set_root: String, - listing_event_id: String, - new_state_root: String, - order_id: String, - previous_state_root: String, - proof_system: String, - result: String, - schema_version: u32, - target_event_id: String, - validator_set_addr: String, - validator_set_event_id: String, -} - -pub async fn publish_validation_receipt( - accepted_event: &RadrootsNostrEvent, - client: &RadrootsNostrClient, - runtime: &TradeListingRuntime, - keys: &RadrootsNostrKeys, - policy: &TradeValidationReceiptProverPolicy, -) -> Result<(), TradeValidationReceiptJobError> { - policy.validate()?; - if policy.backend != TradeValidationReceiptProverBackend::LocalExecute - || policy.proof_mode != RadrootsSp1TradeProofMode::None - { - return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( - policy.backend.as_str(), - )); - } - let (validator_set_addr, validator_set_event_id) = policy.validator_binding()?; - let kind = event_kind_u32(accepted_event)?; - if kind != KIND_ORDER_DECISION { - return Err(TradeValidationReceiptJobError::UnsupportedKind); - } - let rr_event = radroots_event_from_nostr(accepted_event); - let record = order_event_record_from_event(&rr_event)?; - let RadrootsOrderEventRecord::Decision(decision) = record else { - return Err(TradeValidationReceiptJobError::UnsupportedKind); - }; - if !matches!( - decision.payload.decision, - radroots_event::order::RadrootsOrderDecisionOutcome::Accepted { .. } - ) { - return Err(TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "order decision is not accepted".to_string(), - )); - } - let order = { - let state = runtime.state(); - let state = state.lock().await; - state - .get_order(decision.payload.order_id.as_str()) - .cloned() - .ok_or_else(|| { - TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "order is missing from RHI state".to_string(), - ) - })? - }; - if order.status != RadrootsTradeWorkflowState::AgreedPendingValidation { - return Err(TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "order is not awaiting validation".to_string(), - )); - } - let root_event_id = order.root_event_id.as_deref().ok_or_else(|| { - TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "order is missing root event id".to_string(), - ) - })?; - let listing_event_id = order.listing_snapshot_event_id.as_deref().ok_or_else(|| { - TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "order is missing listing snapshot event id".to_string(), - ) - })?; - let target_event_id = accepted_event.id.to_hex(); - if order.last_event_id.as_deref() != Some(target_event_id.as_str()) { - return Err(TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "accepted event is not the current order head".to_string(), - )); - } - let root_event = fetch_event_by_id_io(client, root_event_id).await?; - let listing_event = fetch_event_by_id_io(client, listing_event_id).await?; - verify_root_event(&root_event, &order)?; - let receipt = deterministic_receipt_for_acceptance( - &order, - listing_event_id, - root_event_id, - target_event_id.as_str(), - validator_set_addr, - validator_set_event_id.as_str(), - [&listing_event, &root_event, accepted_event], - )?; - let receipt_parts = validation_receipt_event_build(&order.order_id, &receipt)?; - let receipt_event = signed_event_from_parts( - keys, - receipt_parts.kind, - receipt_parts.content, - receipt_parts.tags, - Some(accepted_event.created_at.as_secs()), - )?; - verify_validation_receipt_event( - &radroots_event_from_nostr(&receipt_event), - RadrootsValidationReceiptExpectedBinding { - order_id: Some(order.order_id.as_str()), - listing_event_id: Some(listing_event_id), - root_event_id: Some(root_event_id), - target_event_id: Some(target_event_id.as_str()), - validator_set_addr: policy.validator_set_addr.as_deref(), - validator_set_event_id: Some(validator_set_event_id.as_str()), - proof_system: Some(RadrootsValidationReceiptProofSystem::None), - public_values_hash: Some(receipt.public_values_hash.as_str()), - ..RadrootsValidationReceiptExpectedBinding::default() - }, - )?; - publish_receipt_with_processed_job(runtime, client, accepted_event, &order, receipt_event).await -} - -async fn publish_receipt_with_processed_job( - runtime: &TradeListingRuntime, - client: &RadrootsNostrClient, - accepted_event: &RadrootsNostrEvent, - order: &TradeOrderState, - receipt_event: RadrootsNostrEvent, -) -> Result<(), TradeValidationReceiptJobError> { - let receipt_event_id = receipt_event.id.to_hex(); - let receipt_event_json = serde_json::to_string(&receipt_event)?; - let job = processed_job_for_receipt(accepted_event, order)?; - match runtime - .processed_jobs() - .claim_job(&job, now_unix_ms(), RHI_PROCESSED_JOB_CLAIM_LEASE_MS) - .await - .map_err(processed_job_store_error)? - { - RhiProcessedJobClaim::Execute => { - mark_receipt_publishing( - runtime, - &job, - receipt_event_id.as_str(), - receipt_event_json.as_str(), - ) - .await?; - let published_event_id = publish_signed_event_io(client, receipt_event).await?; - if published_event_id != receipt_event_id { - return Err(TradeValidationReceiptJobError::DuplicateConflictingReceipt); - } - mark_receipt_published(runtime, &job, receipt_event_id.as_str()).await?; - mark_receipt_completed( - runtime, - &job, - receipt_event_id.as_str(), - receipt_event_json.as_str(), - accepted_event_created_at(accepted_event), - ) - .await - } - RhiProcessedJobClaim::InProgress | RhiProcessedJobClaim::Completed => Ok(()), - RhiProcessedJobClaim::Failed { error_code } => Err( - TradeValidationReceiptJobError::StoredProcessedJobFailed(error_code), - ), - RhiProcessedJobClaim::RecoverReceipt { - receipt_event_id, - receipt_event_json, - } - | RhiProcessedJobClaim::RecoverResult { - receipt_event_id, - receipt_event_json, - .. - } => { - let receipt_event = signed_event_from_json(receipt_event_json.as_str())?; - let published_event_id = publish_signed_event_io(client, receipt_event).await?; - if published_event_id != receipt_event_id { - return Err(TradeValidationReceiptJobError::DuplicateConflictingReceipt); - } - mark_receipt_published(runtime, &job, receipt_event_id.as_str()).await?; - mark_receipt_completed( - runtime, - &job, - receipt_event_id.as_str(), - receipt_event_json.as_str(), - accepted_event_created_at(accepted_event), - ) - .await - } - } -} - -fn deterministic_receipt_for_acceptance<'a>( - order: &TradeOrderState, - listing_event_id: &str, - root_event_id: &str, - target_event_id: &str, - validator_set_addr: RadrootsAddressableCoordinate, - validator_set_event_id: &str, - events: impl IntoIterator<Item = &'a RadrootsNostrEvent>, -) -> Result<RadrootsTradeValidationReceipt, TradeValidationReceiptJobError> { - let canonical_event_json = events - .into_iter() - .map(serde_json::to_string) - .collect::<Result<Vec<_>, _>>()?; - let event_set_root = hash32_for_parts( - "radroots:rhi-validation-event-set:v1", - canonical_event_json.iter().map(String::as_str), - ); - let previous_state_root = hash32_for_parts( - "radroots:rhi-validation-previous-state:v1", - [ - order.order_id.as_str(), - root_event_id, - workflow_state_label(RadrootsTradeWorkflowState::Requested), - ], - ); - let new_state_root = hash32_for_parts( - "radroots:rhi-validation-new-state:v1", - [ - order.order_id.as_str(), - target_event_id, - workflow_state_label(RadrootsTradeWorkflowState::AgreedPendingValidation), - ], - ); - let changed_records_root = hash32_for_parts( - "radroots:rhi-validation-changed-records:v1", - [order.order_id.as_str(), target_event_id], - ); - let public_values = DeterministicReceiptPublicValues { - changed_records_root: changed_records_root.clone(), - event_set_root: event_set_root.clone(), - listing_event_id: listing_event_id.to_string(), - new_state_root: new_state_root.clone(), - order_id: order.order_id.clone(), - previous_state_root: previous_state_root.clone(), - proof_system: RadrootsValidationReceiptProofSystem::None - .as_str() - .to_string(), - result: "valid".to_string(), - schema_version: 1, - target_event_id: target_event_id.to_string(), - validator_set_addr: validator_set_addr.as_str().to_string(), - validator_set_event_id: validator_set_event_id.to_string(), - }; - let public_values_json = serde_json::to_string(&public_values)?; - Ok(RadrootsTradeValidationReceipt { - changed_records_root, - domain: VALIDATION_RECEIPT_DOMAIN.to_string(), - error_bitmap: ZERO_ERROR_BITMAP.to_string(), - event_set_root, - new_state_root, - previous_state_root, - proof: RadrootsValidationReceiptProof { - inline_proof_base64: None, - mode: None, - program_hash: None, - proof_reference: None, - system: RadrootsValidationReceiptProofSystem::None, - verifying_key_hash: None, - }, - public_values_hash: validation_receipt_public_values_hash_hex( - public_values_json.as_bytes(), - ), - receipt_type: RadrootsValidationReceiptType::TradeTransition, - result: RadrootsValidationReceiptResult::Valid, - statement: RadrootsValidationReceiptStatement { - listing_event_id: listing_event_id.to_string(), - root_event_id: root_event_id.to_string(), - target_event_id: target_event_id.to_string(), - validator_set_addr, - validator_set_event_id: validator_set_event_id.to_string(), - statement_type: RadrootsValidationReceiptType::TradeTransition, - }, - version: VALIDATION_RECEIPT_VERSION, - }) -} - -fn verify_root_event( - root_event: &RadrootsNostrEvent, - order: &TradeOrderState, -) -> Result<(), TradeValidationReceiptJobError> { - let rr_event = radroots_event_from_nostr(root_event); - let record = order_event_record_from_event(&rr_event)?; - if record.order_id().as_str() != order.order_id { - return Err(TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "root event order id mismatch".to_string(), - )); - } - if !matches!(record, RadrootsOrderEventRecord::Request(_)) { - return Err(TradeValidationReceiptJobError::InvalidActiveTradeEvent( - "root event is not an order request".to_string(), - )); - } - Ok(()) -} - -fn workflow_state_label(state: RadrootsTradeWorkflowState) -> &'static str { - match state { - RadrootsTradeWorkflowState::Missing => "missing", - RadrootsTradeWorkflowState::Requested => "requested", - RadrootsTradeWorkflowState::AgreedPendingValidation => "agreed_pending_validation", - RadrootsTradeWorkflowState::Committed => "committed", - RadrootsTradeWorkflowState::Declined => "declined", - RadrootsTradeWorkflowState::Cancelled => "cancelled", - RadrootsTradeWorkflowState::ValidationExpired => "validation_expired", - RadrootsTradeWorkflowState::Invalid => "invalid", - } -} - -fn processed_job_for_receipt( - accepted_event: &RadrootsNostrEvent, - order: &TradeOrderState, -) -> Result<RhiProcessedJobState, TradeValidationReceiptJobError> { - Ok(RhiProcessedJobState { - request_id: accepted_event.id.to_hex(), - request_kind: event_kind_u32(accepted_event)?, - request_hash: event_hash_hex(accepted_event)?, - customer_pubkey: order.buyer_pubkey.clone(), - status: RhiProcessedJobStatus::Processing, - receipt_event_id: None, - receipt_event_json: None, - result_event_id: None, - result_event_json: None, - proof_metadata_json: None, - error_code: None, - created_timestamp: accepted_event_created_at(accepted_event), - completed_timestamp: None, - }) -} - -async fn mark_receipt_publishing( - runtime: &TradeListingRuntime, - job: &RhiProcessedJobState, - receipt_event_id: &str, - receipt_event_json: &str, -) -> Result<(), TradeValidationReceiptJobError> { - runtime - .processed_jobs() - .mark_receipt_publishing( - job, - receipt_event_id, - receipt_event_json, - None, - now_unix_ms(), - ) - .await - .map_err(processed_job_store_error)?; - Ok(()) -} - -async fn mark_receipt_published( - runtime: &TradeListingRuntime, - job: &RhiProcessedJobState, - receipt_event_id: &str, -) -> Result<(), TradeValidationReceiptJobError> { - runtime - .processed_jobs() - .mark_receipt_published(job, receipt_event_id, now_unix_ms()) - .await - .map_err(processed_job_store_error)?; - Ok(()) -} - -async fn mark_receipt_completed( - runtime: &TradeListingRuntime, - job: &RhiProcessedJobState, - receipt_event_id: &str, - receipt_event_json: &str, - completed_timestamp: u32, -) -> Result<(), TradeValidationReceiptJobError> { - runtime - .processed_jobs() - .mark_receipt_completed( - job, - receipt_event_id, - receipt_event_json, - completed_timestamp, - now_unix_ms(), - ) - .await - .map_err(processed_job_store_error)?; - Ok(()) -} - -async fn fetch_event_by_id_io( - client: &RadrootsNostrClient, - event_id: &str, -) -> Result<RadrootsNostrEvent, TradeValidationReceiptJobError> { - #[cfg(test)] - if let Some(event) = pop_fetch_event_by_id_hook() { - return event; - } - Ok(radroots_nostr_fetch_event_by_id(client, event_id).await?) -} - -async fn publish_signed_event_io( - client: &RadrootsNostrClient, - event: RadrootsNostrEvent, -) -> Result<String, TradeValidationReceiptJobError> { - #[cfg(test)] - if let Some(result) = pop_publish_signed_event_hook(&event) { - result?; - return Ok(event.id.to_hex()); - } - let output = client.send_event(&event).await?; - Ok(output.val.to_hex()) -} - -fn signed_event_from_parts( - keys: &RadrootsNostrKeys, - kind: u32, - content: String, - tags: Vec<Vec<String>>, - created_at_secs: Option<u64>, -) -> Result<RadrootsNostrEvent, TradeValidationReceiptJobError> { - let mut builder = radroots_nostr_build_event(kind, content, tags)?; - if let Some(created_at_secs) = created_at_secs { - builder = builder.custom_created_at(RadrootsNostrTimestamp::from_secs(created_at_secs)); - } - builder - .sign_with_keys(keys) - .map_err(|_| TradeValidationReceiptJobError::InvalidSignedEvent) -} - -fn signed_event_from_json( - value: &str, -) -> Result<RadrootsNostrEvent, TradeValidationReceiptJobError> { - let event: RadrootsNostrEvent = serde_json::from_str(value)?; - event - .verify() - .map_err(|_| TradeValidationReceiptJobError::InvalidSignedEvent)?; - Ok(event) -} - -fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeValidationReceiptJobError> { - match event.kind { - RadrootsNostrKind::Custom(value) => Ok(u32::from(value)), - _ => Err(TradeValidationReceiptJobError::UnsupportedKind), - } -} - -fn accepted_event_created_at(event: &RadrootsNostrEvent) -> u32 { - u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX) -} - -fn event_hash_hex(event: &RadrootsNostrEvent) -> Result<String, TradeValidationReceiptJobError> { - let canonical_event_json = serde_json::to_string(event)?; - Ok(hash_hex_for_bytes(canonical_event_json.as_bytes())) -} - -fn hash32_for_parts<'a>(domain: &str, parts: impl IntoIterator<Item = &'a str>) -> String { - let mut hasher = Sha256::new(); - hasher.update(domain.as_bytes()); - for part in parts { - hasher.update([0]); - hasher.update(part.as_bytes()); - } - format!("0x{}", lower_hex(hasher.finalize().as_slice())) -} - -fn hash_hex_for_bytes(bytes: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(bytes); - lower_hex(hasher.finalize().as_slice()) -} - -fn lower_hex(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut value = String::with_capacity(bytes.len() * 2); - for byte in bytes { - value.push(HEX[(byte >> 4) as usize] as char); - value.push(HEX[(byte & 0x0f) as usize] as char); - } - value -} - -fn validate_optional_hash32(value: &Option<String>) -> Result<(), TradeValidationReceiptJobError> { - if let Some(value) = value - && (value.len() != 66 || !value.starts_with("0x") || !is_lower_hex(&value[2..])) - { - return Err(TradeValidationReceiptJobError::InvalidHashField); - } - Ok(()) -} - -fn is_lower_hex(value: &str) -> bool { - value - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} - -fn now_unix_ms() -> i64 { - let Ok(elapsed) = SystemTime::now().duration_since(UNIX_EPOCH) else { - return 0; - }; - i64::try_from(elapsed.as_millis()).unwrap_or(i64::MAX) -} - -fn processed_job_store_error(error: RhiProcessedJobStoreError) -> TradeValidationReceiptJobError { - match error { - RhiProcessedJobStoreError::DuplicateConflictingJob => { - TradeValidationReceiptJobError::DuplicateConflictingJob - } - RhiProcessedJobStoreError::DuplicateConflictingReceipt => { - TradeValidationReceiptJobError::DuplicateConflictingReceipt - } - RhiProcessedJobStoreError::DuplicateConflictingResult => { - TradeValidationReceiptJobError::DuplicateConflictingResult - } - other => TradeValidationReceiptJobError::ProcessedJobStore(other), - } -} - -#[cfg(test)] -#[derive(Default)] -struct TradeValidationReceiptTestHooks { - fetch_event_by_id_results: - std::collections::VecDeque<Result<RadrootsNostrEvent, TradeValidationReceiptJobError>>, - publish_event_results: - std::collections::VecDeque<Result<String, TradeValidationReceiptJobError>>, - published_events: Vec<RadrootsNostrEvent>, -} - -#[cfg(test)] -static TRADE_VALIDATION_RECEIPT_TEST_HOOKS: std::sync::OnceLock< - std::sync::Mutex<TradeValidationReceiptTestHooks>, -> = std::sync::OnceLock::new(); - -#[cfg(test)] -fn trade_validation_receipt_test_hooks() --> &'static std::sync::Mutex<TradeValidationReceiptTestHooks> { - TRADE_VALIDATION_RECEIPT_TEST_HOOKS - .get_or_init(|| std::sync::Mutex::new(TradeValidationReceiptTestHooks::default())) -} - -#[cfg(test)] -fn pop_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeValidationReceiptJobError>> -{ - trade_validation_receipt_test_hooks() - .lock() - .expect("trade validation receipt hooks lock") - .fetch_event_by_id_results - .pop_front() -} - -#[cfg(test)] -fn pop_publish_signed_event_hook( - event: &RadrootsNostrEvent, -) -> Option<Result<String, TradeValidationReceiptJobError>> { - let mut hooks = trade_validation_receipt_test_hooks() - .lock() - .expect("trade validation receipt hooks lock"); - hooks.published_events.push(event.clone()); - hooks.publish_event_results.pop_front() -} - -#[cfg(test)] -mod tests { - use super::{ - TradeValidationReceiptProverBackend, TradeValidationReceiptProverPolicy, - trade_validation_receipt_test_hooks, - }; - use radroots_trade_sp1_host::RadrootsSp1TradeProofMode; - - #[test] - fn default_policy_is_enabled_deterministic_receipt_path() { - let policy = TradeValidationReceiptProverPolicy::default(); - assert_eq!( - policy.backend, - TradeValidationReceiptProverBackend::LocalExecute - ); - assert_eq!(policy.proof_mode, RadrootsSp1TradeProofMode::None); - } - - #[test] - fn validator_binding_requires_address_and_event_id() { - let policy = TradeValidationReceiptProverPolicy::default(); - let error = policy.validator_binding().expect_err("binding must fail"); - assert!(error.to_string().contains("validator_set_addr")); - } - - #[test] - fn validator_binding_accepts_canonical_set_reference() { - let policy = TradeValidationReceiptProverPolicy { - validator_set_addr: Some( - "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" - .to_string(), - ), - validator_set_event_id: Some( - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - .to_string(), - ), - ..TradeValidationReceiptProverPolicy::default() - }; - let binding = policy.validator_binding().expect("validator binding"); - assert_eq!( - binding.0.as_str(), - "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" - ); - assert_eq!( - binding.1, - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - ); - } - - #[test] - fn test_hooks_reset() { - *trade_validation_receipt_test_hooks() - .lock() - .expect("trade validation receipt hooks lock") = - super::TradeValidationReceiptTestHooks::default(); - } -} diff --git a/src/lib.rs b/src/lib.rs @@ -6,17 +6,18 @@ pub mod config; pub mod features; pub mod identity_storage; pub mod paths; -pub mod proof_smoke; -pub mod remote_prove; pub mod rhi; pub use cli::Args as cli_args; use anyhow::Result; -use radroots_event::kinds::{KIND_LISTING, ORDER_EVENT_KINDS, TRADE_VALIDATION_EVENT_KINDS}; +use radroots_event::kinds::TRADE_MUTATION_EVENT_KINDS; use std::time::Duration; -use crate::features::trade_listing::state::{TradeListingRuntime, TradeListingRuntimeConfig}; +use crate::features::trade_agreement_attestation::{ + TradeAgreementAttestationRuntime, TradeAgreementAttestationRuntimeConfig, + trade_mutation_subscription_kinds, +}; use crate::identity_storage::load_service_identity; use crate::rhi::{Rhi, start_subscriber_with_policy}; use radroots_identity::RadrootsIdentity; @@ -116,17 +117,18 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> args.service.allow_generate_identity, )?; let keys = identity.keys().clone(); - let trade_listing_runtime = TradeListingRuntime::load(TradeListingRuntimeConfig { - 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_trade_listing_runtime_and_policy( + 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(), - trade_listing_runtime, - settings.config.trade_validation_receipt.clone(), + agreement_attestation_runtime, + settings.config.trade_agreement_attestation.clone(), ); let client = rhi.client.clone(); let service_cfg = settings.config.service.clone(); @@ -139,11 +141,7 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> let md = settings.metadata.clone(); if !relays.is_empty() { - let handler_kinds = [KIND_LISTING] - .into_iter() - .chain(ORDER_EVENT_KINDS) - .chain(TRADE_VALIDATION_EVENT_KINDS) - .collect(); + let handler_kinds = trade_mutation_subscription_kinds(); let handler_spec = RadrootsNostrApplicationHandlerSpec { kinds: handler_kinds, identifier: service_cfg.nip89_identifier.clone(), @@ -167,8 +165,8 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> let handle = start_subscriber_with_policy( client.clone(), keys.clone(), - rhi.trade_listing_runtime.clone(), - rhi.trade_validation_receipt_policy.clone(), + rhi.agreement_attestation_runtime.clone(), + rhi.agreement_attestation_policy.clone(), settings.config.subscriber.backoff.clone(), ) .await; @@ -182,7 +180,7 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> match wait_for_shutdown_or_stopped(handle).await { RunRhiWaitOutcome::Shutdown => { - info!("Shutting down…"); + info!("Shutting down"); stop_handle.stop(); } RunRhiWaitOutcome::Stopped => {} @@ -194,14 +192,19 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> 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, bootstrap_presence, run_rhi, - run_rhi_bootstrap_hook, run_rhi_wait_hook, + RUN_RHI_AUTO_STOP, RUN_RHI_SKIP_SUBSCRIBER, RunRhiWaitOutcome, bootstrap_presence, + release_product_handler_kinds, run_rhi, run_rhi_bootstrap_hook, run_rhi_wait_hook, }; use crate::{cli_args, config}; + use radroots_event::kinds::TRADE_MUTATION_EVENT_KINDS; use std::path::PathBuf; use std::sync::atomic::Ordering; use std::sync::{Mutex, MutexGuard}; @@ -253,8 +256,8 @@ mod tests { ..Default::default() }, }, - trade_validation_receipt: - crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy::default(), + trade_agreement_attestation: + crate::features::trade_agreement_attestation::TradeAgreementAttestationPolicy::default(), }, } } @@ -278,11 +281,6 @@ mod tests { std::env::temp_dir().join(format!("rhi-{suffix}-{nanos}.secret.json")) } - fn cleanup_identity_artifacts(path: &std::path::Path) { - let _ = std::fs::remove_file(path); - let _ = std::fs::remove_file(crate::identity_storage::encrypted_identity_key_path(path)); - } - fn unique_state_path(suffix: &str) -> PathBuf { let nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -294,118 +292,64 @@ mod tests { } #[tokio::test] - async fn run_rhi_completes_with_auto_stop_and_empty_relays() { + async fn run_rhi_starts_and_stops_without_relays() { let _guard = test_guard(); RUN_RHI_AUTO_STOP.store(true, Ordering::Relaxed); - RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed); - let path = unique_identity_path("empty"); - let args = args_for_identity(path.clone()); + let identity_path = unique_identity_path("no-relays"); + let args = args_for_identity(identity_path); let settings = settings_with_relays(Vec::new()); - let result = run_rhi(&settings, &args).await; - RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed); - cleanup_identity_artifacts(&path); - assert!(result.is_ok()); + run_rhi(&settings, &args).await.expect("run rhi"); } #[tokio::test] - async fn run_rhi_covers_presence_success_and_failure_branches() { + async fn run_rhi_bootstraps_release_product_handler_kinds_when_relays_exist() { let _guard = test_guard(); - RUN_RHI_AUTO_STOP.store(true, Ordering::Relaxed); RUN_RHI_SKIP_SUBSCRIBER.store(true, Ordering::Relaxed); - - let path_ok = unique_identity_path("presence-ok"); - let args_ok = args_for_identity(path_ok.clone()); - let settings_ok = settings_with_relays(vec!["wss://relay.example.com".to_string()]); *run_rhi_bootstrap_hook() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Ok(())); - let ok = run_rhi(&settings_ok, &args_ok).await; - cleanup_identity_artifacts(&path_ok); - assert!(ok.is_ok()); - - let path_err = unique_identity_path("presence-err"); - let args_err = args_for_identity(path_err.clone()); - let settings_err = settings_with_relays(vec!["wss://relay.example.com".to_string()]); - *run_rhi_bootstrap_hook() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = - Some(Err("presence failure".to_string())); - let err = run_rhi(&settings_err, &args_err).await; - RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed); - RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed); - cleanup_identity_artifacts(&path_err); - assert!(err.is_ok()); + let identity_path = unique_identity_path("relays"); + let args = args_for_identity(identity_path); + let settings = settings_with_relays(vec!["wss://relay.example.com".to_string()]); + run_rhi(&settings, &args).await.expect("run rhi"); + assert_eq!(release_product_handler_kinds(), TRADE_MUTATION_EVENT_KINDS); } #[tokio::test] - async fn bootstrap_presence_application_handler_path_is_callable() { + async fn run_rhi_stops_on_wait_hook() { let _guard = test_guard(); - let identity_path = unique_identity_path("bootstrap"); - let identity = crate::identity_storage::load_service_identity(Some(&identity_path), true) - .expect("identity"); - let client = radroots_nostr::prelude::RadrootsNostrClient::new(identity.keys().clone()); - let metadata: radroots_nostr::prelude::RadrootsNostrMetadata = - serde_json::from_str(r#"{"name":"bootstrap"}"#).expect("bootstrap metadata"); - let handler_spec = radroots_nostr::prelude::RadrootsNostrApplicationHandlerSpec { - kinds: vec![30402], - identifier: Some("rhi".to_string()), - metadata: Some(metadata.clone()), - extra_tags: Vec::new(), - relays: vec!["wss://relay.example.com".to_string()], - nostrconnect_url: None, - }; - let result = bootstrap_presence(&client, &identity, &metadata, &handler_spec).await; - cleanup_identity_artifacts(&identity_path); - assert!(result.is_err()); - } - - #[tokio::test] - async fn run_rhi_covers_shutdown_wait_branch() { - let _guard = test_guard(); - RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed); - RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed); *run_rhi_wait_hook() .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunRhiWaitOutcome::Shutdown); - - let path = unique_identity_path("shutdown"); - let args = args_for_identity(path.clone()); + .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunRhiWaitOutcome::Stopped); + let identity_path = unique_identity_path("wait-hook"); + let args = args_for_identity(identity_path); let settings = settings_with_relays(Vec::new()); - let result = run_rhi(&settings, &args).await; - cleanup_identity_artifacts(&path); - assert!(result.is_ok()); + run_rhi(&settings, &args).await.expect("run rhi"); } #[tokio::test] - async fn run_rhi_returns_error_when_relay_configuration_is_invalid() { + async fn bootstrap_presence_reports_hook_error() { let _guard = test_guard(); - RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed); - RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed); - - let path = unique_identity_path("invalid-relay"); - let args = args_for_identity(path.clone()); - let settings = settings_with_relays(vec!["not-a-relay-url".to_string()]); - let result = run_rhi(&settings, &args).await; - cleanup_identity_artifacts(&path); - assert!(result.is_err()); - } - - #[tokio::test] - async fn run_rhi_returns_error_when_identity_is_missing() { - let _guard = test_guard(); - RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed); - RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed); - - let args = cli_args { - command: None, - service: radroots_runtime::RadrootsServiceCliArgs { - config: Some(PathBuf::from("config.toml")), - identity: Some(PathBuf::from("/tmp/rhi-lib-missing-identity.secret.json")), - allow_generate_identity: false, - }, + *run_rhi_bootstrap_hook() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = + Some(Err("forced bootstrap failure".to_string())); + let keys = radroots_nostr::prelude::RadrootsNostrKeys::generate(); + let client = radroots_nostr::prelude::RadrootsNostrClient::new(keys.clone()); + let identity = radroots_identity::RadrootsIdentity::new(keys); + let metadata: radroots_nostr::prelude::RadrootsNostrMetadata = + serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"); + let spec = radroots_nostr::prelude::RadrootsNostrApplicationHandlerSpec { + kinds: TRADE_MUTATION_EVENT_KINDS.to_vec(), + identifier: Some("rhi".to_string()), + metadata: Some(metadata.clone()), + extra_tags: Vec::new(), + relays: Vec::new(), + nostrconnect_url: None, }; - let settings = settings_with_relays(Vec::new()); - let result = run_rhi(&settings, &args).await; - assert!(result.is_err()); + let err = bootstrap_presence(&client, &identity, &metadata, &spec) + .await + .expect_err("forced error"); + assert!(format!("{err:#}").contains("forced bootstrap failure")); } } diff --git a/src/main.rs b/src/main.rs @@ -9,9 +9,9 @@ use clap::Parser; use radroots_log::{LogFileLayout, LoggingOptions}; #[cfg(not(test))] use rhi::cli::Command; -use rhi::{cli_args, config, paths, run_rhi}; #[cfg(not(test))] -use rhi::{proof_smoke, remote_prove}; +use rhi::features::trade_agreement_attestation::run_smoke_cli_command; +use rhi::{cli_args, config, paths, run_rhi}; use std::path::PathBuf; use std::process::ExitCode; use tracing::info; @@ -217,8 +217,7 @@ async fn run() -> Result<()> { let args = cli_args::try_parse().map_err(radroots_runtime::RuntimeCliError::from)?; if let Some(command) = args.command { return match command { - Command::ProofSmoke { .. } => proof_smoke::run_cli_command(command).await, - Command::RemoteProve { .. } => remote_prove::run_cli_command(command).await, + Command::AttestationSmoke { .. } => run_smoke_cli_command(command).await, }; } } @@ -245,7 +244,7 @@ mod tests { runtime_startup_report, }; use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys}; - use rhi::features::trade_listing::state::TradeListingRuntime; + use rhi::features::trade_agreement_attestation::TradeAgreementAttestationRuntime; use rhi::{cli_args, config, paths}; use std::path::PathBuf; use std::process::ExitCode; @@ -276,8 +275,8 @@ mod tests { stdout: true, }, subscriber: config::SubscriberConfig::default(), - trade_validation_receipt: - rhi::features::trade_validation_receipt::TradeValidationReceiptProverPolicy::default(), + trade_agreement_attestation: + rhi::features::trade_agreement_attestation::TradeAgreementAttestationPolicy::default(), }, } } @@ -311,7 +310,7 @@ mod tests { "/home/treesap/.radroots/secrets/workers/rhi/identity.secret.json", ), canonical_subscriber_state_path: PathBuf::from( - "/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json", + "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json", ), } } @@ -392,7 +391,7 @@ mod tests { let handle = rhi::rhi::start_subscriber( client, keys, - TradeListingRuntime::new(), + TradeAgreementAttestationRuntime::new(), radroots_runtime::BackoffConfig { base_ms: 1, max_ms: 2, @@ -444,7 +443,7 @@ mod tests { subscriber_state_path: PathBuf::from("/tmp/rhi/state.json"), subscriber_state_path_source: "config_artifact".to_string(), canonical_subscriber_state_path: PathBuf::from( - "/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json" + "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json" ), path_overrides: sample_runtime_contract().path_overrides, default_shared_secret_backend: "encrypted_file".to_string(), diff --git a/src/paths.rs b/src/paths.rs @@ -8,7 +8,7 @@ use radroots_runtime_paths::{ use serde::Serialize; const RHI_RUNTIME_ID: &str = "rhi"; -const SUBSCRIBER_STATE_DIR_NAME: &str = "trade-listing"; +const SUBSCRIBER_STATE_DIR_NAME: &str = "trade-agreement-attestation"; const SUBSCRIBER_STATE_FILE_NAME: &str = "state.json"; const RHI_PATHS_PROFILE_ENV: &str = "RHI_PATHS_PROFILE"; const RHI_PATHS_REPO_LOCAL_ROOT_ENV: &str = "RHI_PATHS_REPO_LOCAL_ROOT"; diff --git a/src/proof_smoke.rs b/src/proof_smoke.rs @@ -1,638 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr(coverage_nightly, coverage(off))] - -use crate::cli::Command; -use radroots_trade_sp1_guest::{ - RADROOTS_SP1_TRADE_KIND_LISTING, RADROOTS_SP1_TRADE_KIND_ORDER_DECISION, - RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST, RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET, - RADROOTS_SP1_TRADE_PROTOCOL_VERSION, RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, - RADROOTS_SP1_TRADE_WITNESS_VERSION, RadrootsSp1TradeCanonicalEventEvidence, - RadrootsSp1TradeEventEvidenceRole, RadrootsSp1TradeEventWorkflowPosition, - RadrootsSp1TradeInventoryBinWitness, RadrootsSp1TradeInventoryCommitmentWitness, - RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeOrderDecisionEventWitness, - RadrootsSp1TradeOrderDecisionWitness, RadrootsSp1TradeOrderItemWitness, - RadrootsSp1TradeOrderRequestWitness, -}; -use radroots_trade_sp1_host::{ - RadrootsSp1TradeProofMode, generate_order_acceptance_proof, - verify_order_acceptance_proof_artifact_structure, -}; -use serde::{Deserialize, Serialize}; -use std::path::Path; -use std::time::Instant; -use thiserror::Error; - -const PROTOCOL_VERSION: &str = "radroots.rhi.proof_smoke.v0"; -const WORKER_NAME: &str = "rhi"; -const SP1_VERSION: &str = "6.2.1"; -const ORDER_ACCEPTANCE_TINY_FIXTURE: &str = "order_acceptance_tiny_v1"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RhiProofSmokeRequest { - pub protocol_version: String, - pub operation: RhiProofSmokeOperation, - pub backend: RhiProofSmokeBackend, - #[serde(default)] - pub fixture: Option<String>, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RhiProofSmokeOperation { - Health, - ProofSmoke, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RhiProofSmokeBackend { - ValidatorSetDeterministic, - LocalExecute, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct RhiProofSmokeResponse { - pub ok: bool, - pub protocol_version: String, - pub operation: RhiProofSmokeOperation, - pub worker_name: String, - pub worker_version: String, - pub git_rev: String, - pub sp1_version: String, - pub backend: RhiProofSmokeBackend, - pub capabilities: Vec<String>, - pub proof_generated: bool, - pub public_values_hash: Option<String>, - pub sp1_program_hash: Option<String>, - pub sp1_verifying_key_hash: Option<String>, - pub event_set_root: Option<String>, - pub reducer_output_root: Option<String>, - pub elapsed_ms: u128, - pub warnings: Vec<String>, - pub error: Option<String>, -} - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum RhiProofSmokeError { - #[error("invalid protocol version")] - InvalidProtocolVersion, - #[error("proof_smoke requires fixture order_acceptance_tiny_v1")] - InvalidFixture, - #[error("local_execute backend is unavailable in this build")] - LocalExecuteUnavailable, - #[error("deterministic proof smoke failed: {0}")] - Deterministic(String), - #[error("SP1 execute proof smoke failed: {0}")] - Sp1Execute(String), -} - -pub async fn run_cli_command(command: Command) -> anyhow::Result<()> { - let Command::ProofSmoke { input, output } = command else { - return Err(anyhow::anyhow!("proof-smoke command expected")); - }; - let request_bytes = read_input(input.as_deref())?; - let response = handle_request_bytes(&request_bytes).await; - let response_bytes = serde_json::to_vec_pretty(&response)?; - write_output(output.as_deref(), &response_bytes)?; - if response.ok { - Ok(()) - } else { - Err(anyhow::anyhow!( - "{}", - response - .error - .as_deref() - .unwrap_or("proof smoke request failed") - )) - } -} - -pub async fn handle_request_bytes(bytes: &[u8]) -> RhiProofSmokeResponse { - let started = Instant::now(); - match serde_json::from_slice::<RhiProofSmokeRequest>(bytes) { - Ok(request) => response_for_request(request, started).await, - Err(error) => response_for_error( - RhiProofSmokeOperation::Health, - RhiProofSmokeBackend::ValidatorSetDeterministic, - started, - error.to_string(), - ), - } -} - -async fn response_for_request( - request: RhiProofSmokeRequest, - started: Instant, -) -> RhiProofSmokeResponse { - if request.protocol_version != PROTOCOL_VERSION { - return response_for_error( - request.operation, - request.backend, - started, - RhiProofSmokeError::InvalidProtocolVersion.to_string(), - ); - } - - match request.operation { - RhiProofSmokeOperation::Health => response_for_success( - request.operation, - request.backend, - started, - None, - None, - None, - None, - None, - Vec::new(), - ), - RhiProofSmokeOperation::ProofSmoke => { - match run_proof_smoke(request.backend, request.fixture).await { - Ok(output) => response_for_success( - request.operation, - request.backend, - started, - Some(output.public_values_hash), - output.sp1_program_hash, - output.sp1_verifying_key_hash, - Some(output.event_set_root), - Some(output.reducer_output_root), - output.warnings, - ), - Err(error) => response_for_error( - request.operation, - request.backend, - started, - error.to_string(), - ), - } - } - } -} - -struct RhiProofSmokeOutput { - public_values_hash: String, - sp1_program_hash: Option<String>, - sp1_verifying_key_hash: Option<String>, - event_set_root: String, - reducer_output_root: String, - warnings: Vec<String>, -} - -async fn run_proof_smoke( - backend: RhiProofSmokeBackend, - fixture: Option<String>, -) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> { - if fixture.as_deref() != Some(ORDER_ACCEPTANCE_TINY_FIXTURE) { - return Err(RhiProofSmokeError::InvalidFixture); - } - - let witness = order_acceptance_tiny_witness(); - match backend { - RhiProofSmokeBackend::ValidatorSetDeterministic => deterministic_smoke(&witness), - RhiProofSmokeBackend::LocalExecute => local_execute_smoke(&witness).await, - } -} - -fn deterministic_smoke( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, -) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> { - let bundle = generate_order_acceptance_proof(witness, RadrootsSp1TradeProofMode::None) - .map_err(|error| RhiProofSmokeError::Deterministic(error.to_string()))?; - verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) - .map_err(|error| RhiProofSmokeError::Deterministic(error.to_string()))?; - Ok(RhiProofSmokeOutput { - public_values_hash: canonical_hex_64(&bundle.execution.public_values_hash)?, - sp1_program_hash: None, - sp1_verifying_key_hash: None, - event_set_root: canonical_hex_64(&bundle.execution.public_values.event_set_root)?, - reducer_output_root: canonical_hex_64(&bundle.execution.public_values.new_state_root)?, - warnings: Vec::new(), - }) -} - -#[cfg(feature = "sp1_proving")] -async fn local_execute_smoke( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, -) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> { - if !radroots_trade_sp1_host::order_acceptance_sp1_guest_elf_available() { - return Err(RhiProofSmokeError::LocalExecuteUnavailable); - } - let execution = radroots_trade_sp1_host::execute_order_acceptance_sp1_public_values(witness) - .await - .map_err(|error| RhiProofSmokeError::Sp1Execute(error.to_string()))? - .execution; - Ok(RhiProofSmokeOutput { - public_values_hash: canonical_hex_64(&execution.public_values_hash)?, - sp1_program_hash: execution - .public_values - .sp1_program_hash - .as_deref() - .map(canonical_hex_64) - .transpose()?, - sp1_verifying_key_hash: execution - .public_values - .sp1_verifying_key_hash - .as_deref() - .map(canonical_hex_64) - .transpose()?, - event_set_root: canonical_hex_64(&execution.public_values.event_set_root)?, - reducer_output_root: canonical_hex_64(&execution.public_values.new_state_root)?, - warnings: Vec::new(), - }) -} - -#[cfg(not(feature = "sp1_proving"))] -async fn local_execute_smoke( - _witness: &RadrootsSp1TradeOrderAcceptanceWitness, -) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> { - Err(RhiProofSmokeError::LocalExecuteUnavailable) -} - -fn response_for_success( - operation: RhiProofSmokeOperation, - backend: RhiProofSmokeBackend, - started: Instant, - public_values_hash: Option<String>, - sp1_program_hash: Option<String>, - sp1_verifying_key_hash: Option<String>, - event_set_root: Option<String>, - reducer_output_root: Option<String>, - warnings: Vec<String>, -) -> RhiProofSmokeResponse { - RhiProofSmokeResponse { - ok: true, - protocol_version: PROTOCOL_VERSION.to_string(), - operation, - worker_name: WORKER_NAME.to_string(), - worker_version: env!("CARGO_PKG_VERSION").to_string(), - git_rev: option_env!("RADROOTS_GIT_REV") - .unwrap_or("unknown") - .to_string(), - sp1_version: SP1_VERSION.to_string(), - backend, - capabilities: capabilities(), - proof_generated: false, - public_values_hash, - sp1_program_hash, - sp1_verifying_key_hash, - event_set_root, - reducer_output_root, - elapsed_ms: started.elapsed().as_millis(), - warnings, - error: None, - } -} - -fn canonical_hex_64(value: &str) -> Result<String, RhiProofSmokeError> { - let candidate = value.strip_prefix("0x").unwrap_or(value); - if candidate.len() == 64 && candidate.bytes().all(|byte| byte.is_ascii_hexdigit()) { - return Ok(candidate.to_ascii_lowercase()); - } - Err(RhiProofSmokeError::Deterministic( - "public value is not canonical 32-byte hex".to_string(), - )) -} - -fn response_for_error( - operation: RhiProofSmokeOperation, - backend: RhiProofSmokeBackend, - started: Instant, - error: String, -) -> RhiProofSmokeResponse { - let mut response = response_for_success( - operation, - backend, - started, - None, - None, - None, - None, - None, - Vec::new(), - ); - response.ok = false; - response.error = Some(error); - response -} - -fn capabilities() -> Vec<String> { - let mut values = vec![ - "health".to_string(), - "proof_smoke".to_string(), - "validator_set_deterministic".to_string(), - ]; - if cfg!(feature = "sp1_proving") - && radroots_trade_sp1_host::order_acceptance_sp1_guest_elf_available() - { - values.push("local_execute".to_string()); - } - values -} - -pub(crate) fn order_acceptance_tiny_witness() -> RadrootsSp1TradeOrderAcceptanceWitness { - RadrootsSp1TradeOrderAcceptanceWitness { - witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION, - proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(), - listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), - request_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), - decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), - validator_set_addr: - "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" - .to_string(), - validator_set_event_id: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - .to_string(), - event_evidence: order_acceptance_tiny_event_evidence(), - request: RadrootsSp1TradeOrderRequestWitness { - order_id: "order-1".to_string(), - listing_addr: - "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" - .to_string(), - buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - items: vec![RadrootsSp1TradeOrderItemWitness { - bin_id: "bin-1".to_string(), - bin_count: 2, - }], - }, - decision: RadrootsSp1TradeOrderDecisionEventWitness { - order_id: "order-1".to_string(), - listing_addr: - "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" - .to_string(), - buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - decision: RadrootsSp1TradeOrderDecisionWitness::Accepted { - inventory_commitments: vec![RadrootsSp1TradeInventoryCommitmentWitness { - bin_id: "bin-1".to_string(), - bin_count: 2, - }], - }, - }, - inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness { - bin_id: "bin-1".to_string(), - listing_capacity: 5, - previous_reserved: 1, - }], - inventory_sequence: 7, - previous_state_root: None, - reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), - radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), - sp1_program_hash: None, - sp1_verifying_key_hash: None, - } -} - -fn order_acceptance_tiny_event_evidence() -> Vec<RadrootsSp1TradeCanonicalEventEvidence> { - vec![ - RadrootsSp1TradeCanonicalEventEvidence { - event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), - signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - kind: RADROOTS_SP1_TRADE_KIND_LISTING, - canonical_event_hash: - "0x1010101010101010101010101010101010101010101010101010101010101010".to_string(), - signature_hash: "0x1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - preverified_signature: true, - role: RadrootsSp1TradeEventEvidenceRole::Seller, - workflow_position: RadrootsSp1TradeEventWorkflowPosition::Listing, - content_hash: "0x1212121212121212121212121212121212121212121212121212121212121212" - .to_string(), - tags_hash: "0x1313131313131313131313131313131313131313131313131313131313131313" - .to_string(), - ordering_key: "001:listing".to_string(), - }, - RadrootsSp1TradeCanonicalEventEvidence { - event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), - signer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - kind: RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST, - canonical_event_hash: - "0x2020202020202020202020202020202020202020202020202020202020202020".to_string(), - signature_hash: "0x2121212121212121212121212121212121212121212121212121212121212121" - .to_string(), - preverified_signature: true, - role: RadrootsSp1TradeEventEvidenceRole::Buyer, - workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderRequest, - content_hash: "0x2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - tags_hash: "0x2323232323232323232323232323232323232323232323232323232323232323" - .to_string(), - ordering_key: "002:order_request".to_string(), - }, - RadrootsSp1TradeCanonicalEventEvidence { - event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), - signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - kind: RADROOTS_SP1_TRADE_KIND_ORDER_DECISION, - canonical_event_hash: - "0x3030303030303030303030303030303030303030303030303030303030303030".to_string(), - signature_hash: "0x3131313131313131313131313131313131313131313131313131313131313131" - .to_string(), - preverified_signature: true, - role: RadrootsSp1TradeEventEvidenceRole::Seller, - workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderDecision, - content_hash: "0x3232323232323232323232323232323232323232323232323232323232323232" - .to_string(), - tags_hash: "0x3333333333333333333333333333333333333333333333333333333333333333" - .to_string(), - ordering_key: "003:order_decision".to_string(), - }, - ] -} - -fn read_input(input: Option<&Path>) -> anyhow::Result<Vec<u8>> { - match input { - Some(path) => Ok(std::fs::read(path)?), - None => { - use std::io::Read; - let mut bytes = Vec::new(); - std::io::stdin().read_to_end(&mut bytes)?; - Ok(bytes) - } - } -} - -fn write_output(output: Option<&Path>, bytes: &[u8]) -> anyhow::Result<()> { - match output { - Some(path) => { - std::fs::write(path, bytes)?; - Ok(()) - } - None => { - println!("{}", String::from_utf8_lossy(bytes)); - Ok(()) - } - } -} - -#[cfg(test)] -mod tests { - use super::{ - PROTOCOL_VERSION, RhiProofSmokeBackend, RhiProofSmokeOperation, RhiProofSmokeRequest, - RhiProofSmokeResponse, handle_request_bytes, - }; - - fn request(operation: RhiProofSmokeOperation, backend: RhiProofSmokeBackend) -> Vec<u8> { - serde_json::to_vec(&RhiProofSmokeRequest { - protocol_version: PROTOCOL_VERSION.to_string(), - operation, - backend, - fixture: Some("order_acceptance_tiny_v1".to_string()), - }) - .expect("request json") - } - - #[tokio::test] - async fn health_returns_worker_capabilities() { - let bytes = serde_json::to_vec(&RhiProofSmokeRequest { - protocol_version: PROTOCOL_VERSION.to_string(), - operation: RhiProofSmokeOperation::Health, - backend: RhiProofSmokeBackend::ValidatorSetDeterministic, - fixture: None, - }) - .expect("request json"); - let response: RhiProofSmokeResponse = handle_request_bytes(&bytes).await; - assert!(response.ok, "{:?}", response.error); - assert_eq!(response.worker_name, "rhi"); - assert!(response.capabilities.contains(&"health".to_string())); - assert!(!response.proof_generated); - } - - #[tokio::test] - async fn deterministic_proof_smoke_returns_public_values() { - let response = handle_request_bytes(&request( - RhiProofSmokeOperation::ProofSmoke, - RhiProofSmokeBackend::ValidatorSetDeterministic, - )) - .await; - assert!(response.ok, "{:?}", response.error); - assert_eq!(response.operation, RhiProofSmokeOperation::ProofSmoke); - assert!(response.public_values_hash.is_some()); - assert!(response.sp1_program_hash.is_none()); - assert!(response.sp1_verifying_key_hash.is_none()); - assert!(response.event_set_root.is_some()); - assert!(response.reducer_output_root.is_some()); - for value in [ - response.public_values_hash.as_deref(), - response.event_set_root.as_deref(), - response.reducer_output_root.as_deref(), - ] { - let value = value.expect("public value"); - assert_eq!(value.len(), 64); - assert!(!value.starts_with("0x")); - assert!(value.bytes().all(|byte| byte.is_ascii_hexdigit())); - } - assert!(!response.proof_generated); - } - - #[tokio::test] - async fn proof_smoke_rejects_unknown_fixture() { - let response = handle_request_bytes( - &serde_json::to_vec(&RhiProofSmokeRequest { - protocol_version: PROTOCOL_VERSION.to_string(), - operation: RhiProofSmokeOperation::ProofSmoke, - backend: RhiProofSmokeBackend::ValidatorSetDeterministic, - fixture: Some("other".to_string()), - }) - .expect("request json"), - ) - .await; - assert!(!response.ok); - assert_eq!( - response.error.as_deref(), - Some("proof_smoke requires fixture order_acceptance_tiny_v1") - ); - } - - #[tokio::test] - async fn proof_smoke_rejects_full_proof_request_fields() { - let response = handle_request_bytes( - br#"{"protocol_version":"radroots.rhi.proof_smoke.v0","operation":"proof_smoke","backend":"validator_set_deterministic","fixture":"order_acceptance_tiny_v1","proof_mode":"core"}"#, - ) - .await; - assert!(!response.ok); - assert!( - response - .error - .as_deref() - .is_some_and(|error| error.contains("unknown field")) - ); - } - - #[cfg(not(feature = "sp1_proving"))] - #[tokio::test] - async fn local_execute_reports_unavailable_without_feature() { - let response = handle_request_bytes(&request( - RhiProofSmokeOperation::ProofSmoke, - RhiProofSmokeBackend::LocalExecute, - )) - .await; - assert!(!response.ok); - assert_eq!( - response.error.as_deref(), - Some("local_execute backend is unavailable in this build") - ); - } - - #[cfg(feature = "sp1_proving")] - #[tokio::test] - async fn local_execute_returns_sp1_public_values_without_proof_generation() { - let deterministic = handle_request_bytes(&request( - RhiProofSmokeOperation::ProofSmoke, - RhiProofSmokeBackend::ValidatorSetDeterministic, - )) - .await; - let response = handle_request_bytes(&request( - RhiProofSmokeOperation::ProofSmoke, - RhiProofSmokeBackend::LocalExecute, - )) - .await; - if !radroots_trade_sp1_host::order_acceptance_sp1_guest_elf_available() { - assert!(!response.ok); - assert_eq!( - response.error.as_deref(), - Some("local_execute backend is unavailable in this build") - ); - assert!(!response.capabilities.contains(&"local_execute".to_string())); - return; - } - assert!(response.ok, "{:?}", response.error); - assert_eq!(response.operation, RhiProofSmokeOperation::ProofSmoke); - assert_eq!(response.backend, RhiProofSmokeBackend::LocalExecute); - assert!(response.capabilities.contains(&"local_execute".to_string())); - assert!(!response.proof_generated); - assert_ne!( - response.public_values_hash, - deterministic.public_values_hash - ); - assert!(response.sp1_program_hash.is_some()); - assert!(response.sp1_verifying_key_hash.is_some()); - for value in [ - response.sp1_program_hash.as_deref(), - response.sp1_verifying_key_hash.as_deref(), - ] { - let value = value.expect("SP1 identity"); - assert_eq!(value.len(), 64); - assert!(!value.starts_with("0x")); - assert!(value.bytes().all(|byte| byte.is_ascii_hexdigit())); - } - assert_eq!(response.event_set_root, deterministic.event_set_root); - assert_eq!( - response.reducer_output_root, - deterministic.reducer_output_root - ); - assert!(response.error.is_none()); - } -} diff --git a/src/remote_prove.rs b/src/remote_prove.rs @@ -1,476 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr(coverage_nightly, coverage(off))] - -use crate::cli::Command; -use radroots_trade_sp1_guest::{ - RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET, RADROOTS_SP1_TRADE_PROTOCOL_VERSION, - RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, RADROOTS_SP1_TRADE_WITNESS_VERSION, -}; -use radroots_trade_sp1_host::{ - RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, RADROOTS_SP1_TRADE_SP1_VERSION_LINE, - RadrootsSp1TradeProofEngine, RadrootsSp1TradeProofMode, RadrootsSp1TradeRemoteProverRequest, - RadrootsSp1TradeRemoteProverResponse, RadrootsSp1TradeRemoteProverStatus, -}; -use std::path::Path; - -pub async fn run_cli_command(command: Command) -> anyhow::Result<()> { - let Command::RemoteProve { - input, - output, - proof_engine, - } = command - else { - return Err(anyhow::anyhow!("remote-prove command expected")); - }; - let engine = RadrootsSp1TradeProofEngine::from_label(proof_engine.as_str()) - .ok_or_else(|| anyhow::anyhow!("invalid proof engine"))?; - let request_bytes = read_input(input.as_deref())?; - let response = handle_request_bytes(&request_bytes, engine).await; - let response_bytes = serde_json::to_vec_pretty(&response)?; - write_output(output.as_deref(), &response_bytes)?; - if response.status == RadrootsSp1TradeRemoteProverStatus::Completed { - Ok(()) - } else { - Err(anyhow::anyhow!( - "{}", - response - .message - .as_deref() - .unwrap_or("remote proof request did not complete") - )) - } -} - -pub async fn handle_request_bytes( - bytes: &[u8], - engine: RadrootsSp1TradeProofEngine, -) -> RadrootsSp1TradeRemoteProverResponse { - let request_id = request_id_from_bytes(bytes); - let request = match serde_json::from_slice::<RadrootsSp1TradeRemoteProverRequest>(bytes) { - Ok(request) => request, - Err(error) => { - return rejected_response(request_id, "invalid_json", error.to_string()); - } - }; - match validate_request(&request) { - Ok(()) => prove_request(request, engine).await, - Err(rejection) => { - rejected_response(request.request_id, rejection.reason, rejection.message) - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct RemoteProveRejection { - reason: &'static str, - message: &'static str, -} - -fn validate_request( - request: &RadrootsSp1TradeRemoteProverRequest, -) -> Result<(), RemoteProveRejection> { - if request.schema_version != RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION { - return Err(rejection( - "invalid_schema_version", - "invalid schema_version", - )); - } - if request.request_id.trim().is_empty() { - return Err(rejection("invalid_request_id", "invalid request_id")); - } - if request.proof_target != RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET { - return Err(rejection( - "unsupported_proof_target", - "unsupported proof_target", - )); - } - if request.proof_mode != RadrootsSp1TradeProofMode::Core { - return Err(rejection( - "unsupported_proof_mode", - "unsupported proof_mode", - )); - } - if request.sp1_version_line != RADROOTS_SP1_TRADE_SP1_VERSION_LINE { - return Err(rejection( - "unsupported_sp1_version", - "unsupported sp1 version", - )); - } - if request.expected_reducer_program_hash != RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH { - return Err(rejection( - "reducer_program_hash_mismatch", - "expected reducer program hash mismatch", - )); - } - if request.expected_protocol_version != RADROOTS_SP1_TRADE_PROTOCOL_VERSION { - return Err(rejection( - "protocol_version_mismatch", - "expected protocol version mismatch", - )); - } - if request.expected_witness_version != RADROOTS_SP1_TRADE_WITNESS_VERSION { - return Err(rejection( - "witness_version_mismatch", - "expected witness version mismatch", - )); - } - if request.witness.witness_version != request.expected_witness_version { - return Err(rejection( - "witness_version_mismatch", - "witness version mismatch", - )); - } - if request.witness.proof_target != request.proof_target { - return Err(rejection( - "proof_target_mismatch", - "witness proof target mismatch", - )); - } - if request.witness.reducer_program_hash != request.expected_reducer_program_hash { - return Err(rejection( - "reducer_program_hash_mismatch", - "witness reducer program hash mismatch", - )); - } - if request.witness.radroots_protocol_version != request.expected_protocol_version { - return Err(rejection( - "protocol_version_mismatch", - "witness protocol version mismatch", - )); - } - if request.witness.sp1_program_hash.as_deref() - != Some(request.expected_sp1_program_hash.as_str()) - { - return Err(rejection( - "sp1_program_hash_mismatch", - "witness SP1 program hash mismatch", - )); - } - if request.witness.sp1_verifying_key_hash.as_deref() - != Some(request.expected_sp1_verifying_key_hash.as_str()) - { - return Err(rejection( - "sp1_verifying_key_hash_mismatch", - "witness SP1 verifying key hash mismatch", - )); - } - for value in [ - request.expected_sp1_program_hash.as_str(), - request.expected_sp1_verifying_key_hash.as_str(), - request.expected_public_values_hash.as_str(), - ] { - if !is_hash32(value) { - return Err(rejection("invalid_hash", "expected hash field is invalid")); - } - } - let execution = radroots_trade_sp1_host::execute_order_acceptance_public_values( - &request.witness, - ) - .map_err(|_| { - rejection( - "public_values_execution_failed", - "public values execution failed", - ) - })?; - if execution.public_values_hash != request.expected_public_values_hash { - return Err(rejection( - "public_values_hash_mismatch", - "expected public values hash mismatch", - )); - } - Ok(()) -} - -fn rejection(reason: &'static str, message: &'static str) -> RemoteProveRejection { - RemoteProveRejection { reason, message } -} - -#[cfg(feature = "sp1_proving")] -async fn prove_request( - request: RadrootsSp1TradeRemoteProverRequest, - engine: RadrootsSp1TradeProofEngine, -) -> RadrootsSp1TradeRemoteProverResponse { - match radroots_trade_sp1_host::generate_order_acceptance_sp1_proof_with_engine( - &request.witness, - request.proof_mode, - engine, - ) - .await - { - Ok(bundle) => completed_response(request, bundle), - Err(error) => failed_response( - request.request_id, - "proof_generation_failed", - error.to_string(), - ), - } -} - -#[cfg(not(feature = "sp1_proving"))] -async fn prove_request( - request: RadrootsSp1TradeRemoteProverRequest, - _engine: RadrootsSp1TradeProofEngine, -) -> RadrootsSp1TradeRemoteProverResponse { - failed_response( - request.request_id, - "proof_generation_unavailable", - "remote-prove requires the sp1_proving feature".to_string(), - ) -} - -#[cfg(feature = "sp1_proving")] -fn completed_response( - request: RadrootsSp1TradeRemoteProverRequest, - bundle: radroots_trade_sp1_host::RadrootsSp1TradeProofBundle, -) -> RadrootsSp1TradeRemoteProverResponse { - if bundle.execution.public_values_hash != request.expected_public_values_hash { - return failed_response( - request.request_id, - "public_values_hash_mismatch", - "generated public values hash mismatch".to_string(), - ); - } - if bundle.proof.program_hash.as_deref() != Some(request.expected_sp1_program_hash.as_str()) { - return failed_response( - request.request_id, - "sp1_program_hash_mismatch", - "generated SP1 program hash mismatch".to_string(), - ); - } - if bundle.proof.verifying_key_hash.as_deref() - != Some(request.expected_sp1_verifying_key_hash.as_str()) - { - return failed_response( - request.request_id, - "sp1_verifying_key_hash_mismatch", - "generated SP1 verifying key hash mismatch".to_string(), - ); - } - RadrootsSp1TradeRemoteProverResponse { - schema_version: RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, - request_id: request.request_id, - status: RadrootsSp1TradeRemoteProverStatus::Completed, - status_url: None, - status_path: None, - proof_system: Some(request.proof_mode.proof_system()), - proof_mode: Some(request.proof_mode), - public_values_hash: Some(bundle.execution.public_values_hash), - sp1_program_hash: bundle.proof.program_hash.clone(), - sp1_verifying_key_hash: bundle.proof.verifying_key_hash.clone(), - proof_artifact: Some(bundle.proof), - resolved_proof_envelope_base64: None, - reason_code: None, - message: None, - detail: None, - } -} - -fn rejected_response( - request_id: String, - reason: impl Into<String>, - message: impl Into<String>, -) -> RadrootsSp1TradeRemoteProverResponse { - terminal_response( - request_id, - RadrootsSp1TradeRemoteProverStatus::Rejected, - reason, - message, - ) -} - -fn failed_response( - request_id: String, - reason: impl Into<String>, - message: impl Into<String>, -) -> RadrootsSp1TradeRemoteProverResponse { - terminal_response( - request_id, - RadrootsSp1TradeRemoteProverStatus::Failed, - reason, - message, - ) -} - -fn terminal_response( - request_id: String, - status: RadrootsSp1TradeRemoteProverStatus, - reason: impl Into<String>, - message: impl Into<String>, -) -> RadrootsSp1TradeRemoteProverResponse { - RadrootsSp1TradeRemoteProverResponse { - schema_version: RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, - request_id, - status, - status_url: None, - status_path: None, - proof_system: None, - proof_mode: None, - public_values_hash: None, - sp1_program_hash: None, - sp1_verifying_key_hash: None, - proof_artifact: None, - resolved_proof_envelope_base64: None, - reason_code: Some(reason.into()), - message: Some(message.into()), - detail: None, - } -} - -fn request_id_from_bytes(bytes: &[u8]) -> String { - serde_json::from_slice::<serde_json::Value>(bytes) - .ok() - .and_then(|value| { - let request_id = value - .get("request_id") - .and_then(serde_json::Value::as_str) - .map(str::trim)?; - if request_id.is_empty() { - None - } else { - Some(request_id.to_owned()) - } - }) - .unwrap_or_else(|| "invalid-request".to_string()) -} - -fn is_hash32(value: &str) -> bool { - value.len() == 66 - && value.starts_with("0x") - && value[2..] - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} - -fn read_input(input: Option<&Path>) -> anyhow::Result<Vec<u8>> { - match input { - Some(path) => Ok(std::fs::read(path)?), - None => { - use std::io::Read; - let mut bytes = Vec::new(); - std::io::stdin().read_to_end(&mut bytes)?; - Ok(bytes) - } - } -} - -fn write_output(output: Option<&Path>, bytes: &[u8]) -> anyhow::Result<()> { - match output { - Some(path) => { - std::fs::write(path, bytes)?; - Ok(()) - } - None => { - println!("{}", String::from_utf8_lossy(bytes)); - Ok(()) - } - } -} - -#[cfg(test)] -mod tests { - use super::handle_request_bytes; - use radroots_trade_sp1_guest::{ - RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET, RADROOTS_SP1_TRADE_PROTOCOL_VERSION, - RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, RADROOTS_SP1_TRADE_WITNESS_VERSION, - }; - use radroots_trade_sp1_host::{ - RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, RADROOTS_SP1_TRADE_SP1_VERSION_LINE, - RadrootsSp1TradeProofEngine, RadrootsSp1TradeProofMode, - RadrootsSp1TradeRemoteProverRequest, RadrootsSp1TradeRemoteProverStatus, - }; - - fn hash32(ch: char) -> String { - format!("0x{}", ch.to_string().repeat(64)) - } - - fn request() -> RadrootsSp1TradeRemoteProverRequest { - let mut witness = crate::proof_smoke::order_acceptance_tiny_witness(); - witness.sp1_program_hash = Some(hash32('a')); - witness.sp1_verifying_key_hash = Some(hash32('b')); - let execution = radroots_trade_sp1_host::execute_order_acceptance_public_values(&witness) - .expect("public values"); - RadrootsSp1TradeRemoteProverRequest { - schema_version: RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, - request_id: "request-1".to_string(), - proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(), - proof_mode: RadrootsSp1TradeProofMode::Core, - sp1_version_line: RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_string(), - witness, - expected_sp1_program_hash: hash32('a'), - expected_sp1_verifying_key_hash: hash32('b'), - expected_public_values_hash: execution.public_values_hash, - expected_reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), - expected_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), - expected_witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION, - } - } - - #[tokio::test] - async fn remote_prove_rejects_unknown_provider_fields() { - let response = handle_request_bytes( - br#"{"schema_version":1,"request_id":"request-1","proof_target":"order_acceptance_v1","proof_mode":"core","sp1_version_line":"sp1-sdk-6.2.1","expected_sp1_program_hash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expected_sp1_verifying_key_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","expected_public_values_hash":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","expected_reducer_program_hash":"0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","expected_protocol_version":"radroots.sp1.trade.v1","expected_witness_version":1,"provider":"runpod"}"#, - RadrootsSp1TradeProofEngine::Cpu, - ) - .await; - assert_eq!(response.request_id, "request-1"); - assert_eq!( - response.status, - RadrootsSp1TradeRemoteProverStatus::Rejected - ); - assert_eq!(response.reason_code.as_deref(), Some("invalid_json")); - } - - #[tokio::test] - async fn remote_prove_rejects_expected_public_values_hash_mismatch() { - let mut request = request(); - request.expected_public_values_hash = hash32('c'); - let response = handle_request_bytes( - &serde_json::to_vec(&request).expect("request json"), - RadrootsSp1TradeProofEngine::Cpu, - ) - .await; - assert_eq!( - response.status, - RadrootsSp1TradeRemoteProverStatus::Rejected - ); - assert_eq!( - response.reason_code.as_deref(), - Some("public_values_hash_mismatch") - ); - } - - #[tokio::test] - async fn remote_prove_rejects_unsupported_modes_before_generation() { - let mut request = request(); - request.proof_mode = RadrootsSp1TradeProofMode::Compressed; - let response = handle_request_bytes( - &serde_json::to_vec(&request).expect("request json"), - RadrootsSp1TradeProofEngine::Cpu, - ) - .await; - assert_eq!( - response.status, - RadrootsSp1TradeRemoteProverStatus::Rejected - ); - assert_eq!( - response.reason_code.as_deref(), - Some("unsupported_proof_mode") - ); - } - - #[cfg(not(feature = "sp1_proving"))] - #[tokio::test] - async fn remote_prove_reports_generation_unavailable_without_proving_feature() { - let request = request(); - let response = handle_request_bytes( - &serde_json::to_vec(&request).expect("request json"), - RadrootsSp1TradeProofEngine::Cpu, - ) - .await; - assert_eq!(response.status, RadrootsSp1TradeRemoteProverStatus::Failed); - assert_eq!( - response.reason_code.as_deref(), - Some("proof_generation_unavailable") - ); - } -} diff --git a/src/rhi.rs b/src/rhi.rs @@ -1,12 +1,15 @@ #![cfg_attr(coverage_nightly, coverage(off))] +use std::sync::Arc; use std::time::{Duration, Instant}; use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys}; use radroots_runtime::{Backoff, BackoffConfig}; +use tokio::sync::Mutex; -use crate::features::trade_listing::state::TradeListingRuntime; -use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; +use crate::features::trade_agreement_attestation::{ + TradeAgreementAttestationPolicy, TradeAgreementAttestationRuntime, +}; #[cfg(not(test))] fn connection_wait_timeout() -> Duration { @@ -32,8 +35,8 @@ fn subscriber_result_hook() async fn run_subscriber_once( client: RadrootsNostrClient, keys: RadrootsNostrKeys, - runtime: TradeListingRuntime, - proof_policy: TradeValidationReceiptProverPolicy, + runtime: TradeAgreementAttestationRuntime, + policy: TradeAgreementAttestationPolicy, stop_rx: tokio::sync::watch::Receiver<bool>, ) -> Result<(), anyhow::Error> { #[cfg(test)] @@ -45,14 +48,8 @@ async fn run_subscriber_once( return result; } - crate::features::trade_listing::subscriber::subscriber( - client, - keys, - runtime, - proof_policy, - stop_rx, - ) - .await + crate::features::trade_agreement_attestation::subscriber(client, keys, runtime, policy, stop_rx) + .await } async fn wait_for_connection_or_stop( @@ -71,44 +68,41 @@ async fn wait_for_connection_or_stop( pub struct Rhi { pub(crate) _started: Instant, pub client: RadrootsNostrClient, - pub(crate) trade_listing_runtime: TradeListingRuntime, - pub(crate) trade_validation_receipt_policy: TradeValidationReceiptProverPolicy, + pub(crate) agreement_attestation_runtime: TradeAgreementAttestationRuntime, + pub(crate) agreement_attestation_policy: TradeAgreementAttestationPolicy, } impl Rhi { pub fn new(keys: RadrootsNostrKeys) -> Self { - Self::with_trade_listing_runtime(keys, TradeListingRuntime::new()) + Self::with_agreement_attestation_runtime(keys, TradeAgreementAttestationRuntime::new()) } - pub fn with_trade_listing_runtime( + pub fn with_agreement_attestation_runtime( keys: RadrootsNostrKeys, - trade_listing_runtime: TradeListingRuntime, + agreement_attestation_runtime: TradeAgreementAttestationRuntime, ) -> Self { - Self::with_trade_listing_runtime_and_policy( + Self::with_agreement_attestation_runtime_and_policy( keys, - trade_listing_runtime, - TradeValidationReceiptProverPolicy::default(), + agreement_attestation_runtime, + TradeAgreementAttestationPolicy::default(), ) } - pub fn with_trade_listing_runtime_and_policy( + pub fn with_agreement_attestation_runtime_and_policy( keys: RadrootsNostrKeys, - trade_listing_runtime: TradeListingRuntime, - trade_validation_receipt_policy: TradeValidationReceiptProverPolicy, + agreement_attestation_runtime: TradeAgreementAttestationRuntime, + agreement_attestation_policy: TradeAgreementAttestationPolicy, ) -> Self { let client = RadrootsNostrClient::new(keys); Self { _started: Instant::now(), client, - trade_listing_runtime, - trade_validation_receipt_policy, + agreement_attestation_runtime, + agreement_attestation_policy, } } } -use std::sync::Arc; -use tokio::sync::Mutex; - pub struct RhiHandle { stop_tx: Arc<Mutex<Option<tokio::sync::watch::Sender<bool>>>>, join: Option<tokio::task::JoinHandle<()>>, @@ -118,7 +112,7 @@ impl Clone for RhiHandle { fn clone(&self) -> Self { Self { stop_tx: Arc::clone(&self.stop_tx), - join: None, // don’t clone the JoinHandle! + join: None, } } } @@ -140,14 +134,14 @@ impl RhiHandle { pub async fn start_subscriber( client: RadrootsNostrClient, keys: RadrootsNostrKeys, - runtime: TradeListingRuntime, + runtime: TradeAgreementAttestationRuntime, backoff_cfg: BackoffConfig, ) -> RhiHandle { start_subscriber_with_policy( client, keys, runtime, - TradeValidationReceiptProverPolicy::default(), + TradeAgreementAttestationPolicy::default(), backoff_cfg, ) .await @@ -156,8 +150,8 @@ pub async fn start_subscriber( pub async fn start_subscriber_with_policy( client: RadrootsNostrClient, keys: RadrootsNostrKeys, - runtime: TradeListingRuntime, - proof_policy: TradeValidationReceiptProverPolicy, + runtime: TradeAgreementAttestationRuntime, + policy: TradeAgreementAttestationPolicy, backoff_cfg: BackoffConfig, ) -> RhiHandle { let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false); @@ -178,7 +172,7 @@ pub async fn start_subscriber_with_policy( client.clone(), keys.clone(), runtime.clone(), - proof_policy.clone(), + policy.clone(), stop_rx.clone(), ) .await; @@ -186,7 +180,7 @@ pub async fn start_subscriber_with_policy( let failed = res.is_err(); if let Err(e) = res { - tracing::error!("Error on job request subscription: {e}"); + tracing::error!("Error on agreement attestation subscription: {e}"); } else { backoff.reset(); } @@ -217,7 +211,7 @@ mod tests { use super::{ Rhi, RhiHandle, start_subscriber, subscriber_result_hook, wait_for_connection_or_stop, }; - use crate::features::trade_listing::state::TradeListingRuntime; + use crate::features::trade_agreement_attestation::TradeAgreementAttestationRuntime; use anyhow::anyhow; use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys}; use radroots_runtime::BackoffConfig; @@ -229,18 +223,7 @@ mod tests { let keys = RadrootsNostrKeys::generate(); let rhi = Rhi::new(keys); let _ = rhi.client.clone(); - let state = rhi.trade_listing_runtime.state(); - state - .lock() - .await - .mark_listing_validated("addr", "evt-listing-1"); - assert!( - rhi.trade_listing_runtime - .state() - .lock() - .await - .is_listing_validated("addr") - ); + assert!(rhi.agreement_attestation_runtime.reports().await.is_empty()); } #[tokio::test] @@ -271,7 +254,7 @@ mod tests { let handle_err = start_subscriber( client_err, keys.clone(), - TradeListingRuntime::new(), + TradeAgreementAttestationRuntime::new(), cfg.clone(), ) .await; @@ -285,7 +268,13 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push_back(Ok(())); - let handle_ok = start_subscriber(client_ok, keys, TradeListingRuntime::new(), cfg).await; + 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; @@ -298,7 +287,7 @@ mod tests { let handle = start_subscriber( client, keys, - TradeListingRuntime::new(), + TradeAgreementAttestationRuntime::new(), BackoffConfig { base_ms: 25, max_ms: 50, @@ -324,7 +313,7 @@ mod tests { let handle = start_subscriber( client, keys, - TradeListingRuntime::new(), + TradeAgreementAttestationRuntime::new(), BackoffConfig { base_ms: 200, max_ms: 200, diff --git a/tests/source_guards.rs b/tests/source_guards.rs @@ -2,172 +2,132 @@ use std::fs; use std::path::Path; #[test] -fn rhi_manifest_has_no_sdk_dependency() { +fn rhi_manifest_has_no_sdk_or_legacy_proof_dependency() { let manifest = read_repo_file("Cargo.toml"); - assert!( - !manifest.contains("radroots_sdk"), - "RHI must not depend on radroots_sdk" - ); -} - -#[test] -fn rhi_validation_receipt_paths_use_validator_set_contract() { - let listing_events = read_repo_file("src/features/trade_listing/handlers/events.rs"); - let receipt_worker = read_repo_file("src/features/trade_validation_receipt.rs"); - - assert!( - listing_events.contains("publish_validation_receipt("), - "trade listing event handler must publish V1 validation receipts from accepted orders" - ); - assert!( - !listing_events.contains("radroots_trade::dvm"), - "trade listing handler must not import the removed DVM contract" - ); - - for required in [ - "validator_set_addr", - "validator_set_event_id", - "validator_set_address_from_str", - "validation_receipt_event_build", - "verify_validation_receipt_event", - "MissingValidatorSetBinding", - "mark_receipt_completed", + for forbidden in [ + "radroots_sdk", + "radroots_trade_sp1_guest", + "radroots_trade_sp1_host", + "sp1_verify", + "sp1_proving", + "sp1_cuda_proving", + "reqwest", + "sqlx", + "libsqlite3-sys", ] { assert!( - receipt_worker.contains(required), - "trade validation receipt worker must retain validator-set receipt contract `{required}`" + !manifest.contains(forbidden), + "RHI manifest must not retain retired dependency `{forbidden}`" ); } +} - for forbidden in [ - "radroots_trade::dvm", - "KIND_TRADE_TRANSITION_PROOF_REQUEST", - "KIND_TRADE_TRANSITION_PROOF_RESULT", - "RadrootsTradeTransitionProofRequest", - "build_transition_proof_result_tags", - "deterministic_none", - "DeterministicNone", +#[test] +fn rhi_release_product_surface_has_no_order_or_receipt_modules() { + for forbidden_path in [ + "src/features/trade_listing/mod.rs", + "src/features/trade_validation_receipt.rs", + "src/proof_smoke.rs", + "src/remote_prove.rs", ] { assert!( - !receipt_worker.contains(forbidden), - "trade validation receipt worker must not retain retired contract `{forbidden}`" + !Path::new(env!("CARGO_MANIFEST_DIR")) + .join(forbidden_path) + .exists(), + "RHI must not retain retired source path `{forbidden_path}`" ); } -} -#[test] -fn rhi_sources_do_not_import_removed_sdk_or_protocol_bypasses() { for (path, source) in rust_sources_under("src") { for forbidden in [ - "radroots_sdk", - "radroots_sdk::protocol::order", - "SdkDvmInventoryBinWitness", - "TradeProtocolClient", - "KIND_TRADE_LISTING_VALIDATE_REQ", - "KIND_TRADE_LISTING_VALIDATION_REQUEST", - "KIND_WORKER_TRADE_TRANSITION_PROOF_REQ", - "KIND_TRADE_TRANSITION_PROOF_REQUEST", - "KIND_TRADE_TRANSITION_PROOF_RESULT", - "radroots_trade::dvm", - "deterministic_none", - "DeterministicNone", + "trade_listing", + "trade_validation_receipt", + "proof_smoke", + "remote_prove", + "KIND_ORDER", + "RadrootsOrder", + "radroots_trade::order", + "radroots_event_codec::order", + "AgreedPendingValidation", + "ValidationExpired", + "order_acceptance", + "KIND_TRADE_VALIDATION_RECEIPT", + "validation_receipt_event_build", + "verify_validation_receipt_event", + "radroots_trade_sp1", + "proof_mode", + "LocalExecute", + "local_execute", ] { assert!( !source.contains(forbidden), - "{path} contains forbidden SDK adoption bypass `{forbidden}`" + "{path} retains retired order or proof surface `{forbidden}`" ); } } } #[test] -fn rhi_processed_job_state_is_durable_workflow_authority() { - let state = read_repo_file("src/features/trade_listing/state.rs"); - let processed_jobs = read_repo_file("src/features/trade_listing/processed_jobs.rs"); - let receipt_worker = read_repo_file("src/features/trade_validation_receipt.rs"); - - assert!( - !state.contains("rhi_processed_jobs: HashMap"), - "RHI JSON subscriber state must not be the processed-job authority" - ); - assert!( - state.contains("processed_jobs: Arc<RhiProcessedJobStore>"), - "TradeListingRuntime must own the processed-job SQLite store" - ); +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"); + let cli = read_repo_file("src/cli.rs"); + let config = read_repo_file("src/config.rs"); for required in [ - "CREATE TABLE IF NOT EXISTS rhi_processed_jobs", - "request_id TEXT PRIMARY KEY", - "CREATE UNIQUE INDEX IF NOT EXISTS rhi_processed_jobs_receipt_event_idx", - "CREATE UNIQUE INDEX IF NOT EXISTS rhi_processed_jobs_result_event_idx", - "pub async fn claim_job(", - "pub async fn mark_receipt_publishing(", - "pub async fn mark_receipt_published(", - "pub async fn mark_result_publishing(", - "pub async fn mark_completed(", - "pub async fn mark_receipt_completed(", - "RhiProcessedJobClaim::InProgress", - "RhiProcessedJobClaim::RecoverReceipt", - "RhiProcessedJobStatus::ReceiptPublishing", - "RhiProcessedJobStatus::ResultPublishing", - "RhiProcessedJobStatus::Completed", - "receipt_event_json", - "result_event_json", - "proof_metadata_json", - "DuplicateConflictingResult", + "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!( - processed_jobs.contains(required), - "RHI processed-job store must retain SQLite workflow authority `{required}`" + worker.contains(required), + "agreement attestation worker must retain release-product requirement `{required}`" ); } - for required in [ - "fn processed_job_for_receipt(", - "async fn publish_receipt_with_processed_job(", - "publish_signed_event_io(", - "mark_receipt_completed(", - "RhiProcessedJobClaim::Completed", - ] { - assert!( - receipt_worker.contains(required), - "RHI receipt worker must retain processed-job workflow guard `{required}`" - ); - } + assert!( + lib.contains("trade_mutation_subscription_kinds()") + && lib.contains("&TRADE_MUTATION_EVENT_KINDS"), + "RHI service presence must advertise canonical release-product trade mutation kinds" + ); + assert!( + cli.contains("attestation-smoke") + && !cli.contains("proof-smoke") + && !cli.contains("remote-prove"), + "RHI CLI must expose only release-product agreement attestation smoke command" + ); + assert!( + config.contains("settings.config.trade_agreement_attestation.validate()?"), + "RHI config loading must validate the canonical attestation policy" + ); } #[test] -fn rhi_validation_receipt_policy_is_enabled_and_validator_set_bound() { +fn rhi_state_paths_are_named_for_agreement_attestation() { + let paths = read_repo_file("src/paths.rs"); let config = read_repo_file("src/config.rs"); - let receipt_worker = read_repo_file("src/features/trade_validation_receipt.rs"); + let main = read_repo_file("src/main.rs"); - for required in [ - "TradeValidationReceiptProverBackend::LocalExecute", - "validator_binding(", - "MissingValidatorSetBinding", - "validator_set_addr", - "validator_set_event_id", - "ProverBackendRequiresNone", - ] { + for source in [paths.as_str(), config.as_str(), main.as_str()] { assert!( - receipt_worker.contains(required), - "trade validation receipt policy must retain enabled validator-set governance `{required}`" + source.contains("trade-agreement-attestation"), + "RHI runtime state paths must use agreement-attestation naming" ); - } - - assert!( - !receipt_worker.contains("Disabled") && !receipt_worker.contains("DeterministicNone"), - "RHI must not expose disabled or deterministic-none validation receipt backends" - ); - - for required in [ - "settings.config.trade_validation_receipt.validate()?", - "TradeValidationReceiptProverBackend::LocalExecute", - ] { assert!( - config.contains(required), - "RHI config loading must retain enabled validator-set policy validation `{required}`" + !source.contains("trade-listing"), + "RHI runtime state paths must not retain trade-listing naming" ); } }