diff --git a/Cargo.lock b/Cargo.lock index 65649ace..162d38c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,18 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -65,6 +77,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anyhow" version = "1.0.102" @@ -97,6 +115,123 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-secp256r1" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3975a01b0a6e3eae0f72ec7ca8598a6620fc72fa5981f6f5cca33b7cd788f633" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -206,6 +341,23 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "auto_ops" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7460f7dd8e100147b82a63afca1a20eb6c231ee36b90ba7272e14951cb58af59" + [[package]] name = "autocfg" version = "1.5.0" @@ -245,6 +397,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -257,6 +415,65 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bft-crdt-derive" +version = "0.1.0" +dependencies = [ + "indexmap 2.13.1", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bft-json-crdt" +version = "0.1.0" +dependencies = [ + "bft-crdt-derive", + "colored 2.2.0", + "criterion", + "fastcrypto", + "indexmap 2.13.1", + "rand 0.8.5", + "random_color", + "serde", + "serde_json", + "serde_with", + "sha2 0.10.9", + "time 0.1.45", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -278,6 +495,15 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "blake3" version = "1.8.4" @@ -292,6 +518,15 @@ dependencies = [ "cpufeatures 0.3.0", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -319,6 +554,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + [[package]] name = "bs58" version = "0.5.1" @@ -362,6 +615,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbc" version = "0.1.2" @@ -455,6 +714,33 @@ dependencies = [ "phf 0.12.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -466,6 +752,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cmake" version = "0.1.58" @@ -475,6 +782,16 @@ dependencies = [ "cc", ] +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "colored" version = "3.1.1" @@ -547,6 +864,21 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -624,6 +956,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.10.5", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -658,6 +1026,24 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -715,14 +1101,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -739,13 +1148,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -816,6 +1249,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.6.0", + "zeroize", +] + [[package]] name = "der" version = "0.7.10" @@ -823,7 +1267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", - "pem-rfc7468", + "pem-rfc7468 0.7.0", "zeroize", ] @@ -834,6 +1278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -847,6 +1292,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -882,7 +1340,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -890,6 +1348,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" @@ -942,17 +1409,52 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki 0.7.3", +] + [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "serde", "signature", ] +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "2.2.0" @@ -977,6 +1479,26 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1095,12 +1617,85 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "fastcrypto" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06674cac3bf7ec9a951971285e6051a45273dc4e265cca27c02a0d4ebcb46f8" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-secp256r1", + "ark-serialize", + "auto_ops", + "base64ct", + "bech32", + "bincode", + "blake2", + "blst", + "bs58 0.4.0", + "curve25519-dalek-ng", + "derive_more 0.99.20", + "digest 0.10.7", + "ecdsa", + "ed25519-consensus", + "elliptic-curve", + "fastcrypto-derive", + "generic-array", + "hex", + "hex-literal", + "hkdf", + "lazy_static", + "num-bigint", + "once_cell", + "p256", + "rand 0.8.5", + "readonly", + "rfc6979", + "rsa 0.8.2", + "schemars 0.8.22", + "secp256k1", + "serde", + "serde_json", + "serde_with", + "sha2 0.10.9", + "sha3", + "signature", + "static_assertions", + "thiserror 1.0.69", + "tokio", + "typenum", + "zeroize", +] + +[[package]] +name = "fastcrypto-derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0c2af2157f416cb885e11d36cd0de2753f6d5384752d364075c835f5f8f891" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1341,8 +1936,21 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ + "serde", "typenum", "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -1354,7 +1962,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1385,6 +1993,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "globset" version = "0.4.18" @@ -1423,6 +2037,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "growable-bloom-filter" version = "2.1.1" @@ -1447,13 +2072,39 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.13.1", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1513,6 +2164,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1525,6 +2185,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hkdf" version = "0.12.4" @@ -1635,14 +2301,17 @@ version = "0.9.0" dependencies = [ "async-stream", "async-trait", + "bft-json-crdt", "bytes", "chrono", "chrono-tz", "eventsource-stream", + "fastcrypto", "filetime", "futures", "homedir", "ignore", + "indexmap 2.13.1", "libc", "libsqlite3-sys", "matrix-sdk", @@ -1954,6 +2623,17 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.13.1" @@ -2120,6 +2800,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + [[package]] name = "konst" version = "0.3.16" @@ -2347,7 +3036,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a962fc9981f823f6555416dcb2ae9ae67ca412d767ee21ecab5150113ee6285b" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro-error2", "proc-macro2", "quote", @@ -2378,7 +3067,7 @@ dependencies = [ "gloo-timers", "http", "imbl", - "indexmap", + "indexmap 2.13.1", "itertools 0.14.0", "js_int", "language-tags", @@ -2472,7 +3161,7 @@ dependencies = [ "aquamarine", "as_variant", "async-trait", - "bs58", + "bs58 0.5.1", "byteorder", "cfg-if", "ctr", @@ -2493,7 +3182,7 @@ dependencies = [ "sha2 0.10.9", "subtle", "thiserror 2.0.18", - "time", + "time 0.3.47", "tokio", "tokio-stream", "tracing", @@ -2685,7 +3374,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -2697,7 +3386,7 @@ checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" dependencies = [ "assert-json-diff", "bytes", - "colored", + "colored 3.1.1", "futures-core", "http", "http-body", @@ -2825,6 +3514,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -2883,7 +3582,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", ] @@ -2913,6 +3612,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2963,6 +3668,24 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -2992,6 +3715,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3002,6 +3731,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3079,15 +3817,37 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der 0.6.1", + "pkcs8 0.9.0", + "spki 0.6.0", + "zeroize", +] + [[package]] name = "pkcs1" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", ] [[package]] @@ -3096,8 +3856,8 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -3112,6 +3872,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "poem" version = "3.1.12" @@ -3159,7 +3947,7 @@ version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "056e2fea6de1cb240ffe23cfc4fc370b629f8be83b5f27e16b7acd5231a72de4" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -3175,7 +3963,7 @@ dependencies = [ "bytes", "derive_more 2.1.1", "futures-util", - "indexmap", + "indexmap 2.13.1", "itertools 0.14.0", "mime", "num-traits", @@ -3197,11 +3985,11 @@ version = "5.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41273b691a3d467a8c44d05506afba9f7b6bd56c9cdf80123de13fe52d7ec587" dependencies = [ - "darling", + "darling 0.20.11", "http", - "indexmap", + "indexmap 2.13.1", "mime", - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "regex", @@ -3281,13 +4069,32 @@ dependencies = [ "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", + "toml_edit 0.25.10+spec-1.1.0", ] [[package]] @@ -3448,6 +4255,20 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.5" @@ -3469,6 +4290,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3489,6 +4320,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -3507,6 +4347,24 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_xoshiro" version = "0.7.0" @@ -3516,6 +4374,35 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "random_color" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f34bd6526786b2ce5141fd37a4084b5da1ebae74595b5b0d05482a7cef7181" +dependencies = [ + "rand 0.7.3", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +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 = "readlock" version = "0.1.11" @@ -3531,6 +4418,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "readonly" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3549,6 +4447,26 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -3664,6 +4582,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rfc7239" version = "0.1.3" @@ -3706,6 +4634,27 @@ dependencies = [ "serde", ] +[[package]] +name = "rsa" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" +dependencies = [ + "byteorder", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1 0.4.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3717,11 +4666,11 @@ dependencies = [ "num-bigint-dig", "num-integer", "num-traits", - "pkcs1", - "pkcs8", + "pkcs1 0.7.5", + "pkcs8 0.10.2", "rand_core 0.6.4", "signature", - "spki", + "spki 0.7.3", "subtle", "zeroize", ] @@ -3779,7 +4728,7 @@ dependencies = [ "form_urlencoded", "getrandom 0.2.17", "http", - "indexmap", + "indexmap 2.13.1", "js-sys", "js_int", "konst", @@ -3792,7 +4741,7 @@ dependencies = [ "serde_html_form", "serde_json", "thiserror 2.0.18", - "time", + "time 0.3.47", "tracing", "url", "uuid", @@ -3808,7 +4757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dbdeccb62cb4ffe3282325de8ba28cbc0fdce7c78a3f11b7241fbfdb9cb9907" dependencies = [ "as_variant", - "indexmap", + "indexmap 2.13.1", "js_int", "js_option", "percent-encoding", @@ -3876,7 +4825,7 @@ checksum = "0a0753312ad577ac462de1742bf2e326b6ba9856ff6f13343aeb17d423fd5426" dependencies = [ "as_variant", "cfg-if", - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "ruma-identifiers-validation", @@ -3893,7 +4842,7 @@ checksum = "146ace2cd59b60ec80d3e801a84e7e6a91e3e01d18a9f5d896ea7ca16a6b8e08" dependencies = [ "base64", "ed25519-dalek", - "pkcs8", + "pkcs8 0.10.2", "rand 0.8.5", "ruma-common", "serde_json", @@ -4083,6 +5032,54 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -4100,6 +5097,40 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -4180,6 +5211,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_html_form" version = "0.2.8" @@ -4187,7 +5229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap", + "indexmap 2.13.1", "itoa", "ryu", "serde_core", @@ -4199,6 +5241,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap 2.13.1", "itoa", "memchr", "serde", @@ -4238,13 +5281,44 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.1", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time 0.3.47", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.13.1", "itoa", "ryu", "serde", @@ -4273,6 +5347,19 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -4295,6 +5382,16 @@ dependencies = [ "digest 0.11.2", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -4398,6 +5495,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -4405,7 +5512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", ] [[package]] @@ -4440,7 +5547,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "hashlink", - "indexmap", + "indexmap 2.13.1", "log", "memchr", "percent-encoding", @@ -4521,7 +5628,7 @@ dependencies = [ "memchr", "percent-encoding", "rand 0.8.5", - "rsa", + "rsa 0.9.10", "sha1", "sha2 0.10.9", "smallvec", @@ -4609,6 +5716,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -4666,6 +5779,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "syn" version = "1.0.109" @@ -4753,6 +5872,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + [[package]] name = "thiserror" version = "1.0.69" @@ -4802,6 +5927,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.47" @@ -4843,6 +5988,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" @@ -4977,7 +6132,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap", + "indexmap 2.13.1", "serde_core", "serde_spanned", "toml_datetime 1.1.1+spec-1.1.0", @@ -4986,6 +6141,12 @@ dependencies = [ "winnow 1.0.1", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -5004,13 +6165,24 @@ 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.1", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.25.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" dependencies = [ - "indexmap", + "indexmap 2.13.1", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.1", @@ -5414,6 +6586,18 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -5516,7 +6700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.13.1", "wasm-encoder", "wasmparser", ] @@ -5573,7 +6757,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.13.1", "semver", ] @@ -6071,6 +7255,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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" @@ -6123,7 +7316,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.13.1", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -6154,7 +7347,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.11.0", - "indexmap", + "indexmap 2.13.1", "log", "serde", "serde_derive", @@ -6173,7 +7366,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.13.1", "log", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8476233c..969b5cf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["server"] +members = ["server", "crates/bft-json-crdt"] resolver = "3" [workspace.dependencies] diff --git a/server/Cargo.toml b/server/Cargo.toml index c6ceb682..2507da17 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -40,6 +40,9 @@ tokio-tungstenite = { workspace = true } libsqlite3-sys = { version = "0.35.0", features = ["bundled"] } sqlx = { workspace = true } wait-timeout = "0.2.1" +bft-json-crdt = { path = "../crates/bft-json-crdt" } +fastcrypto = "0.1.8" +indexmap = { version = "2.2.6", features = ["serde"] } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/server/migrations/20240102000000_crdt_ops.sql b/server/migrations/20240102000000_crdt_ops.sql new file mode 100644 index 00000000..b9978b9e --- /dev/null +++ b/server/migrations/20240102000000_crdt_ops.sql @@ -0,0 +1,14 @@ +-- Stores serialized CRDT SignedOps for pipeline state persistence. +-- On startup, all ops are replayed in sequence order to reconstruct the CRDT document. +CREATE TABLE IF NOT EXISTS crdt_ops ( + op_id TEXT PRIMARY KEY, + seq INTEGER NOT NULL, + op_json TEXT NOT NULL, + created_at TEXT NOT NULL +); + +-- Stores the node keypair seed so the same identity survives restarts. +CREATE TABLE IF NOT EXISTS crdt_node_identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + seed BLOB NOT NULL +); diff --git a/server/src/crdt_state.rs b/server/src/crdt_state.rs new file mode 100644 index 00000000..f4f435a3 --- /dev/null +++ b/server/src/crdt_state.rs @@ -0,0 +1,613 @@ +/// CRDT state layer for pipeline state, backed by SQLite. +/// +/// Replaces the filesystem as the primary source of truth for pipeline item +/// metadata (stage, name, agent, etc.). CRDT ops are persisted to SQLite so +/// state survives restarts. The filesystem `.huskies/work/` directories are +/// still updated as a secondary output for backwards compatibility. +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; + +use bft_json_crdt::json_crdt::*; +use bft_json_crdt::keypair::make_keypair; +use bft_json_crdt::list_crdt::ListCrdt; +use bft_json_crdt::lww_crdt::LwwRegisterCrdt; +use bft_json_crdt::op::ROOT_ID; +use fastcrypto::ed25519::Ed25519KeyPair; +use fastcrypto::traits::ToFromBytes; +use serde_json::json; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::SqlitePool; +use std::path::Path; +use tokio::sync::mpsc; + +use crate::slog; + +// ── CRDT document types ────────────────────────────────────────────── + +#[add_crdt_fields] +#[derive(Clone, CrdtNode, Debug)] +pub struct PipelineDoc { + pub items: ListCrdt, +} + +#[add_crdt_fields] +#[derive(Clone, CrdtNode, Debug)] +pub struct PipelineItemCrdt { + pub story_id: LwwRegisterCrdt, + pub stage: LwwRegisterCrdt, + pub name: LwwRegisterCrdt, + pub agent: LwwRegisterCrdt, + pub retry_count: LwwRegisterCrdt, + pub blocked: LwwRegisterCrdt, + pub depends_on: LwwRegisterCrdt, +} + +// ── Read-side view types ───────────────────────────────────────────── + +/// A snapshot of a single pipeline item derived from the CRDT document. +#[derive(Clone, Debug)] +pub struct PipelineItemView { + pub story_id: String, + pub stage: String, + pub name: Option, + pub agent: Option, + pub retry_count: Option, + pub blocked: Option, + pub depends_on: Option>, +} + +// ── Internal state ─────────────────────────────────────────────────── + +struct CrdtState { + crdt: BaseCrdt, + keypair: Ed25519KeyPair, + /// Maps story_id → index in the ListCrdt for O(1) lookup. + index: HashMap, + /// Channel sender for fire-and-forget op persistence. + persist_tx: mpsc::UnboundedSender, +} + +static CRDT_STATE: OnceLock> = OnceLock::new(); + +// ── Initialisation ─────────────────────────────────────────────────── + +/// Initialise the CRDT state layer. +/// +/// Opens the SQLite database, loads or creates a node keypair, replays any +/// persisted ops to reconstruct state, and spawns a background persistence +/// task. Safe to call only once; subsequent calls are no-ops. +pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> { + if CRDT_STATE.get().is_some() { + return Ok(()); + } + + let options = SqliteConnectOptions::new() + .filename(db_path) + .create_if_missing(true); + let pool = SqlitePool::connect_with(options).await?; + sqlx::migrate!("./migrations").run(&pool).await?; + + // Load or create the node keypair. + let keypair = load_or_create_keypair(&pool).await?; + let mut crdt = BaseCrdt::::new(&keypair); + + // Replay persisted ops to reconstruct state. + let rows: Vec<(String,)> = + sqlx::query_as("SELECT op_json FROM crdt_ops ORDER BY seq ASC") + .fetch_all(&pool) + .await?; + + for (op_json,) in &rows { + if let Ok(signed_op) = serde_json::from_str::(op_json) { + crdt.apply(signed_op); + } else { + slog!("[crdt] Warning: failed to deserialize stored op"); + } + } + + // Build the index from the reconstructed state. + let index = rebuild_index(&crdt); + + slog!( + "[crdt] Initialised: {} ops replayed, {} items indexed", + rows.len(), + index.len() + ); + + // Spawn background persistence task. + let (persist_tx, mut persist_rx) = mpsc::unbounded_channel::(); + + tokio::spawn(async move { + while let Some(op) = persist_rx.recv().await { + let op_json = match serde_json::to_string(&op) { + Ok(j) => j, + Err(e) => { + slog!("[crdt] Failed to serialize op: {e}"); + continue; + } + }; + let op_id = hex::encode(&op.id()); + let seq = op.inner.seq as i64; + let now = chrono::Utc::now().to_rfc3339(); + + let result = sqlx::query( + "INSERT INTO crdt_ops (op_id, seq, op_json, created_at) \ + VALUES (?1, ?2, ?3, ?4) \ + ON CONFLICT(op_id) DO NOTHING", + ) + .bind(&op_id) + .bind(seq) + .bind(&op_json) + .bind(&now) + .execute(&pool) + .await; + + if let Err(e) = result { + slog!("[crdt] Failed to persist op {}: {e}", &op_id[..12]); + } + } + }); + + let state = CrdtState { + crdt, + keypair, + index, + persist_tx, + }; + + let _ = CRDT_STATE.set(Mutex::new(state)); + Ok(()) +} + +/// Load or create the Ed25519 keypair used by this node. +async fn load_or_create_keypair(pool: &SqlitePool) -> Result { + let row: Option<(Vec,)> = + sqlx::query_as("SELECT seed FROM crdt_node_identity WHERE id = 1") + .fetch_optional(pool) + .await?; + + if let Some((seed,)) = row { + // Reconstruct from stored seed. The seed is the 32-byte private key. + if let Ok(kp) = Ed25519KeyPair::from_bytes(&seed) { + return Ok(kp); + } + slog!("[crdt] Stored keypair invalid, regenerating"); + } + + let kp = make_keypair(); + let seed = kp.as_bytes().to_vec(); + sqlx::query("INSERT INTO crdt_node_identity (id, seed) VALUES (1, ?1) ON CONFLICT(id) DO UPDATE SET seed = excluded.seed") + .bind(&seed) + .execute(pool) + .await?; + + Ok(kp) +} + +/// Rebuild the story_id → list index mapping from the current CRDT state. +fn rebuild_index(crdt: &BaseCrdt) -> HashMap { + let mut map = HashMap::new(); + for (i, item) in crdt.doc.items.iter().enumerate() { + if let JsonValue::String(ref sid) = item.story_id.view() { + map.insert(sid.clone(), i); + } + } + map +} + +// ── Write path ─────────────────────────────────────────────────────── + +/// Create a CRDT op via `op_fn`, sign it, apply it, and send it to the +/// persistence channel. The closure receives `&mut CrdtState` so it can +/// mutably access the CRDT document, while `sign` only needs `&keypair`. +fn apply_and_persist(state: &mut CrdtState, op_fn: F) +where + F: FnOnce(&mut CrdtState) -> bft_json_crdt::op::Op, +{ + let raw_op = op_fn(state); + let signed = raw_op.sign(&state.keypair); + state.crdt.apply(signed.clone()); + let _ = state.persist_tx.send(signed); +} + +/// Write a pipeline item state through CRDT operations. +/// +/// If the item exists, updates its registers. If not, inserts a new item +/// into the list. All ops are signed and persisted to SQLite. +pub fn write_item( + story_id: &str, + stage: &str, + name: Option<&str>, + agent: Option<&str>, + retry_count: Option, + blocked: Option, + depends_on: Option<&str>, +) { + let Some(state_mutex) = CRDT_STATE.get() else { + return; + }; + let Ok(mut state) = state_mutex.lock() else { + return; + }; + + if let Some(&idx) = state.index.get(story_id) { + // Update existing item registers. + // Each op is created, signed, applied, and persisted in a block so + // borrows do not overlap between &mut crdt (set) and &keypair (sign). + apply_and_persist(&mut state, |s| { + s.crdt.doc.items[idx].stage.set(stage.to_string()) + }); + + if let Some(n) = name { + apply_and_persist(&mut state, |s| { + s.crdt.doc.items[idx].name.set(n.to_string()) + }); + } + if let Some(a) = agent { + apply_and_persist(&mut state, |s| { + s.crdt.doc.items[idx].agent.set(a.to_string()) + }); + } + if let Some(rc) = retry_count { + apply_and_persist(&mut state, |s| { + s.crdt.doc.items[idx].retry_count.set(rc as f64) + }); + } + if let Some(b) = blocked { + apply_and_persist(&mut state, |s| { + s.crdt.doc.items[idx].blocked.set(b) + }); + } + if let Some(d) = depends_on { + apply_and_persist(&mut state, |s| { + s.crdt.doc.items[idx].depends_on.set(d.to_string()) + }); + } + } else { + // Insert new item. + let item_json: JsonValue = json!({ + "story_id": story_id, + "stage": stage, + "name": name.unwrap_or(""), + "agent": agent.unwrap_or(""), + "retry_count": retry_count.unwrap_or(0) as f64, + "blocked": blocked.unwrap_or(false), + "depends_on": depends_on.unwrap_or(""), + }) + .into(); + + apply_and_persist(&mut state, |s| { + s.crdt.doc.items.insert(ROOT_ID, item_json) + }); + + // Rebuild index after insertion (indices may shift). + state.index = rebuild_index(&state.crdt); + } +} + +// ── Read path ──────────────────────────────────────────────────────── + +/// Read the full pipeline state from the CRDT document. +/// +/// Returns items grouped by stage, or `None` if the CRDT layer is not +/// initialised. +pub fn read_all_items() -> Option> { + let state_mutex = CRDT_STATE.get()?; + let state = state_mutex.lock().ok()?; + + let mut items = Vec::new(); + for item_crdt in state.crdt.doc.items.iter() { + if let Some(view) = extract_item_view(item_crdt) { + items.push(view); + } + } + Some(items) +} + +/// Read a single pipeline item by story_id. +pub fn read_item(story_id: &str) -> Option { + let state_mutex = CRDT_STATE.get()?; + let state = state_mutex.lock().ok()?; + let &idx = state.index.get(story_id)?; + extract_item_view(&state.crdt.doc.items[idx]) +} + +/// Extract a `PipelineItemView` from a `PipelineItemCrdt`. +fn extract_item_view(item: &PipelineItemCrdt) -> Option { + let story_id = match item.story_id.view() { + JsonValue::String(s) if !s.is_empty() => s, + _ => return None, + }; + let stage = match item.stage.view() { + JsonValue::String(s) if !s.is_empty() => s, + _ => return None, + }; + let name = match item.name.view() { + JsonValue::String(s) if !s.is_empty() => Some(s), + _ => None, + }; + let agent = match item.agent.view() { + JsonValue::String(s) if !s.is_empty() => Some(s), + _ => None, + }; + let retry_count = match item.retry_count.view() { + JsonValue::Number(n) if n > 0.0 => Some(n as i64), + _ => None, + }; + let blocked = match item.blocked.view() { + JsonValue::Bool(b) => Some(b), + _ => None, + }; + let depends_on = match item.depends_on.view() { + JsonValue::String(s) if !s.is_empty() => { + serde_json::from_str::>(&s).ok() + } + _ => None, + }; + + Some(PipelineItemView { + story_id, + stage, + name, + agent, + retry_count, + blocked, + depends_on, + }) +} + +/// Hex-encode a byte slice (no external dep needed). +mod hex { + pub fn encode(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() + } +} + +// ── Tests ──────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use bft_json_crdt::json_crdt::OpState; + + #[test] + fn crdt_doc_insert_and_view() { + let kp = make_keypair(); + let mut crdt = BaseCrdt::::new(&kp); + + let item_json: JsonValue = json!({ + "story_id": "10_story_test", + "stage": "2_current", + "name": "Test Story", + "agent": "coder-opus", + "retry_count": 0.0, + "blocked": false, + "depends_on": "", + }) + .into(); + + let op = crdt.doc.items.insert(ROOT_ID, item_json).sign(&kp); + assert_eq!(crdt.apply(op), OpState::Ok); + + let view = crdt.doc.items.view(); + assert_eq!(view.len(), 1); + + let item = &crdt.doc.items[0]; + assert_eq!(item.story_id.view(), JsonValue::String("10_story_test".to_string())); + assert_eq!(item.stage.view(), JsonValue::String("2_current".to_string())); + } + + #[test] + fn crdt_doc_update_stage() { + let kp = make_keypair(); + let mut crdt = BaseCrdt::::new(&kp); + + let item_json: JsonValue = json!({ + "story_id": "20_story_move", + "stage": "1_backlog", + "name": "Move Me", + "agent": "", + "retry_count": 0.0, + "blocked": false, + "depends_on": "", + }) + .into(); + + let insert_op = crdt.doc.items.insert(ROOT_ID, item_json).sign(&kp); + crdt.apply(insert_op); + + // Update stage + let stage_op = crdt.doc.items[0].stage.set("2_current".to_string()).sign(&kp); + crdt.apply(stage_op); + + assert_eq!( + crdt.doc.items[0].stage.view(), + JsonValue::String("2_current".to_string()) + ); + } + + #[test] + fn crdt_ops_replay_reconstructs_state() { + let kp = make_keypair(); + let mut crdt1 = BaseCrdt::::new(&kp); + + // Build state with a series of ops. + let item_json: JsonValue = json!({ + "story_id": "30_story_replay", + "stage": "1_backlog", + "name": "Replay Test", + "agent": "", + "retry_count": 0.0, + "blocked": false, + "depends_on": "", + }) + .into(); + + let op1 = crdt1.doc.items.insert(ROOT_ID, item_json).sign(&kp); + crdt1.apply(op1.clone()); + + let op2 = crdt1.doc.items[0].stage.set("2_current".to_string()).sign(&kp); + crdt1.apply(op2.clone()); + + let op3 = crdt1.doc.items[0].name.set("Updated Name".to_string()).sign(&kp); + crdt1.apply(op3.clone()); + + // Replay ops on a fresh CRDT. + let mut crdt2 = BaseCrdt::::new(&kp); + crdt2.apply(op1); + crdt2.apply(op2); + crdt2.apply(op3); + + assert_eq!( + crdt1.doc.items[0].stage.view(), + crdt2.doc.items[0].stage.view() + ); + assert_eq!( + crdt1.doc.items[0].name.view(), + crdt2.doc.items[0].name.view() + ); + } + + #[test] + fn extract_item_view_parses_crdt_item() { + let kp = make_keypair(); + let mut crdt = BaseCrdt::::new(&kp); + + let item_json: JsonValue = json!({ + "story_id": "40_story_view", + "stage": "3_qa", + "name": "View Test", + "agent": "coder-1", + "retry_count": 2.0, + "blocked": true, + "depends_on": "[10,20]", + }) + .into(); + + let op = crdt.doc.items.insert(ROOT_ID, item_json).sign(&kp); + crdt.apply(op); + + let view = extract_item_view(&crdt.doc.items[0]).unwrap(); + assert_eq!(view.story_id, "40_story_view"); + assert_eq!(view.stage, "3_qa"); + assert_eq!(view.name.as_deref(), Some("View Test")); + assert_eq!(view.agent.as_deref(), Some("coder-1")); + assert_eq!(view.retry_count, Some(2)); + assert_eq!(view.blocked, Some(true)); + assert_eq!(view.depends_on, Some(vec![10, 20])); + } + + #[test] + fn rebuild_index_maps_story_ids() { + let kp = make_keypair(); + let mut crdt = BaseCrdt::::new(&kp); + + for (sid, stage) in &[("10_story_a", "1_backlog"), ("20_story_b", "2_current")] { + let item: JsonValue = json!({ + "story_id": sid, + "stage": stage, + "name": "", + "agent": "", + "retry_count": 0.0, + "blocked": false, + "depends_on": "", + }) + .into(); + let op = crdt.doc.items.insert(ROOT_ID, item).sign(&kp); + crdt.apply(op); + } + + let index = rebuild_index(&crdt); + assert_eq!(index.len(), 2); + assert!(index.contains_key("10_story_a")); + assert!(index.contains_key("20_story_b")); + } + + #[tokio::test] + async fn init_and_write_read_roundtrip() { + let tmp = tempfile::tempdir().unwrap(); + let db_path = tmp.path().join("crdt_test.db"); + + // Init directly (not via the global singleton, for test isolation). + let options = SqliteConnectOptions::new() + .filename(&db_path) + .create_if_missing(true); + let pool = SqlitePool::connect_with(options).await.unwrap(); + sqlx::migrate!("./migrations").run(&pool).await.unwrap(); + + let keypair = make_keypair(); + let mut crdt = BaseCrdt::::new(&keypair); + + // Insert and update like write_item does. + let item_json: JsonValue = json!({ + "story_id": "50_story_roundtrip", + "stage": "1_backlog", + "name": "Roundtrip", + "agent": "", + "retry_count": 0.0, + "blocked": false, + "depends_on": "", + }) + .into(); + + let insert_op = crdt.doc.items.insert(ROOT_ID, item_json).sign(&keypair); + crdt.apply(insert_op.clone()); + + // Persist the op. + let op_json = serde_json::to_string(&insert_op).unwrap(); + let op_id = hex::encode(&insert_op.id()); + let now = chrono::Utc::now().to_rfc3339(); + sqlx::query( + "INSERT INTO crdt_ops (op_id, seq, op_json, created_at) VALUES (?1, ?2, ?3, ?4)", + ) + .bind(&op_id) + .bind(insert_op.inner.seq as i64) + .bind(&op_json) + .bind(&now) + .execute(&pool) + .await + .unwrap(); + + // Reconstruct from DB. + let rows: Vec<(String,)> = + sqlx::query_as("SELECT op_json FROM crdt_ops ORDER BY seq ASC") + .fetch_all(&pool) + .await + .unwrap(); + + let mut crdt2 = BaseCrdt::::new(&keypair); + for (json_str,) in &rows { + let op: SignedOp = serde_json::from_str(json_str).unwrap(); + crdt2.apply(op); + } + + let view = extract_item_view(&crdt2.doc.items[0]).unwrap(); + assert_eq!(view.story_id, "50_story_roundtrip"); + assert_eq!(view.stage, "1_backlog"); + assert_eq!(view.name.as_deref(), Some("Roundtrip")); + } + + #[test] + fn signed_op_serialization_roundtrip() { + let kp = make_keypair(); + let mut crdt = BaseCrdt::::new(&kp); + + let item: JsonValue = json!({ + "story_id": "60_story_serde", + "stage": "1_backlog", + "name": "Serde Test", + "agent": "", + "retry_count": 0.0, + "blocked": false, + "depends_on": "", + }) + .into(); + + let op = crdt.doc.items.insert(ROOT_ID, item).sign(&kp); + let json_str = serde_json::to_string(&op).unwrap(); + let deserialized: SignedOp = serde_json::from_str(&json_str).unwrap(); + + assert_eq!(op.id(), deserialized.id()); + assert_eq!(op.inner.seq, deserialized.inner.seq); + } +} diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index e259a47c..607be1dc 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -87,17 +87,13 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> { Ok(()) } -/// Shadow-write a pipeline item move to SQLite. +/// Write a pipeline item state to both the CRDT layer and the legacy SQLite +/// shadow table. /// /// Reads front matter from `file_path` (the post-move location) to extract -/// metadata. The write is fire-and-forget — errors are logged but never -/// propagate to the caller. If the database has not been initialised this is a -/// complete no-op. +/// metadata. The CRDT layer is the primary write path; the legacy shadow +/// table is kept for backwards compatibility. Both writes are fire-and-forget. pub fn shadow_write(story_id: &str, stage: &str, file_path: &Path) { - let Some(db) = PIPELINE_DB.get() else { - return; - }; - let (name, agent, retry_count, blocked, depends_on) = match std::fs::read_to_string(file_path) { Ok(contents) => match parse_front_matter(&contents) { @@ -113,18 +109,30 @@ pub fn shadow_write(story_id: &str, stage: &str, file_path: &Path) { Err(_) => (None, None, None, None, None), }; - let msg = PipelineWriteMsg { - story_id: story_id.to_string(), - stage: stage.to_string(), - name, - agent, + // Primary: write through CRDT ops (persisted to SQLite crdt_ops table). + crate::crdt_state::write_item( + story_id, + stage, + name.as_deref(), + agent.as_deref(), retry_count, blocked, - depends_on, - }; + depends_on.as_deref(), + ); - // Ignore send errors: the background task may have exited (e.g. in tests). - let _ = db.tx.send(msg); + // Legacy: fire-and-forget to the pipeline_items shadow table. + if let Some(db) = PIPELINE_DB.get() { + let msg = PipelineWriteMsg { + story_id: story_id.to_string(), + stage: stage.to_string(), + name, + agent, + retry_count, + blocked, + depends_on, + }; + let _ = db.tx.send(msg); + } } #[cfg(test)] diff --git a/server/src/http/workflow/mod.rs b/server/src/http/workflow/mod.rs index 952bb10e..adb28a50 100644 --- a/server/src/http/workflow/mod.rs +++ b/server/src/http/workflow/mod.rs @@ -72,8 +72,62 @@ pub struct PipelineState { } /// Load the full pipeline state (all 5 active stages). +/// +/// Reads from the CRDT document when available, falling back to the +/// filesystem for any items not yet in the CRDT (e.g. first run before +/// migration). Agent assignments are always overlaid from the in-memory +/// agent pool. pub fn load_pipeline_state(ctx: &AppContext) -> Result { let agent_map = build_active_agent_map(ctx); + + // Try CRDT-first read. + if let Some(crdt_items) = crate::crdt_state::read_all_items() { + let mut state = PipelineState { + backlog: Vec::new(), + current: Vec::new(), + qa: Vec::new(), + merge: Vec::new(), + done: Vec::new(), + }; + + for item in crdt_items { + let agent = agent_map.get(&item.story_id).cloned(); + let story = UpcomingStory { + story_id: item.story_id, + name: item.name, + error: None, + merge_failure: None, + agent, + review_hold: None, + qa: None, + retry_count: item.retry_count.map(|r| r as u32), + blocked: item.blocked, + depends_on: item.depends_on, + }; + match item.stage.as_str() { + "1_backlog" => state.backlog.push(story), + "2_current" => state.current.push(story), + "3_qa" => state.qa.push(story), + "4_merge" => state.merge.push(story), + "5_done" => state.done.push(story), + _ => {} // ignore archived or unknown stages + } + } + + // Sort each stage for deterministic output. + state.backlog.sort_by(|a, b| a.story_id.cmp(&b.story_id)); + state.current.sort_by(|a, b| a.story_id.cmp(&b.story_id)); + state.qa.sort_by(|a, b| a.story_id.cmp(&b.story_id)); + state.merge.sort_by(|a, b| a.story_id.cmp(&b.story_id)); + state.done.sort_by(|a, b| a.story_id.cmp(&b.story_id)); + + // Merge in any filesystem-only items not yet in the CRDT. + merge_filesystem_items(ctx, &mut state, &agent_map)?; + + return Ok(state); + } + + // Fallback: filesystem-only read (CRDT not initialised). Ok(PipelineState { backlog: load_stage_items(ctx, "1_backlog", &HashMap::new())?, current: load_stage_items(ctx, "2_current", &agent_map)?, @@ -83,6 +137,38 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result { }) } +/// Merge filesystem items that are not already present in the CRDT state. +fn merge_filesystem_items( + ctx: &AppContext, + state: &mut PipelineState, + agent_map: &HashMap, +) -> Result<(), String> { + let stages = [ + ("1_backlog", &mut state.backlog), + ("2_current", &mut state.current), + ("3_qa", &mut state.qa), + ("4_merge", &mut state.merge), + ("5_done", &mut state.done), + ]; + + for (stage_dir, stage_vec) in stages { + let empty_map = HashMap::new(); + let map = if stage_dir == "2_current" || stage_dir == "3_qa" || stage_dir == "4_merge" { + agent_map + } else { + &empty_map + }; + let fs_items = load_stage_items(ctx, stage_dir, map)?; + for fs_item in fs_items { + if !stage_vec.iter().any(|s| s.story_id == fs_item.story_id) { + stage_vec.push(fs_item); + } + } + stage_vec.sort_by(|a, b| a.story_id.cmp(&b.story_id)); + } + Ok(()) +} + /// Build a map from story_id → AgentAssignment for all pending/running agents. fn build_active_agent_map(ctx: &AppContext) -> HashMap { let agents = match ctx.agents.list_agents() { diff --git a/server/src/main.rs b/server/src/main.rs index a0ab3328..6d0e2c0e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,6 +6,7 @@ mod agent_log; mod agents; mod chat; mod config; +pub mod crdt_state; mod db; mod http; mod io; @@ -283,7 +284,7 @@ async fn main() -> Result<(), std::io::Error> { log_buffer::global().set_log_file(log_dir.join("server.log")); } - // Initialise the SQLite pipeline shadow-write database. + // Initialise the SQLite pipeline shadow-write database and CRDT state layer. // Clone the path out before the await so we don't hold the MutexGuard across // an await point. let pipeline_db_path = app_state @@ -292,10 +293,13 @@ async fn main() -> Result<(), std::io::Error> { .unwrap() .as_ref() .map(|root| root.join(".huskies").join("pipeline.db")); - if let Some(db_path) = pipeline_db_path - && let Err(e) = db::init(&db_path).await - { - slog!("[db] Failed to initialise pipeline.db: {e}"); + if let Some(ref db_path) = pipeline_db_path { + if let Err(e) = db::init(db_path).await { + slog!("[db] Failed to initialise pipeline.db: {e}"); + } + if let Err(e) = crdt_state::init(db_path).await { + slog!("[crdt] Failed to initialise CRDT state layer: {e}"); + } } let workflow = Arc::new(std::sync::Mutex::new(WorkflowState::default()));