diff --git a/code/Cargo.lock b/code/Cargo.lock index b1f9366b5..4b09d2389 100644 --- a/code/Cargo.lock +++ b/code/Cargo.lock @@ -4,24 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "advisory-lock" @@ -70,14 +64,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -118,9 +112,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -133,44 +127,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arbitrary" @@ -234,9 +228,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -246,9 +240,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" dependencies = [ "async-lock", "cfg-if", @@ -257,10 +251,9 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.44", + "rustix", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -317,20 +310,21 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attohttpc" -version = "0.24.1" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "http 0.2.12", + "base64 0.22.1", + "http", "log", "url", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -342,7 +336,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.3.1", + "http", "http-body", "http-body-util", "hyper", @@ -375,7 +369,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.3.1", + "http", "http-body", "http-body-util", "mime", @@ -389,17 +383,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -434,9 +428,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97d56060ee67d285efb8001fec9d2a4c710c32efd2e14b5cbb5ba71930fc2d" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "beef" @@ -452,9 +446,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "blake2" @@ -540,9 +534,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -561,18 +555,18 @@ dependencies = [ [[package]] name = "bytesize" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" dependencies = [ "serde", ] [[package]] name = "camino" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" dependencies = [ "serde", ] @@ -616,9 +610,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.16" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "shlex", ] @@ -661,9 +655,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -712,9 +706,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" dependencies = [ "clap_builder", "clap_derive", @@ -722,9 +716,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" dependencies = [ "anstream", "anstyle", @@ -734,9 +728,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -746,15 +740,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "color-eyre" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" dependencies = [ "backtrace", "color-spantrace", @@ -767,9 +761,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" dependencies = [ "once_cell", "owo-colors", @@ -779,9 +773,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -928,9 +922,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1006,9 +1000,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -1016,9 +1010,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -1030,9 +1024,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -1055,15 +1049,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-encoding-macro" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9724adfcf41f45bf652b3995837669d73c4d49a1b5ac1ff82905ac7d9b5558" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1071,9 +1065,9 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", "syn", @@ -1081,9 +1075,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -1106,9 +1100,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -1190,6 +1184,12 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +[[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" @@ -1231,14 +1231,14 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -1289,12 +1289,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1310,9 +1310,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", @@ -1358,12 +1358,12 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", - "miniz_oxide 0.8.5", + "miniz_oxide", ] [[package]] @@ -1374,9 +1374,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" @@ -1531,15 +1531,16 @@ checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" [[package]] name = "generator" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" dependencies = [ + "cc", "cfg-if", "libc", "log", "rustversion", - "windows 0.58.0", + "windows 0.61.3", ] [[package]] @@ -1555,27 +1556,29 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1590,9 +1593,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -1613,17 +1616,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", - "indexmap 2.8.0", + "http", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -1632,9 +1635,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -1657,9 +1660,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -1683,15 +1686,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1724,9 +1721,9 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.0", + "rand 0.9.2", "ring", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tinyvec", "tokio", @@ -1747,7 +1744,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.0", + "rand 0.9.2", "resolv-conf", "smallvec", "thiserror 2.0.12", @@ -1773,28 +1770,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -1813,7 +1788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -1824,7 +1799,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http", "http-body", "pin-project-lite", ] @@ -1867,7 +1842,7 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http 1.3.1", + "http", "http-body", "httparse", "httpdate", @@ -1880,18 +1855,20 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", - "http 1.3.1", + "http", "http-body", "hyper", + "libc", "pin-project-lite", - "socket2", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -1899,16 +1876,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.2", ] [[package]] @@ -1922,21 +1900,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1945,31 +1924,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1977,67 +1936,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2057,9 +2003,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -2100,20 +2046,20 @@ dependencies = [ [[package]] name = "igd-next" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06464e726471718db9ad3fefc020529fabcde03313a0fc3967510e2db5add12" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" dependencies = [ "async-trait", "attohttpc", "bytes", "futures", - "http 1.3.1", + "http", "http-body-util", "hyper", "hyper-util", "log", - "rand 0.9.0", + "rand 0.9.2", "tokio", "url", "xmltree", @@ -2138,15 +2084,39 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "serde", ] +[[package]] +name = "informalsystems-malachitebft-actor-test" +version = "0.5.0-pre" +dependencies = [ + "async-trait", + "axum", + "bytesize", + "eyre", + "informalsystems-malachitebft-config", + "informalsystems-malachitebft-core-consensus", + "informalsystems-malachitebft-core-types", + "informalsystems-malachitebft-engine", + "informalsystems-malachitebft-metrics", + "informalsystems-malachitebft-test-framework", + "informalsystems_malachitebft_actor_app_proposal", + "ractor", + "rand 0.8.5", + "serde_json", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "informalsystems-malachitebft-app" version = "0.5.0-pre" @@ -2692,7 +2662,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "serde_with 3.12.0", + "serde_with 3.14.0", "tempfile", ] @@ -2731,6 +2701,52 @@ dependencies = [ "testdir", ] +[[package]] +name = "informalsystems_malachitebft_actor_app_proposal" +version = "0.3.0-pre" +dependencies = [ + "async-trait", + "bytes", + "bytesize", + "clap", + "color-eyre", + "config", + "derive-where", + "eyre", + "hex", + "informalsystems-malachitebft-app", + "informalsystems-malachitebft-codec", + "informalsystems-malachitebft-config", + "informalsystems-malachitebft-core-consensus", + "informalsystems-malachitebft-core-types", + "informalsystems-malachitebft-engine", + "informalsystems-malachitebft-metrics", + "informalsystems-malachitebft-network", + "informalsystems-malachitebft-proto", + "informalsystems-malachitebft-signing-ed25519", + "informalsystems-malachitebft-sync", + "informalsystems-malachitebft-test-cli", + "informalsystems-malachitebft-test-mempool", + "informalsystems-malachitebft-wal", + "itertools 0.14.0", + "libp2p-identity", + "prost", + "prost-build", + "protox", + "ractor", + "rand 0.8.5", + "redb", + "serde", + "serde_json", + "sha3", + "tempfile", + "thiserror 2.0.12", + "tikv-jemallocator", + "tokio", + "toml", + "tracing", +] + [[package]] name = "inout" version = "0.1.4" @@ -2742,11 +2758,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "libc", ] @@ -2757,7 +2773,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.10", "widestring", "windows-sys 0.48.0", "winreg", @@ -2812,7 +2828,7 @@ dependencies = [ "num-traits", "serde", "serde_json", - "serde_with 3.12.0", + "serde_with 3.14.0", ] [[package]] @@ -2848,7 +2864,7 @@ checksum = "bbc2a4da0d9e52ccfe6306801a112e81a8fc0c76aa3e4449fefeda7fef72bb34" dependencies = [ "lambdaworks-math", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", ] @@ -2870,9 +2886,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libp2p" @@ -2884,7 +2900,7 @@ dependencies = [ "either", "futures", "futures-timer", - "getrandom 0.2.15", + "getrandom 0.2.16", "libp2p-allow-block-list", "libp2p-connection-limits", "libp2p-core", @@ -2987,7 +3003,7 @@ dependencies = [ "fnv", "futures", "futures-timer", - "getrandom 0.2.15", + "getrandom 0.2.16", "hashlink", "hex_fmt", "libp2p-core", @@ -2999,7 +3015,7 @@ dependencies = [ "rand 0.8.5", "regex", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "tracing", "web-time", ] @@ -3040,7 +3056,7 @@ dependencies = [ "rand 0.8.5", "sec1", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "thiserror 2.0.12", "tracing", "zeroize", @@ -3066,7 +3082,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "smallvec", "thiserror 2.0.12", "tracing", @@ -3088,7 +3104,7 @@ dependencies = [ "libp2p-swarm", "rand 0.8.5", "smallvec", - "socket2", + "socket2 0.5.10", "tokio", "tracing", ] @@ -3167,7 +3183,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustls", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -3251,7 +3267,7 @@ dependencies = [ "if-watch", "libc", "libp2p-core", - "socket2", + "socket2 0.5.10", "tokio", "tracing", ] @@ -3269,7 +3285,7 @@ dependencies = [ "rcgen", "ring", "rustls", - "rustls-webpki 0.103.3", + "rustls-webpki", "thiserror 2.0.12", "x509-parser", "yasna", @@ -3302,42 +3318,36 @@ dependencies = [ "thiserror 2.0.12", "tracing", "yamux 0.12.1", - "yamux 0.13.4", + "yamux 0.13.6", ] [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", ] [[package]] name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -3345,9 +3355,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "logos" @@ -3402,9 +3412,15 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lz4_flex" version = "0.11.5" @@ -3414,12 +3430,6 @@ dependencies = [ "twox-hash", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "matchers" version = "0.1.0" @@ -3437,9 +3447,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miette" @@ -3477,31 +3487,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -3566,9 +3567,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "multistream-select" @@ -3665,7 +3666,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", @@ -3743,19 +3744,19 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] [[package]] name = "object" -version = "0.32.2" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -3771,14 +3772,20 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" dependencies = [ "critical-section", "portable-atomic", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "oorandom" version = "11.1.5" @@ -3805,9 +3812,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" [[package]] name = "p256" @@ -3818,7 +3825,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -3829,9 +3836,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -3839,9 +3846,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -3894,7 +3901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.8.0", + "indexmap 2.10.0", ] [[package]] @@ -3969,17 +3976,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.4" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", - "rustix 0.38.44", - "tracing", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.60.2", ] [[package]] @@ -4007,9 +4013,18 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -4023,7 +4038,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.23", + "zerocopy", ] [[package]] @@ -4038,9 +4053,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.30" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn", @@ -4066,9 +4081,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -4187,12 +4202,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-protobuf" version = "0.8.1" @@ -4217,32 +4226,35 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", + "cfg_aliases", "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.9" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", "ring", "rustc-hash", "rustls", @@ -4256,14 +4268,14 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -4277,6 +4289,12 @@ 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 = "ractor" version = "0.14.7" @@ -4306,13 +4324,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.23", ] [[package]] @@ -4341,7 +4358,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4350,7 +4367,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", ] [[package]] @@ -4397,11 +4414,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -4410,11 +4427,31 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.11.1" @@ -4461,13 +4498,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resolv-conf" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", -] +checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" [[package]] name = "rfc6979" @@ -4487,7 +4520,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -4513,9 +4546,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -4543,69 +4576,46 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys 0.9.2", - "windows-sys 0.59.0", + "linux-raw-sys", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -4614,9 +4624,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rw-stream-sink" @@ -4644,6 +4654,30 @@ dependencies = [ "winapi-util", ] +[[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.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -4707,9 +4741,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -4740,9 +4774,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -4776,19 +4810,21 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.8.0", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", - "serde_with_macros 3.12.0", + "serde_with_macros 3.14.0", "time", ] @@ -4806,9 +4842,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", @@ -4831,9 +4867,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -4867,9 +4903,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -4884,20 +4920,23 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "size-of" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e36eca171fddeda53901b0a436573b3f2391eaa9189d439b2bd8ea8cebd7e3" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snow" @@ -4912,20 +4951,30 @@ dependencies = [ "rand_core 0.6.4", "ring", "rustc_version", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", ] [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spki" version = "0.7.3" @@ -4974,7 +5023,7 @@ dependencies = [ "num-integer", "num-traits", "rfc6979", - "sha2 0.10.8", + "sha2 0.10.9", "starknet-curve", "starknet-types-core", "zeroize", @@ -4991,16 +5040,20 @@ dependencies = [ [[package]] name = "starknet-types-core" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1b9e01ccb217ab6d475c5cda05dbb22c30029f7bb52b192a010a00d77a3d74" +checksum = "87af771d7f577931913089f9ca9a9f85d8a6238d59b2977f4c383d133c8abd3b" dependencies = [ + "blake2", + "digest 0.10.7", "lambdaworks-crypto", "lambdaworks-math", "num-bigint", "num-integer", "num-traits", "serde", + "size-of", + "zeroize", ] [[package]] @@ -5051,9 +5104,9 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -5068,9 +5121,9 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -5097,7 +5150,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys", ] @@ -5120,14 +5173,14 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.2", + "rustix", "windows-sys 0.59.0", ] @@ -5188,12 +5241,11 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -5218,9 +5270,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -5233,15 +5285,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -5249,9 +5301,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -5284,9 +5336,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" dependencies = [ "backtrace", "bytes", @@ -5297,10 +5349,10 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5316,9 +5368,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -5329,9 +5381,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -5341,20 +5393,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.25" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", @@ -5364,9 +5416,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" @@ -5422,9 +5474,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -5433,9 +5485,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -5574,12 +5626,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5594,11 +5640,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.15.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -5634,15 +5682,15 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -5733,9 +5781,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", @@ -5744,9 +5792,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" @@ -5791,21 +5839,24 @@ dependencies = [ [[package]] name = "windows" -version = "0.58.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.61.2", ] [[package]] @@ -5820,22 +5871,33 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.58.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-result 0.2.0", + "windows-link", + "windows-result 0.3.4", "windows-strings", - "windows-targets 0.52.6", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -5844,9 +5906,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", @@ -5855,9 +5917,19 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] [[package]] name = "windows-result" @@ -5870,21 +5942,20 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -5914,6 +5985,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5938,13 +6018,39 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5957,6 +6063,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -5969,6 +6081,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -5981,12 +6099,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5999,6 +6129,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6011,6 +6147,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6023,6 +6165,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6035,11 +6183,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.7" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -6056,24 +6210,18 @@ dependencies = [ [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x25519-dalek" @@ -6106,9 +6254,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xmltree" @@ -6136,16 +6284,16 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.4" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17610762a1207ee816c6fadc29220904753648aba0a9ed61c7b8336e80a559c4" +checksum = "2b2dd50a6d6115feb3e5d7d0efd45e8ca364b6c83722c1e9c602f5764e0e9597" dependencies = [ "futures", "log", "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.9.2", "static_assertions", "web-time", ] @@ -6167,9 +6315,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -6179,9 +6327,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -6191,38 +6339,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.23" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.8.23", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", @@ -6270,11 +6398,22 @@ dependencies = [ "syn", ] +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -6283,9 +6422,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/code/Cargo.toml b/code/Cargo.toml index ecf3edb67..6809c9528 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -37,6 +37,8 @@ members = [ # Examples "examples/channel", + "examples/actor/proposal", + "examples/actor/test", ] [workspace.package] diff --git a/code/crates/app-channel/src/connector.rs b/code/crates/app-channel/src/connector.rs index b0fc1bece..f71616cc9 100644 --- a/code/crates/app-channel/src/connector.rs +++ b/code/crates/app-channel/src/connector.rs @@ -200,6 +200,28 @@ where } } + HostMsg::ReceivedProposal { + proposer, + height, + round, + value, + reply_to, + } => { + let (reply, rx) = oneshot::channel(); + + self.sender + .send(AppMsg::ReceivedProposal { + proposer, + height, + round, + value, + reply, + }) + .await?; + + reply_to.send(rx.await?)?; + } + HostMsg::GetValidatorSet { height, reply_to } => { let (reply, rx) = oneshot::channel(); diff --git a/code/crates/app-channel/src/msgs.rs b/code/crates/app-channel/src/msgs.rs index 72e4ea447..e9e530869 100644 --- a/code/crates/app-channel/src/msgs.rs +++ b/code/crates/app-channel/src/msgs.rs @@ -141,6 +141,22 @@ pub enum AppMsg { reply: Reply>>, }, + /// Notifies the application that consensus has received a proposal over the network. + /// + /// The application MUST respond with the complete proposed value. + ReceivedProposal { + /// Peer whom the proposal was received from + proposer: Ctx::Address, + /// Height of the proposal + height: Ctx::Height, + /// Round of the proposal + round: Round, + /// Value of the proposal + value: Ctx::Value, + /// Channel for returning validity of the proposal + reply: Reply>, + }, + /// Requests the validator set for a specific height GetValidatorSet { /// Height of the validator set to retrieve diff --git a/code/crates/core-consensus/src/handle/proposal.rs b/code/crates/core-consensus/src/handle/proposal.rs index 33908d0c2..1bbdef8d0 100644 --- a/code/crates/core-consensus/src/handle/proposal.rs +++ b/code/crates/core-consensus/src/handle/proposal.rs @@ -3,7 +3,7 @@ use crate::handle::signature::verify_signature; use crate::handle::validator_set::get_validator_set; use crate::input::Input; use crate::prelude::*; -use crate::types::{ConsensusMsg, ProposedValue, SignedConsensusMsg, WalEntry}; +use crate::types::{ConsensusMsg, SignedConsensusMsg, WalEntry}; use crate::util::pretty::PrettyProposal; /// Handles an incoming consensus proposal message. @@ -100,21 +100,6 @@ where ); } - if state.params.value_payload.proposal_only() { - // TODO - pass the received value up to the host that will verify and give back validity and extension. - // Currently starknet Context defines value as BlockHash, we need a PoC app for this. - let new_value = ProposedValue { - height: signed_proposal.height(), - round: signed_proposal.round(), - valid_round: signed_proposal.pol_round(), - proposer: signed_proposal.validator_address().clone(), - value: signed_proposal.value().clone(), - validity: Validity::Valid, - }; - - state.store_value(&new_value); - } - if let Some(full_proposal) = state.full_proposal_at_round_and_value( &proposal_height, proposal_round, diff --git a/code/crates/engine/src/consensus.rs b/code/crates/engine/src/consensus.rs index 7b8dea410..404938f7c 100644 --- a/code/crates/engine/src/consensus.rs +++ b/code/crates/engine/src/consensus.rs @@ -569,12 +569,35 @@ where return Ok(()); } + let proposal_clone = proposal.clone(); + if let Err(e) = self .process_input(&myself, state, ConsensusInput::Proposal(proposal)) .await { error!(%from, "Error when processing proposal: {e}"); } + + if state.consensus.params.value_payload.proposal_only() { + self.host + .call_and_forward( + |reply_to| HostMsg::ReceivedProposal { + proposer: proposal_clone.validator_address().clone(), + height: proposal_clone.height(), + round: proposal_clone.round(), + value: proposal_clone.value().clone(), + reply_to, + }, + &myself, + |value| { + Msg::ReceivedProposedValue(value, ValueOrigin::Consensus) + }, + None, + ) + .map_err(|e| { + eyre!("Error when forwarding proposal to host: {e}") + })?; + } } NetworkEvent::PolkaCertificate(from, certificate) => { diff --git a/code/crates/engine/src/host.rs b/code/crates/engine/src/host.rs index a5ece4712..0efbce6e1 100644 --- a/code/crates/engine/src/host.rs +++ b/code/crates/engine/src/host.rs @@ -136,7 +136,16 @@ pub enum HostMsg { reply_to: RpcReplyPort>, }, - /// Requests the validator set for a specific height + /// Received a proposal from a validator + ReceivedProposal { + proposer: Ctx::Address, + height: Ctx::Height, + round: Round, + value: Ctx::Value, + reply_to: RpcReplyPort>, + }, + + /// Get the validator set at a given height GetValidatorSet { height: Ctx::Height, reply_to: RpcReplyPort>, diff --git a/code/crates/starknet/host/src/actor.rs b/code/crates/starknet/host/src/actor.rs index 0dc713cba..5946a2f54 100644 --- a/code/crates/starknet/host/src/actor.rs +++ b/code/crates/starknet/host/src/actor.rs @@ -208,6 +208,10 @@ impl Host { value_bytes, reply_to, } => on_process_synced_value(value_bytes, height, round, proposer, reply_to), + + HostMsg::ReceivedProposal { .. } => { + panic!("ProposalOnly not supported"); + } } } } diff --git a/code/crates/starknet/test/src/lib.rs b/code/crates/starknet/test/src/lib.rs index 8fbfa3460..cdefc5432 100644 --- a/code/crates/starknet/test/src/lib.rs +++ b/code/crates/starknet/test/src/lib.rs @@ -49,7 +49,7 @@ pub struct TestRunner { fn temp_dir(id: NodeId) -> PathBuf { TempDir::with_prefix(format!("malachitebft-test-app-{id}-")) .unwrap() - .into_path() + .keep() } #[async_trait] diff --git a/code/crates/test/app/src/app.rs b/code/crates/test/app/src/app.rs index c33cdca83..83ffa33f5 100644 --- a/code/crates/test/app/src/app.rs +++ b/code/crates/test/app/src/app.rs @@ -363,6 +363,10 @@ pub async fn run( error!("Failed to send VerifyVoteExtension reply"); } } + + AppMsg::ReceivedProposal { .. } => { + panic!("ReceivedProposal should not be called in test app"); + } } } diff --git a/code/crates/test/framework/src/node.rs b/code/crates/test/framework/src/node.rs index a33b33bca..e3e48956a 100644 --- a/code/crates/test/framework/src/node.rs +++ b/code/crates/test/framework/src/node.rs @@ -160,6 +160,26 @@ where }) } + pub fn expect_wal_replay_at_crash_height(&mut self, get_height: F) -> &mut Self + where + F: Fn(&State) -> Option + Send + Sync + 'static, + { + self.on_event(move |event, state| { + let Event::WalReplayBegin(height, count) = event else { + return Ok(HandlerResult::WaitForNextEvent); + }; + + let expected_height = get_height(state).expect("Should have recorded crash height"); + info!("Replaying WAL at height {height} with {count} messages"); + + if height.as_u64() != expected_height { + bail!("Unexpected WAL replay at height {height}, expected {expected_height}") + } + + Ok(HandlerResult::ContinueTest) + }) + } + pub fn expect_vote_rebroadcast( &mut self, at_height: u64, diff --git a/code/crates/test/tests/it/main.rs b/code/crates/test/tests/it/main.rs index 9dfffba98..8479e2e5c 100644 --- a/code/crates/test/tests/it/main.rs +++ b/code/crates/test/tests/it/main.rs @@ -56,7 +56,7 @@ pub struct TestRunner { fn temp_dir(id: NodeId) -> PathBuf { TempDir::with_prefix(format!("malachitebft-test-app-{id}")) .unwrap() - .into_path() + .keep() } #[derive(Clone)] diff --git a/code/examples/actor/proposal/Cargo.toml b/code/examples/actor/proposal/Cargo.toml new file mode 100644 index 000000000..21849fece --- /dev/null +++ b/code/examples/actor/proposal/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "informalsystems_malachitebft_actor_app_proposal" +version = "0.3.0-pre" +edition = "2021" +publish = false + +[[bin]] +name = "actor-app-proposal" +path = "app/src/main.rs" + +[dependencies] +malachitebft-test-cli = { workspace = true } +malachitebft-engine = { workspace = true } +malachitebft-app = { workspace = true } +malachitebft-codec = { workspace = true } +malachitebft-config = { workspace = true } +malachitebft-core-consensus = { workspace = true, features = ["debug"] } +malachitebft-core-types = { workspace = true } +malachitebft-network = { workspace = true } +malachitebft-metrics = { workspace = true } +malachitebft-proto = { workspace = true } +malachitebft-signing-ed25519 = { workspace = true, features = ["serde", "rand"] } +malachitebft-sync = { workspace = true } +malachitebft-test-mempool = { workspace = true } +malachitebft-wal = { workspace = true } + +async-trait = { workspace = true } +bytes = { workspace = true, features = ["serde"] } +bytesize = { workspace = true } +color-eyre = { workspace = true } +config = { workspace = true } +derive-where = { workspace = true } +eyre = { workspace = true } +itertools = { workspace = true } +libp2p-identity = { workspace = true } +prost = { workspace = true } +ractor = { workspace = true } +rand = { workspace = true } +redb = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha3 = { workspace = true } +toml = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +hex = { version = "0.4", features = ["serde"] } + +[lints] +workspace = true + +[dev-dependencies] +tempfile = { workspace = true } +clap = { workspace = true } + +[target.'cfg(target_os = "linux")'.dependencies] +tikv-jemallocator = { workspace = true, features = ["background_threads"] } + +[build-dependencies] +prost-build = { workspace = true } +protox = { workspace = true } \ No newline at end of file diff --git a/code/examples/actor/proposal/README.md b/code/examples/actor/proposal/README.md new file mode 100644 index 000000000..e9c51587d --- /dev/null +++ b/code/examples/actor/proposal/README.md @@ -0,0 +1,55 @@ + +# Example actor-based app + +This is an example application using the actor based integration with malachite + +## Run a local testnet + +### Prerequisites + +Before running the examples, make sure you have the required development environment setup as specified in this [Setup](../../../CONTRIBUTING_CODE.md#setup) section. + +### Compile the app + +``` +cargo build +``` + +### Setup the testnet + +Generate configuration and genesis for three nodes using the `testnet` command: + +``` +cargo run --bin actor-app-proposal -- testnet --nodes 3 --home nodes +``` + +This will create the configuration for three nodes in the `nodes` folder. Feel free to inspect this folder and look at the generated files. + +### Spawn the nodes + +``` +bash ./examples/actor/proposal/spawn.bash --nodes 3 --home nodes +``` + +If successful, the logs for each node can then be found at `nodes/X/logs/node.log`. + +``` +tail -f nodes/0/logs/node.log +``` + +Check the metrics + +For the block time: + +``` +curl -s localhost:29000/metrics | grep 'time_per_block_[sum|count]' +``` + +For the number of rounds per block: + +``` +curl -s localhost:29000/metrics | grep consensus_round +``` + +Press `Ctrl-C` to stop all the nodes. + diff --git a/code/examples/actor/proposal/app/src/main.rs b/code/examples/actor/proposal/app/src/main.rs new file mode 100644 index 000000000..7738bdae7 --- /dev/null +++ b/code/examples/actor/proposal/app/src/main.rs @@ -0,0 +1,189 @@ +use color_eyre::eyre::Context; + +use informalsystems_malachitebft_actor_app_proposal::codec::ProtobufCodec; +use informalsystems_malachitebft_actor_app_proposal::node::{ActorNode, ConfigSource}; +use malachitebft_app::node::Node; +use malachitebft_config::{LogFormat, LogLevel}; +use malachitebft_test_cli::args::{Args, Commands}; +use malachitebft_test_cli::{logging, runtime}; + +// Use jemalloc on Linux +#[cfg(target_os = "linux")] +#[global_allocator] +static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + +pub fn main() -> color_eyre::Result<()> { + color_eyre::install().expect("Failed to install global error handler"); + + // Load command-line arguments and possible configuration file. + let args = Args::new(); + + let home_dir = args.get_home_dir()?; + let config_file = args.get_config_file_path()?; + + match &args.command { + Commands::Start(cmd) => { + // Redefine the node with the valid configuration. + let node = ActorNode::new(home_dir, ConfigSource::File(config_file), cmd.start_height); + + let config = node.load_config()?; + + // This is a drop guard responsible for flushing any remaining logs when the program terminates. + // It must be assigned to a binding that is not _, as _ will result in the guard being dropped immediately. + let _guard = logging::init(config.logging.log_level, config.logging.log_format); + + let metrics = config.metrics.enabled.then(|| config.metrics.clone()); + + let rt = runtime::build_runtime(config.runtime)?; + + rt.block_on(cmd.run(node, metrics)) + .wrap_err("Failed to run `start` command") + } + + Commands::Init(cmd) => { + let _guard = logging::init(LogLevel::Info, LogFormat::Plaintext); + + let node = &ActorNode::new(home_dir.clone(), ConfigSource::Default, None); + + cmd.run( + node, + &config_file, + &args.get_genesis_file_path().unwrap(), + &args.get_priv_validator_key_file_path().unwrap(), + ) + .wrap_err("Failed to run `init` command") + } + + Commands::Testnet(cmd) => { + let _guard = logging::init(LogLevel::Info, LogFormat::Plaintext); + + let node = &ActorNode { + home_dir: home_dir.clone(), + config_source: ConfigSource::Default, + start_height: None, + }; + + cmd.run(node, &home_dir) + .wrap_err("Failed to run `testnet` command") + } + + Commands::DistributedTestnet(cmd) => { + let _guard = logging::init(LogLevel::Info, LogFormat::Plaintext); + + let node = &ActorNode { + home_dir: home_dir.clone(), + config_source: ConfigSource::Default, + start_height: None, + }; + + cmd.run(node, &home_dir) + .wrap_err("Failed to run `distributed-testnet` command") + } + + Commands::DumpWal(cmd) => { + let _guard = logging::init(LogLevel::Info, LogFormat::Plaintext); + + cmd.run(ProtobufCodec) + .wrap_err("Failed to run `dump-wal` command") + } + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use clap::Parser; + use color_eyre::eyre; + use color_eyre::eyre::eyre; + + use malachitebft_test_cli::args::{Args, Commands}; + use malachitebft_test_cli::cmd::init::*; + + use informalsystems_malachitebft_actor_app_proposal::node::{ActorNode, ConfigSource}; + + #[test] + fn running_init_creates_config_files() -> eyre::Result<()> { + let tmp = tempfile::tempdir()?; + let config_dir = tmp.path().join("config"); + + let args = Args::parse_from(["test", "--home", tmp.path().to_str().unwrap(), "init"]); + let cmd = InitCmd::default(); + + let node = &ActorNode { + home_dir: tmp.path().to_owned(), + config_source: ConfigSource::Default, + start_height: None, + }; + + cmd.run( + node, + &args.get_config_file_path().unwrap(), + &args.get_genesis_file_path().unwrap(), + &args.get_priv_validator_key_file_path().unwrap(), + ) + .expect("Failed to run init command"); + + let files = fs::read_dir(&config_dir)?.flatten().collect::>(); + + assert!(has_file(&files, &config_dir.join("config.toml"))); + assert!(has_file(&files, &config_dir.join("genesis.json"))); + assert!(has_file( + &files, + &config_dir.join("priv_validator_key.json") + )); + + Ok(()) + } + + #[test] + fn running_testnet_creates_all_configs() -> eyre::Result<()> { + let tmp = tempfile::tempdir()?; + + let args = Args::parse_from([ + "test", + "--home", + tmp.path().to_str().unwrap(), + "testnet", + "--nodes", + "3", + ]); + + let Commands::Testnet(ref cmd) = args.command else { + return Err(eyre!("not testnet command")); + }; + + let node = &ActorNode { + home_dir: tmp.path().to_owned(), + config_source: ConfigSource::Default, + start_height: None, + }; + + cmd.run(node, &args.get_home_dir().unwrap()) + .expect("Failed to run init command"); + + let files = fs::read_dir(&tmp)?.flatten().collect::>(); + + assert_eq!(files.len(), 3); + + assert!(has_file(&files, &tmp.path().join("0"))); + assert!(has_file(&files, &tmp.path().join("1"))); + assert!(has_file(&files, &tmp.path().join("2"))); + + for node in 0..3 { + let node_dir = tmp.path().join(node.to_string()).join("config"); + let files = fs::read_dir(&node_dir)?.flatten().collect::>(); + + assert!(has_file(&files, &node_dir.join("config.toml"))); + assert!(has_file(&files, &node_dir.join("genesis.json"))); + assert!(has_file(&files, &node_dir.join("priv_validator_key.json"))); + } + + Ok(()) + } + + fn has_file(files: &[fs::DirEntry], path: &PathBuf) -> bool { + files.iter().any(|f| &f.path() == path) + } +} diff --git a/code/examples/actor/proposal/build.rs b/code/examples/actor/proposal/build.rs new file mode 100644 index 000000000..a453b77e1 --- /dev/null +++ b/code/examples/actor/proposal/build.rs @@ -0,0 +1,23 @@ +fn main() -> Result<(), Box> { + let protos = &[ + "common.proto", + "block.proto", + "consensus.proto", + "liveness.proto", + "sync.proto", + ]; + + for proto in protos { + println!("cargo:rerun-if-changed={proto}"); + } + + let fds = protox::compile(protos, ["./proto"])?; + + let mut config = prost_build::Config::new(); + config.bytes(["."]); + config.enable_type_names(); + config.default_package_filename("test"); + config.compile_fds(fds)?; + + Ok(()) +} diff --git a/code/examples/actor/proposal/proto/block.proto b/code/examples/actor/proposal/proto/block.proto new file mode 100644 index 000000000..0f107be68 --- /dev/null +++ b/code/examples/actor/proposal/proto/block.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package p2p; + +import "common.proto"; + +message Block { + uint64 height = 1; + TransactionBatch transactions = 2; + Hash block_hash = 3; +} \ No newline at end of file diff --git a/code/examples/actor/proposal/proto/common.proto b/code/examples/actor/proposal/proto/common.proto new file mode 100644 index 000000000..fa27d1275 --- /dev/null +++ b/code/examples/actor/proposal/proto/common.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package p2p; + +message Hash { + bytes elements = 1; +} + +message Address { + bytes value = 1; +} + +message ConsensusTransaction { + bytes tx = 1; + bytes hash = 2; +} + +message TransactionBatch { + repeated ConsensusTransaction transactions = 1; +} \ No newline at end of file diff --git a/code/examples/actor/proposal/proto/consensus.proto b/code/examples/actor/proposal/proto/consensus.proto new file mode 100644 index 000000000..765ba8cdf --- /dev/null +++ b/code/examples/actor/proposal/proto/consensus.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +package p2p; + +import "common.proto"; +import "block.proto"; + +message Value { + Block value = 1; +} + +message ValueId { + optional Hash value = 1; +} + +message Signature { + bytes bytes = 1; +} + +enum VoteType { + PREVOTE = 0; + PRECOMMIT = 1; +} + +message Vote { + VoteType vote_type = 1; + uint64 height = 2; + uint32 round = 3; + ValueId value = 4; + Address validator_address = 5; +} + +message SignedMessage { + oneof message { + Proposal proposal = 1; + Vote vote = 2; + } + Signature signature = 3; +} + +message Proposal { + uint64 height = 1; + uint32 round = 2; + Value value = 3; + optional uint32 pol_round = 4; + Address validator_address = 5; +} + +message ProposalData { + Block block = 1; +} \ No newline at end of file diff --git a/code/examples/actor/proposal/proto/liveness.proto b/code/examples/actor/proposal/proto/liveness.proto new file mode 100644 index 000000000..e2f5127df --- /dev/null +++ b/code/examples/actor/proposal/proto/liveness.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package p2p; + +import "common.proto"; +import "consensus.proto"; + +message PolkaSignature { + // TODO: Add flag (no vote, nil, value?) + Address validator_address = 1; + Signature signature = 2; +} + +message PolkaCertificate { + uint64 height = 1; + uint32 round = 2; + ValueId value_id = 3; + repeated PolkaSignature signatures = 4; +} + +message RoundSignature { + VoteType vote_type = 1; + Address validator_address = 2; + Signature signature = 3; + // This is optional since a vote can be NIL. + optional ValueId value_id = 4; +} + +enum RoundCertificateType { + ROUND_CERT_PRECOMMIT = 0; + ROUND_CERT_SKIP = 1; +} + +message RoundCertificate { + uint64 height = 1; + uint32 round = 2; + RoundCertificateType cert_type = 3; + repeated RoundSignature signatures = 4; +} + +message LivenessMessage { + oneof message { + SignedMessage vote = 1; + PolkaCertificate polka_certificate = 2; + RoundCertificate round_certificate = 3; + } +} diff --git a/code/examples/actor/proposal/proto/sync.proto b/code/examples/actor/proposal/proto/sync.proto new file mode 100644 index 000000000..48a2369ee --- /dev/null +++ b/code/examples/actor/proposal/proto/sync.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +import "common.proto"; +import "consensus.proto"; +import "liveness.proto"; +import "block.proto"; + +package p2p; + +message PeerId { + bytes id = 1; +} + +message Status { + PeerId peer_id = 1; + uint64 height = 2; + uint64 earliest_height = 3; +} + +message ValueRequest { + uint64 height = 1; +} + +message ValueResponse { + uint64 height = 1; + Block value = 2; + CommitCertificate certificate = 3; +} + +message CommitSignature { + // TODO: Add flag (no vote, nil, value?) + Address validator_address = 1; + Signature signature = 2; +} + +message CommitCertificate { + uint64 height = 1; + uint32 round = 2; + ValueId value_id = 3; + repeated CommitSignature signatures = 4; +} + +message SyncRequest { + ValueRequest value_request = 1; +} + +message SyncResponse { + ValueResponse value_response = 1; +} + +message ProposedValue { + uint64 height = 1; + uint32 round = 2; + optional uint32 valid_round = 3; + Address proposer = 4; + Value value = 5; + bool validity = 6; +} + diff --git a/code/examples/actor/proposal/spawn.bash b/code/examples/actor/proposal/spawn.bash new file mode 100755 index 000000000..7a1c10008 --- /dev/null +++ b/code/examples/actor/proposal/spawn.bash @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +# This script takes: +# - a number of nodes to run as an argument, +# - the home directory for the nodes configuration folders + +function help { + echo "Usage: spawn.sh [--help] --nodes NODES_COUNT --home NODES_HOME [--app APP_BINARY] [--no-reset]" +} + +# Parse arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --help) help; exit 0 ;; + --nodes) NODES_COUNT="$2"; shift ;; + --home) NODES_HOME="$2"; shift ;; + --app) APP_BINARY="$2"; shift ;; + --no-reset) NO_RESET=1; shift ;; + *) echo "Unknown parameter passed: $1"; help; exit 1 ;; + esac + shift +done + +# Check required arguments +if [[ -z "$NODES_COUNT" ]]; then + help + exit 1 +fi + +if [[ -z "$NODES_HOME" ]]; then + help + exit 1 +fi + +if [[ -z "$APP_BINARY" ]]; then + APP_BINARY="informalsystems_malachitebft_actor_app_proposal" +fi + +# Environment variables +export MALACHITE__CONSENSUS__P2P__PROTOCOL__TYPE="gossipsub" +export MALACHITE__CONSENSUS__TIMEOUT_PROPOSE="3s" +export MALACHITE__CONSENSUS__TIMEOUT_PROPOSE_DELTA="500ms" +export MALACHITE__CONSENSUS__TIMEOUT_PREVOTE="1s" +export MALACHITE__CONSENSUS__TIMEOUT_PREVOTE_DELTA="500ms" +export MALACHITE__CONSENSUS__TIMEOUT_PRECOMMIT="1s" +export MALACHITE__CONSENSUS__TIMEOUT_PRECOMMIT_DELTA="500ms" +export MALACHITE__MEMPOOL__MAX_TX_COUNT="10000" +export MALACHITE__MEMPOOL__GOSSIP_BATCH_SIZE=0 +export MALACHITE__MEMPOOL__LOAD_TYPE="uniform" +export MALACHITE__MEMPOOL__LOAD_UNIFORM__INTERVAL="500ms" +export MALACHITE__MEMPOOL__LOAD_UNIFORM__SIZE="256B" +export MALACHITE__MEMPOOL__LOAD_UNIFORM__COUNT="5000" +export MALACHITE__TEST__MAX_BLOCK_SIZE="128KiB" + +export MALACHITE__TEST__VALUE_PAYLOAD="proposal-only" +export MALACHITE__TEST__TIME_ALLOWANCE_FACTOR=0.3 +export MALACHITE__TEST__EXEC_TIME_PER_TX="0ms" +export MALACHITE__TEST__MAX_RETAIN_BLOCKS=10000 +export MALACHITE__TEST__VOTE_EXTENSIONS__ENABLED="false" +export MALACHITE__VALUE_SYNC__ENABLED="true" +export MALACHITE__VALUE_SYNC__STATUS_UPDATE_INTERVAL="10s" +export MALACHITE__VALUE_SYNC__REQUEST_TIMEOUT="10s" + +echo "Compiling '$APP_BINARY'..." +cargo build -p $APP_BINARY + +# Create nodes and logs directories, run nodes +for NODE in $(seq 0 $((NODES_COUNT - 1))); do + if [[ -z "$NO_RESET" ]]; then + echo "[Node $NODE] Resetting the database..." + rm -rf "$NODES_HOME/$NODE/db" + mkdir -p "$NODES_HOME/$NODE/db" + rm -rf "$NODES_HOME/$NODE/wal" + mkdir -p "$NODES_HOME/$NODE/wal" + fi + + rm -rf "$NODES_HOME/$NODE/logs" + mkdir -p "$NODES_HOME/$NODE/logs" + + rm -rf "$NODES_HOME/$NODE/traces" + mkdir -p "$NODES_HOME/$NODE/traces" + + echo "[Node $NODE] Spawning node..." + cargo run -p $APP_BINARY -- start --home "$NODES_HOME/$NODE" > "$NODES_HOME/$NODE/logs/node.log" 2>&1 & + echo $! > "$NODES_HOME/$NODE/node.pid" +done + +# Function to handle cleanup on interrupt +function exit_and_cleanup { + echo "Stopping all nodes..." + for NODE in $(seq 0 $((NODES_COUNT - 1))); do + NODE_PID=$(cat "$NODES_HOME/$NODE/node.pid") + echo "[Node $NODE] Stopping node (PID: $NODE_PID)..." + kill "$NODE_PID" + done + exit 0 +} + +# Trap the INT signal (Ctrl+C) to run the cleanup function +trap exit_and_cleanup INT + +echo "Spawned $NODES_COUNT nodes." +echo "Press Ctrl+C to stop the nodes." + +# Keep the script running +while true; do sleep 1; done diff --git a/code/examples/actor/proposal/src/actor.rs b/code/examples/actor/proposal/src/actor.rs new file mode 100644 index 000000000..c3f14b799 --- /dev/null +++ b/code/examples/actor/proposal/src/actor.rs @@ -0,0 +1,534 @@ +use sha3::Digest; +use std::path::PathBuf; +use std::time::Duration; + +use bytes::Bytes; +use itertools::Itertools; +use ractor::{async_trait, Actor, ActorProcessingErr, RpcReplyPort, SpawnErr}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use tokio::time::Instant; +use tracing::{debug, error, info, warn}; + +use crate::types::{ + Address, Block, Ed25519Provider, Hash, Height, MockContext, ValidatorSet, Value, +}; +use malachitebft_core_consensus::Role; +use malachitebft_core_types::{CommitCertificate, Round, Validity}; +use malachitebft_engine::host::{LocallyProposedValue, Next, ProposedValue}; +use malachitebft_proto::Protobuf; +use malachitebft_sync::RawDecidedValue; + +use crate::app::App; +use crate::metrics::Metrics; +use crate::state::HostState; + +pub struct Host { + metrics: Metrics, + span: tracing::Span, +} + +pub type HostRef = malachitebft_engine::host::HostRef; +pub type HostMsg = malachitebft_engine::host::HostMsg; + +impl Host { + #[allow(clippy::too_many_arguments)] + pub async fn spawn( + home_dir: PathBuf, + signing_provider: Ed25519Provider, + host: App, + metrics: Metrics, + span: tracing::Span, + ) -> Result { + let db_dir = home_dir.join("db"); + std::fs::create_dir_all(&db_dir).map_err(|e| SpawnErr::StartupFailed(e.into()))?; + let db_path = db_dir.join("blocks.db"); + + let ctx = MockContext::new(); + + let (actor_ref, _) = Actor::spawn( + None, + Self::new(metrics, span), + HostState::new( + ctx, + signing_provider, + host, + db_path, + &mut StdRng::from_entropy(), + ) + .await, + ) + .await?; + + Ok(actor_ref) + } + + pub fn new(metrics: Metrics, span: tracing::Span) -> Self { + Self { metrics, span } + } +} + +#[async_trait] +impl Actor for Host { + type Arguments = HostState; + type State = HostState; + type Msg = HostMsg; + + async fn pre_start( + &self, + _myself: HostRef, + initial_state: Self::State, + ) -> Result { + Ok(initial_state) + } + + async fn handle( + &self, + _myself: HostRef, + msg: Self::Msg, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + if let Err(e) = self.handle_msg(_myself, msg, state).await { + error!(%e, "Failed to handle message"); + } + + Ok(()) + } +} + +impl Host { + #[tracing::instrument( + name = "host", + parent = &self.span, + skip_all, + fields(height = %state.height, round = %state.round), + )] + async fn handle_msg( + &self, + _myself: HostRef, + msg: HostMsg, + state: &mut HostState, + ) -> Result<(), ActorProcessingErr> { + match msg { + HostMsg::ConsensusReady { reply_to } => on_consensus_ready(state, reply_to).await, + + HostMsg::StartedRound { + height, + round, + proposer, + role, + reply_to, + } => on_started_round(state, height, round, proposer, role, reply_to).await, + + HostMsg::GetHistoryMinHeight { reply_to } => { + on_get_history_min_height(state, reply_to).await + } + + HostMsg::GetValue { + height, + round, + timeout, + reply_to, + } => on_get_value(state, height, round, timeout, reply_to).await, + + HostMsg::RestreamValue { .. } => { + panic!("RestreamValue is not supported for ProposalOnly mode") + } + + HostMsg::ReceivedProposalPart { .. } => { + panic!("ReceivedProposalPart is not supported for ProposalOnly mode") + } + + HostMsg::ReceivedProposal { + proposer, + height, + round, + value, + reply_to, + } => on_received_proposal(state, proposer, height, round, value, reply_to).await, + + HostMsg::GetValidatorSet { height, reply_to } => { + on_get_validator_set(state, height, reply_to).await + } + + HostMsg::Decided { + certificate, + reply_to, + .. + } => on_decided(state, certificate, &self.metrics, reply_to).await, + + HostMsg::GetDecidedValue { height, reply_to } => { + on_get_decided_block(height, state, reply_to).await + } + + HostMsg::ProcessSyncedValue { + height, + round, + proposer, + value_bytes, + reply_to, + } => { + on_process_synced_value(state, value_bytes, height, round, proposer, reply_to).await + } + + HostMsg::ExtendVote { reply_to, .. } => { + reply_to.send(None)?; + Ok(()) + } + + HostMsg::VerifyVoteExtension { reply_to, .. } => { + reply_to.send(Ok(()))?; + Ok(()) + } + } + } +} + +async fn on_consensus_ready( + state: &mut HostState, + reply_to: RpcReplyPort<(Height, ValidatorSet)>, +) -> Result<(), ActorProcessingErr> { + let latest_block_height = state + .block_store + .max_decided_value_height() + .await + .unwrap_or_default(); + let start_height = latest_block_height.increment(); + if reply_to + .send((start_height, state.app.validator_set.clone())) + .is_err() + { + error!("Failed to send ConsensusReady reply"); + } + + Ok(()) +} + +async fn on_started_round( + state: &mut HostState, + height: Height, + round: Round, + proposer: Address, + role: Role, + reply_to: RpcReplyPort>>, +) -> Result<(), ActorProcessingErr> { + state.height = height; + state.round = round; + state.proposer = Some(proposer); + state.role = role; + + // If we have already built or seen one or more values for this height and round, + // feed them back to consensus. This may happen when we are restarting after a crash. + let proposals = state + .block_store + .get_undecided_proposals(height, round) + .await?; + + if reply_to.send(proposals).is_err() { + error!("Failed to send undecided proposals"); + } + Ok(()) +} + +async fn on_get_history_min_height( + state: &mut HostState, + reply_to: RpcReplyPort, +) -> Result<(), ActorProcessingErr> { + let history_min_height = state + .block_store + .min_decided_value_height() + .await + .unwrap_or_default(); + reply_to.send(history_min_height)?; + + Ok(()) +} + +async fn on_get_validator_set( + state: &mut HostState, + _height: Height, + reply_to: RpcReplyPort>, +) -> Result<(), ActorProcessingErr> { + reply_to.send(Some(state.app.validator_set.clone()))?; + Ok(()) +} + +async fn on_get_value( + state: &mut HostState, + height: Height, + round: Round, + timeout: Duration, + reply_to: RpcReplyPort>, +) -> Result<(), ActorProcessingErr> { + if let Some(value) = find_previously_built_value(state, height, round).await? { + info!(%height, %round, hash = ?value.value.value.block_hash, "Returning previously built value"); + + reply_to.send(LocallyProposedValue::new( + value.height, + value.round, + value.value, + ))?; + + return Ok(()); + } + + let deadline = Instant::now() + timeout; + + debug!(%height, %round, "Building new proposal..."); + + let block = state + .app + .build_new_proposal(height, round, deadline) + .await?; + + let proposal = ProposedValue { + height, + round, + valid_round: Round::Nil, + proposer: state.app.address, + value: Value::new(block.clone()), + validity: Validity::Valid, + }; + debug!(%height, %round, block_hash = ?block.block_hash, "Storing proposed value from assembled block"); + + if let Err(e) = state.block_store.store_undecided_proposal(proposal).await { + error!(%e, %height, %round, "Failed to store the proposed value"); + } + + reply_to.send(LocallyProposedValue::new(height, round, Value::new(block)))?; + + Ok(()) +} + +/// If we have already built a block for this height and round, return it to consensus +/// This may happen when we are restarting after a crash and replaying the WAL. +async fn find_previously_built_value( + state: &mut HostState, + height: Height, + round: Round, +) -> Result>, ActorProcessingErr> { + let values = state + .block_store + .get_undecided_proposals(height, round) + .await?; + + let proposed_value = values.into_iter().find(|v| v.proposer == state.app.address); + + Ok(proposed_value) +} + +async fn on_process_synced_value( + state: &mut HostState, + value_bytes: Bytes, + height: Height, + round: Round, + proposer: Address, + reply_to: RpcReplyPort>, +) -> Result<(), ActorProcessingErr> { + let maybe_block = Block::from_bytes(value_bytes.as_ref()); + if let Ok(block) = maybe_block { + let validity = verify_proposal_validity(&block); + let proposed_value = ProposedValue { + height, + round, + valid_round: Round::Nil, + proposer, + value: Value::new(block), + validity, + }; + + state + .block_store + .store_undecided_proposal(proposed_value.clone()) + .await?; + + reply_to.send(proposed_value)?; + } + + Ok(()) +} + +async fn on_received_proposal( + state: &mut HostState, + proposer: Address, + height: Height, + round: Round, + value: Value, + reply_to: RpcReplyPort>, +) -> Result<(), ActorProcessingErr> { + let validity = verify_proposal_validity(&value.value); + let proposed_value = ProposedValue { + height, + round, + valid_round: Round::Nil, + proposer, + value, + validity, + }; + + // TODO - should we store invalid values? + state + .block_store + .store_undecided_proposal(proposed_value.clone()) + .await?; + reply_to.send(proposed_value)?; + + Ok(()) +} + +fn verify_proposal_validity(block: &Block) -> Validity { + let mut hasher = sha3::Keccak256::new(); + + for tx in block.transactions.to_vec().iter() { + hasher.update(tx.hash().as_bytes()); + } + + let transaction_commitment = Hash::new(hasher.finalize().into()); + + let valid_proposal = transaction_commitment == block.block_hash; + + if valid_proposal { + Validity::Valid + } else { + error!( + "ProposalCommitment hash mismatch: {:?} != {:?}", + transaction_commitment, block.block_hash + ); + Validity::Invalid + } +} + +async fn on_get_decided_block( + height: Height, + state: &mut HostState, + reply_to: RpcReplyPort>>, +) -> Result<(), ActorProcessingErr> { + debug!(%height, "Received request for block"); + + match state.block_store.get_decided_value(height).await { + Ok(None) => { + let min = state + .block_store + .min_decided_value_height() + .await + .unwrap_or_default(); + let max = state + .block_store + .max_decided_value_height() + .await + .unwrap_or_default(); + + warn!(%height, "No block for this height, available blocks: {min}..={max}"); + + reply_to.send(None)?; + } + + Ok(Some(block)) => { + let block = RawDecidedValue { + value_bytes: block.block.to_bytes().unwrap(), + certificate: block.certificate, + }; + + debug!(%height, "Found decided block in store"); + reply_to.send(Some(block))?; + } + Err(e) => { + error!(%e, %height, "Failed to get decided block"); + reply_to.send(None)?; + } + } + + Ok(()) +} + +async fn on_decided( + state: &mut HostState, + certificate: CommitCertificate, + metrics: &Metrics, + reply_to: RpcReplyPort>, +) -> Result<(), ActorProcessingErr> { + let (height, round) = (certificate.height, certificate.round); + + let proposals = state + .block_store + .get_undecided_proposals(height, round) + .await?; + + let Some(proposal) = proposals + .into_iter() + .find(|p| p.value.id() == certificate.value_id) + else { + error!( + value_id = ?certificate.value_id, + height = ?height, + round = ?round, + "Trying to commit a value for which there is no proposal" + ); + return Ok(()); + }; + + let block = proposal.value.value; + if let Err(e) = state + .block_store + .store_decided_block(&certificate, &block) + .await + { + error!(%e, %height, %round, "Failed to store the block"); + } + + // Update metrics + let tx_count: usize = block.transactions.len(); + let block_size: usize = block.size_bytes(); + + metrics.block_tx_count.observe(tx_count as f64); + metrics.block_size_bytes.observe(block_size as f64); + metrics.finalized_txes.inc_by(tx_count as u64); + + // Prune the block store, keeping only the last `max_retain_blocks` blocks + prune_block_store(state).await; + + // Notify the App of the decision + state.app.decision(block, certificate).await; + + // Start the next height + if reply_to + .send(Next::Start( + state.height.increment(), + state.app.validator_set.clone(), + )) + .is_err() + { + error!("Failed to send StartHeight reply"); + } + + Ok(()) +} + +async fn prune_block_store(state: &mut HostState) { + let max_height = state + .block_store + .max_decided_value_height() + .await + .unwrap_or_default(); + let max_retain_blocks = state.app.params.max_retain_blocks as u64; + + // Compute the height to retain blocks higher than + let retain_height = max_height.as_u64().saturating_sub(max_retain_blocks); + if retain_height <= 1 { + // No need to prune anything, since we would retain every blocks + return; + } + + let retain_height = Height::new(retain_height); + match state.block_store.prune(retain_height).await { + Ok(pruned) => { + debug!( + %retain_height, pruned_heights = pruned.iter().join(", "), + "Pruned the block store" + ); + } + Err(e) => { + error!(%e, %retain_height, "Failed to prune the block store"); + } + } +} diff --git a/code/examples/actor/proposal/src/app.rs b/code/examples/actor/proposal/src/app.rs new file mode 100644 index 000000000..1cf8ac3ca --- /dev/null +++ b/code/examples/actor/proposal/src/app.rs @@ -0,0 +1,83 @@ +use std::time::Duration; +use tokio::time::Instant; + +use bytesize::ByteSize; +use malachitebft_core_types::{CommitCertificate, Round}; +use malachitebft_signing_ed25519::PrivateKey; + +use crate::mempool::{MempoolMsg, MempoolRef}; +use crate::proposal::build_proposal_task; + +use crate::types::{Address, Block, Height, MockContext, ValidatorSet}; + +#[derive(Copy, Clone, Debug)] +pub struct AppParams { + pub max_block_size: ByteSize, + pub time_allowance_factor: f32, + pub exec_time_per_tx: Duration, + pub max_retain_blocks: usize, +} + +pub struct App { + pub params: AppParams, + pub mempool: MempoolRef, + pub address: Address, + pub private_key: PrivateKey, + pub validator_set: ValidatorSet, +} + +impl App { + pub fn new( + params: AppParams, + mempool: MempoolRef, + address: Address, + private_key: PrivateKey, + validator_set: ValidatorSet, + ) -> Self { + Self { + params, + mempool, + address, + private_key, + validator_set, + } + } + + #[tracing::instrument(skip_all, fields(%height, %round))] + pub async fn build_new_proposal( + &mut self, + height: Height, + round: Round, + deadline: Instant, + ) -> Result> { + let address = self.address; + let params = self.params; + let mempool = self.mempool.clone(); + build_proposal_task(height, round, address, params, deadline, mempool).await + } + + /// Update the Context about which decision has been made. It is responsible for pinging any + /// relevant components in the node to update their states accordingly. + /// + /// Params: + /// - brock_hash - The ID of the content which has been decided. + /// - precommits - The list of precommits from the round the decision was made (both for and against). + /// - height - The height of the decision. + pub async fn decision(&self, block: Block, certificate: CommitCertificate) { + // Gather hashes of all the tx-es included in the block, + // so that we can notify the mempool to remove them. + let mut tx_hashes = vec![]; + for tx in block.transactions.to_vec().iter() { + tx_hashes.push(tx.hash().clone()); + } + // Notify the mempool to remove corresponding txs + if let Err(err) = self.mempool.cast(MempoolMsg::Remove { tx_hashes }) { + tracing::error!( + error = ?err, + height = ?certificate.height, + block_hash = ?certificate.value_id, + "Failed to notify mempool about decided transactions" + ); + } + } +} diff --git a/code/examples/actor/proposal/src/codec/mod.rs b/code/examples/actor/proposal/src/codec/mod.rs new file mode 100644 index 000000000..0dd9a89d1 --- /dev/null +++ b/code/examples/actor/proposal/src/codec/mod.rs @@ -0,0 +1,2 @@ +pub mod protobuf; +pub use protobuf::*; diff --git a/code/examples/actor/proposal/src/codec/protobuf.rs b/code/examples/actor/proposal/src/codec/protobuf.rs new file mode 100644 index 000000000..04eed446c --- /dev/null +++ b/code/examples/actor/proposal/src/codec/protobuf.rs @@ -0,0 +1,575 @@ +use bytes::Bytes; +use prost::Message; + +use malachitebft_app::engine::util::streaming::StreamMessage; +use malachitebft_codec::Codec; +use malachitebft_core_consensus::{LivenessMsg, ProposedValue, SignedConsensusMsg}; +use malachitebft_core_types::{ + CommitCertificate, CommitSignature, NilOrVal, PolkaCertificate, PolkaSignature, Round, + RoundCertificate, RoundCertificateType, RoundSignature, SignedProposal, SignedVote, Validity, +}; +use malachitebft_proto::{Error as ProtoError, Protobuf}; + +use malachitebft_signing_ed25519::Signature; +use malachitebft_sync::{self as sync, PeerId}; + +use crate::types::MockContext as TestContext; +use crate::types::{ + Address, Block, BlockHash, Height, Proposal, ProposalPart, TransactionBatch, Value, ValueId, + Vote, +}; + +use crate::types::proto; + +use crate::types::{decode_votetype, encode_votetype}; + +#[derive(Copy, Clone, Debug)] +pub struct ProtobufCodec; + +impl Codec for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result { + Protobuf::from_bytes(&bytes) + } + + fn encode(&self, msg: &Value) -> Result { + Protobuf::to_bytes(msg) + } +} + +impl Codec for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, _bytes: Bytes) -> Result { + Ok(ProposalPart {}) + } + + fn encode(&self, _msg: &ProposalPart) -> Result { + Ok(Bytes::new()) + } +} + +impl Codec for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result { + let proto = proto::Signature::decode(bytes.as_ref())?; + decode_signature(proto) + } + + fn encode(&self, msg: &Signature) -> Result { + Ok(Bytes::from( + proto::Signature { + bytes: Bytes::copy_from_slice(msg.to_bytes().as_ref()), + } + .encode_to_vec(), + )) + } +} + +impl Codec> for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result, Self::Error> { + let proto = proto::SignedMessage::decode(bytes.as_ref())?; + + let signature = proto + .signature + .ok_or_else(|| ProtoError::missing_field::("signature")) + .and_then(decode_signature)?; + + let proto_message = proto + .message + .ok_or_else(|| ProtoError::missing_field::("message"))?; + + match proto_message { + proto::signed_message::Message::Proposal(proto) => { + let proposal = Proposal::from_proto(proto)?; + Ok(SignedConsensusMsg::Proposal(SignedProposal::new( + proposal, signature, + ))) + } + proto::signed_message::Message::Vote(vote) => { + let vote = Vote::from_proto(vote)?; + Ok(SignedConsensusMsg::Vote(SignedVote::new(vote, signature))) + } + } + } + + fn encode(&self, msg: &SignedConsensusMsg) -> Result { + match msg { + SignedConsensusMsg::Vote(vote) => { + let proto = proto::SignedMessage { + message: Some(proto::signed_message::Message::Vote( + vote.message.to_proto()?, + )), + signature: Some(encode_signature(&vote.signature)), + }; + Ok(Bytes::from(proto.encode_to_vec())) + } + SignedConsensusMsg::Proposal(proposal) => { + let proto = proto::SignedMessage { + message: Some(proto::signed_message::Message::Proposal( + proposal.message.to_proto()?, + )), + signature: Some(encode_signature(&proposal.signature)), + }; + Ok(Bytes::from(proto.encode_to_vec())) + } + } + } +} + +pub fn encode_round_certificate( + certificate: &RoundCertificate, +) -> Result { + Ok(proto::RoundCertificate { + height: certificate.height.as_u64(), + round: certificate.round.as_u32().expect("round should not be nil"), + cert_type: match certificate.cert_type { + RoundCertificateType::Precommit => { + proto::RoundCertificateType::RoundCertPrecommit.into() + } + RoundCertificateType::Skip => proto::RoundCertificateType::RoundCertSkip.into(), + }, + signatures: certificate + .round_signatures + .iter() + .map(|sig| -> Result { + let value_id = match &sig.value_id { + NilOrVal::Nil => None, + NilOrVal::Val(value_id) => Some(value_id.clone().to_proto()?), + }; + Ok(proto::RoundSignature { + vote_type: encode_votetype(sig.vote_type).into(), + validator_address: Some(sig.address.to_proto()?), + signature: Some(encode_signature(&sig.signature)), + value_id, + }) + }) + .collect::, _>>()?, + }) +} + +pub fn decode_round_certificate( + certificate: proto::RoundCertificate, +) -> Result, ProtoError> { + Ok(RoundCertificate { + height: Height::new(certificate.height), + round: Round::new(certificate.round), + cert_type: match proto::RoundCertificateType::try_from(certificate.cert_type) + .map_err(|_| ProtoError::Other("Unknown RoundCertificateType".into()))? + { + proto::RoundCertificateType::RoundCertPrecommit => RoundCertificateType::Precommit, + proto::RoundCertificateType::RoundCertSkip => RoundCertificateType::Skip, + }, + round_signatures: certificate + .signatures + .into_iter() + .map(|sig| -> Result, ProtoError> { + let vote_type = decode_votetype(sig.vote_type()); + let address = sig.validator_address.ok_or_else(|| { + ProtoError::missing_field::("validator_address") + })?; + + let signature = sig.signature.ok_or_else(|| { + ProtoError::missing_field::("signature") + })?; + + let value_id = match sig.value_id { + None => NilOrVal::Nil, + Some(value_id) => NilOrVal::Val(ValueId::from_proto(value_id)?), + }; + + let signature = decode_signature(signature)?; + let address = Address::from_proto(address)?; + Ok(RoundSignature::new(vote_type, value_id, address, signature)) + }) + .collect::, _>>()?, + }) +} + +impl Codec> for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result, Self::Error> { + let msg = proto::LivenessMessage::decode(bytes.as_ref())?; + match msg.message { + Some(proto::liveness_message::Message::Vote(vote)) => { + Ok(LivenessMsg::Vote(decode_vote(vote)?)) + } + Some(proto::liveness_message::Message::PolkaCertificate(cert)) => Ok( + LivenessMsg::PolkaCertificate(decode_polka_certificate(cert)?), + ), + Some(proto::liveness_message::Message::RoundCertificate(cert)) => Ok( + LivenessMsg::SkipRoundCertificate(decode_round_certificate(cert)?), + ), + None => Err(ProtoError::missing_field::( + "message", + )), + } + } + + fn encode(&self, msg: &LivenessMsg) -> Result { + match msg { + LivenessMsg::Vote(vote) => { + let message = encode_vote(vote)?; + Ok(Bytes::from( + proto::LivenessMessage { + message: Some(proto::liveness_message::Message::Vote(message)), + } + .encode_to_vec(), + )) + } + LivenessMsg::PolkaCertificate(cert) => { + let message = encode_polka_certificate(cert)?; + Ok(Bytes::from( + proto::LivenessMessage { + message: Some(proto::liveness_message::Message::PolkaCertificate(message)), + } + .encode_to_vec(), + )) + } + LivenessMsg::SkipRoundCertificate(cert) => { + let message = encode_round_certificate(cert)?; + Ok(Bytes::from( + proto::LivenessMessage { + message: Some(proto::liveness_message::Message::RoundCertificate(message)), + } + .encode_to_vec(), + )) + } + } + } +} + +impl Codec> for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, _bytes: Bytes) -> Result, Self::Error> { + panic!("Streaming not supported"); + } + + fn encode(&self, _msg: &StreamMessage) -> Result { + panic!("Streaming not supported"); + } +} + +impl Codec> for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result, Self::Error> { + let proto = proto::ProposedValue::decode(bytes.as_ref())?; + + let proposer = proto + .proposer + .ok_or_else(|| ProtoError::missing_field::("proposer"))?; + + let value = proto + .value + .ok_or_else(|| ProtoError::missing_field::("value"))?; + + Ok(ProposedValue { + height: Height::new(proto.height), + round: Round::new(proto.round), + valid_round: proto.valid_round.map(Round::new).unwrap_or(Round::Nil), + proposer: Address::from_proto(proposer)?, + value: Value::from_proto(value)?, + validity: Validity::from_bool(proto.validity), + }) + } + + fn encode(&self, msg: &ProposedValue) -> Result { + let proto = proto::ProposedValue { + height: msg.height.as_u64(), + round: msg.round.as_u32().unwrap(), + valid_round: msg.valid_round.as_u32(), + proposer: Some(msg.proposer.to_proto()?), + value: Some(msg.value.to_proto()?), + validity: msg.validity.to_bool(), + }; + + Ok(Bytes::from(proto.encode_to_vec())) + } +} + +impl Codec> for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result, Self::Error> { + let proto = proto::Status::decode(bytes.as_ref())?; + + let proto_peer_id = proto + .peer_id + .ok_or_else(|| ProtoError::missing_field::("peer_id"))?; + + Ok(sync::Status { + peer_id: PeerId::from_bytes(proto_peer_id.id.as_ref()).unwrap(), + tip_height: Height::new(proto.height), + history_min_height: Height::new(proto.earliest_height), + }) + } + + fn encode(&self, msg: &sync::Status) -> Result { + let proto = proto::Status { + peer_id: Some(proto::PeerId { + id: Bytes::from(msg.peer_id.to_bytes()), + }), + height: msg.tip_height.as_u64(), + earliest_height: msg.history_min_height.as_u64(), + }; + + Ok(Bytes::from(proto.encode_to_vec())) + } +} + +impl Codec> for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result, Self::Error> { + let proto = proto::SyncRequest::decode(bytes.as_ref())?; + let request = proto + .value_request + .ok_or_else(|| ProtoError::missing_field::("value_request"))?; + + Ok(sync::Request::ValueRequest(sync::ValueRequest::new( + Height::new(request.height), + ))) + } + + fn encode(&self, msg: &sync::Request) -> Result { + let proto = match msg { + sync::Request::ValueRequest(req) => proto::SyncRequest { + value_request: Some(proto::ValueRequest { + height: req.height.as_u64(), + }), + }, + }; + + Ok(Bytes::from(proto.encode_to_vec())) + } +} + +impl Codec> for ProtobufCodec { + type Error = ProtoError; + + fn decode(&self, bytes: Bytes) -> Result, Self::Error> { + decode_sync_response(proto::SyncResponse::decode(bytes)?) + } + + fn encode(&self, response: &sync::Response) -> Result { + encode_sync_response(response).map(|proto| proto.encode_to_vec().into()) + } +} + +pub fn decode_sync_response( + proto_response: proto::SyncResponse, +) -> Result, ProtoError> { + let response = proto_response + .value_response + .ok_or_else(|| ProtoError::missing_field::("value_response"))?; + + let certificate = response + .certificate + .ok_or_else(|| ProtoError::missing_field::("certificate"))?; + + Ok(sync::Response::ValueResponse(sync::ValueResponse::new( + Height::new(certificate.height), + response + .value + .map(|b| { + let block = + Block { + height: Height::new(response.height), + transactions: TransactionBatch::from_proto(b.transactions.ok_or_else( + || ProtoError::missing_field::("transactions"), + )?)?, + block_hash: BlockHash::from_proto(b.block_hash.ok_or_else(|| { + ProtoError::missing_field::("block_hash") + })?)?, + }; + Ok::, ProtoError>(sync::RawDecidedValue { + value_bytes: block.to_bytes()?, + certificate: decode_commit_certificate(certificate.clone())?, + }) + }) + .transpose()?, + ))) +} + +pub fn encode_sync_response( + response: &sync::Response, +) -> Result { + let proto = match response { + sync::Response::ValueResponse(value_response) => proto::SyncResponse { + value_response: Some(proto::ValueResponse { + height: value_response.height.as_u64(), + value: value_response + .value + .as_ref() + .map(|v| Block::from_bytes(&v.value_bytes).and_then(|b| b.to_proto())) + .transpose()?, + certificate: value_response + .value + .as_ref() + .map(|v| encode_commit_certificate(&v.certificate)) + .transpose()?, + }), + }, + }; + + Ok(proto) +} + +// NOTE: Will be used again in #997 +#[allow(dead_code)] +pub(crate) fn decode_polka_certificate( + certificate: proto::PolkaCertificate, +) -> Result, ProtoError> { + let value_id = certificate + .value_id + .ok_or_else(|| ProtoError::missing_field::("value_id")) + .and_then(ValueId::from_proto)?; + + Ok(PolkaCertificate { + height: Height::new(certificate.height), + round: Round::new(certificate.round), + value_id, + polka_signatures: certificate + .signatures + .into_iter() + .map(|sig| -> Result, ProtoError> { + let address = sig.validator_address.ok_or_else(|| { + ProtoError::missing_field::("validator_address") + })?; + let signature = sig.signature.ok_or_else(|| { + ProtoError::missing_field::("signature") + })?; + let signature = decode_signature(signature)?; + let address = Address::from_proto(address)?; + Ok(PolkaSignature::new(address, signature)) + }) + .collect::, _>>()?, + }) +} + +#[allow(dead_code)] +pub(crate) fn encode_polka_certificate( + polka_certificate: &PolkaCertificate, +) -> Result { + Ok(proto::PolkaCertificate { + height: polka_certificate.height.as_u64(), + round: polka_certificate + .round + .as_u32() + .expect("round should not be nil"), + value_id: Some(polka_certificate.value_id.to_proto()?), + signatures: polka_certificate + .polka_signatures + .iter() + .map(|sig| -> Result { + let address = sig.address.to_proto()?; + let signature = encode_signature(&sig.signature); + Ok(proto::PolkaSignature { + validator_address: Some(address), + signature: Some(signature), + }) + }) + .collect::, _>>()?, + }) +} + +pub fn decode_commit_certificate( + certificate: proto::CommitCertificate, +) -> Result, ProtoError> { + let value_id = certificate + .value_id + .ok_or_else(|| ProtoError::missing_field::("value_id")) + .and_then(ValueId::from_proto)?; + + let commit_signatures = certificate + .signatures + .into_iter() + .map(|sig| -> Result, ProtoError> { + let address = sig.validator_address.ok_or_else(|| { + ProtoError::missing_field::("validator_address") + })?; + let signature = sig.signature.ok_or_else(|| { + ProtoError::missing_field::("signature") + })?; + let signature = decode_signature(signature)?; + let address = Address::from_proto(address)?; + Ok(CommitSignature::new(address, signature)) + }) + .collect::, _>>()?; + + let certificate = CommitCertificate { + height: Height::new(certificate.height), + round: Round::new(certificate.round), + value_id, + commit_signatures, + }; + + Ok(certificate) +} + +pub fn encode_commit_certificate( + certificate: &CommitCertificate, +) -> Result { + Ok(proto::CommitCertificate { + height: certificate.height.as_u64(), + round: certificate.round.as_u32().expect("round should not be nil"), + value_id: Some(certificate.value_id.to_proto()?), + signatures: certificate + .commit_signatures + .iter() + .map(|sig| -> Result { + let address = sig.address.to_proto()?; + let signature = encode_signature(&sig.signature); + Ok(proto::CommitSignature { + validator_address: Some(address), + signature: Some(signature), + }) + }) + .collect::, _>>()?, + }) +} + +pub fn decode_vote(msg: proto::SignedMessage) -> Result, ProtoError> { + let signature = msg + .signature + .ok_or_else(|| ProtoError::missing_field::("signature"))?; + + let vote = match msg.message { + Some(proto::signed_message::Message::Vote(v)) => Ok(v), + _ => Err(ProtoError::Other( + "Invalid message type: not a vote".to_string(), + )), + }?; + + let signature = decode_signature(signature)?; + let vote = Vote::from_proto(vote)?; + Ok(SignedVote::new(vote, signature)) +} + +pub fn encode_vote(vote: &SignedVote) -> Result { + Ok(proto::SignedMessage { + message: Some(proto::signed_message::Message::Vote( + vote.message.to_proto()?, + )), + signature: Some(encode_signature(&vote.signature)), + }) +} + +pub fn decode_signature(signature: proto::Signature) -> Result { + let bytes = <[u8; 64]>::try_from(signature.bytes.as_ref()) + .map_err(|_| ProtoError::Other("Invalid signature length".to_string()))?; + Ok(Signature::from_bytes(bytes)) +} + +pub fn encode_signature(signature: &Signature) -> proto::Signature { + proto::Signature { + bytes: Bytes::copy_from_slice(signature.to_bytes().as_ref()), + } +} diff --git a/code/examples/actor/proposal/src/config.rs b/code/examples/actor/proposal/src/config.rs new file mode 100644 index 000000000..cff23fc40 --- /dev/null +++ b/code/examples/actor/proposal/src/config.rs @@ -0,0 +1,86 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use malachitebft_app::node::NodeConfig; + +pub use malachitebft_app::config::{ + ConsensusConfig, LogFormat, LogLevel, LoggingConfig, MempoolConfig, MetricsConfig, + RuntimeConfig, TestConfig, TimeoutConfig, ValueSyncConfig, +}; + +/// Malachite configuration options +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Config { + /// A custom human-readable name for this node + pub moniker: String, + + /// Log configuration options + pub logging: LoggingConfig, + + /// Consensus configuration options + pub consensus: ConsensusConfig, + + /// Mempool configuration options + pub mempool: MempoolConfig, + + /// Sync configuration options + pub value_sync: ValueSyncConfig, + + /// Metrics configuration options + pub metrics: MetricsConfig, + + /// Runtime configuration options + pub runtime: RuntimeConfig, + + /// Test configuration + #[serde(default)] + pub test: TestConfig, +} + +impl NodeConfig for Config { + fn moniker(&self) -> &str { + &self.moniker + } + + fn consensus(&self) -> &ConsensusConfig { + &self.consensus + } + + fn value_sync(&self) -> &ValueSyncConfig { + &self.value_sync + } +} + +/// load_config parses the environment variables and loads the provided config file path +/// to create a Config struct. +pub fn load_config(path: impl AsRef, prefix: Option<&str>) -> eyre::Result { + ::config::Config::builder() + .add_source(::config::File::from(path.as_ref())) + .add_source( + ::config::Environment::with_prefix(prefix.unwrap_or("MALACHITE")).separator("__"), + ) + .build()? + .try_deserialize() + .map_err(Into::into) +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// +// #[test] +// fn parse_default_config_file() { +// let file = include_str!("../config.toml"); +// let config = toml::from_str::(file).unwrap(); +// assert_eq!(config.consensus.timeouts, TimeoutConfig::default()); +// +// let tmp_file = std::env::temp_dir().join("config-test.toml"); +// std::fs::write(&tmp_file, file).unwrap(); +// +// let config = load_config(&tmp_file, None).unwrap(); +// assert_eq!(config.consensus.timeouts, TimeoutConfig::default()); +// +// std::fs::remove_file(tmp_file).unwrap(); +// } +// } diff --git a/code/examples/actor/proposal/src/lib.rs b/code/examples/actor/proposal/src/lib.rs new file mode 100644 index 000000000..91b1fc89b --- /dev/null +++ b/code/examples/actor/proposal/src/lib.rs @@ -0,0 +1,13 @@ +pub mod actor; +pub mod app; +pub mod codec; +pub mod config; +pub mod mempool; +pub mod metrics; + +pub mod node; +pub mod proposal; +pub mod spawn; +pub mod state; +pub mod store; +pub mod types; diff --git a/code/examples/actor/proposal/src/mempool/actor.rs b/code/examples/actor/proposal/src/mempool/actor.rs new file mode 100644 index 000000000..432c35754 --- /dev/null +++ b/code/examples/actor/proposal/src/mempool/actor.rs @@ -0,0 +1,265 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use async_trait::async_trait; +use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort}; +use tracing::{error, info, trace}; + +use crate::types::{Hash, Transaction, TransactionBatch}; +use malachitebft_proto::Protobuf; +use malachitebft_test_mempool::types::MempoolTransactionBatch; +use malachitebft_test_mempool::{Event as NetworkEvent, NetworkMsg, PeerId}; + +use super::network::{MempoolNetworkMsg, MempoolNetworkRef}; + +pub type MempoolMsg = Msg; +pub type MempoolRef = ActorRef; + +pub struct Mempool { + network: MempoolNetworkRef, + gossip_batch_size: usize, + max_tx_count: usize, + span: tracing::Span, +} + +pub enum Msg { + NetworkEvent(Arc), + AddBatch(TransactionBatch), + Reap { + height: u64, + num_txes: usize, + reply: RpcReplyPort>, + }, + Remove { + tx_hashes: Vec, + }, +} + +impl From> for Msg { + fn from(event: Arc) -> Self { + Self::NetworkEvent(event) + } +} + +#[derive(Default)] +pub struct State { + transactions: BTreeMap, +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn add_tx(&mut self, tx: Transaction) { + self.transactions.entry(tx.hash().clone()).or_insert(tx); + } + + pub fn remove_tx(&mut self, hash: &Hash) { + self.transactions.remove(hash); + } +} + +impl Mempool { + pub fn new( + mempool_network: MempoolNetworkRef, + gossip_batch_size: usize, + max_tx_count: usize, + span: tracing::Span, + ) -> Self { + Self { + network: mempool_network, + gossip_batch_size, + max_tx_count, + span, + } + } + + pub async fn spawn( + mempool_network: MempoolNetworkRef, + gossip_batch_size: usize, + max_tx_count: usize, + span: tracing::Span, + ) -> Result { + let node = Self::new(mempool_network, gossip_batch_size, max_tx_count, span); + let (actor_ref, _) = Actor::spawn(None, node, ()).await?; + Ok(actor_ref) + } + + pub async fn handle_network_event( + &self, + event: &NetworkEvent, + myself: MempoolRef, + state: &mut State, + ) -> Result<(), ractor::ActorProcessingErr> { + match event { + NetworkEvent::Listening(address) => { + info!(%address, "Listening"); + } + NetworkEvent::PeerConnected(peer_id) => { + info!(%peer_id, "Connected to peer"); + } + NetworkEvent::PeerDisconnected(peer_id) => { + info!(%peer_id, "Disconnected from peer"); + } + NetworkEvent::Message(_channel, from, _msg_id, msg) => { + trace!(%from, size = msg.size_bytes(), "Received message"); + + self.handle_network_msg(from, msg, myself, state).await?; + } + } + + Ok(()) + } + + pub async fn handle_network_msg( + &self, + from: &PeerId, + msg: &NetworkMsg, + _myself: MempoolRef, + state: &mut State, + ) -> Result<(), ractor::ActorProcessingErr> { + match msg { + NetworkMsg::TransactionBatch(batch) => { + let batch = match TransactionBatch::from_any(&batch.transaction_batch) { + Ok(batch) => batch, + Err(e) => { + error!("Failed to decode transaction batch: {e}"); + return Ok(()); + } + }; + + trace!(%from, "Received batch from network with {} transactions", batch.len()); + + self.add_batch(batch, state); + } + } + + Ok(()) + } + + fn add_batch(&self, batch: TransactionBatch, state: &mut State) { + for tx in batch.into_vec() { + if state.transactions.len() < self.max_tx_count { + state.add_tx(tx); + } else { + trace!("Mempool is full, dropping transaction batch"); + break; + } + } + } + + fn gossip_batch(&self, batch: &TransactionBatch) -> Result<(), ActorProcessingErr> { + assert!(self.gossip_batch_size > 0, "Gossip must be enabled"); + + trace!("Broadcasting transaction batch to network"); + + for batch in batch.as_slice().chunks(self.gossip_batch_size) { + let tx_batch = TransactionBatch::new(batch.to_vec()).to_any().unwrap(); + let mempool_batch = MempoolTransactionBatch::new(tx_batch); + self.network + .cast(MempoolNetworkMsg::BroadcastMsg(mempool_batch))?; + } + + Ok(()) + } + + fn gossip_enabled(&self) -> bool { + self.gossip_batch_size > 0 + } + + async fn handle_msg( + &self, + myself: MempoolRef, + msg: Msg, + state: &mut State, + ) -> Result<(), ractor::ActorProcessingErr> { + match msg { + Msg::NetworkEvent(event) => { + self.handle_network_event(&event, myself, state).await?; + } + + Msg::AddBatch(batch) => { + trace!("Received batch of {} transactions", batch.len()); + + // If mempool gossip is enabled, broadcast the transactions to the network + if self.gossip_enabled() { + self.gossip_batch(&batch)?; + } + + self.add_batch(batch, state); + } + + Msg::Reap { + num_txes: count, + reply, + .. + } => { + let mut txes = Vec::with_capacity(count); + + for _ in 0..count { + if let Some((_, tx)) = state.transactions.pop_first() { + txes.push(tx); + } else { + // No more transactions to reap + break; + } + } + + reply.send(txes)?; + } + + Msg::Remove { tx_hashes } => { + tx_hashes.iter().for_each(|hash| state.remove_tx(hash)); + } + } + + Ok(()) + } +} + +#[async_trait] +impl Actor for Mempool { + type Msg = Msg; + type State = State; + type Arguments = (); + + #[tracing::instrument("host.mempool", parent = &self.span, skip_all)] + async fn pre_start( + &self, + myself: MempoolRef, + _args: (), + ) -> Result { + self.network.link(myself.get_cell()); + + self.network + .cast(MempoolNetworkMsg::Subscribe(Box::new(myself.clone())))?; + + Ok(State::new()) + } + + #[tracing::instrument("host.mempool", parent = &self.span, skip_all)] + async fn handle( + &self, + myself: MempoolRef, + msg: MempoolMsg, + state: &mut State, + ) -> Result<(), ractor::ActorProcessingErr> { + if let Err(e) = self.handle_msg(myself, msg, state).await { + error!("Error processing message: {e:?}"); + } + + Ok(()) + } + + #[tracing::instrument("host.mempool", parent = &self.span, skip_all)] + async fn post_stop( + &self, + _myself: MempoolRef, + _state: &mut State, + ) -> Result<(), ActorProcessingErr> { + info!("Stopping..."); + + Ok(()) + } +} diff --git a/code/examples/actor/proposal/src/mempool/load.rs b/code/examples/actor/proposal/src/mempool/load.rs new file mode 100644 index 000000000..8b7c2f68b --- /dev/null +++ b/code/examples/actor/proposal/src/mempool/load.rs @@ -0,0 +1,186 @@ +use std::time::Duration; + +use async_trait::async_trait; +use bytesize::ByteSize; +use ractor::{concurrency::JoinHandle, Actor, ActorProcessingErr, ActorRef}; +use rand::rngs::SmallRng; +use rand::seq::IteratorRandom; +use rand::{Rng, RngCore, SeedableRng}; +use tracing::info; + +use malachitebft_config::mempool_load::{NonUniformLoadConfig, UniformLoadConfig}; +use malachitebft_config::MempoolLoadType; + +use crate::mempool::{MempoolMsg, MempoolRef}; +use crate::types::{Transaction, TransactionBatch}; + +pub type MempoolLoadMsg = Msg; +pub type MempoolLoadRef = ActorRef; + +pub enum Msg { + GenerateTransactions { count: usize, size: ByteSize }, +} + +#[derive(Debug)] +pub struct State { + ticker: JoinHandle<()>, +} + +#[derive(Debug, Default)] +pub struct Params { + pub load_type: MempoolLoadType, +} + +pub struct MempoolLoad { + params: Params, + mempool: MempoolRef, + span: tracing::Span, +} + +impl MempoolLoad { + pub fn new(params: Params, mempool: MempoolRef, span: tracing::Span) -> Self { + Self { + params, + mempool, + span, + } + } + + pub async fn spawn( + params: Params, + mempool: MempoolRef, + span: tracing::Span, + ) -> Result { + let actor = Self::new(params, mempool, span); + let (actor_ref, _) = Actor::spawn(None, actor, ()).await?; + Ok(actor_ref) + } + + pub fn generate_transactions(count: usize, size: ByteSize) -> Vec { + let mut transactions: Vec = Vec::with_capacity(count); + let mut rng = SmallRng::from_entropy(); + + for _ in 0..count { + let mut tx_bytes = vec![0; size.as_u64() as usize]; + rng.fill_bytes(&mut tx_bytes); + let tx = Transaction::new(tx_bytes); + transactions.push(tx); + } + transactions + } + + fn generate_non_uniform_load_params( + params: &NonUniformLoadConfig, + ) -> (usize, ByteSize, Duration) { + let mut rng = SmallRng::from_entropy(); + + // Determine if this iteration should generate a spike + let is_spike = rng.gen_bool(params.spike_probability); + + // Vary transaction count and size + let count_variation = rng.gen_range(params.count_variation.clone()); + let size_variation = rng.gen_range(params.size_variation.clone()); + + let count = if is_spike { + (params.base_count + count_variation) as usize * params.spike_multiplier + } else { + (params.base_count + count_variation) as usize + }; + let size = (params.base_size + size_variation) as u64; + + // Get sleep duration + let sleep_duration = + Duration::from_millis(params.sleep_interval.clone().choose(&mut rng).unwrap()); + + (count.max(1), ByteSize::b(size.max(1)), sleep_duration) + } + + async fn run_uniform_load(params: UniformLoadConfig, myself: MempoolLoadRef) { + loop { + // Create and send the message + let msg = Msg::GenerateTransactions { + count: params.count, + size: params.size, + }; + + if let Err(er) = myself.cast(msg) { + tracing::error!(?er, ?myself, "Channel closed, stopping load generator"); + break; + } + + tokio::time::sleep(params.interval).await; + } + } + + async fn run_non_uniform_load(params: NonUniformLoadConfig, myself: MempoolLoadRef) { + loop { + let (count, size, sleep_duration) = Self::generate_non_uniform_load_params(¶ms); + + // Create and send the message + let msg = Msg::GenerateTransactions { count, size }; + + if let Err(er) = myself.cast(msg) { + tracing::error!(?er, ?myself, "Channel closed, stopping load generator"); + break; + } + + tokio::time::sleep(sleep_duration).await; + } + } +} + +#[async_trait] +impl Actor for MempoolLoad { + type Msg = Msg; + type State = State; + type Arguments = (); + + async fn pre_start( + &self, + myself: MempoolLoadRef, + _args: (), + ) -> Result { + self.mempool.link(myself.get_cell()); + + let ticker = match self.params.load_type.clone() { + MempoolLoadType::NoLoad => tokio::spawn(async {}), + MempoolLoadType::UniformLoad(uniform_load_config) => { + tokio::spawn(Self::run_uniform_load(uniform_load_config, myself.clone())) + } + MempoolLoadType::NonUniformLoad(non_uniform_load_config) => tokio::spawn( + Self::run_non_uniform_load(non_uniform_load_config, myself.clone()), + ), + }; + + Ok(State { ticker }) + } + + async fn post_stop( + &self, + _myself: ActorRef, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + info!("Stopping..."); + state.ticker.abort(); + Ok(()) + } + + #[tracing::instrument("host.mempool_load", parent = &self.span, skip_all)] + async fn handle( + &self, + _myself: MempoolLoadRef, + msg: Msg, + _state: &mut State, + ) -> Result<(), ActorProcessingErr> { + match msg { + Msg::GenerateTransactions { count, size } => { + let transactions = Self::generate_transactions(count, size); + let tx_batch = TransactionBatch::new(transactions); + + self.mempool.cast(MempoolMsg::AddBatch(tx_batch))?; + + Ok(()) + } + } + } +} diff --git a/code/examples/actor/proposal/src/mempool/mod.rs b/code/examples/actor/proposal/src/mempool/mod.rs new file mode 100644 index 000000000..feed3bcad --- /dev/null +++ b/code/examples/actor/proposal/src/mempool/mod.rs @@ -0,0 +1,7 @@ +pub mod actor; +pub mod load; +pub mod network; + +pub use actor::{Mempool, MempoolMsg, MempoolRef}; +pub use load::{MempoolLoad, MempoolLoadMsg, MempoolLoadRef, Params}; +pub use network::{MempoolNetwork, MempoolNetworkMsg, MempoolNetworkRef}; diff --git a/code/examples/actor/proposal/src/mempool/network.rs b/code/examples/actor/proposal/src/mempool/network.rs new file mode 100644 index 000000000..39470107f --- /dev/null +++ b/code/examples/actor/proposal/src/mempool/network.rs @@ -0,0 +1,193 @@ +use std::collections::BTreeSet; +use std::sync::Arc; + +use async_trait::async_trait; +use libp2p_identity::Keypair; +use ractor::ActorProcessingErr; +use ractor::ActorRef; +use ractor::{Actor, RpcReplyPort}; +use tokio::task::JoinHandle; +use tracing::error; + +use malachitebft_engine::util::output_port::{OutputPort, OutputPortSubscriber}; +use malachitebft_metrics::SharedRegistry; +use malachitebft_test_mempool::handle::CtrlHandle; +use malachitebft_test_mempool::types::MempoolTransactionBatch; +use malachitebft_test_mempool::Channel::Mempool; +use malachitebft_test_mempool::{Config, Event, NetworkMsg, PeerId}; + +pub type MempoolNetworkMsg = Msg; +pub type MempoolNetworkRef = ActorRef; + +pub struct MempoolNetwork { + span: tracing::Span, +} + +impl MempoolNetwork { + pub async fn spawn( + keypair: Keypair, + config: Config, + metrics: SharedRegistry, + span: tracing::Span, + ) -> Result, ractor::SpawnErr> { + let args = Args { + keypair, + config, + metrics, + }; + + let (actor_ref, _) = Actor::spawn(None, Self { span }, args).await?; + Ok(actor_ref) + } +} + +pub struct Args { + pub keypair: Keypair, + pub config: Config, + pub metrics: SharedRegistry, +} + +pub enum State { + Stopped, + Running { + peers: BTreeSet, + output_port: OutputPort>, + ctrl_handle: CtrlHandle, + recv_task: JoinHandle<()>, + }, +} + +pub enum Msg { + /// Subscribe to gossip events + Subscribe(OutputPortSubscriber>), + + /// Broadcast a message to all peers + BroadcastMsg(MempoolTransactionBatch), + + /// Request the number of connected peers + GetState { reply: RpcReplyPort }, + + // Internal message + #[doc(hidden)] + NewEvent(Event), +} + +#[async_trait] +impl Actor for MempoolNetwork { + type Msg = Msg; + type State = State; + type Arguments = Args; + + async fn pre_start( + &self, + myself: ActorRef, + args: Args, + ) -> Result { + let handle = + malachitebft_test_mempool::spawn(args.keypair, args.config, args.metrics).await?; + let (mut recv_handle, ctrl_handle) = handle.split(); + + let recv_task = tokio::spawn(async move { + while let Some(event) = recv_handle.recv().await { + if let Err(e) = myself.cast(Msg::NewEvent(event)) { + error!("Actor has died, stopping gossip mempool: {e:?}"); + break; + } + } + }); + + Ok(State::Running { + peers: BTreeSet::new(), + output_port: OutputPort::default(), + ctrl_handle, + recv_task, + }) + } + + async fn post_start( + &self, + _myself: ActorRef, + _state: &mut State, + ) -> Result<(), ActorProcessingErr> { + Ok(()) + } + + #[tracing::instrument(name = "gossip.mempool", parent = &self.span, skip_all)] + async fn handle( + &self, + _myself: ActorRef, + msg: Msg, + state: &mut State, + ) -> Result<(), ActorProcessingErr> { + let State::Running { + peers, + output_port, + ctrl_handle, + .. + } = state + else { + return Ok(()); + }; + + match msg { + Msg::Subscribe(subscriber) => subscriber.subscribe_to_port(output_port), + + Msg::BroadcastMsg(batch) => { + match NetworkMsg::TransactionBatch(batch).to_network_bytes() { + Ok(bytes) => { + ctrl_handle.broadcast(Mempool, bytes).await?; + } + Err(e) => { + error!("Failed to serialize transaction batch: {e}"); + } + } + } + + Msg::NewEvent(event) => { + match event { + Event::PeerConnected(peer_id) => { + peers.insert(peer_id); + } + Event::PeerDisconnected(peer_id) => { + peers.remove(&peer_id); + } + _ => {} + } + + let event = Arc::new(event); + output_port.send(event); + } + + Msg::GetState { reply } => { + let number_peers = match state { + State::Stopped => 0, + State::Running { peers, .. } => peers.len(), + }; + + reply.send(number_peers)?; + } + } + + Ok(()) + } + + async fn post_stop( + &self, + _myself: ActorRef, + state: &mut State, + ) -> Result<(), ActorProcessingErr> { + let state = std::mem::replace(state, State::Stopped); + + if let State::Running { + ctrl_handle, + recv_task, + .. + } = state + { + ctrl_handle.wait_shutdown().await?; + recv_task.await?; + } + + Ok(()) + } +} diff --git a/code/examples/actor/proposal/src/metrics/metrics.rs b/code/examples/actor/proposal/src/metrics/metrics.rs new file mode 100644 index 000000000..b21548937 --- /dev/null +++ b/code/examples/actor/proposal/src/metrics/metrics.rs @@ -0,0 +1,81 @@ +use std::ops::Deref; +use std::sync::Arc; + +use malachitebft_metrics::prometheus::metrics::counter::Counter; +use malachitebft_metrics::prometheus::metrics::histogram::{linear_buckets, Histogram}; +use malachitebft_metrics::SharedRegistry; + +#[derive(Clone, Debug)] +pub struct Metrics(Arc); + +impl Deref for Metrics { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Debug)] +pub struct Inner { + /// Number of blocks finalized + pub finalized_blocks: Counter, + + /// Number of transactions finalized + pub finalized_txes: Counter, + + /// Block size in terms of # of transactions + pub block_tx_count: Histogram, + + /// Size of each block in bytes + pub block_size_bytes: Histogram, +} + +impl Metrics { + pub fn new() -> Self { + Self(Arc::new(Inner { + finalized_blocks: Counter::default(), + finalized_txes: Counter::default(), + block_tx_count: Histogram::new(linear_buckets(0.0, 32.0, 128)), + block_size_bytes: Histogram::new(linear_buckets(0.0, 64.0 * 1024.0, 128)), + })) + } + + pub fn register(registry: &SharedRegistry) -> Self { + let metrics = Self::new(); + + registry.with_prefix("app_actor_proposal", |registry| { + registry.register( + "finalized_blocks", + "Number of blocks finalized", + metrics.finalized_blocks.clone(), + ); + + registry.register( + "finalized_txes", + "Number of transactions finalized", + metrics.finalized_txes.clone(), + ); + + registry.register( + "block_tx_count", + "Block size in terms of # of transactions", + metrics.block_tx_count.clone(), + ); + + registry.register( + "block_size_bytes", + "Size of each block in bytes", + metrics.block_size_bytes.clone(), + ); + }); + + metrics + } +} + +impl Default for Metrics { + fn default() -> Self { + Self::new() + } +} diff --git a/code/examples/actor/proposal/src/metrics/mod.rs b/code/examples/actor/proposal/src/metrics/mod.rs new file mode 100644 index 000000000..71cda450f --- /dev/null +++ b/code/examples/actor/proposal/src/metrics/mod.rs @@ -0,0 +1,4 @@ +#[allow(clippy::module_inception)] +pub mod metrics; + +pub use metrics::Metrics; diff --git a/code/examples/actor/proposal/src/node.rs b/code/examples/actor/proposal/src/node.rs new file mode 100644 index 000000000..33b233cf4 --- /dev/null +++ b/code/examples/actor/proposal/src/node.rs @@ -0,0 +1,489 @@ +#![allow(clippy::too_many_arguments)] + +use std::path::PathBuf; + +use ractor::async_trait; +use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use tokio::task::JoinHandle; + +use malachitebft_app::events::{RxEvent, TxEvent}; +use malachitebft_app::node::{ + CanGeneratePrivateKey, CanMakeConfig, CanMakeDistributedConfig, CanMakeGenesis, + CanMakePrivateKeyFile, MakeConfigSettings, Node, NodeHandle, +}; +use malachitebft_app::types::Keypair; +use malachitebft_config::{mempool_load::UniformLoadConfig, ValueSyncConfig}; +use malachitebft_core_types::VotingPower; +use malachitebft_engine::node::NodeRef; + +use crate::config::{load_config, Config}; +use crate::spawn::spawn_node_actor; + +use crate::types::{ + Address, Ed25519Provider, Height, MockContext, PrivateKey, PublicKey, Validator, ValidatorSet, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Genesis { + pub validator_set: ValidatorSet, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PrivateKeyFile { + pub private_key: PrivateKey, + pub public_key: PublicKey, + pub address: Address, +} + +impl From for PrivateKeyFile { + fn from(private_key: PrivateKey) -> Self { + let public_key = private_key.public_key(); + let address = Address::from_public_key(&public_key); + + Self { + private_key, + public_key, + address, + } + } +} + +pub struct Handle { + pub actor: NodeRef, + pub handle: JoinHandle<()>, + pub tx_event: TxEvent, +} + +#[async_trait] +impl NodeHandle for Handle { + fn subscribe(&self) -> RxEvent { + self.tx_event.subscribe() + } + + async fn kill(&self, _reason: Option) -> eyre::Result<()> { + self.actor.kill_and_wait(None).await?; + self.handle.abort(); + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub enum ConfigSource { + File(PathBuf), + Value(Box), + Default, +} + +#[derive(Clone, Debug)] +pub struct ActorNode { + pub home_dir: PathBuf, + pub config_source: ConfigSource, + pub start_height: Option, +} + +impl ActorNode { + pub fn new(home_dir: PathBuf, config_source: ConfigSource, start_height: Option) -> Self { + Self { + home_dir, + config_source, + start_height, + } + } + + pub fn genesis_file(&self) -> PathBuf { + self.home_dir.join("config").join("genesis.json") + } + + pub fn private_key_file(&self) -> PathBuf { + self.home_dir.join("config").join("priv_validator_key.json") + } +} + +#[async_trait] +impl Node for ActorNode { + type Context = MockContext; + type Config = Config; + type Genesis = Genesis; + type PrivateKeyFile = PrivateKeyFile; + type SigningProvider = Ed25519Provider; + type NodeHandle = Handle; + + fn get_home_dir(&self) -> PathBuf { + self.home_dir.to_owned() + } + + fn load_config(&self) -> eyre::Result { + match self.config_source { + ConfigSource::File(ref path) => load_config(path, Some("MALACHITE")), + ConfigSource::Value(ref config) => Ok(*config.clone()), + ConfigSource::Default => Ok(default_config()), + } + } + + fn get_address(&self, pk: &PublicKey) -> Address { + Address::from_public_key(pk) + } + + fn get_public_key(&self, pk: &PrivateKey) -> PublicKey { + pk.public_key() + } + + fn get_keypair(&self, pk: PrivateKey) -> Keypair { + Keypair::ed25519_from_bytes(pk.inner().to_bytes()).unwrap() + } + + fn load_private_key(&self, file: Self::PrivateKeyFile) -> PrivateKey { + file.private_key + } + + fn load_private_key_file(&self) -> eyre::Result { + let private_key = std::fs::read_to_string(self.private_key_file())?; + serde_json::from_str(&private_key).map_err(|e| e.into()) + } + + fn get_signing_provider(&self, private_key: PrivateKey) -> Self::SigningProvider { + Self::SigningProvider::new(private_key) + } + + fn load_genesis(&self) -> eyre::Result { + let genesis = std::fs::read_to_string(self.genesis_file())?; + serde_json::from_str(&genesis).map_err(|e| e.into()) + } + + async fn start(&self) -> eyre::Result { + let config = self.load_config()?; + + let span = tracing::error_span!("node", moniker = %config.moniker); + let _enter = span.enter(); + + let priv_key_file = self.load_private_key_file()?; + let private_key = self.load_private_key(priv_key_file); + let genesis = self.load_genesis()?; + + let tx_event = TxEvent::new(); + + let start_height = self.start_height.map(Height::new); + + let (actor, handle) = spawn_node_actor( + config.clone(), + self.home_dir.clone(), + genesis.validator_set, + private_key, + start_height, + tx_event.clone(), + span.clone(), + ) + .await; + + Ok(Handle { + actor, + handle, + tx_event, + }) + } + + async fn run(self) -> eyre::Result<()> { + let handle = self.start().await?; + handle.actor.wait(None).await.map_err(Into::into) + } +} + +impl CanGeneratePrivateKey for ActorNode { + fn generate_private_key(&self, rng: R) -> PrivateKey + where + R: RngCore + CryptoRng, + { + PrivateKey::generate(rng) + } +} + +impl CanMakePrivateKeyFile for ActorNode { + fn make_private_key_file(&self, private_key: PrivateKey) -> Self::PrivateKeyFile { + PrivateKeyFile::from(private_key) + } +} + +impl CanMakeGenesis for ActorNode { + fn make_genesis(&self, validators: Vec<(PublicKey, VotingPower)>) -> Self::Genesis { + let validators = validators + .into_iter() + .map(|(pk, vp)| Validator::new(pk, vp)); + + let validator_set = ValidatorSet::new(validators); + + Genesis { validator_set } + } +} + +impl CanMakeConfig for ActorNode { + fn make_config(index: usize, total: usize, settings: MakeConfigSettings) -> Self::Config { + make_config(index, total, settings) + } +} + +impl CanMakeDistributedConfig for ActorNode { + fn make_distributed_config( + index: usize, + total: usize, + machines: Vec, + bootstrap_set_size: usize, + settings: MakeConfigSettings, + ) -> Self::Config { + make_distributed_config(index, total, machines, bootstrap_set_size, settings) + } +} + +/// Generate configuration for node "index" out of "total" number of nodes. +fn make_config(index: usize, total: usize, settings: MakeConfigSettings) -> Config { + use itertools::Itertools; + use rand::seq::IteratorRandom; + use rand::Rng; + + use malachitebft_config::*; + + const CONSENSUS_BASE_PORT: usize = 27000; + const MEMPOOL_BASE_PORT: usize = 28000; + const METRICS_BASE_PORT: usize = 29000; + + let consensus_port = CONSENSUS_BASE_PORT + index; + let mempool_port = MEMPOOL_BASE_PORT + index; + let metrics_port = METRICS_BASE_PORT + index; + + Config { + moniker: format!("actor-app-proposal-{index}"), + consensus: ConsensusConfig { + queue_capacity: 1000, + value_payload: ValuePayload::ProposalOnly, + timeouts: TimeoutConfig::default(), + p2p: P2pConfig { + protocol: PubSubProtocol::default(), + listen_addr: settings.transport.multiaddr("127.0.0.1", consensus_port), + persistent_peers: if settings.discovery.enabled { + let mut rng = rand::thread_rng(); + let count = if total > 1 { + rng.gen_range(1..=(total / 2)) + } else { + 0 + }; + let peers = (0..total) + .filter(|j| *j != index) + .choose_multiple(&mut rng, count); + + peers + .iter() + .unique() + .map(|index| { + settings + .transport + .multiaddr("127.0.0.1", CONSENSUS_BASE_PORT + index) + }) + .collect() + } else { + (0..total) + .filter(|j| *j != index) + .map(|j| { + settings + .transport + .multiaddr("127.0.0.1", CONSENSUS_BASE_PORT + j) + }) + .collect() + }, + discovery: settings.discovery, + ..Default::default() + }, + }, + mempool: MempoolConfig { + p2p: P2pConfig { + protocol: PubSubProtocol::default(), + listen_addr: settings.transport.multiaddr("127.0.0.1", mempool_port), + persistent_peers: (0..total) + .filter(|j| *j != index) + .map(|j| { + settings + .transport + .multiaddr("127.0.0.1", MEMPOOL_BASE_PORT + j) + }) + .collect(), + discovery: DiscoveryConfig { + enabled: false, + ..settings.discovery + }, + ..Default::default() + }, + max_tx_count: 10000, + gossip_batch_size: 0, + load: MempoolLoadConfig { + load_type: MempoolLoadType::UniformLoad(UniformLoadConfig::default()), + }, + }, + metrics: MetricsConfig { + enabled: true, + listen_addr: format!("127.0.0.1:{metrics_port}").parse().unwrap(), + }, + runtime: settings.runtime, + value_sync: ValueSyncConfig::default(), + logging: LoggingConfig::default(), + test: TestConfig::default(), + } +} + +fn make_distributed_config( + index: usize, + _total: usize, + machines: Vec, + bootstrap_set_size: usize, + settings: MakeConfigSettings, +) -> Config { + use itertools::Itertools; + use malachitebft_config::*; + use std::time::Duration; + + const CONSENSUS_BASE_PORT: usize = 27000; + const MEMPOOL_BASE_PORT: usize = 28000; + const METRICS_BASE_PORT: usize = 29000; + + let machine = machines[index % machines.len()].clone(); + let consensus_port = CONSENSUS_BASE_PORT + (index / machines.len()); + let mempool_port = MEMPOOL_BASE_PORT + (index / machines.len()); + let metrics_port = METRICS_BASE_PORT + (index / machines.len()); + + Config { + moniker: format!("actor-app-proposal-{index}"), + consensus: ConsensusConfig { + queue_capacity: 1000, + value_payload: ValuePayload::ProposalOnly, + timeouts: TimeoutConfig::default(), + p2p: P2pConfig { + protocol: PubSubProtocol::default(), + listen_addr: settings.transport.multiaddr(&machine, consensus_port), + persistent_peers: if settings.discovery.enabled { + let peers = + ((index.saturating_sub(bootstrap_set_size))..index).collect::>(); + + peers + .iter() + .unique() + .map(|j| { + settings.transport.multiaddr( + &machines[j % machines.len()].clone(), + CONSENSUS_BASE_PORT + (j / machines.len()), + ) + }) + .collect() + } else { + let peers = (0..index).collect::>(); + + peers + .iter() + .map(|j| { + settings.transport.multiaddr( + &machines[*j % machines.len()], + CONSENSUS_BASE_PORT + (*j / machines.len()), + ) + }) + .collect() + }, + discovery: settings.discovery, + ..Default::default() + }, + }, + mempool: MempoolConfig { + p2p: P2pConfig { + protocol: PubSubProtocol::default(), + listen_addr: settings.transport.multiaddr(&machine, mempool_port), + persistent_peers: vec![], + discovery: DiscoveryConfig { + enabled: false, + ..DiscoveryConfig::default() + }, + ..Default::default() + }, + max_tx_count: 10000, + gossip_batch_size: 0, + load: MempoolLoadConfig { + load_type: MempoolLoadType::UniformLoad(UniformLoadConfig::default()), + }, + }, + value_sync: ValueSyncConfig { + enabled: false, + status_update_interval: Duration::from_secs(0), + request_timeout: Duration::from_secs(0), + ..Default::default() + }, + metrics: MetricsConfig { + enabled: true, + listen_addr: format!("{machine}:{metrics_port}").parse().unwrap(), + }, + runtime: settings.runtime, + logging: LoggingConfig::default(), + test: TestConfig::default(), + } +} + +fn default_config() -> Config { + use malachitebft_config::{DiscoveryConfig, RuntimeConfig, TransportProtocol}; + + make_config( + 1, + 3, + MakeConfigSettings { + runtime: RuntimeConfig::single_threaded(), + transport: TransportProtocol::Tcp, + discovery: DiscoveryConfig::default(), + value_sync: ValueSyncConfig::default(), + }, + ) +} + +#[test] +fn test_actor_node() { + // Create temp folder for configuration files + let temp_dir = + tempfile::TempDir::with_prefix("proposal-only-node-").expect("Failed to create temp dir"); + + let temp_path = temp_dir.path().to_owned(); + + if std::env::var("KEEP_TEMP").is_ok() { + std::mem::forget(temp_dir); + } + + std::fs::create_dir_all(temp_path.join("config")).unwrap(); + + // Create default configuration + let node = ActorNode::new(temp_path.clone(), ConfigSource::Default, Some(1)); + + // Create configuration files + use malachitebft_test_cli::*; + + let priv_keys = new::generate_private_keys(&node, 1, true); + let pub_keys = priv_keys.iter().map(|pk| node.get_public_key(pk)).collect(); + let genesis = new::generate_genesis(&node, pub_keys, true); + + file::save_priv_validator_key( + &node, + &node.private_key_file(), + &PrivateKeyFile::from(priv_keys[0].clone()), + ) + .unwrap(); + + file::save_genesis(&node, &node.genesis_file(), &genesis).unwrap(); + + let config = node.load_config().unwrap(); + + // Run the node for a few seconds + const TIMEOUT: u64 = 3; + use tokio::time::{timeout, Duration}; + let rt = malachitebft_test_cli::runtime::build_runtime(config.runtime).unwrap(); + let result = rt.block_on(async { timeout(Duration::from_secs(TIMEOUT), node.run()).await }); + + // Check that the node did not quit before the timeout. + assert!(result.is_err()); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "deadline has elapsed"); + let io_error: std::io::Error = error.into(); + assert_eq!( + io_error.to_string(), + std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out").to_string() + ); +} diff --git a/code/examples/actor/proposal/src/proposal.rs b/code/examples/actor/proposal/src/proposal.rs new file mode 100644 index 000000000..dda25da43 --- /dev/null +++ b/code/examples/actor/proposal/src/proposal.rs @@ -0,0 +1,100 @@ +#![allow(clippy::too_many_arguments)] +use sha3::Digest; +use std::time::SystemTime; + +use bytesize::ByteSize; +use eyre::eyre; +use tokio::time::Instant; +use tracing::{debug, trace}; + +use malachitebft_core_types::Round; + +use crate::app::AppParams; +use crate::mempool::{MempoolMsg, MempoolRef}; +use crate::types::{Address, Block, Hash, Height, TransactionBatch}; + +pub async fn build_proposal_task( + height: Height, + round: Round, + _proposer: Address, // TODO: add to block def + params: AppParams, + deadline: Instant, + mempool: MempoolRef, +) -> Result> { + // TODO - if needed, use this deadline to stop the build_new_proposal + let start = Instant::now(); + let build_duration = (deadline - start).mul_f32(params.time_allowance_factor); + + let _build_deadline = start + build_duration; + let _now = SystemTime::UNIX_EPOCH.elapsed().unwrap().as_secs(); + + let mut block_tx_count = 0; + let mut block_size = 0; + + trace!(%height, %round, "Building local value"); + + let max_block_size = params.max_block_size.as_u64() as usize; + let mut hasher = sha3::Keccak256::new(); + + let mut txes = Vec::new(); + let mut full_block = false; + + 'reap: loop { + let reaped_txes = mempool + .call( + |reply| MempoolMsg::Reap { + height: height.as_u64(), + num_txes: 1, + reply, + }, + Some(build_duration), + ) + .await? + .success_or(eyre!("Failed to reap transactions from the mempool"))?; + + if reaped_txes.is_empty() { + debug!("No more transactions to reap"); + break 'reap; + } + + 'txes: for tx in reaped_txes { + if block_size + tx.size_bytes() > max_block_size { + full_block = true; + break 'txes; + } + + block_size += tx.size_bytes(); + block_tx_count += 1; + + txes.push(tx.clone()); + hasher.update(tx.clone().hash().as_bytes()); + } + + let exec_time = params.exec_time_per_tx * txes.len() as u32; + tokio::time::sleep(exec_time).await; + + if full_block { + debug!("Max block size reached, stopping tx generation"); + break 'reap; + } else if start.elapsed() >= build_duration { + debug!("Time allowance exceeded, stopping tx generation"); + break 'reap; + } + } + + let transaction_commitment = Hash::new(hasher.finalize().into()); + + let built_block = Block::new( + height, + TransactionBatch::new(txes), + transaction_commitment.clone(), + ); + + let block_size = ByteSize::b(block_size as u64); + debug!( + tx_count = %block_tx_count, size = %block_size, transaction_commitment = ?transaction_commitment, + "Built block in {:?}", start.elapsed() + ); + + Ok(built_block) +} diff --git a/code/examples/actor/proposal/src/spawn.rs b/code/examples/actor/proposal/src/spawn.rs new file mode 100644 index 000000000..87cb81699 --- /dev/null +++ b/code/examples/actor/proposal/src/spawn.rs @@ -0,0 +1,386 @@ +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use tokio::task::JoinHandle; + +use malachitebft_config::{self as config, MempoolConfig, MempoolLoadConfig, ValueSyncConfig}; +use malachitebft_core_types::ValuePayload; +use malachitebft_engine::consensus::{Consensus, ConsensusParams, ConsensusRef}; +use malachitebft_engine::host::HostRef; +use malachitebft_engine::network::{Network, NetworkRef}; +use malachitebft_engine::node::{Node, NodeRef}; +use malachitebft_engine::sync::{Params as SyncParams, Sync, SyncRef}; +use malachitebft_engine::util::events::TxEvent; +use malachitebft_engine::wal::{Wal, WalRef}; +use malachitebft_metrics::{Metrics as ConsensusMetrics, SharedRegistry}; +use malachitebft_network::Keypair; +use malachitebft_sync as sync; +use malachitebft_test_mempool::Config as MempoolNetworkConfig; + +use crate::actor::Host; +use crate::codec::ProtobufCodec; + +use crate::app::{App, AppParams}; +use crate::mempool::network::{MempoolNetwork, MempoolNetworkRef}; +use crate::mempool::{Mempool, MempoolRef}; +use crate::mempool::{MempoolLoad, MempoolLoadRef, Params}; + +use crate::config::Config; +use crate::metrics::Metrics as AppMetrics; +use crate::types::{Address, Ed25519Provider, Height, MockContext, PrivateKey, ValidatorSet}; + +pub async fn spawn_node_actor( + cfg: Config, + home_dir: PathBuf, + initial_validator_set: ValidatorSet, + private_key: PrivateKey, + start_height: Option, + tx_event: TxEvent, + span: tracing::Span, +) -> (NodeRef, JoinHandle<()>) { + let ctx = MockContext::new(); + + let start_height = start_height.unwrap_or(Height::new(1)); + + let registry = SharedRegistry::global().with_moniker(cfg.moniker.as_str()); + + let consensus_metrics = ConsensusMetrics::register(®istry); + let app_metrics = AppMetrics::register(®istry); + let sync_metrics: malachitebft_sync::Metrics = sync::Metrics::register(®istry); + + let address = Address::from_public_key(&private_key.public_key()); + + // Spawn mempool and its gossip layer + let mempool_network = spawn_mempool_network_actor(&cfg, &private_key, ®istry, &span).await; + let mempool = spawn_mempool_actor(mempool_network, &cfg.mempool, &span).await; + spawn_mempool_load_actor(&cfg.mempool.load, mempool.clone(), &span).await; + + // Spawn consensus gossip + let network: ractor::ActorRef> = + spawn_network_actor(&cfg, &private_key, ®istry, &span).await; + + // Spawn the host actor + let host = spawn_host_actor( + &home_dir, + &cfg, + &address, + &private_key, + &initial_validator_set, + mempool, + app_metrics, + &span, + ) + .await; + + let sync = spawn_sync_actor( + ctx, + network.clone(), + host.clone(), + &cfg.value_sync, + sync_metrics, + &span, + ) + .await; + + let wal = spawn_wal_actor(&ctx, ProtobufCodec, &home_dir, ®istry, &span).await; + + // Spawn consensus + let signing_provider = Ed25519Provider::new(private_key.clone()); + let consensus = spawn_consensus_actor( + start_height, + initial_validator_set, + address, + ctx, + cfg, + signing_provider, + network.clone(), + host.clone(), + wal.clone(), + sync.clone(), + consensus_metrics, + tx_event, + &span, + ) + .await; + + // Spawn the node actor + let node = Node::new(ctx, network, consensus, wal, sync, host, span); + + let (actor_ref, handle) = node.spawn().await.unwrap(); + + (actor_ref, handle) +} + +async fn spawn_wal_actor( + ctx: &MockContext, + codec: ProtobufCodec, + home_dir: &Path, + registry: &SharedRegistry, + span: &tracing::Span, +) -> WalRef { + let wal_dir = home_dir.join("wal"); + std::fs::create_dir_all(&wal_dir).unwrap(); + let wal_file = wal_dir.join("consensus.wal"); + + Wal::spawn(ctx, codec, wal_file, registry.clone(), span.clone()) + .await + .unwrap() +} + +async fn spawn_sync_actor( + ctx: MockContext, + network: NetworkRef, + host: HostRef, + config: &ValueSyncConfig, + sync_metrics: sync::Metrics, + span: &tracing::Span, +) -> Option> { + if !config.enabled { + return None; + } + + let params = SyncParams { + status_update_interval: config.status_update_interval, + request_timeout: config.request_timeout, + }; + + let scoring_strategy = match config.scoring_strategy { + config::ScoringStrategy::Ema => sync::scoring::Strategy::Ema, + }; + + let sync_config = sync::Config { + enabled: config.enabled, + max_request_size: config.max_request_size.as_u64() as usize, + max_response_size: config.max_response_size.as_u64() as usize, + request_timeout: config.request_timeout, + parallel_requests: config.parallel_requests as u64, + scoring_strategy, + inactive_threshold: (!config.inactive_threshold.is_zero()) + .then_some(config.inactive_threshold), + }; + + let actor_ref = Sync::spawn( + ctx, + network, + host, + params, + sync_config, + sync_metrics, + span.clone(), + ) + .await + .unwrap(); + + Some(actor_ref) +} + +#[allow(clippy::too_many_arguments)] +async fn spawn_consensus_actor( + initial_height: Height, + initial_validator_set: ValidatorSet, + address: Address, + ctx: MockContext, + cfg: Config, + signing_provider: Ed25519Provider, + network: NetworkRef, + host: HostRef, + wal: WalRef, + sync: Option>, + consensus_metrics: ConsensusMetrics, + tx_event: TxEvent, + span: &tracing::Span, +) -> ConsensusRef { + let consensus_params = ConsensusParams { + initial_height, + initial_validator_set, + address, + threshold_params: Default::default(), + value_payload: match cfg.consensus.value_payload { + config::ValuePayload::ProposalOnly => ValuePayload::ProposalOnly, + config::ValuePayload::PartsOnly | config::ValuePayload::ProposalAndParts => { + panic!( + "PartsOnly and ProposalAndParts modes are not supported for actor-app-proposal" + ) + } + }, + }; + + Consensus::spawn( + ctx, + consensus_params, + cfg.consensus, + Box::new(signing_provider), + network, + host, + wal, + sync, + consensus_metrics, + tx_event, + span.clone(), + ) + .await + .unwrap() +} + +async fn spawn_network_actor( + cfg: &Config, + private_key: &PrivateKey, + registry: &SharedRegistry, + span: &tracing::Span, +) -> NetworkRef { + use malachitebft_network as gossip; + + let bootstrap_protocol = match cfg.consensus.p2p.discovery.bootstrap_protocol { + config::BootstrapProtocol::Kademlia => gossip::BootstrapProtocol::Kademlia, + config::BootstrapProtocol::Full => gossip::BootstrapProtocol::Full, + }; + + let selector = match cfg.consensus.p2p.discovery.selector { + config::Selector::Kademlia => gossip::Selector::Kademlia, + config::Selector::Random => gossip::Selector::Random, + }; + + let config_gossip = gossip::Config { + listen_addr: cfg.consensus.p2p.listen_addr.clone(), + persistent_peers: cfg.consensus.p2p.persistent_peers.clone(), + discovery: gossip::DiscoveryConfig { + enabled: cfg.consensus.p2p.discovery.enabled, + bootstrap_protocol, + selector, + num_outbound_peers: cfg.consensus.p2p.discovery.num_outbound_peers, + num_inbound_peers: cfg.consensus.p2p.discovery.num_inbound_peers, + ephemeral_connection_timeout: cfg.consensus.p2p.discovery.ephemeral_connection_timeout, + ..Default::default() + }, + idle_connection_timeout: Duration::from_secs(15 * 60), + transport: gossip::TransportProtocol::from_multiaddr(&cfg.consensus.p2p.listen_addr) + .unwrap_or_else(|| { + panic!( + "No valid transport protocol found in listen address: {}", + cfg.consensus.p2p.listen_addr + ) + }), + pubsub_protocol: match cfg.consensus.p2p.protocol { + config::PubSubProtocol::GossipSub(_) => gossip::PubSubProtocol::GossipSub, + config::PubSubProtocol::Broadcast => gossip::PubSubProtocol::Broadcast, + }, + gossipsub: match cfg.consensus.p2p.protocol { + config::PubSubProtocol::GossipSub(config) => gossip::GossipSubConfig { + mesh_n: config.mesh_n(), + mesh_n_high: config.mesh_n_high(), + mesh_n_low: config.mesh_n_low(), + mesh_outbound_min: config.mesh_outbound_min(), + }, + config::PubSubProtocol::Broadcast => gossip::GossipSubConfig::default(), + }, + rpc_max_size: cfg.consensus.p2p.rpc_max_size.as_u64() as usize, + pubsub_max_size: cfg.consensus.p2p.pubsub_max_size.as_u64() as usize, + enable_sync: true, + }; + + let keypair = make_keypair(private_key); + let codec = ProtobufCodec; + + Network::spawn( + keypair, + config_gossip, + registry.clone(), + codec, + span.clone(), + ) + .await + .unwrap() +} + +fn make_keypair(pk: &PrivateKey) -> Keypair { + Keypair::ed25519_from_bytes(pk.inner().to_bytes()).unwrap() +} + +async fn spawn_mempool_actor( + mempool_network: MempoolNetworkRef, + mempool_config: &MempoolConfig, + span: &tracing::Span, +) -> MempoolRef { + Mempool::spawn( + mempool_network, + mempool_config.gossip_batch_size, + mempool_config.max_tx_count, + span.clone(), + ) + .await + .unwrap() +} + +async fn spawn_mempool_load_actor( + mempool_load_config: &MempoolLoadConfig, + mempool: MempoolRef, + span: &tracing::Span, +) -> MempoolLoadRef { + MempoolLoad::spawn( + Params { + load_type: mempool_load_config.load_type.clone(), + }, + mempool, + span.clone(), + ) + .await + .unwrap() +} + +async fn spawn_mempool_network_actor( + cfg: &Config, + private_key: &PrivateKey, + registry: &SharedRegistry, + span: &tracing::Span, +) -> MempoolNetworkRef { + let keypair = make_keypair(private_key); + + let config = MempoolNetworkConfig { + listen_addr: cfg.mempool.p2p.listen_addr.clone(), + persistent_peers: cfg.mempool.p2p.persistent_peers.clone(), + idle_connection_timeout: Duration::from_secs(15 * 60), + }; + + MempoolNetwork::spawn(keypair, config, registry.clone(), span.clone()) + .await + .unwrap() +} + +#[allow(clippy::too_many_arguments)] +async fn spawn_host_actor( + home_dir: &Path, + cfg: &Config, + address: &Address, + private_key: &PrivateKey, + initial_validator_set: &ValidatorSet, + mempool: MempoolRef, + app_metrics: AppMetrics, + span: &tracing::Span, +) -> HostRef { + let mock_params = AppParams { + max_block_size: cfg.test.max_block_size, + time_allowance_factor: cfg.test.time_allowance_factor, + exec_time_per_tx: cfg.test.exec_time_per_tx, + max_retain_blocks: cfg.test.max_retain_blocks, + }; + + let app = App::new( + mock_params, + mempool.clone(), + *address, + private_key.clone(), + initial_validator_set.clone(), + ); + + let signing_provider = Ed25519Provider::new(private_key.clone()); + + Host::spawn( + home_dir.to_owned(), + signing_provider, + app, + app_metrics, + span.clone(), + ) + .await + .unwrap() +} diff --git a/code/examples/actor/proposal/src/state.rs b/code/examples/actor/proposal/src/state.rs new file mode 100644 index 000000000..406c6c551 --- /dev/null +++ b/code/examples/actor/proposal/src/state.rs @@ -0,0 +1,50 @@ +use malachitebft_core_consensus::Role; +use std::path::Path; + +use crate::types::{Address, Ed25519Provider, Height, MockContext}; +use rand::RngCore; + +use malachitebft_core_types::Round; +use malachitebft_engine::consensus::ConsensusRef; + +use crate::app::App; +use crate::store::BlockStore; + +pub struct HostState { + pub ctx: MockContext, + pub signing_provider: Ed25519Provider, + pub height: Height, + pub round: Round, + pub proposer: Option
, + pub role: Role, + pub app: App, + pub consensus: Option>, + pub block_store: BlockStore, + pub nonce: u64, +} + +impl HostState { + pub async fn new( + ctx: MockContext, + signing_provider: Ed25519Provider, + app: App, + db_path: impl AsRef, + rng: &mut R, + ) -> Self + where + R: RngCore, + { + Self { + ctx, + signing_provider, + height: Height::new(1), + round: Round::Nil, + proposer: None, + role: Role::None, + app, + consensus: None, + block_store: BlockStore::new(db_path).await.unwrap(), + nonce: rng.next_u64(), + } + } +} diff --git a/code/examples/actor/proposal/src/store/block_store.rs b/code/examples/actor/proposal/src/store/block_store.rs new file mode 100644 index 000000000..c35d8044c --- /dev/null +++ b/code/examples/actor/proposal/src/store/block_store.rs @@ -0,0 +1,91 @@ +use std::{path::Path, sync::Arc}; + +use malachitebft_core_consensus::ProposedValue; +use malachitebft_core_types::{CommitCertificate, Round}; + +use crate::types::{Block, Height, MockContext}; + +use super::{db::Db, error::StoreError, types::DecidedBlock}; + +pub struct BlockStore { + db: Arc, +} + +impl Clone for BlockStore { + fn clone(&self) -> Self { + BlockStore { + db: Arc::clone(&self.db), + } + } +} + +impl BlockStore { + pub async fn new(path: impl AsRef) -> Result { + let path = path.as_ref().to_owned(); + tokio::task::spawn_blocking(move || { + let db = Db::new(path)?; + db.create_tables()?; + Ok(Self { db: Arc::new(db) }) + }) + .await? + } + + pub async fn min_decided_value_height(&self) -> Option { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.first_key()) + .await + .ok() + .flatten() + } + + pub async fn max_decided_value_height(&self) -> Option { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.last_key()) + .await + .ok() + .flatten() + } + + pub async fn get_decided_value( + &self, + height: Height, + ) -> Result, StoreError> { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.get_decided_block(height)).await? + } + + pub async fn store_decided_block( + &self, + certificate: &CommitCertificate, + block: &Block, + ) -> Result<(), StoreError> { + let decided_block = DecidedBlock { + block: block.clone(), + certificate: certificate.clone(), + }; + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.insert_decided_block(decided_block)).await? + } + + pub async fn store_undecided_proposal( + &self, + value: ProposedValue, + ) -> Result<(), StoreError> { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.insert_undecided_value(value)).await? + } + + pub async fn get_undecided_proposals( + &self, + height: Height, + round: Round, + ) -> Result>, StoreError> { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.get_undecided_values(height, round)).await? + } + + pub async fn prune(&self, retain_height: Height) -> Result, StoreError> { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.prune(retain_height)).await? + } +} diff --git a/code/examples/actor/proposal/src/store/db.rs b/code/examples/actor/proposal/src/store/db.rs new file mode 100644 index 000000000..1c4054a5d --- /dev/null +++ b/code/examples/actor/proposal/src/store/db.rs @@ -0,0 +1,205 @@ +use std::ops::RangeBounds; +use std::path::Path; + +use bytes::Bytes; +use redb::ReadableTable; +use tracing::error; + +use malachitebft_codec::Codec; +use malachitebft_core_consensus::ProposedValue; +use malachitebft_core_types::Round; +use malachitebft_proto::Protobuf; + +use crate::codec::ProtobufCodec; +use crate::types::{Block, BlockHash, Height, MockContext}; + +use super::error::StoreError; +use super::keys::{HeightKey, UndecidedValueKey}; +use super::types::{decode_certificate, encode_certificate, DecidedBlock}; + +const CERTIFICATES_TABLE: redb::TableDefinition> = + redb::TableDefinition::new("certificates"); + +const DECIDED_BLOCKS_TABLE: redb::TableDefinition> = + redb::TableDefinition::new("decided_blocks"); + +const UNDECIDED_VALUES_TABLE: redb::TableDefinition> = + redb::TableDefinition::new("undecided_blocks"); + +pub struct Db { + db: redb::Database, +} + +impl Db { + pub fn new(path: impl AsRef) -> Result { + Ok(Self { + db: redb::Database::create(path).map_err(StoreError::Database)?, + }) + } + + pub fn get_decided_block(&self, height: Height) -> Result, StoreError> { + let tx = self.db.begin_read()?; + let block = { + let table = tx.open_table(DECIDED_BLOCKS_TABLE)?; + let value = table.get(&height)?; + value.and_then(|value| Block::from_bytes(&value.value()).ok()) + }; + let certificate = { + let table = tx.open_table(CERTIFICATES_TABLE)?; + let value = table.get(&height)?; + value.and_then(|value| decode_certificate(&value.value()).ok()) + }; + + let decided_block = block + .zip(certificate) + .map(|(block, certificate)| DecidedBlock { block, certificate }); + + Ok(decided_block) + } + + pub fn insert_decided_block(&self, decided_block: DecidedBlock) -> Result<(), StoreError> { + let height = decided_block.block.height; + + let tx = self.db.begin_write()?; + { + let mut blocks = tx.open_table(DECIDED_BLOCKS_TABLE)?; + blocks.insert(height, decided_block.block.to_bytes()?.to_vec())?; + } + { + let mut certificates = tx.open_table(CERTIFICATES_TABLE)?; + certificates.insert(height, encode_certificate(&decided_block.certificate)?)?; + } + tx.commit()?; + + Ok(()) + } + + #[tracing::instrument(skip(self))] + pub fn get_undecided_values( + &self, + height: Height, + round: Round, + ) -> Result>, StoreError> { + let tx = self.db.begin_read()?; + let mut values = Vec::new(); + + let from = (height, round, BlockHash::new([0; 32])); + let to = (height, round, BlockHash::new([255; 32])); + + let table = tx.open_table(UNDECIDED_VALUES_TABLE)?; + let keys = self.undecided_values_range(&table, from..to)?; + + for key in keys { + if let Ok(Some(value)) = table.get(&key) { + let Ok(value) = ProtobufCodec.decode(Bytes::from(value.value())) else { + error!(hash = ?key.2, "Failed to decode ProposedValue"); + continue; + }; + + values.push(value); + } + } + + Ok(values) + } + + pub fn insert_undecided_value( + &self, + value: ProposedValue, + ) -> Result<(), StoreError> { + let key = ( + value.height, + value.round, + value.value.id().as_hash().clone(), + ); + let value = ProtobufCodec.encode(&value)?; + let tx = self.db.begin_write()?; + { + let mut table = tx.open_table(UNDECIDED_VALUES_TABLE)?; + table.insert(key, value.to_vec())?; + } + tx.commit()?; + Ok(()) + } + + pub fn height_range( + &self, + table: &Table, + range: impl RangeBounds, + ) -> Result, StoreError> + where + Table: redb::ReadableTable>, + { + Ok(table + .range(range)? + .flatten() + .map(|(key, _)| key.value()) + .collect::>()) + } + + pub fn undecided_values_range
( + &self, + table: &Table, + range: impl RangeBounds<(Height, Round, BlockHash)>, + ) -> Result, StoreError> + where + Table: redb::ReadableTable>, + { + Ok(table + .range(range)? + .flatten() + .map(|(key, _)| key.value()) + .collect::>()) + } + + pub fn prune(&self, retain_height: Height) -> Result, StoreError> { + let tx = self.db.begin_write().unwrap(); + let pruned = { + let mut undecided = tx.open_table(UNDECIDED_VALUES_TABLE)?; + let keys = self.undecided_values_range( + &undecided, + ..(retain_height, Round::Nil, BlockHash::new([0; 32])), + )?; + for key in keys { + undecided.remove(key)?; + } + + let mut decided = tx.open_table(DECIDED_BLOCKS_TABLE)?; + let mut certificates = tx.open_table(CERTIFICATES_TABLE)?; + + let keys = self.height_range(&decided, ..retain_height)?; + for key in &keys { + decided.remove(key)?; + certificates.remove(key)?; + } + keys + }; + tx.commit()?; + + Ok(pruned) + } + + pub fn first_key(&self) -> Option { + let tx = self.db.begin_read().unwrap(); + let table = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); + let (key, _) = table.first().ok()??; + Some(key.value()) + } + + pub fn last_key(&self) -> Option { + let tx = self.db.begin_read().unwrap(); + let table = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); + let (key, _) = table.last().ok()??; + Some(key.value()) + } + + pub fn create_tables(&self) -> Result<(), StoreError> { + let tx = self.db.begin_write()?; + // Implicitly creates the tables if they do not exist yet + let _ = tx.open_table(DECIDED_BLOCKS_TABLE)?; + let _ = tx.open_table(CERTIFICATES_TABLE)?; + let _ = tx.open_table(UNDECIDED_VALUES_TABLE)?; + tx.commit()?; + Ok(()) + } +} diff --git a/code/examples/actor/proposal/src/store/error.rs b/code/examples/actor/proposal/src/store/error.rs new file mode 100644 index 000000000..44a256925 --- /dev/null +++ b/code/examples/actor/proposal/src/store/error.rs @@ -0,0 +1,32 @@ +use malachitebft_proto::Error as ProtoError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum StoreError { + #[error("Database error: {0}")] + Database(#[from] redb::DatabaseError), + + #[error("Storage error: {0}")] + Storage(#[from] redb::StorageError), + + #[error("Table error: {0}")] + Table(#[from] redb::TableError), + + #[error("Commit error: {0}")] + Commit(#[from] redb::CommitError), + + #[error("Transaction error: {0}")] + Transaction(#[from] Box), + + #[error("Failed to encode/decode Protobuf: {0}")] + Protobuf(#[from] ProtoError), + + #[error("Failed to join on task: {0}")] + TaskJoin(#[from] tokio::task::JoinError), +} + +impl From for StoreError { + fn from(err: redb::TransactionError) -> Self { + Self::Transaction(Box::new(err)) + } +} diff --git a/code/examples/actor/proposal/src/store/keys.rs b/code/examples/actor/proposal/src/store/keys.rs new file mode 100644 index 000000000..6b55fe1bb --- /dev/null +++ b/code/examples/actor/proposal/src/store/keys.rs @@ -0,0 +1,120 @@ +use crate::types::{BlockHash, Height}; +use core::mem::size_of; +use malachitebft_core_types::Round; + +pub type UndecidedValueKey = (HeightKey, RoundKey, BlockHashKey); + +#[derive(Copy, Clone, Debug)] +pub struct HeightKey; + +impl redb::Value for HeightKey { + type SelfType<'a> = Height; + type AsBytes<'a> = [u8; size_of::()]; + + fn fixed_width() -> Option { + Some(size_of::()) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + let height = ::from_bytes(data); + + Height::new(height) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + ::as_bytes(&value.as_u64()) + } + + fn type_name() -> redb::TypeName { + redb::TypeName::new("Height") + } +} + +impl redb::Key for HeightKey { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + ::compare(data1, data2) + } +} + +#[derive(Copy, Clone, Debug)] +pub struct RoundKey; + +impl redb::Value for RoundKey { + type SelfType<'a> = Round; + type AsBytes<'a> = [u8; size_of::()]; + + fn fixed_width() -> Option { + Some(size_of::()) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + let round = ::from_bytes(data); + Round::from(round) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + ::as_bytes(&value.as_i64()) + } + + fn type_name() -> redb::TypeName { + redb::TypeName::new("Round") + } +} + +impl redb::Key for RoundKey { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + ::compare(data1, data2) + } +} + +#[derive(Copy, Clone, Debug)] +pub struct BlockHashKey; + +impl redb::Value for BlockHashKey { + type SelfType<'a> = BlockHash; + type AsBytes<'a> = &'a [u8; 32]; + + fn fixed_width() -> Option { + Some(32) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + let bytes = <[u8; 32] as redb::Value>::from_bytes(data); + BlockHash::new(bytes) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + value.as_bytes() + } + + fn type_name() -> redb::TypeName { + redb::TypeName::new("BlockHash") + } +} + +impl redb::Key for BlockHashKey { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + <[u8; 32] as redb::Key>::compare(data1, data2) + } +} diff --git a/code/examples/actor/proposal/src/store/mod.rs b/code/examples/actor/proposal/src/store/mod.rs new file mode 100644 index 000000000..84e8165d8 --- /dev/null +++ b/code/examples/actor/proposal/src/store/mod.rs @@ -0,0 +1,7 @@ +pub mod block_store; +pub mod db; +pub mod error; +pub mod keys; +pub mod types; + +pub use self::block_store::BlockStore; diff --git a/code/examples/actor/proposal/src/store/types.rs b/code/examples/actor/proposal/src/store/types.rs new file mode 100644 index 000000000..de4e6f3e2 --- /dev/null +++ b/code/examples/actor/proposal/src/store/types.rs @@ -0,0 +1,24 @@ +use malachitebft_core_types::CommitCertificate; +use malachitebft_proto::Error as ProtoError; +use prost::Message; + +use crate::codec; +use crate::types::{proto, Block, MockContext}; + +#[derive(Clone, Debug)] +pub struct DecidedBlock { + pub block: Block, + pub certificate: CommitCertificate, +} + +pub fn decode_certificate(bytes: &[u8]) -> Result, ProtoError> { + let proto = proto::CommitCertificate::decode(bytes)?; + codec::decode_commit_certificate(proto) +} + +pub fn encode_certificate( + certificate: &CommitCertificate, +) -> Result, ProtoError> { + let proto = codec::encode_commit_certificate(certificate)?; + Ok(proto.encode_to_vec()) +} diff --git a/code/examples/actor/proposal/src/types/address.rs b/code/examples/actor/proposal/src/types/address.rs new file mode 100644 index 000000000..54fe411d1 --- /dev/null +++ b/code/examples/actor/proposal/src/types/address.rs @@ -0,0 +1,81 @@ +use core::fmt; +use serde::{Deserialize, Serialize}; + +use crate::types::signing::{Hashable, PublicKey}; +use malachitebft_proto::{Error as ProtoError, Protobuf}; + +use super::proto; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Address( + #[serde( + serialize_with = "hex::serde::serialize_upper", + deserialize_with = "hex::serde::deserialize" + )] + [u8; Self::LENGTH], +); + +impl Address { + const LENGTH: usize = 20; + + #[cfg_attr(coverage_nightly, coverage(off))] + pub const fn new(value: [u8; Self::LENGTH]) -> Self { + Self(value) + } + + #[cfg_attr(coverage_nightly, coverage(off))] + pub fn from_public_key(public_key: &PublicKey) -> Self { + let hash = public_key.hash(); + let mut address = [0; Self::LENGTH]; + address.copy_from_slice(&hash[..Self::LENGTH]); + Self(address) + } + + pub fn into_inner(self) -> [u8; Self::LENGTH] { + self.0 + } +} + +impl fmt::Display for Address { + #[cfg_attr(coverage_nightly, coverage(off))] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.0.iter() { + write!(f, "{byte:02X}")?; + } + Ok(()) + } +} + +impl fmt::Debug for Address { + #[cfg_attr(coverage_nightly, coverage(off))] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Address({self})") + } +} + +impl malachitebft_core_types::Address for Address {} + +impl Protobuf for Address { + type Proto = proto::Address; + + fn from_proto(proto: Self::Proto) -> Result { + if proto.value.len() != Self::LENGTH { + return Err(ProtoError::Other(format!( + "Invalid address length: expected {}, got {}", + Self::LENGTH, + proto.value.len() + ))); + } + + let mut address = [0; Self::LENGTH]; + address.copy_from_slice(&proto.value); + Ok(Self(address)) + } + + fn to_proto(&self) -> Result { + Ok(proto::Address { + value: self.0.to_vec().into(), + }) + } +} diff --git a/code/examples/actor/proposal/src/types/block.rs b/code/examples/actor/proposal/src/types/block.rs new file mode 100644 index 000000000..6a6f4ffa8 --- /dev/null +++ b/code/examples/actor/proposal/src/types/block.rs @@ -0,0 +1,67 @@ +use malachitebft_proto::{Error as ProtoError, Protobuf}; +use prost::Message; +use serde::{Deserialize, Serialize}; + +use super::proto; +use crate::types::{hash::BlockHash, height::Height, transaction::TransactionBatch}; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Block { + pub height: Height, + pub transactions: TransactionBatch, + pub block_hash: BlockHash, +} + +impl Block { + pub fn new(height: Height, transactions: TransactionBatch, block_hash: BlockHash) -> Self { + Self { + height, + transactions, + block_hash, + } + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let proto = proto::Block::decode(bytes)?; + Self::from_proto(proto) + } + + /// The size of the block in bytes + pub fn size_bytes(&self) -> usize { + let tx_size = self.transactions.size_bytes(); + let block_hash_size = self.block_hash.to_vec().len(); + let height_size = std::mem::size_of::(); + tx_size + block_hash_size + height_size + } +} + +impl Protobuf for Block { + type Proto = proto::Block; + + fn from_proto(proto: Self::Proto) -> Result { + let transactions = proto + .transactions + .ok_or_else(|| ProtoError::missing_field::("transactions"))?; + + let block_hash = proto + .block_hash + .ok_or_else(|| ProtoError::missing_field::("block_hash"))?; + + Ok(Self { + height: Height::new(proto.height), + transactions: TransactionBatch::from_proto(transactions)?, + block_hash: BlockHash::from_bytes(&block_hash.elements) + .map_err(ProtoError::invalid_data::)?, + }) + } + + fn to_proto(&self) -> Result { + Ok(Self::Proto { + height: self.height.to_proto()?, + transactions: Some(self.transactions.to_proto()?), + block_hash: Some(proto::Hash { + elements: self.block_hash.to_vec().into(), + }), + }) + } +} diff --git a/code/examples/actor/proposal/src/types/context.rs b/code/examples/actor/proposal/src/types/context.rs new file mode 100644 index 000000000..684a6bb3c --- /dev/null +++ b/code/examples/actor/proposal/src/types/context.rs @@ -0,0 +1,91 @@ +use bytes::Bytes; + +use malachitebft_core_types::{Context, NilOrVal, Round, ValidatorSet as _}; + +use super::{ + address::*, height::*, proposal::*, proposal_part::*, signing::*, validator_set::*, value::*, + vote::*, +}; + +#[derive(Copy, Clone, Debug, Default)] +pub struct MockContext; + +impl MockContext { + pub fn new() -> Self { + Self + } + + pub fn select_proposer<'a>( + &self, + validator_set: &'a ValidatorSet, + height: Height, + round: Round, + ) -> &'a Validator { + assert!(validator_set.count() > 0); + assert!(round != Round::Nil && round.as_i64() >= 0); + + let proposer_index = { + let height = height.as_u64() as usize; + let round = round.as_i64() as usize; + + (height - 1 + round) % validator_set.count() + }; + + validator_set + .get_by_index(proposer_index) + .expect("proposer_index is valid") + } +} + +impl Context for MockContext { + type Address = Address; + type ProposalPart = ProposalPart; + type Height = Height; + type Proposal = Proposal; + type ValidatorSet = ValidatorSet; + type Validator = Validator; + type Value = Value; + type Vote = Vote; + type Extension = Bytes; + type SigningScheme = Ed25519; + + fn select_proposer<'a>( + &self, + validator_set: &'a Self::ValidatorSet, + height: Self::Height, + round: Round, + ) -> &'a Self::Validator { + self.select_proposer(validator_set, height, round) + } + + fn new_proposal( + &self, + height: Height, + round: Round, + value: Value, + pol_round: Round, + address: Address, + ) -> Proposal { + Proposal::new(height, round, value, pol_round, address) + } + + fn new_prevote( + &self, + height: Height, + round: Round, + value_id: NilOrVal, + address: Address, + ) -> Vote { + Vote::new_prevote(height, round, value_id, address) + } + + fn new_precommit( + &self, + height: Height, + round: Round, + value_id: NilOrVal, + address: Address, + ) -> Vote { + Vote::new_precommit(height, round, value_id, address) + } +} diff --git a/code/examples/actor/proposal/src/types/hash.rs b/code/examples/actor/proposal/src/types/hash.rs new file mode 100644 index 000000000..5084de9e2 --- /dev/null +++ b/code/examples/actor/proposal/src/types/hash.rs @@ -0,0 +1,97 @@ +use bytes::Bytes; +use sha3::{Digest, Sha3_256}; +use std::fmt; + +use serde::{Deserialize, Serialize}; + +use malachitebft_proto::{Error as ProtoError, Protobuf}; + +use super::proto; +// Define the size of the hash (32 bytes for SHA3-256). +const HASH_SIZE: usize = 32; + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)] +pub struct Hash([u8; HASH_SIZE]); + +impl fmt::Debug for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in &self.0 { + write!(f, "{byte:02X}")?; + } + Ok(()) + } +} + +impl Hash { + /// Create a new `Hash` from a fixed-size array of 32 bytes. + pub fn new(data: [u8; HASH_SIZE]) -> Hash { + Hash(data) + } + + /// Create an empty `Hash` initialized to zero. + pub fn new_empty() -> Hash { + Hash([0; HASH_SIZE]) + } + + /// Get the hash as a vector of bytes. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + /// Get the hash as a byte slice. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Convert the hash to a hexadecimal string. + pub fn to_hex_string(&self) -> String { + self.0 + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + } + + /// Compare this hash with another for equality. + pub fn is_eq(&self, other: &Hash) -> bool { + self.0 == other.0 + } + + /// Create a `Hash` from a slice of bytes. + /// Returns an error if the slice length is not 32. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != HASH_SIZE { + return Err("Invalid hash length"); + } + let mut hash = [0u8; HASH_SIZE]; + hash.copy_from_slice(bytes); + Ok(Hash(hash)) + } + + /// Compute a SHA3-256 hash of the input data and return it as a `Hash` instance. + pub fn compute_hash(data: &[u8]) -> Hash { + let mut hasher = Sha3_256::new(); + hasher.update(data); + let sha3_256_hash = hasher.finalize(); + + let mut hash = [0u8; 32]; + hash.copy_from_slice(&sha3_256_hash); + Hash(hash) + } +} + +impl Protobuf for Hash { + type Proto = proto::Hash; + + fn from_proto(proto: Self::Proto) -> Result { + Hash::from_bytes(&proto.elements) + .map_err(|_| ProtoError::invalid_data::("invalid hash")) + } + + fn to_proto(&self) -> Result { + Ok(proto::Hash { + elements: Bytes::from(self.to_vec()), + }) + } +} + +pub type BlockHash = Hash; diff --git a/code/examples/actor/proposal/src/types/height.rs b/code/examples/actor/proposal/src/types/height.rs new file mode 100644 index 000000000..b38fb5317 --- /dev/null +++ b/code/examples/actor/proposal/src/types/height.rs @@ -0,0 +1,72 @@ +use core::fmt; +use malachitebft_proto::{Error as ProtoError, Protobuf}; +use serde::{Deserialize, Serialize}; + +/// A blockchain height +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Height(u64); + +impl Height { + pub const fn new(height: u64) -> Self { + Self(height) + } + + pub const fn as_u64(&self) -> u64 { + self.0 + } + + pub fn increment(&self) -> Self { + Self(self.0 + 1) + } + + pub fn decrement(&self) -> Option { + self.0.checked_sub(1).map(Self) + } +} + +impl Default for Height { + fn default() -> Self { + malachitebft_core_types::Height::ZERO + } +} + +impl fmt::Display for Height { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Debug for Height { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Height({})", self.0) + } +} + +impl malachitebft_core_types::Height for Height { + const ZERO: Self = Self(0); + const INITIAL: Self = Self(1); + + fn increment_by(&self, n: u64) -> Self { + Self(self.0 + n) + } + + fn decrement_by(&self, n: u64) -> Option { + Some(Self(self.0.saturating_sub(n))) + } + + fn as_u64(&self) -> u64 { + self.0 + } +} + +impl Protobuf for Height { + type Proto = u64; + + fn from_proto(proto: Self::Proto) -> Result { + Ok(Self(proto)) + } + + fn to_proto(&self) -> Result { + Ok(self.0) + } +} diff --git a/code/examples/actor/proposal/src/types/mod.rs b/code/examples/actor/proposal/src/types/mod.rs new file mode 100644 index 000000000..76185dc80 --- /dev/null +++ b/code/examples/actor/proposal/src/types/mod.rs @@ -0,0 +1,31 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] + +mod address; +mod block; +mod context; +mod hash; +mod height; +mod proposal; +mod proposal_part; +mod signing; +mod transaction; +mod validator_set; +mod value; +mod vote; + +//pub mod codec; +pub mod proto; +//pub mod utils; + +pub use address::*; +pub use block::*; +pub use context::*; +pub use hash::*; +pub use height::*; +pub use proposal::*; +pub use proposal_part::*; +pub use signing::*; +pub use transaction::*; +pub use validator_set::*; +pub use value::*; +pub use vote::*; diff --git a/code/examples/actor/proposal/src/types/proposal.rs b/code/examples/actor/proposal/src/types/proposal.rs new file mode 100644 index 000000000..2c18be369 --- /dev/null +++ b/code/examples/actor/proposal/src/types/proposal.rs @@ -0,0 +1,103 @@ +use bytes::Bytes; +use malachitebft_core_types::Round; +use malachitebft_proto::{Error as ProtoError, Protobuf}; + +use super::proto; + +use crate::types::{address::Address, context::MockContext, height::Height, value::Value}; + +/// A proposal for a value in a round +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Proposal { + pub height: Height, + pub round: Round, + pub value: Value, + pub pol_round: Round, + pub validator_address: Address, +} + +impl Proposal { + pub fn new( + height: Height, + round: Round, + value: Value, + pol_round: Round, + validator_address: Address, + ) -> Self { + Self { + height, + round, + value, + pol_round, + validator_address, + } + } + + pub fn to_bytes(&self) -> Bytes { + Protobuf::to_bytes(self).unwrap() + } + + pub fn to_sign_bytes(&self) -> Bytes { + Protobuf::to_bytes(self).unwrap() + } +} + +impl malachitebft_core_types::Proposal for Proposal { + fn height(&self) -> Height { + self.height + } + + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> &Value { + &self.value + } + + fn take_value(self) -> Value { + self.value + } + + fn pol_round(&self) -> Round { + self.pol_round + } + + fn validator_address(&self) -> &Address { + &self.validator_address + } +} + +impl Protobuf for Proposal { + type Proto = proto::Proposal; + + #[cfg_attr(coverage_nightly, coverage(off))] + fn to_proto(&self) -> Result { + Ok(Self::Proto { + height: self.height.to_proto()?, + round: self.round.as_u32().expect("round should not be nil"), + value: Some(self.value.to_proto()?), + pol_round: self.pol_round.as_u32(), + validator_address: Some(self.validator_address.to_proto()?), + }) + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn from_proto(proto: Self::Proto) -> Result { + Ok(Self { + height: Height::from_proto(proto.height)?, + round: Round::new(proto.round), + value: Value::from_proto( + proto + .value + .ok_or_else(|| ProtoError::missing_field::("value"))?, + )?, + pol_round: Round::from(proto.pol_round), + validator_address: Address::from_proto( + proto + .validator_address + .ok_or_else(|| ProtoError::missing_field::("validator_address"))?, + )?, + }) + } +} diff --git a/code/examples/actor/proposal/src/types/proposal_part.rs b/code/examples/actor/proposal/src/types/proposal_part.rs new file mode 100644 index 000000000..19c9a8d29 --- /dev/null +++ b/code/examples/actor/proposal/src/types/proposal_part.rs @@ -0,0 +1,14 @@ +use crate::types::context::MockContext; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProposalPart {} + +impl malachitebft_core_types::ProposalPart for ProposalPart { + fn is_first(&self) -> bool { + true + } + + fn is_last(&self) -> bool { + true + } +} diff --git a/code/examples/actor/proposal/src/types/proto.rs b/code/examples/actor/proposal/src/types/proto.rs new file mode 100644 index 000000000..a5850afec --- /dev/null +++ b/code/examples/actor/proposal/src/types/proto.rs @@ -0,0 +1,3 @@ +#![allow(missing_docs)] + +include!(concat!(env!("OUT_DIR"), "/p2p.rs")); diff --git a/code/examples/actor/proposal/src/types/signing.rs b/code/examples/actor/proposal/src/types/signing.rs new file mode 100644 index 000000000..0741d505a --- /dev/null +++ b/code/examples/actor/proposal/src/types/signing.rs @@ -0,0 +1,109 @@ +use bytes::Bytes; + +use malachitebft_core_types::{ + SignedExtension, SignedProposal, SignedProposalPart, SignedVote, SigningProvider, +}; + +use crate::types::{ + context::MockContext, proposal::Proposal, proposal_part::ProposalPart, vote::Vote, +}; + +pub use malachitebft_signing_ed25519::*; + +pub trait Hashable { + type Output; + fn hash(&self) -> Self::Output; +} + +impl Hashable for PublicKey { + type Output = [u8; 32]; + + fn hash(&self) -> [u8; 32] { + use sha3::{Digest, Keccak256}; + let mut hasher = Keccak256::new(); + hasher.update(self.as_bytes()); + hasher.finalize().into() + } +} + +#[derive(Debug)] +pub struct Ed25519Provider { + private_key: PrivateKey, +} + +impl Ed25519Provider { + pub fn new(private_key: PrivateKey) -> Self { + Self { private_key } + } + + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + pub fn sign(&self, data: &[u8]) -> Signature { + self.private_key.sign(data) + } + + pub fn verify(&self, data: &[u8], signature: &Signature, public_key: &PublicKey) -> bool { + public_key.verify(data, signature).is_ok() + } +} + +impl SigningProvider for Ed25519Provider { + fn sign_vote(&self, vote: Vote) -> SignedVote { + let signature = self.sign(&vote.to_sign_bytes()); + SignedVote::new(vote, signature) + } + + fn verify_signed_vote( + &self, + vote: &Vote, + signature: &Signature, + public_key: &PublicKey, + ) -> bool { + public_key.verify(&vote.to_sign_bytes(), signature).is_ok() + } + + fn sign_proposal(&self, proposal: Proposal) -> SignedProposal { + let signature = self.private_key.sign(&proposal.to_sign_bytes()); + SignedProposal::new(proposal, signature) + } + + fn verify_signed_proposal( + &self, + proposal: &Proposal, + signature: &Signature, + public_key: &PublicKey, + ) -> bool { + public_key + .verify(&proposal.to_sign_bytes(), signature) + .is_ok() + } + + fn sign_proposal_part(&self, _proposal_part: ProposalPart) -> SignedProposalPart { + panic!("Not applicable for ProposalOnly mode"); + } + + fn verify_signed_proposal_part( + &self, + _proposal_part: &ProposalPart, + _signature: &Signature, + _public_key: &PublicKey, + ) -> bool { + panic!("Not applicable for ProposalOnly mode"); + } + + fn sign_vote_extension(&self, extension: Bytes) -> SignedExtension { + let signature = self.private_key.sign(extension.as_ref()); + malachitebft_core_types::SignedMessage::new(extension, signature) + } + + fn verify_signed_vote_extension( + &self, + extension: &Bytes, + signature: &Signature, + public_key: &PublicKey, + ) -> bool { + public_key.verify(extension.as_ref(), signature).is_ok() + } +} diff --git a/code/examples/actor/proposal/src/types/transaction.rs b/code/examples/actor/proposal/src/types/transaction.rs new file mode 100644 index 000000000..b4f42a920 --- /dev/null +++ b/code/examples/actor/proposal/src/types/transaction.rs @@ -0,0 +1,160 @@ +use core::fmt; + +use crate::types::hash::Hash; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +use malachitebft_proto::{Error as ProtoError, Protobuf}; + +use super::proto; + +/// Transaction +#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)] +pub struct Transaction { + data: Bytes, + hash: Hash, +} + +impl Transaction { + /// Create a new transaction from bytes + pub fn new(data: impl Into) -> Self { + let data = data.into(); + let hash = Self::compute_hash(&data); + Self { data, hash } + } + + /// Get bytes from a transaction + pub fn to_bytes(&self) -> Bytes { + self.data.clone() + } + + /// Get bytes from a transaction + pub fn as_bytes(&self) -> &[u8] { + self.data.as_ref() + } + + /// Size of this transaction in bytes + pub fn size_bytes(&self) -> usize { + self.data.len() + } + + /// Hash of this transaction + pub fn hash(&self) -> &Hash { + &self.hash + } + + /// Compute the hash of a transaction + pub fn compute_hash(bytes: &[u8]) -> Hash { + use sha3::Digest; + let mut hasher = sha3::Keccak256::new(); + hasher.update(bytes); + Hash::new(hasher.finalize().into()) + } +} + +impl fmt::Debug for Transaction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Transaction({:?}, {} bytes)", + self.hash, + self.size_bytes() + ) + } +} + +impl Protobuf for Transaction { + type Proto = proto::ConsensusTransaction; + + fn from_proto(proto: Self::Proto) -> Result { + let data = proto.tx; + let hash = Self::compute_hash(&data); + Ok(Self { data, hash }) + } + + fn to_proto(&self) -> Result { + Ok(Self::Proto { + tx: self.data.clone(), + hash: Bytes::from(self.hash.to_vec()), + }) + } +} + +/// Transaction batch (used by mempool) +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct TransactionBatch(Vec); + +impl TransactionBatch { + /// Create a new transaction batch + pub fn new(txes: Vec) -> Self { + TransactionBatch(txes) + } + + /// Add a transaction to the batch + pub fn push(&mut self, tx: Transaction) { + self.0.push(tx); + } + + /// Add a set of transaction to the batch + pub fn append(&mut self, txes: TransactionBatch) { + let mut txes1 = txes.clone(); + self.0.append(&mut txes1.0); + } + + /// Get the number of transactions in the batch + pub fn len(&self) -> usize { + self.0.len() + } + + /// Whether or not the batch is empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Get transactions from a batch + pub fn into_vec(self) -> Vec { + self.0 + } + + /// Get transactions from a batch + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + /// Get transactions from a batch + pub fn as_slice(&self) -> &[Transaction] { + &self.0 + } + + /// The size of this batch in bytes + pub fn size_bytes(&self) -> usize { + self.as_slice() + .iter() + .map(|tx| tx.size_bytes()) + .sum::() + } +} + +impl Protobuf for TransactionBatch { + type Proto = proto::TransactionBatch; + + fn from_proto(proto: Self::Proto) -> Result { + Ok(Self::new( + proto + .transactions + .into_iter() + .map(Transaction::from_proto) + .collect::, _>>()?, + )) + } + + fn to_proto(&self) -> Result { + Ok(proto::TransactionBatch { + transactions: self + .as_slice() + .iter() + .map(Transaction::to_proto) + .collect::>()?, + }) + } +} diff --git a/code/examples/actor/proposal/src/types/validator_set.rs b/code/examples/actor/proposal/src/types/validator_set.rs new file mode 100644 index 000000000..5e0455bfe --- /dev/null +++ b/code/examples/actor/proposal/src/types/validator_set.rs @@ -0,0 +1,181 @@ +use core::slice; +use std::sync::Arc; + +use malachitebft_core_types::VotingPower; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::types::{address::Address, context::MockContext, signing::PublicKey}; + +/// A validator is a public key and voting power +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Validator { + pub address: Address, + pub public_key: PublicKey, + pub voting_power: VotingPower, +} + +impl Validator { + #[cfg_attr(coverage_nightly, coverage(off))] + pub fn new(public_key: PublicKey, voting_power: VotingPower) -> Self { + Self { + address: Address::from_public_key(&public_key), + public_key, + voting_power, + } + } +} + +impl PartialOrd for Validator { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Validator { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.address.cmp(&other.address) + } +} + +impl malachitebft_core_types::Validator for Validator { + fn address(&self) -> &Address { + &self.address + } + + fn public_key(&self) -> &PublicKey { + &self.public_key + } + + fn voting_power(&self) -> VotingPower { + self.voting_power + } +} + +/// A validator set contains a list of validators sorted by address. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ValidatorSet { + pub validators: Arc>, +} + +impl Serialize for ValidatorSet { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.validators.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ValidatorSet { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let validators = Vec::::deserialize(deserializer)?; + Ok(Self { + validators: Arc::new(validators), + }) + } +} + +impl ValidatorSet { + pub fn new(validators: impl IntoIterator) -> Self { + let mut validators: Vec<_> = validators.into_iter().collect(); + ValidatorSet::sort_validators(&mut validators); + + assert!(!validators.is_empty()); + + Self { + validators: Arc::new(validators), + } + } + + /// Get the number of validators in the set + pub fn len(&self) -> usize { + self.validators.len() + } + + /// Check if the set is empty + pub fn is_empty(&self) -> bool { + self.validators.is_empty() + } + + /// Iterate over the validators in the set + pub fn iter(&self) -> slice::Iter { + self.validators.iter() + } + + /// The total voting power of the validator set + pub fn total_voting_power(&self) -> VotingPower { + self.validators.iter().map(|v| v.voting_power).sum() + } + + /// Get a validator by its index + pub fn get_by_index(&self, index: usize) -> Option<&Validator> { + self.validators.get(index) + } + + /// Get a validator by its address + pub fn get_by_address(&self, address: &Address) -> Option<&Validator> { + self.validators.iter().find(|v| &v.address == address) + } + + fn sort_validators(vals: &mut Vec) { + // Sort the validators according to the current Tendermint requirements + // + // use core::cmp::Reverse; + // + // (first by validator power, descending, then by address, ascending) + // vals.sort_unstable_by(|v1, v2| { + // let a = (Reverse(v1.voting_power), &v1.address); + // let b = (Reverse(v2.voting_power), &v2.address); + // a.cmp(&b) + // }); + + vals.dedup(); + } +} + +impl malachitebft_core_types::ValidatorSet for ValidatorSet { + fn count(&self) -> usize { + self.validators.len() + } + + fn total_voting_power(&self) -> VotingPower { + self.total_voting_power() + } + + fn get_by_address(&self, address: &Address) -> Option<&Validator> { + self.get_by_address(address) + } + + fn get_by_index(&self, index: usize) -> Option<&Validator> { + self.validators.get(index) + } +} + +#[cfg(test)] +mod tests { + use rand::rngs::StdRng; + use rand::SeedableRng; + + use super::*; + + use crate::types::signing::PrivateKey; + + #[test] + fn new_validator_set_vp() { + let mut rng = StdRng::seed_from_u64(0x42); + + let sk1 = PrivateKey::generate(&mut rng); + let sk2 = PrivateKey::generate(&mut rng); + let sk3 = PrivateKey::generate(&mut rng); + + let v1 = Validator::new(sk1.public_key(), 1); + let v2 = Validator::new(sk2.public_key(), 2); + let v3 = Validator::new(sk3.public_key(), 3); + + let vs = ValidatorSet::new(vec![v1, v2, v3]); + assert_eq!(vs.total_voting_power(), 6); + } +} diff --git a/code/examples/actor/proposal/src/types/value.rs b/code/examples/actor/proposal/src/types/value.rs new file mode 100644 index 000000000..6521704b9 --- /dev/null +++ b/code/examples/actor/proposal/src/types/value.rs @@ -0,0 +1,112 @@ +use bytes::Bytes; +use core::fmt; +use malachitebft_proto::{Error as ProtoError, Protobuf}; +use serde::{Deserialize, Serialize}; + +use super::block::Block; +use super::hash::Hash; +use super::proto; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct ValueId(Hash); + +impl ValueId { + pub fn new(id: Hash) -> Self { + Self(id) + } + + pub const fn as_hash(&self) -> &Hash { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl From for ValueId { + fn from(value: Hash) -> Self { + Self::new(value) + } +} + +impl fmt::Display for ValueId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl Protobuf for ValueId { + type Proto = proto::ValueId; + + #[cfg_attr(coverage_nightly, coverage(off))] + fn from_proto(proto: Self::Proto) -> Result { + let bytes = proto + .value + .ok_or_else(|| ProtoError::missing_field::("value"))?; + + Ok(ValueId::new(Hash::from_bytes(&bytes.elements).map_err( + |_| ProtoError::invalid_data::("invalid hash"), + )?)) + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn to_proto(&self) -> Result { + Ok(proto::ValueId { + value: Some(proto::Hash { + elements: Bytes::from(self.0.to_vec()), + }), + }) + } +} + +/// The value to decide on +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Value { + pub value: Block, +} + +impl Value { + pub fn new(value: Block) -> Self { + Self { value } + } + + pub fn id(&self) -> ValueId { + ValueId::new(self.value.block_hash.clone()) + } + + pub fn size_bytes(&self) -> usize { + std::mem::size_of_val(&self.value) + } +} + +impl Protobuf for Value { + type Proto = proto::Value; + + #[cfg_attr(coverage_nightly, coverage(off))] + fn from_proto(proto: Self::Proto) -> Result { + let value = proto + .value + .ok_or_else(|| ProtoError::missing_field::("value"))?; + + Ok(Value { + value: Block::from_proto(value) + .map_err(|_| ProtoError::invalid_data::("invalid block"))?, + }) + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn to_proto(&self) -> Result { + Ok(proto::Value { + value: Some(self.value.to_proto()?), + }) + } +} + +impl malachitebft_core_types::Value for Value { + type Id = ValueId; + + fn id(&self) -> ValueId { + ValueId::new(self.value.block_hash.clone()) + } +} diff --git a/code/examples/actor/proposal/src/types/vote.rs b/code/examples/actor/proposal/src/types/vote.rs new file mode 100644 index 000000000..6356c0390 --- /dev/null +++ b/code/examples/actor/proposal/src/types/vote.rs @@ -0,0 +1,161 @@ +use bytes::Bytes; +use malachitebft_core_types::{NilOrVal, Round, SignedExtension, VoteType}; +use malachitebft_proto::{Error as ProtoError, Protobuf}; + +use crate::types::{address::Address, context::MockContext, height::Height, value::ValueId}; + +pub use malachitebft_core_types::Extension; + +use super::proto; + +/// A vote for a value in a round +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Vote { + pub typ: VoteType, + pub height: Height, + pub round: Round, + pub value: NilOrVal, + pub validator_address: Address, + pub extension: Option>, +} + +impl Vote { + pub fn new_prevote( + height: Height, + round: Round, + value: NilOrVal, + validator_address: Address, + ) -> Self { + Self { + typ: VoteType::Prevote, + height, + round, + value, + validator_address, + extension: None, + } + } + + pub fn new_precommit( + height: Height, + round: Round, + value: NilOrVal, + address: Address, + ) -> Self { + Self { + typ: VoteType::Precommit, + height, + round, + value, + validator_address: address, + extension: None, + } + } + + pub fn to_bytes(&self) -> Bytes { + Protobuf::to_bytes(self).unwrap() + } + + pub fn to_sign_bytes(&self) -> Bytes { + let vote = Self { + extension: None, + ..self.clone() + }; + + Protobuf::to_bytes(&vote).unwrap() + } +} + +impl malachitebft_core_types::Vote for Vote { + fn height(&self) -> Height { + self.height + } + + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> &NilOrVal { + &self.value + } + + fn take_value(self) -> NilOrVal { + self.value + } + + fn vote_type(&self) -> VoteType { + self.typ + } + + fn validator_address(&self) -> &Address { + &self.validator_address + } + + fn extension(&self) -> Option<&SignedExtension> { + self.extension.as_ref() + } + + fn take_extension(&mut self) -> Option> { + self.extension.take() + } + + fn extend(self, extension: SignedExtension) -> Self { + Self { + extension: Some(extension), + ..self + } + } +} + +impl Protobuf for Vote { + type Proto = proto::Vote; + + #[cfg_attr(coverage_nightly, coverage(off))] + fn from_proto(proto: Self::Proto) -> Result { + Ok(Self { + typ: decode_votetype(proto.vote_type()), + height: Height::from_proto(proto.height)?, + round: Round::new(proto.round), + value: match proto.value { + Some(value) => NilOrVal::Val(ValueId::from_proto(value)?), + None => NilOrVal::Nil, + }, + validator_address: Address::from_proto( + proto + .validator_address + .ok_or_else(|| ProtoError::missing_field::("validator_address"))?, + )?, + extension: Default::default(), + }) + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn to_proto(&self) -> Result { + Ok(Self::Proto { + vote_type: encode_votetype(self.typ).into(), + height: self.height.to_proto()?, + round: self.round.as_u32().expect("round should not be nil"), + value: match &self.value { + NilOrVal::Nil => None, + NilOrVal::Val(v) => Some(v.to_proto()?), + }, + validator_address: Some(self.validator_address.to_proto()?), + }) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn encode_votetype(vote_type: VoteType) -> proto::VoteType { + match vote_type { + VoteType::Prevote => proto::VoteType::Prevote, + VoteType::Precommit => proto::VoteType::Precommit, + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn decode_votetype(vote_type: proto::VoteType) -> VoteType { + match vote_type { + proto::VoteType::Prevote => VoteType::Prevote, + proto::VoteType::Precommit => VoteType::Precommit, + } +} diff --git a/code/examples/actor/test/Cargo.toml b/code/examples/actor/test/Cargo.toml new file mode 100644 index 000000000..f9a1c96bc --- /dev/null +++ b/code/examples/actor/test/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "informalsystems-malachitebft-actor-test" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +# Actor proposal dependencies +informalsystems_malachitebft_actor_app_proposal = { path = "../proposal" } + +# Core malachite dependencies +malachitebft-engine.workspace = true +malachitebft-core-types.workspace = true +malachitebft-config.workspace = true +malachitebft-core-consensus.workspace = true +malachitebft-metrics.workspace = true +malachitebft-test-framework.workspace = true + +async-trait.workspace = true +axum.workspace = true +bytesize.workspace = true +eyre.workspace = true +rand.workspace = true +ractor.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +tempfile.workspace = true + +[lints] +workspace = true \ No newline at end of file diff --git a/code/examples/actor/test/src/lib.rs b/code/examples/actor/test/src/lib.rs new file mode 100644 index 000000000..90fed3eeb --- /dev/null +++ b/code/examples/actor/test/src/lib.rs @@ -0,0 +1,242 @@ +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; + +use async_trait::async_trait; +use rand::rngs::StdRng; +use rand::SeedableRng; + +use informalsystems_malachitebft_actor_app_proposal::config::Config; +use informalsystems_malachitebft_actor_app_proposal::node::{ActorNode, ConfigSource, Handle}; +use informalsystems_malachitebft_actor_app_proposal::types::{ + Height, MockContext, PrivateKey, Validator, ValidatorSet, +}; +use malachitebft_config::mempool_load::UniformLoadConfig; +use malachitebft_test_framework::HasTestRunner; +use malachitebft_test_framework::{NodeRunner, TestNode}; + +pub use malachitebft_test_framework::TestBuilder as GenTestBuilder; +pub use malachitebft_test_framework::{ + CanMakeConfig, CanMakeGenesis, CanMakePrivateKeyFile, EngineHandle, HandlerResult, Node, + NodeId, TestParams, +}; + +use tempfile::TempDir; + +#[cfg(test)] +pub mod tests; + +pub type TestBuilder = GenTestBuilder; + +impl HasTestRunner for MockContext { + type Runner = TestRunner; +} + +#[derive(Clone)] +pub struct TestRunner { + pub id: usize, + pub params: TestParams, + pub nodes_count: usize, + pub start_height: HashMap, + pub home_dir: HashMap, + pub private_keys: HashMap, + pub validator_set: ValidatorSet, + pub consensus_base_port: usize, + pub mempool_base_port: usize, + pub metrics_base_port: usize, +} + +fn temp_dir(id: NodeId) -> PathBuf { + TempDir::with_prefix(format!("malachitebft-test-actor-{id}-")) + .unwrap() + .keep() +} + +#[async_trait] +impl NodeRunner for TestRunner { + type NodeHandle = Handle; + + fn new(id: usize, nodes: &[TestNode], params: TestParams) -> Self { + let nodes_count = nodes.len(); + let base_port = 20_000 + id * 1000; + + let (validators, private_keys) = make_validators(nodes); + let validator_set = ValidatorSet::new(validators); + + let start_height = nodes + .iter() + .map(|node| (node.id, node.start_height)) + .collect(); + + let home_dir = nodes + .iter() + .map(|node| (node.id, temp_dir(node.id))) + .collect(); + + Self { + id, + params, + nodes_count, + start_height, + home_dir, + private_keys, + validator_set, + consensus_base_port: base_port, + mempool_base_port: base_port + 100, + metrics_base_port: base_port + 200, + } + } + + async fn spawn(&self, id: NodeId) -> eyre::Result { + let home_dir = &self.home_dir[&id].clone(); + + let app = ActorNode::new( + home_dir.clone(), + ConfigSource::Value(Box::new(self.generate_config(id))), + Some(self.start_height[&id].as_u64()), + ); + + let validators = self + .validator_set + .validators + .iter() + .map(|val| (val.public_key, val.voting_power)) + .collect(); + + let genesis = app.make_genesis(validators); + fs::create_dir_all(app.genesis_file().parent().unwrap())?; + fs::write(app.genesis_file(), serde_json::to_string(&genesis)?)?; + + let priv_key_file = app.make_private_key_file(self.private_keys[&id].clone()); + fs::create_dir_all(app.private_key_file().parent().unwrap())?; + fs::write( + app.private_key_file(), + serde_json::to_string(&priv_key_file)?, + )?; + + let handle = app.start().await?; + + Ok(handle) + } + + async fn reset_db(&self, id: NodeId) -> eyre::Result<()> { + let db_dir = self.home_dir[&id].join("db"); + fs::remove_dir_all(&db_dir)?; + fs::create_dir_all(&db_dir)?; + Ok(()) + } +} + +impl TestRunner { + fn generate_config(&self, node: NodeId) -> Config { + let mut config = self.generate_default_config(node); + apply_params(&mut config, &self.params); + config + } + + fn generate_default_config(&self, node: NodeId) -> Config { + use malachitebft_config::*; + + let transport = transport_from_env(TransportProtocol::Tcp); + let protocol = PubSubProtocol::default(); + let i = node - 1; + + Config { + moniker: format!("actor-node-{node}"), + logging: LoggingConfig::default(), + consensus: ConsensusConfig { + value_payload: ValuePayload::ProposalOnly, + queue_capacity: 100, + timeouts: TimeoutConfig::default(), + p2p: P2pConfig { + protocol, + discovery: DiscoveryConfig::default(), + listen_addr: transport.multiaddr("127.0.0.1", self.consensus_base_port + i), + persistent_peers: (0..self.nodes_count) + .filter(|j| i != *j) + .map(|j| transport.multiaddr("127.0.0.1", self.consensus_base_port + j)) + .collect(), + ..Default::default() + }, + }, + mempool: MempoolConfig { + p2p: P2pConfig { + protocol, + listen_addr: transport.multiaddr("127.0.0.1", self.mempool_base_port + i), + persistent_peers: (0..self.nodes_count) + .filter(|j| i != *j) + .map(|j| transport.multiaddr("127.0.0.1", self.mempool_base_port + j)) + .collect(), + ..Default::default() + }, + max_tx_count: 10000, + gossip_batch_size: 0, + load: MempoolLoadConfig { + load_type: MempoolLoadType::UniformLoad(UniformLoadConfig::default()), + }, + }, + value_sync: ValueSyncConfig { + enabled: true, + status_update_interval: Duration::from_secs(2), + request_timeout: Duration::from_secs(5), + ..Default::default() + }, + metrics: MetricsConfig { + enabled: false, + listen_addr: format!("127.0.0.1:{}", self.metrics_base_port + i) + .parse() + .unwrap(), + }, + runtime: RuntimeConfig::single_threaded(), + test: TestConfig { + stable_block_times: true, + ..TestConfig::default() + }, + } + } +} + +use malachitebft_config::{TransportProtocol, ValuePayload}; + +fn transport_from_env(default: TransportProtocol) -> TransportProtocol { + if let Ok(protocol) = std::env::var("MALACHITE_TRANSPORT") { + TransportProtocol::from_str(&protocol).unwrap_or(default) + } else { + default + } +} + +fn make_validators( + nodes: &[TestNode], +) -> (Vec, HashMap) { + let mut rng = StdRng::seed_from_u64(0x42); + + let mut validators = Vec::new(); + let mut private_keys = HashMap::new(); + + for node in nodes { + let sk = PrivateKey::generate(&mut rng); + let val = Validator::new(sk.public_key(), node.voting_power); + + private_keys.insert(node.id, sk); + + if node.voting_power > 0 { + validators.push(val); + } + } + + (validators, private_keys) +} + +fn apply_params(config: &mut Config, params: &TestParams) { + config.consensus.value_payload = ValuePayload::ProposalOnly; + config.value_sync.enabled = params.enable_value_sync; + config.consensus.p2p.protocol = params.protocol; + config.test.max_block_size = params.block_size; + config.test.txs_per_part = 0; + config.test.vote_extensions.enabled = params.vote_extensions.is_some(); + config.test.vote_extensions.size = params.vote_extensions.unwrap_or_default(); + config.test.max_retain_blocks = params.max_retain_blocks; +} diff --git a/code/examples/actor/test/src/tests/full_nodes.rs b/code/examples/actor/test/src/tests/full_nodes.rs new file mode 100644 index 000000000..c105fd755 --- /dev/null +++ b/code/examples/actor/test/src/tests/full_nodes.rs @@ -0,0 +1,181 @@ +use std::time::Duration; + +use crate::{TestBuilder, TestParams}; + +#[tokio::test] +pub async fn basic_full_node() { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + // Add 3 validators with different voting powers + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(20) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(30) + .start() + .wait_until(HEIGHT) + .success(); + + // Add 2 full nodes that should follow consensus but not participate + test.add_node() + .full_node() + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .full_node() + .start() + .wait_until(HEIGHT) + .success(); + + test.build().run(Duration::from_secs(30)).await +} + +#[tokio::test] +pub async fn full_node_crash_and_sync() { + const HEIGHT: u64 = 10; + + let mut test = TestBuilder::<()>::new(); + + // Add validators + test.add_node() + .with_voting_power(20) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(20) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(20) + .start() + .wait_until(HEIGHT) + .success(); + + // Add a full node that crashes and needs to sync + test.add_node() + .full_node() + .start() + .wait_until(3) + .crash() + .reset_db() + .restart_after(Duration::from_secs(5)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: true, + ..Default::default() + }, + ) + .await +} + +#[tokio::test] +pub async fn late_starting_full_node() { + const HEIGHT: u64 = 10; + + let mut test = TestBuilder::<()>::new(); + + // Add validators that start immediately + test.add_node() + .with_voting_power(20) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(20) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(20) + .start() + .wait_until(HEIGHT) + .success(); + + // Add a full node that starts late + test.add_node() + .full_node() + .start_after(1, Duration::from_secs(10)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: true, + ..Default::default() + }, + ) + .await +} + +#[tokio::test] +pub async fn mixed_validator_and_full_node_failures() { + const HEIGHT: u64 = 10; + + let mut test = TestBuilder::<()>::new(); + + // Add stable validators + test.add_node() + .with_voting_power(30) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(30) + .start() + .wait_until(HEIGHT) + .success(); + + // Add a validator that crashes + test.add_node() + .with_voting_power(20) + .start() + .wait_until(5) + .crash() + .restart_after(Duration::from_secs(10)) + .wait_until(HEIGHT) + .success(); + + // Add full nodes - one stable, one that crashes + test.add_node() + .full_node() + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .full_node() + .start() + .wait_until(6) + .crash() + .restart_after(Duration::from_secs(15)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: true, + ..Default::default() + }, + ) + .await +} diff --git a/code/examples/actor/test/src/tests/mod.rs b/code/examples/actor/test/src/tests/mod.rs new file mode 100644 index 000000000..351e0ba5e --- /dev/null +++ b/code/examples/actor/test/src/tests/mod.rs @@ -0,0 +1,7 @@ +pub mod full_nodes; +pub mod n3f0; +pub mod n3f0_pubsub_protocol; +pub mod n3f1; +pub mod value_sync; +pub mod vote_rebroadcast; +pub mod wal; diff --git a/code/examples/actor/test/src/tests/n3f0.rs b/code/examples/actor/test/src/tests/n3f0.rs new file mode 100644 index 000000000..1d6d75601 --- /dev/null +++ b/code/examples/actor/test/src/tests/n3f0.rs @@ -0,0 +1,16 @@ +use std::time::Duration; + +use crate::TestBuilder; + +#[tokio::test] +pub async fn all_correct_nodes() { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + test.add_node().start().wait_until(HEIGHT).success(); + test.add_node().start().wait_until(HEIGHT).success(); + test.add_node().start().wait_until(HEIGHT).success(); + + test.build().run(Duration::from_secs(30)).await +} diff --git a/code/examples/actor/test/src/tests/n3f0_pubsub_protocol.rs b/code/examples/actor/test/src/tests/n3f0_pubsub_protocol.rs new file mode 100644 index 000000000..553cc72be --- /dev/null +++ b/code/examples/actor/test/src/tests/n3f0_pubsub_protocol.rs @@ -0,0 +1,73 @@ +use std::time::Duration; + +use bytesize::ByteSize; +use malachitebft_config::{GossipSubConfig, PubSubProtocol}; + +use crate::{TestBuilder, TestParams}; + +async fn run_test(params: TestParams) { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + test.add_node().start().wait_until(HEIGHT).success(); + test.add_node().start().wait_until(HEIGHT).success(); + test.add_node().start().wait_until(HEIGHT).success(); + + test.build() + .run_with_params(Duration::from_secs(30), params) + .await +} + +#[tokio::test] +pub async fn broadcast_custom_config_1ktx() { + let params = TestParams { + enable_value_sync: false, + protocol: PubSubProtocol::Broadcast, + block_size: ByteSize::kib(1), + tx_size: ByteSize::kib(1), + + ..Default::default() + }; + + run_test(params).await +} + +#[tokio::test] +pub async fn broadcast_custom_config_2ktx() { + let params = TestParams { + enable_value_sync: false, + protocol: PubSubProtocol::Broadcast, + block_size: ByteSize::kib(2), + tx_size: ByteSize::kib(2), + ..Default::default() + }; + + run_test(params).await +} + +#[tokio::test] +pub async fn gossip_custom_config_1ktx() { + let params = TestParams { + enable_value_sync: false, + protocol: PubSubProtocol::GossipSub(GossipSubConfig::default()), + block_size: ByteSize::kib(1), + tx_size: ByteSize::kib(1), + ..Default::default() + }; + + run_test(params).await +} + +#[tokio::test] +pub async fn gossip_custom_config_2ktx() { + let params = TestParams { + enable_value_sync: false, + protocol: PubSubProtocol::GossipSub(GossipSubConfig::default()), + block_size: ByteSize::kib(2), + tx_size: ByteSize::kib(2), + ..Default::default() + }; + + run_test(params).await +} diff --git a/code/examples/actor/test/src/tests/n3f1.rs b/code/examples/actor/test/src/tests/n3f1.rs new file mode 100644 index 000000000..e165d2f7e --- /dev/null +++ b/code/examples/actor/test/src/tests/n3f1.rs @@ -0,0 +1,105 @@ +use std::time::Duration; + +use crate::TestBuilder; + +#[tokio::test] +pub async fn proposer_fails_to_start() { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + test.add_node().with_voting_power(1).success(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.build().run(Duration::from_secs(30)).await +} + +#[tokio::test] +pub async fn one_node_fails_to_start() { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node().with_voting_power(1).success(); + + test.build().run(Duration::from_secs(30)).await +} + +#[tokio::test] +pub async fn proposer_crashes_at_height_2() { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node() + .with_voting_power(1) + .start() + .wait_until(2) + .crash() + .success(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.build().run(Duration::from_secs(30)).await +} + +#[tokio::test] +pub async fn one_node_crashes_at_height_3() { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node() + .with_voting_power(1) + .start() + .wait_until(3) + .crash() + .success(); + + test.build().run(Duration::from_secs(30)).await +} diff --git a/code/examples/actor/test/src/tests/value_sync.rs b/code/examples/actor/test/src/tests/value_sync.rs new file mode 100644 index 000000000..989763dfd --- /dev/null +++ b/code/examples/actor/test/src/tests/value_sync.rs @@ -0,0 +1,166 @@ +use std::time::Duration; + +use crate::{TestBuilder, TestParams}; + +#[tokio::test] +pub async fn crash_restart_from_start() { + const HEIGHT: u64 = 10; + + let mut test = TestBuilder::<()>::new(); + + // Node 1 starts with 10 voting power. + test.add_node() + .with_voting_power(10) + .start() + // Wait until it reaches height 10 + .wait_until(HEIGHT) + // Record a successful test for this node + .success(); + + // Node 2 starts with 10 voting power, in parallel with node 1 and with the same behaviour + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT) + .success(); + + // Node 3 starts with 5 voting power, in parallel with node 1 and 2. + test.add_node() + .with_voting_power(5) + .start() + // Wait until the node reaches height 2... + .wait_until(2) + // ...and then kills it + .crash() + // Reset the database so that the node has to do Sync from height 1 + .reset_db() + // After that, it waits 5 seconds before restarting the node + .restart_after(Duration::from_secs(5)) + // Wait until the node reached the expected height + .wait_until(HEIGHT) + // Record a successful test for this node + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), // Timeout for the whole test + TestParams { + enable_value_sync: true, // Enable Sync + ..Default::default() + }, + ) + .await +} + +#[tokio::test] +pub async fn crash_restart_from_latest() { + const HEIGHT: u64 = 10; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(2) + .crash() + // We do not reset the database so that the node can restart from the latest height + .restart_after(Duration::from_secs(5)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: true, + ..Default::default() + }, + ) + .await +} + +#[tokio::test] +pub async fn aggressive_pruning() { + const HEIGHT: u64 = 15; + + let mut test = TestBuilder::<()>::new(); + + // Node 1 starts with 10 voting power. + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT) + .success(); + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT) + .success(); + + test.add_node() + .with_voting_power(5) + .start() + .wait_until(2) + .crash() + .reset_db() + .restart_after(Duration::from_secs(5)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), // Timeout for the whole test + TestParams { + enable_value_sync: true, // Enable Sync + max_retain_blocks: 10, // Prune blocks older than 10 + ..Default::default() + }, + ) + .await +} + +#[tokio::test] +pub async fn start_late() { + const HEIGHT: u64 = 3; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT * 2) + .success(); + + test.add_node() + .with_voting_power(10) + .start() + .wait_until(HEIGHT * 2) + .success(); + + test.add_node() + .with_voting_power(5) + .start_after(1, Duration::from_secs(10)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(30), + TestParams { + enable_value_sync: true, + ..Default::default() + }, + ) + .await +} diff --git a/code/examples/actor/test/src/tests/vote_rebroadcast.rs b/code/examples/actor/test/src/tests/vote_rebroadcast.rs new file mode 100644 index 000000000..424cd89ae --- /dev/null +++ b/code/examples/actor/test/src/tests/vote_rebroadcast.rs @@ -0,0 +1,130 @@ +use std::time::Duration; + +use malachitebft_core_types::VoteType; + +use crate::{TestBuilder, TestParams}; + +#[tokio::test] +pub async fn crash_restart_from_start() { + const HEIGHT: u64 = 10; + const CRASH_HEIGHT: u64 = 4; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .start() + .wait_until(CRASH_HEIGHT) + .expect_vote_rebroadcast(CRASH_HEIGHT, 0, VoteType::Prevote) + .wait_until(HEIGHT) + .success(); + + test.add_node() + .start() + .wait_until(CRASH_HEIGHT) + .expect_vote_rebroadcast(CRASH_HEIGHT, 0, VoteType::Prevote) + .wait_until(HEIGHT) + .success(); + + test.add_node() + .start() + // Wait until the node reaches height 4... + .wait_until(4) + // ...then kill it + .crash() + // Reset the database so that the node has to do Sync from height 1 + .reset_db() + // After that, it waits 5 seconds before restarting the node + .restart_after(Duration::from_secs(5)) + // Wait until the node reached the expected height + .wait_until(HEIGHT) + // Record a successful test for this node + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + // Enable Sync to allow the node to catch up to the latest height + enable_value_sync: true, + ..TestParams::default() + }, + ) + .await +} + +#[tokio::test] +pub async fn crash_restart_from_latest() { + const HEIGHT: u64 = 10; + const CRASH_HEIGHT: u64 = 4; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .start() + .wait_until(CRASH_HEIGHT) + .expect_vote_rebroadcast(CRASH_HEIGHT, 0, VoteType::Prevote) + .wait_until(HEIGHT) + .success(); + + test.add_node() + .start() + .wait_until(CRASH_HEIGHT) + .expect_vote_rebroadcast(CRASH_HEIGHT, 0, VoteType::Prevote) + .wait_until(HEIGHT) + .success(); + + test.add_node() + .start() + .wait_until(CRASH_HEIGHT) + .crash() + // We do not reset the database so that the node can restart from the latest height + .restart_after(Duration::from_secs(5)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: false, + ..Default::default() + }, + ) + .await +} + +#[tokio::test] +pub async fn start_late() { + const HEIGHT: u64 = 5; + + let mut test = TestBuilder::<()>::new(); + + test.add_node() + .start() + .wait_until(1) + .expect_vote_rebroadcast(1, 0, VoteType::Prevote) + .wait_until(HEIGHT) + .success(); + + test.add_node() + .start() + .wait_until(1) + .expect_vote_rebroadcast(1, 0, VoteType::Prevote) + .wait_until(HEIGHT) + .success(); + + test.add_node() + .start_after(1, Duration::from_secs(10)) + .wait_until(HEIGHT) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: false, + ..Default::default() + }, + ) + .await +} diff --git a/code/examples/actor/test/src/tests/wal.rs b/code/examples/actor/test/src/tests/wal.rs new file mode 100644 index 000000000..4c2437a1a --- /dev/null +++ b/code/examples/actor/test/src/tests/wal.rs @@ -0,0 +1,143 @@ +use std::time::Duration; + +use eyre::bail; +use tracing::info; + +use informalsystems_malachitebft_actor_app_proposal::types::MockContext; +use malachitebft_core_consensus::LocallyProposedValue; +use malachitebft_core_types::SignedVote; +use malachitebft_engine::util::events::Event; + +use crate::{HandlerResult, TestBuilder, TestParams}; + +#[tokio::test] +async fn proposer_crashes_after_proposing() { + #[derive(Clone, Debug, Default)] + struct State { + first_proposed_value: Option>, + actual_crash_height: Option, + } + + const CRASH_HEIGHT: u64 = 4; + + let mut test = TestBuilder::::new(); + + test.add_node().with_voting_power(10).start().success(); + test.add_node().with_voting_power(10).start().success(); + + test.add_node() + .with_voting_power(40) + .start() + .wait_until(CRASH_HEIGHT) + // Wait until this node proposes a value + .on_event(|event, state| match event { + Event::ProposedValue(value) => { + let height = value.height.as_u64(); + info!( + "Proposer proposed block at height {}: {:?}", + height, value.value + ); + state.first_proposed_value = Some(value); + state.actual_crash_height = Some(height); + Ok(HandlerResult::ContinueTest) + } + _ => Ok(HandlerResult::WaitForNextEvent), + }) + // Crash right after + .crash() + // Restart after 5 seconds + .restart_after(Duration::from_secs(5)) + // Check that we replay messages from the WAL at the actual crash height + .expect_wal_replay_at_crash_height(|state| state.actual_crash_height) + // Wait until it proposes a value again, while replaying WAL + // Check that it is the same value as the first time + .on_proposed_value(|value, state| { + let Some(first_value) = state.first_proposed_value.as_ref() else { + bail!("Proposer did not propose a block"); + }; + + if first_value.value == value.value { + info!("Proposer re-proposed the same block: {:?}", value.value); + Ok(HandlerResult::ContinueTest) + } else { + bail!( + "Proposer just equivocated: expected {:?}, got {:?}", + first_value.value, + value.value + ) + } + }) + .success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: false, + ..TestParams::default() + }, + ) + .await +} + +#[tokio::test] +async fn non_proposer_crashes_after_voting() { + #[derive(Clone, Debug, Default)] + struct State { + first_vote: Option>, + } + + const CRASH_HEIGHT: u64 = 3; + + let mut test = TestBuilder::::new(); + + test.add_node() + .with_voting_power(40) + .start() + .wait_until(CRASH_HEIGHT) + // Wait until this node proposes a value + .on_vote(|vote, state| { + info!("Non-proposer voted"); + state.first_vote = Some(vote); + + Ok(HandlerResult::ContinueTest) + }) + // Crash immediately to minimize height progression + .crash() + // Restart after 5 seconds + .restart_after(Duration::from_secs(5)) + // Check that we replay messages from the WAL + .expect_wal_replay(CRASH_HEIGHT) + // Wait until it proposes a value again, while replaying WAL + // Check that it is the same value as the first time + .on_vote(|vote, state| { + let Some(first_vote) = state.first_vote.as_ref() else { + bail!("Non-proposer did not vote") + }; + + if first_vote.message.value == vote.message.value { + info!("Non-proposer voted the same way: {first_vote:?}"); + Ok(HandlerResult::ContinueTest) + } else { + bail!( + "Non-proposer just equivocated: expected {:?}, got {:?}", + first_vote.message.value, + vote.message.value + ) + } + }) + .success(); + + test.add_node().with_voting_power(10).start().success(); + test.add_node().with_voting_power(10).start().success(); + + test.build() + .run_with_params( + Duration::from_secs(60), + TestParams { + enable_value_sync: false, + ..TestParams::default() + }, + ) + .await +} diff --git a/code/examples/channel/src/app.rs b/code/examples/channel/src/app.rs index e9e774bf6..3413ecde7 100644 --- a/code/examples/channel/src/app.rs +++ b/code/examples/channel/src/app.rs @@ -335,6 +335,10 @@ pub async fn run(state: &mut State, channels: &mut Channels) -> eyr } } } + + AppMsg::ReceivedProposal { .. } => { + panic!("ReceivedProposal should not be called in the channel app example"); + } } }