diff --git a/.github/workflows/build-and-upload-binaries-ci.yml b/.github/workflows/build-and-upload-binaries-ci.yml index 5429baf2b4..849bd32a17 100644 --- a/.github/workflows/build-and-upload-binaries-ci.yml +++ b/.github/workflows/build-and-upload-binaries-ci.yml @@ -109,6 +109,7 @@ jobs: cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/nym_service_provider_directory.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/nym_name_service.wasm $OUTPUT_DIR + cp contracts/target/wasm32-unknown-unknown/release/nym_ephemera.wasm $OUTPUT_DIR - name: Deploy branch to CI www continue-on-error: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 19988720df..c04cce1b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +- Add Ephemera functionality in nym-api ([#3731]) + +[#3731]: https://github.com/nymtech/nym/pull/3731 + ## [1.1.27] (2023-08-16) - fix serialisation of contract types ([#3752]) diff --git a/Cargo.lock b/Cargo.lock index bb62518ca4..f14848f51b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,187 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64 0.21.2", + "bitflags 1.3.2", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.8.5", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2 0.4.9", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie 0.16.2", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.4.9", + "time 0.3.25", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "addr2line" version = "0.20.0" @@ -23,6 +204,25 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", +] + [[package]] name = "aead" version = "0.5.2" @@ -33,6 +233,17 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher 0.2.5", +] + [[package]] name = "aes" version = "0.7.5" @@ -57,20 +268,54 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead 0.4.3", + "aes 0.7.5", + "cipher 0.3.0", + "ctr 0.8.0", + "ghash 0.4.4", + "subtle 2.4.1", +] + [[package]] name = "aes-gcm" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" dependencies = [ - "aead", + "aead 0.5.2", "aes 0.8.3", "cipher 0.4.4", "ctr 0.9.2", - "ghash", + "ghash 0.5.0", "subtle 2.4.1", ] +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher 0.2.5", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher 0.2.5", + "opaque-debug 0.3.0", +] + [[package]] name = "ahash" version = "0.7.6" @@ -89,6 +334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", + "getrandom 0.2.10", "once_cell", "version_check", ] @@ -102,6 +348,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -201,6 +462,15 @@ name = "anyhow" version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +dependencies = [ + "backtrace", +] + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" [[package]] name = "argon2" @@ -214,6 +484,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "array-bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b1c5a481ec30a5abd8dfbd94ab5cf1bb4e9a66be7f1b3b322f2f1170c200fd" + [[package]] name = "arrayref" version = "0.3.7" @@ -226,6 +502,79 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "asn1-rs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33" +dependencies = [ + "asn1-rs-derive 0.1.0", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.25", +] + +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive 0.4.0", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.25", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-channel" version = "1.9.0" @@ -247,6 +596,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg 1.1.0", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +dependencies = [ + "event-listener", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -280,6 +658,19 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "asynchronous-codec" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a0daa378f5fd10634e44b0a29b2a87b890657658e072a30d6f26e57ddee182" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "atoi" version = "0.4.0" @@ -304,6 +695,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + [[package]] name = "atty" version = "0.2.14" @@ -390,6 +787,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + [[package]] name = "base16ct" version = "0.1.1" @@ -435,6 +838,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", +] + [[package]] name = "bip32" version = "0.5.1" @@ -550,6 +973,22 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "block-modes" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" +dependencies = [ + "block-padding", + "cipher 0.2.5", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "bls12_381" version = "0.5.0" @@ -584,6 +1023,27 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bs58" version = "0.4.0" @@ -626,6 +1086,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cast" version = "0.3.0" @@ -648,12 +1128,32 @@ dependencies = [ "libc", ] +[[package]] +name = "ccm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca1a8fbc20b50ac9673ff014abfb2b5f4085ee1a850d408f14a159c5853ac7" +dependencies = [ + "aead 0.3.2", + "cipher 0.2.5", + "subtle 2.4.1", +] + [[package]] name = "cesu8" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -670,6 +1170,18 @@ dependencies = [ "keystream", ] +[[package]] +name = "chacha20" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "zeroize", +] + [[package]] name = "chacha20" version = "0.9.1" @@ -681,16 +1193,29 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "chacha20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +dependencies = [ + "aead 0.4.3", + "chacha20 0.8.2", + "cipher 0.3.0", + "poly1305 0.7.2", + "zeroize", +] + [[package]] name = "chacha20poly1305" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead", - "chacha20", + "aead 0.5.2", + "chacha20 0.9.1", "cipher 0.4.4", - "poly1305", + "poly1305 0.8.0", "zeroize", ] @@ -737,6 +1262,15 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "cipher" version = "0.3.0" @@ -757,6 +1291,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "3.2.25" @@ -896,6 +1441,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml 0.5.11", +] + [[package]] name = "console-api" version = "0.5.0" @@ -944,6 +1503,23 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.25", + "version_check", +] + [[package]] name = "cookie" version = "0.17.0" @@ -971,6 +1547,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cosmos-sdk-proto" version = "0.19.0" @@ -1191,7 +1776,7 @@ dependencies = [ "autocfg 1.1.0", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.9.0", "scopeguard", ] @@ -1239,6 +1824,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-bigint" version = "0.4.9" @@ -1385,6 +1976,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +dependencies = [ + "cfg-if", + "fiat-crypto", + "packed_simd_2", + "platforms", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1500,8 +2105,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", ] [[package]] @@ -1518,13 +2133,38 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", "quote", "syn 1.0.109", ] @@ -1552,6 +2192,32 @@ dependencies = [ "parking_lot_core 0.9.8", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "data-encoding-macro" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c904b33cc60130e1aeea4956ab803d08a3f4a0ca82d64ed757afac3891f2bb99" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fdf3fce3ce863539ec1d7fd1b6dcc3c645663376b43ed376bbf887733e4f772" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + [[package]] name = "der" version = "0.6.1" @@ -1559,6 +2225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1572,6 +2239,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82" +dependencies = [ + "asn1-rs 0.3.1", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs 0.5.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.3.7" @@ -1592,6 +2287,50 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn 1.0.109", +] + [[package]] name = "devise" version = "0.4.1" @@ -1696,6 +2435,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1714,6 +2464,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dtoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d09067bfacaa79114679b279d7f5885b53295b1e2cfb4e79c8e4bd3d633169" + [[package]] name = "dyn-clone" version = "1.0.12" @@ -1785,7 +2541,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "ed25519 1.5.3", "rand 0.7.3", "serde", @@ -1800,7 +2556,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "hashbrown 0.12.3", "hex", "rand_core 0.6.4", @@ -1828,6 +2584,8 @@ dependencies = [ "ff 0.12.1", "generic-array 0.14.7", "group 0.12.1", + "hkdf 0.12.3", + "pem-rfc7468", "pkcs8 0.9.0", "rand_core 0.6.4", "sec1 0.3.0", @@ -1863,6 +2621,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "enum-iterator" version = "1.1.3" @@ -1906,6 +2676,56 @@ dependencies = [ "regex", ] +[[package]] +name = "ephemera" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "array-bytes", + "assert_matches", + "async-trait", + "asynchronous-codec", + "blake2 0.10.6", + "bs58 0.4.0", + "bytes", + "cfg-if", + "chrono", + "clap 4.3.21", + "config", + "digest 0.10.7", + "dirs 5.0.1", + "enum-as-inner", + "futures", + "futures-util", + "lazy_static", + "libp2p", + "libp2p-identity", + "log", + "lru", + "nym-config", + "nym-ephemera-common", + "nym-task", + "pretty_env_logger", + "rand 0.8.5", + "refinery", + "reqwest", + "rocksdb", + "rusqlite", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite 0.18.0", + "tokio-util", + "toml 0.7.6", + "unsigned-varint", + "utoipa", + "utoipa-swagger-ui", + "uuid 1.4.1", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2014,6 +2834,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.9.0" @@ -2069,6 +2901,12 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "figment" version = "0.10.10" @@ -2224,6 +3062,7 @@ dependencies = [ "futures-core", "futures-task", "futures-util", + "num_cpus", ] [[package]] @@ -2269,6 +3108,17 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "futures-rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2411eed028cdf8c8034eaf21f9915f956b6c3abec4d4c7949ee67f0721127bd" +dependencies = [ + "futures-io", + "rustls 0.20.8", + "webpki 0.22.0", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -2281,6 +3131,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" @@ -2315,7 +3171,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows", + "windows 0.48.0", ] [[package]] @@ -2377,6 +3233,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug 0.3.0", + "polyval 0.5.3", +] + [[package]] name = "ghash" version = "0.5.0" @@ -2384,7 +3250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" dependencies = [ "opaque-debug 0.3.0", - "polyval", + "polyval 0.6.1", ] [[package]] @@ -2538,6 +3404,15 @@ dependencies = [ "ahash 0.7.6", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -2649,6 +3524,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "hidapi" version = "1.5.0" @@ -2699,6 +3580,17 @@ 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.9" @@ -2855,7 +3747,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows 0.48.0", ] [[package]] @@ -2894,6 +3786,35 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if-addrs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc0fa01ffc752e9dbc72818cdb072cd028b86be5e09dd04c5a643704fe101a9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "if-watch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9465340214b296cd17a0009acdb890d6160010b8adf8f78a00d0d7ab270f79f" +dependencies = [ + "async-io", + "core-foundation", + "fnv", + "futures", + "if-addrs", + "ipnet", + "log", + "rtnetlink", + "system-configuration", + "tokio", + "windows 0.34.0", +] + [[package]] name = "indenter" version = "0.3.3" @@ -2994,6 +3915,25 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" +[[package]] +name = "interceptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8a11ae2da61704edada656798b61c94b35ecac2c58eb955156987d5e6be90b" +dependencies = [ + "async-trait", + "bytes", + "log", + "rand 0.8.5", + "rtcp", + "rtp", + "thiserror", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + [[package]] name = "inventory" version = "0.1.11" @@ -3016,6 +3956,29 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipconfig" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +dependencies = [ + "socket2 0.4.9", + "widestring", + "winapi", + "winreg", +] + [[package]] name = "ipnet" version = "2.8.0" @@ -3047,7 +4010,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix", + "rustix 0.38.8", "windows-sys 0.48.0", ] @@ -3193,12 +4156,24 @@ dependencies = [ "libc", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "ledger" version = "0.1.0" @@ -3265,12 +4240,429 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + [[package]] name = "libm" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +[[package]] +name = "libp2p" +version = "0.51.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f210d259724eae82005b5c48078619b7745edb7b76de370b03f8ba59ea103097" +dependencies = [ + "bytes", + "futures", + "futures-timer", + "getrandom 0.2.10", + "instant", + "libp2p-allow-block-list", + "libp2p-connection-limits", + "libp2p-core", + "libp2p-dns", + "libp2p-gossipsub", + "libp2p-identity", + "libp2p-kad", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-noise", + "libp2p-quic", + "libp2p-request-response", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-webrtc", + "libp2p-yamux", + "multiaddr", + "pin-project", +] + +[[package]] +name = "libp2p-allow-block-list" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510daa05efbc25184458db837f6f9a5143888f1caa742426d92e1833ddd38a50" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "void", +] + +[[package]] +name = "libp2p-connection-limits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4caa33f1d26ed664c4fe2cca81a08c8e07d4c1c04f2f4ac7655c2dd85467fda0" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "void", +] + +[[package]] +name = "libp2p-core" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1df63c0b582aa434fb09b2d86897fa2b419ffeccf934b36f87fcedc8e835c2" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-identity", + "log", + "multiaddr", + "multihash", + "multistream-select", + "once_cell", + "parking_lot 0.12.1", + "pin-project", + "quick-protobuf", + "rand 0.8.5", + "rw-stream-sink", + "serde", + "smallvec", + "thiserror", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-dns" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146ff7034daae62077c415c2376b8057368042df6ab95f5432ad5e88568b1554" +dependencies = [ + "futures", + "libp2p-core", + "log", + "parking_lot 0.12.1", + "smallvec", + "trust-dns-resolver", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.44.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b34b6da8165c0bde35c82db8efda39b824776537e73973549e76cadb3a77c5" +dependencies = [ + "asynchronous-codec", + "base64 0.21.2", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "hex_fmt", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "log", + "prometheus-client", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "regex", + "serde", + "sha2 0.10.7", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "wasm-timer", +] + +[[package]] +name = "libp2p-identity" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2d584751cecb2aabaa56106be6be91338a60a0f4e420cf2af639204f596fc1" +dependencies = [ + "bs58 0.4.0", + "ed25519-dalek", + "log", + "multiaddr", + "multihash", + "quick-protobuf", + "rand 0.8.5", + "serde", + "sha2 0.10.7", + "thiserror", + "zeroize", +] + +[[package]] +name = "libp2p-kad" +version = "0.43.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39d5ef876a2b2323d63c258e63c2f8e36f205fe5a11f0b3095d59635650790ff" +dependencies = [ + "arrayvec", + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "log", + "quick-protobuf", + "rand 0.8.5", + "serde", + "sha2 0.10.7", + "smallvec", + "thiserror", + "uint", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-mdns" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19983e1f949f979a928f2c603de1cf180cc0dc23e4ac93a62651ccb18341460b" +dependencies = [ + "data-encoding", + "futures", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "log", + "rand 0.8.5", + "smallvec", + "socket2 0.4.9", + "tokio", + "trust-dns-proto", + "void", +] + +[[package]] +name = "libp2p-metrics" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42ec91e227d7d0dafa4ce88b333cdf5f277253873ab087555c92798db2ddd46" +dependencies = [ + "libp2p-core", + "libp2p-gossipsub", + "libp2p-kad", + "libp2p-swarm", + "prometheus-client", +] + +[[package]] +name = "libp2p-noise" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3673da89d29936bc6435bafc638e2f184180d554ce844db65915113f86ec5e" +dependencies = [ + "bytes", + "curve25519-dalek 3.2.0", + "futures", + "libp2p-core", + "libp2p-identity", + "log", + "once_cell", + "quick-protobuf", + "rand 0.8.5", + "sha2 0.10.7", + "snow", + "static_assertions", + "thiserror", + "x25519-dalek 1.1.1", + "zeroize", +] + +[[package]] +name = "libp2p-quic" +version = "0.7.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b26abd81cd2398382a1edfe739b539775be8a90fa6914f39b2ab49571ec735" +dependencies = [ + "bytes", + "futures", + "futures-timer", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-tls", + "log", + "parking_lot 0.12.1", + "quinn-proto", + "rand 0.8.5", + "rustls 0.20.8", + "thiserror", + "tokio", +] + +[[package]] +name = "libp2p-request-response" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffdb374267d42dc5ed5bc53f6e601d4a64ac5964779c6e40bb9e4f14c1e30d5" +dependencies = [ + "async-trait", + "futures", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.5", + "smallvec", +] + +[[package]] +name = "libp2p-swarm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903b3d592d7694e56204d211f29d31bc004be99386644ba8731fc3e3ef27b296" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm-derive", + "log", + "rand 0.8.5", + "smallvec", + "tokio", + "void", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fba456131824ab6acd4c7bf61e9c0f0a3014b5fc9868ccb8e10d344594cdc4f" +dependencies = [ + "heck 0.4.1", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "libp2p-tcp" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d33698596d7722d85d3ab0c86c2c322254fce1241e91208e3679b4eb3026cf" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core", + "log", + "socket2 0.4.9", + "tokio", +] + +[[package]] +name = "libp2p-tls" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff08d13d0dc66e5e9ba6279c1de417b84fa0d0adc3b03e5732928c180ec02781" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core", + "libp2p-identity", + "rcgen 0.10.0", + "ring", + "rustls 0.20.8", + "thiserror", + "webpki 0.22.0", + "x509-parser 0.14.0", + "yasna", +] + +[[package]] +name = "libp2p-webrtc" +version = "0.4.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba48592edbc2f60b4bc7c10d65445b0c3964c07df26fdf493b6880d33be36f8" +dependencies = [ + "async-trait", + "asynchronous-codec", + "bytes", + "futures", + "futures-timer", + "hex", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-noise", + "log", + "multihash", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "rcgen 0.9.3", + "serde", + "stun", + "thiserror", + "tinytemplate", + "tokio", + "tokio-util", + "webrtc", +] + +[[package]] +name = "libp2p-yamux" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd21d950662700a385d4c6d68e2f5f54d778e97068cdd718522222ef513bda" +dependencies = [ + "futures", + "libp2p-core", + "log", + "thiserror", + "yamux", +] + +[[package]] +name = "librocksdb-sys" +version = "0.10.0+7.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe4d5874f5ff2bc616e55e8c6086d478fcda13faf9495768a4aa1c22042d30b" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + [[package]] name = "libsqlite3-sys" version = "0.24.2" @@ -3294,6 +4686,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.5" @@ -3312,6 +4716,24 @@ dependencies = [ "keystream", ] +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + [[package]] name = "lock_api" version = "0.4.10" @@ -3343,6 +4765,34 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03f1160296536f10c833a82dca22267d5486734230d47bf00bf435885814ba1e" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "macro_rules_attribute" version = "0.1.3" @@ -3359,6 +4809,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" +[[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" @@ -3392,12 +4848,30 @@ dependencies = [ "serde", ] +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -3413,6 +4887,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3460,6 +4944,79 @@ dependencies = [ "version_check", ] +[[package]] +name = "multiaddr" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b36f567c7099511fa8612bbbb52dda2419ce0bdbacf31714e3a5ffdb766d3bd" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "log", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +dependencies = [ + "base-x", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" +dependencies = [ + "core2", + "digest 0.10.7", + "multihash-derive", + "serde", + "serde-big-array", + "sha2 0.10.7", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "multistream-select" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8552ab875c1313b97b8d20cb857b9fd63e2d1d6a0a1b53ce9821e575405f27a" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -3478,12 +5035,96 @@ dependencies = [ "tempfile", ] +[[package]] +name = "netlink-packet-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345b8ab5bd4e71a2986663e88c56856699d060e78e152e6e9d7966fcd5491297" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ea4302b9759a7a88242299225ea3688e63c85ea136371bb6cf94fd674efaab" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-proto" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b4b14489ab424703c092062176d52ba55485a89c076b4f9db05092b7223aa6" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror", + "tokio", +] + +[[package]] +name = "netlink-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "no-std-compat" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -3531,6 +5172,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -3542,6 +5194,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg 1.1.0", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -3549,7 +5211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg 1.1.0", - "libm", + "libm 0.2.7", ] [[package]] @@ -3566,11 +5228,14 @@ dependencies = [ name = "nym-api" version = "1.1.26" dependencies = [ + "actix-web", "anyhow", + "array-bytes", "async-trait", "bip39", "bs58 0.4.0", "cfg-if", + "chrono", "clap 4.3.21", "console-subscriber", "cosmwasm-std", @@ -3579,7 +5244,9 @@ dependencies = [ "cw3", "cw4", "dirs 4.0.0", + "ephemera", "futures", + "futures-util", "getset", "humantime-serde", "lazy_static", @@ -3597,6 +5264,7 @@ dependencies = [ "nym-credentials", "nym-crypto", "nym-dkg", + "nym-ephemera-common", "nym-gateway-client", "nym-inclusion-probability", "nym-mixnet-contract-common", @@ -3621,15 +5289,18 @@ dependencies = [ "rocket_okapi", "schemars", "serde", + "serde_derive", "serde_json", "sqlx 0.6.3", "tap", + "tempfile", "thiserror", "time 0.3.25", "tokio", "tokio-stream", "ts-rs", "url", + "uuid 1.4.1", "zeroize", ] @@ -4007,7 +5678,7 @@ dependencies = [ "serde_bytes", "subtle-encoding", "thiserror", - "x25519-dalek", + "x25519-dalek 1.1.1", "zeroize", ] @@ -4034,6 +5705,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ephemera-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "nym-contracts-common", +] + [[package]] name = "nym-execute" version = "0.1.0" @@ -4449,10 +6130,10 @@ name = "nym-outfox" version = "0.1.0" dependencies = [ "blake3", - "chacha20", - "chacha20poly1305", + "chacha20 0.9.1", + "chacha20poly1305 0.10.1", "criterion", - "curve25519-dalek", + "curve25519-dalek 3.2.0", "fastrand 1.9.0", "getrandom 0.2.10", "log", @@ -4467,7 +6148,7 @@ dependencies = [ name = "nym-pemstore" version = "0.3.0" dependencies = [ - "pem", + "pem 0.8.3", ] [[package]] @@ -4813,7 +6494,7 @@ dependencies = [ name = "nym-store-cipher" version = "0.1.0" dependencies = [ - "aes-gcm", + "aes-gcm 0.10.2", "argon2", "generic-array 0.14.7", "getrandom 0.2.10", @@ -4904,6 +6585,7 @@ dependencies = [ "nym-coconut-interface", "nym-config", "nym-contracts-common", + "nym-ephemera-common", "nym-group-contract-common", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -4968,6 +6650,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a" +dependencies = [ + "asn1-rs 0.3.1", +] + +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs 0.5.2", +] + [[package]] name = "okapi" version = "0.7.0-rc.1" @@ -5175,6 +6875,38 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.7", +] + +[[package]] +name = "p384" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.7", +] + +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if", + "libm 0.1.4", +] + [[package]] name = "pairing" version = "0.20.0" @@ -5264,6 +6996,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -5297,6 +7035,12 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "peg" version = "0.7.0" @@ -5335,6 +7079,24 @@ dependencies = [ "regex", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -5443,6 +7205,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + [[package]] name = "plotters" version = "0.3.5" @@ -5487,6 +7255,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures", + "opaque-debug 0.3.0", + "universal-hash 0.4.1", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -5495,7 +7274,19 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures", "opaque-debug 0.3.0", - "universal-hash", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug 0.3.0", + "universal-hash 0.4.1", ] [[package]] @@ -5507,7 +7298,7 @@ dependencies = [ "cfg-if", "cpufeatures", "opaque-debug 0.3.0", - "universal-hash", + "universal-hash 0.5.1", ] [[package]] @@ -5536,6 +7327,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml 0.5.11", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -5582,6 +7383,29 @@ dependencies = [ "yansi 1.0.0-rc.1", ] +[[package]] +name = "prometheus-client" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6fa99d535dd930d1249e6c79cb3c2915f9172a540fe2b02a4c8f9ca954721e" +dependencies = [ + "dtoa", + "itoa", + "parking_lot 0.12.1", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b6a5217beb0ad503ee7fa752d451c905113d70721b937126158f3106a48cc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "prost" version = "0.11.9" @@ -5637,6 +7461,46 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quick-protobuf-codec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1693116345026436eb2f10b677806169c1a1260c1c60eaaffe3fb5a29ae23d8b" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror", + "unsigned-varint", +] + +[[package]] +name = "quinn-proto" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c10f662eee9c94ddd7135043e544f3c82fa839a1e7b865911331961b53186c" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring", + "rustc-hash", + "rustls 0.20.8", + "slab", + "thiserror", + "tinyvec", + "tracing", + "webpki 0.22.0", +] + [[package]] name = "quote" version = "1.0.32" @@ -5879,6 +7743,31 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rcgen" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" +dependencies = [ + "pem 1.1.1", + "ring", + "time 0.3.25", + "x509-parser 0.13.2", + "yasna", +] + +[[package]] +name = "rcgen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" +dependencies = [ + "pem 1.1.1", + "ring", + "time 0.3.25", + "yasna", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -5937,6 +7826,50 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "refinery" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb0436d0dd7bd8d4fce1e828751fa79742b08e35f27cfea7546f8a322b5ef24" +dependencies = [ + "refinery-core", + "refinery-macros", +] + +[[package]] +name = "refinery-core" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19206547cd047e8f4dfa6b20c30d3ecaf24be05841b6aa0aa926a47a3d0662bb" +dependencies = [ + "async-trait", + "cfg-if", + "lazy_static", + "log", + "regex", + "rusqlite", + "serde", + "siphasher", + "thiserror", + "time 0.3.25", + "toml 0.7.6", + "url", + "walkdir", +] + +[[package]] +name = "refinery-macros" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94d4b9241859ba19eaa5c04c86e782eb3aa0aae2c5868e0cfa90c856e58a174" +dependencies = [ + "proc-macro2", + "quote", + "refinery-core", + "regex", + "syn 2.0.28", +] + [[package]] name = "regex" version = "1.9.3" @@ -6019,6 +7952,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error 1.2.3", +] + [[package]] name = "rfc6979" version = "0.3.1" @@ -6140,7 +8083,7 @@ version = "0.5.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936012c99162a03a67f37f9836d5f938f662e26f2717809761a9ac46432090f4" dependencies = [ - "cookie", + "cookie 0.17.0", "either", "futures", "http", @@ -6183,19 +8126,125 @@ version = "0.8.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c43f8edc57d88750a220b0ec1870a36c1106204ec99cc35131b49de3b954a4a" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", "rocket_http", "syn 1.0.109", ] +[[package]] +name = "rocksdb" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015439787fce1e75d55f279078d33ff14b4af5d93d995e8838ee4631301c8a99" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rtcp" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1919efd6d4a6a85d13388f9487549bb8e359f17198cc03ffd72f79b553873691" +dependencies = [ + "bytes", + "thiserror", + "webrtc-util", +] + +[[package]] +name = "rtnetlink" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322c53fd76a18698f1c27381d58091de3a043d356aa5bd0d510608b565f469a0" +dependencies = [ + "futures", + "log", + "netlink-packet-route", + "netlink-proto", + "nix", + "thiserror", + "tokio", +] + +[[package]] +name = "rtp" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a095411ff00eed7b12e4c6a118ba984d113e1079582570d56a5ee723f11f80" +dependencies = [ + "async-trait", + "bytes", + "rand 0.8.5", + "serde", + "thiserror", + "webrtc-util", +] + +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags 1.3.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.7.0", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rust-embed" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b68543d5527e158213414a92832d2aab11a84d2571a5eb021ebe22c43aab066" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4e0f0ced47ded9a68374ac145edd65a6c1fa13a96447b873660b2a568a0fd7" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 1.0.109", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" +dependencies = [ + "sha2 0.10.7", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.2.3" @@ -6214,6 +8263,29 @@ dependencies = [ "semver 1.0.18", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.8" @@ -6223,7 +8295,7 @@ dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.5", "windows-sys 0.48.0", ] @@ -6279,6 +8351,17 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rw-stream-sink" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26338f5e09bb721b85b135ea05af7767c90b52f6de4f087d4f4a3a9d64e7dc04" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + [[package]] name = "ryu" version = "1.0.15" @@ -6390,6 +8473,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdp" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d22a5ef407871893fd72b4562ee15e4742269b173959db4b8df6f538c414e13" +dependencies = [ + "rand 0.8.5", + "substring", + "thiserror", + "url", +] + [[package]] name = "sec1" version = "0.3.0" @@ -6489,6 +8584,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd31f59f6fe2b0c055371bb2f16d7f0aa7d8881676c04a55b1596d1a17cd10a4" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.0" @@ -6651,6 +8755,21 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs 4.0.0", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook" version = "0.3.17" @@ -6701,6 +8820,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "slab" version = "0.4.8" @@ -6749,6 +8874,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "snow" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +dependencies = [ + "aes-gcm 0.9.4", + "blake2 0.10.6", + "chacha20poly1305 0.9.1", + "curve25519-dalek 4.0.0-rc.1", + "rand_core 0.6.4", + "ring", + "rustc_version 0.4.0", + "sha2 0.10.7", + "subtle 2.4.1", +] + [[package]] name = "socket2" version = "0.4.9" @@ -6781,7 +8923,7 @@ dependencies = [ "bs58 0.4.0", "byteorder", "chacha", - "curve25519-dalek", + "curve25519-dalek 3.2.0", "digest 0.9.0", "hkdf 0.11.0", "hmac 0.11.0", @@ -7041,6 +9183,12 @@ dependencies = [ "loom", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.3" @@ -7098,6 +9246,34 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "stun" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e94b1ec00bad60e6410e058b52f1c66de3dc5fe4d62d09b3e52bb7d3b73e25" +dependencies = [ + "base64 0.13.1", + "crc 3.0.1", + "lazy_static", + "md-5", + "rand 0.8.5", + "ring", + "subtle 2.4.1", + "thiserror", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "subtle" version = "1.0.0" @@ -7153,6 +9329,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "sysinfo" version = "0.27.8" @@ -7168,6 +9356,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -7183,7 +9392,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix", + "rustix 0.38.8", "windows-sys 0.48.0", ] @@ -7552,6 +9761,18 @@ dependencies = [ "tungstenite 0.17.3", ] +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.18.0", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -7560,7 +9781,10 @@ checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", + "hashbrown 0.12.3", "pin-project-lite", "slab", "tokio", @@ -7770,6 +9994,52 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "rand 0.8.5", + "smallvec", + "socket2 0.4.9", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "lru-cache", + "parking_lot 0.12.1", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.4" @@ -7853,6 +10123,44 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "turn" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4712ee30d123ec7ae26d1e1b218395a16c87cdbaf4b3925d170d684af62ea5e8" +dependencies = [ + "async-trait", + "base64 0.13.1", + "futures", + "log", + "md-5", + "rand 0.8.5", + "ring", + "stun", + "thiserror", + "tokio", + "webrtc-util", +] + [[package]] name = "typenum" version = "1.16.0" @@ -7874,6 +10182,18 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "uncased" version = "0.9.9" @@ -7957,6 +10277,16 @@ dependencies = [ "extension-traits", ] +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array 0.14.7", + "subtle 2.4.1", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -7967,6 +10297,16 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "unsigned-varint" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" +dependencies = [ + "asynchronous-codec", + "bytes", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -8009,6 +10349,48 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "utoipa" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98" +dependencies = [ + "indexmap 1.9.3", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b" +dependencies = [ + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.28", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062bba5a3568e126ac72049a63254f4cb1da2eb713db0c1ab2a4c76be191db8c" +dependencies = [ + "actix-web", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "0.8.2" @@ -8022,6 +10404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "getrandom 0.2.10", + "serde", "wasm-bindgen", ] @@ -8060,6 +10443,21 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -8169,6 +10567,21 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-utils" version = "0.1.0" @@ -8249,6 +10662,221 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "webrtc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3bc9049bdb2cea52f5fd4f6f728184225bdb867ed0dc2410eab6df5bdd67bb" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "hex", + "interceptor", + "lazy_static", + "log", + "rand 0.8.5", + "rcgen 0.9.3", + "regex", + "ring", + "rtcp", + "rtp", + "rustls 0.19.1", + "sdp", + "serde", + "serde_json", + "sha2 0.10.7", + "stun", + "thiserror", + "time 0.3.25", + "tokio", + "turn", + "url", + "waitgroup", + "webrtc-data", + "webrtc-dtls", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef36a4d12baa6e842582fe9ec16a57184ba35e1a09308307b67d43ec8883100" +dependencies = [ + "bytes", + "derive_builder", + "log", + "thiserror", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-dtls" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942be5bd85f072c3128396f6e5a9bfb93ca8c1939ded735d177b7bcba9a13d05" +dependencies = [ + "aes 0.6.0", + "aes-gcm 0.10.2", + "async-trait", + "bincode", + "block-modes", + "byteorder", + "ccm", + "curve25519-dalek 3.2.0", + "der-parser 8.2.0", + "elliptic-curve 0.12.3", + "hkdf 0.12.3", + "hmac 0.12.1", + "log", + "oid-registry 0.6.1", + "p256", + "p384", + "rand 0.8.5", + "rand_core 0.6.4", + "rcgen 0.9.3", + "ring", + "rustls 0.19.1", + "sec1 0.3.0", + "serde", + "sha1", + "sha2 0.10.7", + "signature 1.6.4", + "subtle 2.4.1", + "thiserror", + "tokio", + "webpki 0.21.4", + "webrtc-util", + "x25519-dalek 2.0.0-pre.1", + "x509-parser 0.13.2", +] + +[[package]] +name = "webrtc-ice" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a03cc11e9a7d7b4f9f99870558fe37a102b65b93f8045392fef7c67b39e80" +dependencies = [ + "arc-swap", + "async-trait", + "crc 3.0.1", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "stun", + "thiserror", + "tokio", + "turn", + "url", + "uuid 1.4.1", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f08dfd7a6e3987e255c4dbe710dde5d94d0f0574f8a21afa95d171376c143106" +dependencies = [ + "log", + "socket2 0.4.9", + "thiserror", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f72e1650a8ae006017d1a5280efb49e2610c19ccc3c0905b03b648aee9554991" +dependencies = [ + "byteorder", + "bytes", + "rand 0.8.5", + "rtp", + "thiserror", +] + +[[package]] +name = "webrtc-sctp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d47adcd9427eb3ede33d5a7f3424038f63c965491beafcc20bc650a2f6679c0" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc 3.0.1", + "log", + "rand 0.8.5", + "thiserror", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6183edc4c1c6c0175f8812eefdce84dfa0aea9c3ece71c2bf6ddd3c964de3da5" +dependencies = [ + "aead 0.4.3", + "aes 0.7.5", + "aes-gcm 0.9.4", + "async-trait", + "byteorder", + "bytes", + "ctr 0.8.0", + "hmac 0.11.0", + "log", + "rtcp", + "rtp", + "sha-1 0.9.8", + "subtle 2.4.1", + "thiserror", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f1db1727772c05cf7a2cfece52c3aca8045ca1e176cd517d323489aa3c6d87" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "cc", + "ipnet", + "lazy_static", + "libc", + "log", + "nix", + "rand 0.8.5", + "thiserror", + "tokio", + "winapi", +] + +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + [[package]] name = "winapi" version = "0.3.9" @@ -8280,6 +10908,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +dependencies = [ + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", +] + [[package]] name = "windows" version = "0.48.0" @@ -8349,6 +10990,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -8361,6 +11008,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -8373,6 +11026,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -8385,6 +11044,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -8409,6 +11074,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -8474,12 +11145,74 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "rand_core 0.5.1", "serde", "zeroize", ] +[[package]] +name = "x25519-dalek" +version = "2.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c" +dependencies = [ + "asn1-rs 0.3.1", + "base64 0.13.1", + "data-encoding", + "der-parser 7.0.0", + "lazy_static", + "nom", + "oid-registry 0.4.0", + "ring", + "rusticata-macros", + "thiserror", + "time 0.3.25", +] + +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs 0.5.2", + "base64 0.13.1", + "data-encoding", + "der-parser 8.2.0", + "lazy_static", + "nom", + "oid-registry 0.6.1", + "rusticata-macros", + "thiserror", + "time 0.3.25", +] + +[[package]] +name = "yamux" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d9ba232399af1783a58d8eb26f6b5006fbefe2dc9ef36bd283324792d03ea5" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot 0.12.1", + "rand 0.8.5", + "static_assertions", +] + [[package]] name = "yansi" version = "0.5.1" @@ -8492,6 +11225,15 @@ version = "1.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time 0.3.25", +] + [[package]] name = "zeroize" version = "1.6.0" @@ -8511,3 +11253,45 @@ dependencies = [ "quote", "syn 2.0.28", ] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.5+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 8ba5452d11..696f1bd9f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", + "common/cosmwasm-smart-contracts/ephemera", "common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/multisig-contract", diff --git a/clients/webassembly/Cargo.lock b/clients/webassembly/Cargo.lock index c5aa4a2830..8449b45ff6 100644 --- a/clients/webassembly/Cargo.lock +++ b/clients/webassembly/Cargo.lock @@ -2729,6 +2729,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ephemera-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "nym-contracts-common", +] + [[package]] name = "nym-explorer-api-requests" version = "0.1.0" @@ -3145,6 +3155,7 @@ dependencies = [ "nym-coconut-interface", "nym-config", "nym-contracts-common", + "nym-ephemera-common", "nym-group-contract-common", "nym-mixnet-contract-common", "nym-multisig-contract-common", diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 952d106e97..f3d2716143 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -13,6 +13,7 @@ colored = "2.0" nym-coconut-dkg-common = { path = "../../cosmwasm-smart-contracts/coconut-dkg" } nym-contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } +nym-ephemera-common = { path = "../../cosmwasm-smart-contracts/ephemera" } nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" } nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ephemera_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ephemera_query_client.rs new file mode 100644 index 0000000000..66595be1a1 --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ephemera_query_client.rs @@ -0,0 +1,79 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::collect_paged; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::error::NyxdError; +use crate::nyxd::CosmWasmClient; +use async_trait::async_trait; +use nym_ephemera_common::msg::QueryMsg as EphemeraQueryMsg; +use nym_ephemera_common::peers::PagedPeerResponse; +use nym_ephemera_common::types::JsonPeerInfo; +use serde::Deserialize; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EphemeraQueryClient { + async fn query_ephemera_contract(&self, query: EphemeraQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn get_peers_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + let request = EphemeraQueryMsg::GetPeers { start_after, limit }; + self.query_ephemera_contract(request).await + } +} + +// extension trait to the query client to deal with the paged queries +// (it didn't feel appropriate to combine it with the existing trait +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PagedEphemeraQueryClient: EphemeraQueryClient { + async fn get_all_ephemera_peers(&self) -> Result, NyxdError> { + collect_paged!(self, get_peers_paged, peers) + } +} + +#[async_trait] +impl PagedEphemeraQueryClient for T where T: EphemeraQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EphemeraQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_ephemera_contract(&self, query: EphemeraQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + let ephemera_contract_address = &self + .ephemera_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("ephemera contract"))?; + self.query_contract_smart(ephemera_contract_address, &query) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_query_variants_are_covered( + client: C, + msg: EphemeraQueryMsg, + ) { + match msg { + EphemeraQueryMsg::GetPeers { limit, start_after } => { + client.get_peers_paged(start_after, limit).ignore() + } + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ephemera_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ephemera_signing_client.rs new file mode 100644 index 0000000000..a79ca3fe5a --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ephemera_signing_client.rs @@ -0,0 +1,86 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{Coin, Fee, SigningCosmWasmClient}; +use crate::signing::signer::OfflineSigner; +use async_trait::async_trait; +use nym_ephemera_common::msg::ExecuteMsg as EphemeraExecuteMsg; +use nym_ephemera_common::types::JsonPeerInfo; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EphemeraSigningClient { + async fn execute_ephemera_contract( + &self, + fee: Option, + msg: EphemeraExecuteMsg, + memo: String, + funds: Vec, + ) -> Result; + + async fn register_as_peer( + &self, + peer_info: JsonPeerInfo, + fee: Option, + ) -> Result { + let req = EphemeraExecuteMsg::RegisterPeer { peer_info }; + + self.execute_ephemera_contract(fee, req, "registering as peer".to_string(), vec![]) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EphemeraSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<::Error>, +{ + async fn execute_ephemera_contract( + &self, + fee: Option, + msg: EphemeraExecuteMsg, + memo: String, + funds: Vec, + ) -> Result { + let ephemera_contract_address = self + .ephemera_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("ephemera contract"))?; + + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); + let signer_address = &self.signer_addresses()?[0]; + + self.execute( + signer_address, + ephemera_contract_address, + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_execute_variants_are_covered( + client: C, + msg: EphemeraExecuteMsg, + ) { + match msg { + EphemeraExecuteMsg::RegisterPeer { peer_info } => { + client.register_as_peer(peer_info, None).ignore() + } + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index 545fdf05ec..dac63a2530 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -10,6 +10,7 @@ use std::str::FromStr; // query clients mod coconut_bandwidth_query_client; mod dkg_query_client; +mod ephemera_query_client; mod group_query_client; mod mixnet_query_client; mod multisig_query_client; @@ -20,6 +21,7 @@ mod vesting_query_client; // signing clients mod coconut_bandwidth_signing_client; mod dkg_signing_client; +mod ephemera_signing_client; mod group_signing_client; mod mixnet_signing_client; mod multisig_signing_client; @@ -32,6 +34,7 @@ pub use coconut_bandwidth_query_client::{ CoconutBandwidthQueryClient, PagedCoconutBandwidthQueryClient, }; pub use dkg_query_client::{DkgQueryClient, PagedDkgQueryClient}; +pub use ephemera_query_client::{EphemeraQueryClient, PagedEphemeraQueryClient}; pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient}; pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient}; pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient}; @@ -42,6 +45,7 @@ pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient}; // re-export signing traits pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; pub use dkg_signing_client::DkgSigningClient; +pub use ephemera_signing_client::EphemeraSigningClient; pub use group_signing_client::GroupSigningClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_signing_client::MultisigSigningClient; @@ -61,6 +65,9 @@ pub trait NymContractsProvider { fn group_contract_address(&self) -> Option<&AccountId>; fn multisig_contract_address(&self) -> Option<&AccountId>; + // ephemera-related + fn ephemera_contract_address(&self) -> Option<&AccountId>; + // SPs fn name_service_contract_address(&self) -> Option<&AccountId>; fn service_provider_contract_address(&self) -> Option<&AccountId>; @@ -76,6 +83,8 @@ pub struct TypedNymContracts { pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, + pub ephemera_contract_address: Option, + pub service_provider_directory_contract_address: Option, pub name_service_contract_address: Option, } @@ -109,6 +118,10 @@ impl TryFrom for TypedNymContracts { .coconut_dkg_contract_address .map(|addr| addr.parse()) .transpose()?, + ephemera_contract_address: value + .ephemera_contract_address + .map(|addr| addr.parse()) + .transpose()?, service_provider_directory_contract_address: value .service_provider_directory_contract_address .map(|addr| addr.parse()) diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index d0cf1e0b3e..16027a1970 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -259,6 +259,10 @@ impl NymContractsProvider for NyxdClient { self.config.contracts.multisig_contract_address.as_ref() } + fn ephemera_contract_address(&self) -> Option<&AccountId> { + self.config.contracts.ephemera_contract_address.as_ref() + } + fn name_service_contract_address(&self) -> Option<&AccountId> { self.config.contracts.name_service_contract_address.as_ref() } diff --git a/common/client-libs/validator-client/src/nyxd/traits/mod.rs b/common/client-libs/validator-client/src/nyxd/traits/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index fe91fc3d6d..a751ba2414 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -17,6 +17,7 @@ pub mod helpers; pub mod legacy_helpers; pub const NYM_DIR: &str = ".nym"; +pub const DEFAULT_NYM_APIS_DIR: &str = "nym-api"; pub const DEFAULT_CONFIG_DIR: &str = "config"; pub const DEFAULT_DATA_DIR: &str = "data"; pub const DEFAULT_CONFIG_FILENAME: &str = "config.toml"; diff --git a/common/cosmwasm-smart-contracts/ephemera/Cargo.toml b/common/cosmwasm-smart-contracts/ephemera/Cargo.toml new file mode 100644 index 0000000000..fda88a18f5 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ephemera/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nym-ephemera-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-utils = { workspace = true } + +contracts-common = { path = "../contracts-common", package = "nym-contracts-common" } + +[features] +schema = [] \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/ephemera/src/lib.rs b/common/cosmwasm-smart-contracts/ephemera/src/lib.rs new file mode 100644 index 0000000000..f53ac15b7e --- /dev/null +++ b/common/cosmwasm-smart-contracts/ephemera/src/lib.rs @@ -0,0 +1,3 @@ +pub mod msg; +pub mod peers; +pub mod types; diff --git a/common/cosmwasm-smart-contracts/ephemera/src/msg.rs b/common/cosmwasm-smart-contracts/ephemera/src/msg.rs new file mode 100644 index 0000000000..5024a73b0b --- /dev/null +++ b/common/cosmwasm-smart-contracts/ephemera/src/msg.rs @@ -0,0 +1,33 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "schema")] +use crate::peers::PagedPeerResponse; +use crate::types::JsonPeerInfo; +use cosmwasm_schema::cw_serde; +#[cfg(feature = "schema")] +use cosmwasm_schema::QueryResponses; + +#[cw_serde] +pub struct InstantiateMsg { + pub group_addr: String, + pub mix_denom: String, +} + +#[cw_serde] +pub enum ExecuteMsg { + RegisterPeer { peer_info: JsonPeerInfo }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(PagedPeerResponse))] + GetPeers { + limit: Option, + start_after: Option, + }, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/common/cosmwasm-smart-contracts/ephemera/src/peers.rs b/common/cosmwasm-smart-contracts/ephemera/src/peers.rs new file mode 100644 index 0000000000..d2eafcad08 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ephemera/src/peers.rs @@ -0,0 +1,25 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::JsonPeerInfo; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; + +#[cw_serde] +pub struct PagedPeerResponse { + pub peers: Vec, + pub per_page: usize, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +impl PagedPeerResponse { + pub fn new(peers: Vec, per_page: usize, start_next_after: Option) -> Self { + PagedPeerResponse { + peers, + per_page, + start_next_after, + } + } +} diff --git a/common/cosmwasm-smart-contracts/ephemera/src/types.rs b/common/cosmwasm-smart-contracts/ephemera/src/types.rs new file mode 100644 index 0000000000..cc68e6dc54 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ephemera/src/types.rs @@ -0,0 +1,29 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; + +#[cw_serde] +pub struct JsonPeerInfo { + /// The cosmos address of the peer, used in interacting with the chain. + pub cosmos_address: Addr, + /// The TCP/IP address of the peer. + /// Expected formats: + /// 1. `:` + /// 2. `/ip4//tcp/` - this is the format used by libp2p multiaddr + pub ip_address: String, + ///Serialized public key. + pub public_key: String, +} + +impl JsonPeerInfo { + #[must_use] + pub fn new(cosmos_address: Addr, ip_address: String, public_key: String) -> Self { + Self { + cosmos_address, + ip_address, + public_key, + } + } +} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 33373dcc52..8de0ed465c 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -30,6 +30,7 @@ pub struct NymContracts { pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, + pub ephemera_contract_address: Option, pub service_provider_directory_contract_address: Option, pub name_service_contract_address: Option, } @@ -128,6 +129,9 @@ impl NymNetworkDetails { .with_coconut_dkg_contract(Some( var(var_names::COCONUT_DKG_CONTRACT_ADDRESS).expect("coconut dkg contract not set"), )) + .with_ephemera_contract(Some( + var(var_names::EPHEMERA_CONTRACT_ADDRESS).expect("ephemera contract not set"), + )) .with_service_provider_directory_contract(get_optional_env( var_names::SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS, )) @@ -159,6 +163,7 @@ impl NymNetworkDetails { coconut_dkg_contract_address: parse_optional_str( mainnet::COCONUT_DKG_CONTRACT_ADDRESS, ), + ephemera_contract_address: parse_optional_str(mainnet::EPHEMERA_CONTRACT_ADDRESS), service_provider_directory_contract_address: None, name_service_contract_address: None, }, @@ -253,6 +258,12 @@ impl NymNetworkDetails { self } + #[must_use] + pub fn with_ephemera_contract>(mut self, contract: Option) -> Self { + self.contracts.ephemera_contract_address = contract.map(Into::into); + self + } + #[must_use] pub fn with_service_provider_directory_contract>( mut self, diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index f2ec070533..fbc25c4b00 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -20,6 +20,7 @@ pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = pub(crate) const GROUP_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub(crate) const EPHEMERA_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; @@ -84,6 +85,10 @@ pub fn export_to_env() { var_names::COCONUT_DKG_CONTRACT_ADDRESS, COCONUT_DKG_CONTRACT_ADDRESS, ); + set_var_to_default( + var_names::EPHEMERA_CONTRACT_ADDRESS, + EPHEMERA_CONTRACT_ADDRESS, + ); set_var_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, @@ -126,6 +131,10 @@ pub fn export_to_env_if_not_set() { var_names::COCONUT_DKG_CONTRACT_ADDRESS, COCONUT_DKG_CONTRACT_ADDRESS, ); + set_var_conditionally_to_default( + var_names::EPHEMERA_CONTRACT_ADDRESS, + EPHEMERA_CONTRACT_ADDRESS, + ); set_var_conditionally_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index 7639efa8e4..26b65b7f95 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -18,6 +18,7 @@ pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS"; pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS"; pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS"; +pub const EPHEMERA_CONTRACT_ADDRESS: &str = "EPHEMERA_CONTRACT_ADDRESS"; pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS"; pub const SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str = diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 1895d7c925..812afbaa8d 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1259,6 +1259,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ephemera" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-multi-test", + "cw-storage-plus", + "cw4", + "cw4-group", + "lazy_static", + "nym-ephemera-common", + "nym-group-contract-common", + "rusty-fork", + "serde", + "thiserror", +] + +[[package]] +name = "nym-ephemera-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "nym-contracts-common", +] + [[package]] name = "nym-group-contract-common" version = "0.1.0" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 0887794260..8063d2f543 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,6 +3,7 @@ members = [ "coconut-bandwidth", "coconut-dkg", "coconut-test", + "ephemera", "mixnet", "mixnet-vesting-integration-tests", "multisig/cw3-flex-multisig", diff --git a/contracts/ephemera/.cargo/config b/contracts/ephemera/.cargo/config new file mode 100644 index 0000000000..2f698e22b3 --- /dev/null +++ b/contracts/ephemera/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema --features=schema-gen" \ No newline at end of file diff --git a/contracts/ephemera/Cargo.toml b/contracts/ephemera/Cargo.toml new file mode 100644 index 0000000000..376eec374e --- /dev/null +++ b/contracts/ephemera/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "nym-ephemera" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "schema" +required-features = ["schema-gen"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +nym-ephemera-common = { path = "../../common/cosmwasm-smart-contracts/ephemera" } + +cosmwasm-schema = { workspace = true, optional = true } +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +cw-storage-plus = { workspace = true } +cw-controllers = { workspace = true } +cw4 = { workspace = true } + +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = "1.0.23" + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw4-group = { path = "../multisig/cw4-group" } +nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" } +lazy_static = "1.4" +rusty-fork = "0.3" + +[features] +schema-gen = ["nym-ephemera-common/schema", "cosmwasm-schema"] diff --git a/contracts/ephemera/Makefile b/contracts/ephemera/Makefile new file mode 100644 index 0000000000..f138e2c879 --- /dev/null +++ b/contracts/ephemera/Makefile @@ -0,0 +1,2 @@ +generate-schema: + cargo schema \ No newline at end of file diff --git a/contracts/ephemera/schema/nym-ephemera.json b/contracts/ephemera/schema/nym-ephemera.json new file mode 100644 index 0000000000..4a0efed6bd --- /dev/null +++ b/contracts/ephemera/schema/nym-ephemera.json @@ -0,0 +1,194 @@ +{ + "contract_name": "nym-ephemera", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "group_addr", + "mix_denom" + ], + "properties": { + "group_addr": { + "type": "string" + }, + "mix_denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "register_peer" + ], + "properties": { + "register_peer": { + "type": "object", + "required": [ + "peer_info" + ], + "properties": { + "peer_info": { + "$ref": "#/definitions/JsonPeerInfo" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "JsonPeerInfo": { + "type": "object", + "required": [ + "cosmos_address", + "ip_address", + "public_key" + ], + "properties": { + "cosmos_address": { + "description": "The cosmos address of the peer, used in interacting with the chain.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "ip_address": { + "description": "The TCP/IP address of the peer. Expected formats: 1. `:` 2. `/ip4//tcp/` - this is the format used by libp2p multiaddr", + "type": "string" + }, + "public_key": { + "description": "Serialized public key.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "get_peers" + ], + "properties": { + "get_peers": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "get_peers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedPeerResponse", + "type": "object", + "required": [ + "peers", + "per_page" + ], + "properties": { + "peers": { + "type": "array", + "items": { + "$ref": "#/definitions/JsonPeerInfo" + } + }, + "per_page": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "JsonPeerInfo": { + "type": "object", + "required": [ + "cosmos_address", + "ip_address", + "public_key" + ], + "properties": { + "cosmos_address": { + "description": "The cosmos address of the peer, used in interacting with the chain.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "ip_address": { + "description": "The TCP/IP address of the peer. Expected formats: 1. `:` 2. `/ip4//tcp/` - this is the format used by libp2p multiaddr", + "type": "string" + }, + "public_key": { + "description": "Serialized public key.", + "type": "string" + } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/contracts/ephemera/schema/raw/execute.json b/contracts/ephemera/schema/raw/execute.json new file mode 100644 index 0000000000..becf9117ca --- /dev/null +++ b/contracts/ephemera/schema/raw/execute.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "register_peer" + ], + "properties": { + "register_peer": { + "type": "object", + "required": [ + "peer_info" + ], + "properties": { + "peer_info": { + "$ref": "#/definitions/JsonPeerInfo" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "JsonPeerInfo": { + "type": "object", + "required": [ + "cosmos_address", + "ip_address", + "public_key" + ], + "properties": { + "cosmos_address": { + "description": "The cosmos address of the peer, used in interacting with the chain.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "ip_address": { + "description": "The TCP/IP address of the peer. Expected formats: 1. `:` 2. `/ip4//tcp/` - this is the format used by libp2p multiaddr", + "type": "string" + }, + "public_key": { + "description": "Serialized public key.", + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/ephemera/schema/raw/instantiate.json b/contracts/ephemera/schema/raw/instantiate.json new file mode 100644 index 0000000000..907c7aa20f --- /dev/null +++ b/contracts/ephemera/schema/raw/instantiate.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "group_addr", + "mix_denom" + ], + "properties": { + "group_addr": { + "type": "string" + }, + "mix_denom": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/ephemera/schema/raw/migrate.json b/contracts/ephemera/schema/raw/migrate.json new file mode 100644 index 0000000000..7fbe8c5708 --- /dev/null +++ b/contracts/ephemera/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/ephemera/schema/raw/query.json b/contracts/ephemera/schema/raw/query.json new file mode 100644 index 0000000000..77c2991f6b --- /dev/null +++ b/contracts/ephemera/schema/raw/query.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "get_peers" + ], + "properties": { + "get_peers": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/ephemera/schema/raw/response_to_get_peers.json b/contracts/ephemera/schema/raw/response_to_get_peers.json new file mode 100644 index 0000000000..3b3c85bb5b --- /dev/null +++ b/contracts/ephemera/schema/raw/response_to_get_peers.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedPeerResponse", + "type": "object", + "required": [ + "peers", + "per_page" + ], + "properties": { + "peers": { + "type": "array", + "items": { + "$ref": "#/definitions/JsonPeerInfo" + } + }, + "per_page": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "JsonPeerInfo": { + "type": "object", + "required": [ + "cosmos_address", + "ip_address", + "public_key" + ], + "properties": { + "cosmos_address": { + "description": "The cosmos address of the peer, used in interacting with the chain.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "ip_address": { + "description": "The TCP/IP address of the peer. Expected formats: 1. `:` 2. `/ip4//tcp/` - this is the format used by libp2p multiaddr", + "type": "string" + }, + "public_key": { + "description": "Serialized public key.", + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/ephemera/src/bin/schema.rs b/contracts/ephemera/src/bin/schema.rs new file mode 100644 index 0000000000..8df2100755 --- /dev/null +++ b/contracts/ephemera/src/bin/schema.rs @@ -0,0 +1,14 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::write_api; +use nym_ephemera_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/ephemera/src/contract.rs b/contracts/ephemera/src/contract.rs new file mode 100644 index 0000000000..0cfa78a410 --- /dev/null +++ b/contracts/ephemera/src/contract.rs @@ -0,0 +1,97 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::ContractError; +use crate::peers::queries::query_peers_paged; +use crate::peers::transactions::try_register_peer; +use crate::state::{State, STATE}; +use cosmwasm_std::{ + entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, +}; +use cw4::Cw4Contract; +use nym_ephemera_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +/// Instantiate the contract. +/// +/// `deps` contains Storage, API and Querier +/// `env` contains block, message and contract info +/// `msg` is the contract initialization message, sort of like a constructor call. +#[entry_point] +pub fn instantiate( + deps: DepsMut<'_>, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| { + ContractError::InvalidGroup { + addr: msg.group_addr.clone(), + } + })?); + + let state = State { + group_addr, + mix_denom: msg.mix_denom, + }; + STATE.save(deps.storage, &state)?; + + Ok(Response::default()) +} + +/// Handle an incoming message +#[entry_point] +pub fn execute( + deps: DepsMut<'_>, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::RegisterPeer { peer_info } => try_register_peer(deps, info, peer_info), + } +} + +#[entry_point] +pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { + let response = match msg { + QueryMsg::GetPeers { limit, start_after } => { + to_binary(&query_peers_paged(deps, start_after, limit)?)? + } + }; + + Ok(response) +} + +#[entry_point] +pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::Addr; + + #[test] + fn initialize_contract() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + let init_msg = InstantiateMsg { + group_addr: "group_addr".to_string(), + mix_denom: "uatom".to_string(), + }; + + let sender = mock_info("sender", &[]); + let res = instantiate(deps.as_mut(), env, sender, init_msg); + assert!(res.is_ok()); + + let expected_state = State { + group_addr: Cw4Contract::new(Addr::unchecked("group_addr")), + mix_denom: "uatom".to_string(), + }; + let state = STATE.load(deps.as_ref().storage).unwrap(); + assert_eq!(state, expected_state); + } +} diff --git a/contracts/ephemera/src/error.rs b/contracts/ephemera/src/error.rs new file mode 100644 index 0000000000..2ee74c6ab0 --- /dev/null +++ b/contracts/ephemera/src/error.rs @@ -0,0 +1,21 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::StdError; +use thiserror::Error; + +/// Custom errors for contract failure conditions. +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error("Group contract invalid address '{addr}'")] + InvalidGroup { addr: String }, + + #[error("This potential ephemera peer is not in the ephemera group")] + Unauthorized, + + #[error("This sender is already registered")] + AlreadyRegistered, +} diff --git a/contracts/ephemera/src/lib.rs b/contracts/ephemera/src/lib.rs new file mode 100644 index 0000000000..fa30231382 --- /dev/null +++ b/contracts/ephemera/src/lib.rs @@ -0,0 +1,8 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod contract; +pub mod error; +mod peers; +mod state; +mod support; diff --git a/contracts/ephemera/src/peers/mod.rs b/contracts/ephemera/src/peers/mod.rs new file mode 100644 index 0000000000..e82ae1570a --- /dev/null +++ b/contracts/ephemera/src/peers/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/ephemera/src/peers/queries.rs b/contracts/ephemera/src/peers/queries.rs new file mode 100644 index 0000000000..fc24e6d583 --- /dev/null +++ b/contracts/ephemera/src/peers/queries.rs @@ -0,0 +1,164 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::peers::storage::{PEERS, PEERS_PAGE_DEFAULT_LIMIT, PEERS_PAGE_MAX_LIMIT}; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; +use nym_ephemera_common::peers::PagedPeerResponse; + +pub fn query_peers_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(PEERS_PAGE_DEFAULT_LIMIT) + .min(PEERS_PAGE_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.map(Bound::exclusive); + + let peers = PEERS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = peers + .last() + .map(|peer_info| peer_info.cosmos_address.clone()); + + Ok(PagedPeerResponse::new(peers, limit, start_next_after)) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::peers::storage::{PEERS_PAGE_DEFAULT_LIMIT, PEERS_PAGE_MAX_LIMIT}; + use crate::support::tests::fixtures::peer_fixture; + use crate::support::tests::helpers::init_contract; + use cosmwasm_std::DepsMut; + + fn fill_peers(deps: DepsMut<'_>, size: usize) { + for n in 0..size { + let peer = peer_fixture(&format!("peer{}", n)); + PEERS + .save(deps.storage, peer.cosmos_address.clone(), &peer) + .unwrap(); + } + } + + fn remove_peers(deps: DepsMut<'_>, size: usize) { + for n in 0..size { + let peer = peer_fixture(&format!("peer{}", n)); + PEERS.remove(deps.storage, peer.cosmos_address); + } + } + + #[test] + fn peers_empty_on_init() { + let deps = init_contract(); + + let page1 = query_peers_paged(deps.as_ref(), None, None).unwrap(); + assert_eq!(0, page1.peers.len() as u32); + } + + #[test] + fn peers_paged_retrieval_obeys_limits() { + let mut deps = init_contract(); + let limit = 2; + + fill_peers(deps.as_mut(), 1000); + + let page1 = query_peers_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); + assert_eq!(limit, page1.peers.len() as u32); + + remove_peers(deps.as_mut(), 1000); + } + + #[test] + fn peers_paged_retrieval_has_default_limit() { + let mut deps = init_contract(); + + fill_peers(deps.as_mut(), 1000); + + // query without explicitly setting a limit + let page1 = query_peers_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!(PEERS_PAGE_DEFAULT_LIMIT, page1.peers.len() as u32); + + remove_peers(deps.as_mut(), 1000); + } + + #[test] + fn peers_paged_retrieval_has_max_limit() { + let mut deps = init_contract(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * PEERS_PAGE_MAX_LIMIT; + + fill_peers(deps.as_mut(), 1000); + + let page1 = query_peers_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = PEERS_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.peers.len() as u32); + + remove_peers(deps.as_mut(), 1000); + } + + #[test] + fn peers_pagination_works() { + let mut deps = init_contract(); + + let per_page = 2; + + fill_peers(deps.as_mut(), 1); + let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.peers.len()); + remove_peers(deps.as_mut(), 1); + + fill_peers(deps.as_mut(), 2); + // page1 should have 2 results on it + let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.peers.len()); + remove_peers(deps.as_mut(), 2); + + fill_peers(deps.as_mut(), 3); + // page1 still has 2 results + let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.peers.len()); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_peers_paged( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.peers.len()); + remove_peers(deps.as_mut(), 3); + + fill_peers(deps.as_mut(), 4); + let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let start_after = page1.start_next_after.unwrap(); + let page2 = query_peers_paged( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.peers.len()); + remove_peers(deps.as_mut(), 4); + } +} diff --git a/contracts/ephemera/src/peers/storage.rs b/contracts/ephemera/src/peers/storage.rs new file mode 100644 index 0000000000..b3a1734109 --- /dev/null +++ b/contracts/ephemera/src/peers/storage.rs @@ -0,0 +1,11 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Addr; +use cw_storage_plus::Map; +use nym_ephemera_common::types::JsonPeerInfo; + +pub(crate) const PEERS_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const PEERS_PAGE_DEFAULT_LIMIT: u32 = 50; + +pub(crate) const PEERS: Map<'_, Addr, JsonPeerInfo> = Map::new("prs"); diff --git a/contracts/ephemera/src/peers/transactions.rs b/contracts/ephemera/src/peers/transactions.rs new file mode 100644 index 0000000000..6aa587bcb2 --- /dev/null +++ b/contracts/ephemera/src/peers/transactions.rs @@ -0,0 +1,63 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::ContractError; +use crate::peers::storage::PEERS; +use crate::state::STATE; +use cosmwasm_std::{DepsMut, MessageInfo, Response}; +use nym_ephemera_common::types::JsonPeerInfo; + +pub fn try_register_peer( + deps: DepsMut<'_>, + info: MessageInfo, + peer_info: JsonPeerInfo, +) -> Result { + if PEERS.may_load(deps.storage, info.sender.clone())?.is_none() { + if STATE + .load(deps.storage)? + .group_addr + .is_voting_member(&deps.querier, &info.sender, None)? + .is_some() + { + PEERS.save(deps.storage, info.sender, &peer_info)?; + Ok(Default::default()) + } else { + Err(ContractError::Unauthorized {}) + } + } else { + Err(ContractError::AlreadyRegistered) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::support::tests::fixtures::peer_fixture; + use crate::support::tests::helpers; + use crate::support::tests::helpers::GROUP_MEMBERS; + use cosmwasm_std::testing::mock_info; + use cw4::Member; + + #[test] + fn peer_registration() { + let mut deps = helpers::init_contract(); + let peer_info = peer_fixture("owner"); + let info = mock_info("owner", &[]); + + let ret = try_register_peer(deps.as_mut(), info.clone(), peer_info.clone()).unwrap_err(); + assert_eq!(ret, ContractError::Unauthorized); + + GROUP_MEMBERS.lock().unwrap().push(( + Member { + addr: "owner".to_string(), + weight: 10, + }, + 1, + )); + + try_register_peer(deps.as_mut(), info.clone(), peer_info.clone()).unwrap(); + + let ret = try_register_peer(deps.as_mut(), info, peer_info).unwrap_err(); + assert_eq!(ret, ContractError::AlreadyRegistered); + } +} diff --git a/contracts/ephemera/src/state/mod.rs b/contracts/ephemera/src/state/mod.rs new file mode 100644 index 0000000000..6dec1262d2 --- /dev/null +++ b/contracts/ephemera/src/state/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cw4::Cw4Contract; +use cw_storage_plus::Item; +use serde::{Deserialize, Serialize}; + +// unique items +pub const STATE: Item = Item::new("state"); + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct State { + pub mix_denom: String, + pub group_addr: Cw4Contract, +} diff --git a/contracts/ephemera/src/support/mod.rs b/contracts/ephemera/src/support/mod.rs new file mode 100644 index 0000000000..070bbc8eed --- /dev/null +++ b/contracts/ephemera/src/support/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub mod tests; diff --git a/contracts/ephemera/src/support/tests/fixtures.rs b/contracts/ephemera/src/support/tests/fixtures.rs new file mode 100644 index 0000000000..bf1f469b6d --- /dev/null +++ b/contracts/ephemera/src/support/tests/fixtures.rs @@ -0,0 +1,15 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Addr; +use nym_ephemera_common::types::JsonPeerInfo; + +pub const TEST_MIX_DENOM: &str = "unym"; + +pub fn peer_fixture(cosmos_address: &str) -> JsonPeerInfo { + JsonPeerInfo { + cosmos_address: Addr::unchecked(cosmos_address), + ip_address: "127.0.0.1".to_string(), + public_key: "random_key".to_string(), + } +} diff --git a/contracts/ephemera/src/support/tests/helpers.rs b/contracts/ephemera/src/support/tests/helpers.rs new file mode 100644 index 0000000000..df665eba1e --- /dev/null +++ b/contracts/ephemera/src/support/tests/helpers.rs @@ -0,0 +1,74 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::instantiate; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; +use cosmwasm_std::{ + from_binary, to_binary, ContractResult, Empty, MemoryStorage, OwnedDeps, QuerierResult, + SystemResult, WasmQuery, +}; +use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse}; +use lazy_static::lazy_static; +use nym_ephemera_common::msg::InstantiateMsg; +use std::sync::Mutex; + +use super::fixtures::TEST_MIX_DENOM; + +pub const ADMIN_ADDRESS: &str = "admin address"; +pub const GROUP_CONTRACT: &str = "group contract address"; + +lazy_static! { + pub static ref GROUP_MEMBERS: Mutex> = Mutex::new(vec![]); +} + +fn querier_handler(query: &WasmQuery) -> QuerierResult { + let bin = match query { + WasmQuery::Smart { contract_addr, msg } => { + if contract_addr != GROUP_CONTRACT { + panic!("Not supported"); + } + match from_binary(msg) { + Ok(Cw4QueryMsg::Member { addr, at_height }) => { + let weight = GROUP_MEMBERS.lock().unwrap().iter().find_map(|(m, h)| { + if m.addr == addr { + if let Some(height) = at_height { + if height != *h { + return None; + } + } + Some(m.weight) + } else { + None + } + }); + to_binary(&MemberResponse { weight }).unwrap() + } + Ok(Cw4QueryMsg::ListMembers { .. }) => { + let members = GROUP_MEMBERS + .lock() + .unwrap() + .iter() + .map(|m| m.0.clone()) + .collect(); + to_binary(&MemberListResponse { members }).unwrap() + } + _ => panic!("Not supported"), + } + } + _ => panic!("Not supported"), + }; + SystemResult::Ok(ContractResult::Ok(bin)) +} + +pub fn init_contract() -> OwnedDeps> { + let mut deps = mock_dependencies(); + deps.querier.update_wasm(querier_handler); + let msg = InstantiateMsg { + group_addr: GROUP_CONTRACT.to_string(), + mix_denom: TEST_MIX_DENOM.to_string(), + }; + let env = mock_env(); + let info = mock_info(ADMIN_ADDRESS, &[]); + instantiate(deps.as_mut(), env, info, msg).unwrap(); + deps +} diff --git a/contracts/ephemera/src/support/tests/mod.rs b/contracts/ephemera/src/support/tests/mod.rs new file mode 100644 index 0000000000..44f93cb286 --- /dev/null +++ b/contracts/ephemera/src/support/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod fixtures; +pub mod helpers; diff --git a/envs/local.env b/envs/local.env index 7efbad719e..0fba848a3e 100644 --- a/envs/local.env +++ b/envs/local.env @@ -17,6 +17,7 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m MULTISIG_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m COCONUT_DKG_CONTRACT_ADDRESS=n1vwtgazgpancsfel04y7syc95ausmat47cjtldqzkdmx6phyrwx2qvkv32p +EPHEMERA_CONTRACT_ADDRESS=n1vwtgazgpancsfel04y7syc95ausmat47cjtldqzkdmx6phyrwx2qvkv32p REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" NYXD="http://127.0.0.1:26657" diff --git a/envs/mainnet.env b/envs/mainnet.env index 4092b50ef8..4bf406bd64 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -17,6 +17,7 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_DKG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +EPHEMERA_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://rpc.nymtech.net" diff --git a/envs/qa-qwerty.env b/envs/qa-qwerty.env index 219d9c7a6c..9080c9d37b 100644 --- a/envs/qa-qwerty.env +++ b/envs/qa-qwerty.env @@ -17,6 +17,7 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19d2nwj7fdhxqmyvgy8lf3ad49a6vmww4shryhrkj2mq GROUP_CONTRACT_ADDRESS=n1fqquzw4mk0pkamgr2ywt2v7h2j9nuyjjn4gvpy8zlpp6xn0uyuzqfm28l5 MULTISIG_CONTRACT_ADDRESS=n1gaq3666chd5348apj8cka8t2mckv7azp9espyr7wgpxyuzur5d0sazpysy COCONUT_DKG_CONTRACT_ADDRESS=n18yadscxw8v35dds7ksv3j0svmjh3h6e7tmxpadk96mvgz27zygkshuf4vs +EPHEMERA_CONTRACT_ADDRESS=n18yadscxw8v35dds7ksv3j0svmjh3h6e7tmxpadk96mvgz27zygkshuf4vs REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1qsn2655eflc0nx2uwqtwyv5kad5dwm4c0gn72yr4q4de5r3jaz2slvqjgt NAME_SERVICE_CONTRACT_ADDRESS=n1cm2u5vfjd3zalfw0p65xyh4tcrw3hjlm0960gzhewga449h4mgas77mjkl diff --git a/envs/qa.env b/envs/qa.env index fbaa9fd556..82bb8b9d3c 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -17,9 +17,10 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs COCONUT_DKG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs +EPHEMERA_CONTRACT_ADDRESS=n1fc7nakzuyfn2qzkclafcsc54asamnclg064962lwne40w2lq558qftzjza REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" NYXD="https://qa-validator.nymtech.net" NYM_API="https://qa-validator-api.nymtech.net/api/" -DKG_TIME_CONFIGURATION="600,300,300,60,60,1209600" \ No newline at end of file +DKG_TIME_CONFIGURATION="600,300,300,60,60,1209600" diff --git a/envs/sandbox.env b/envs/sandbox.env index 921bc5e215..95b0b4d49a 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -19,6 +19,7 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq74 GROUP_CONTRACT_ADDRESS=n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju MULTISIG_CONTRACT_ADDRESS=n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k COCONUT_DKG_CONTRACT_ADDRESS=n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a +EPHEMERA_CONTRACT_ADDRESS="nymt1gwk6muhmzeuxje7df7rjvqwl2vex0kj4t2hwuzmyx5k62kfusu5qk4k5z4" STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" NYXD="https://sandbox-validator1.nymtech.net" diff --git a/ephemera/Cargo.toml b/ephemera/Cargo.toml new file mode 100644 index 0000000000..c831726134 --- /dev/null +++ b/ephemera/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "ephemera" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "ephemera" +path = "bin/main.rs" + +[dependencies] +## internal +nym-task = { path = "../common/task" } + +actix-web = "4" +anyhow = { version = "1.0.66", features = ["backtrace"] } +array-bytes = "6.0.0" +async-trait = "0.1.59" +asynchronous-codec = "0.6.1" +blake2 = "0.10.6" +bs58 = "0.4.0" +bytes = "1.3.0" +cfg-if = "1.0.0" +chrono = { version = "0.4.24", default-features = false, features = ["clock"] } +clap = { version = "4.0.32", features = ["derive"] } +config = { version = "0.13", default-features = false, features = ["toml"] } +digest = "0.10.6" +dirs = "5.0.0" +futures = "0.3.18" +futures-util = "0.3.25" +lazy_static = "1.4.0" +libp2p = { version = "0.51.3", default-features = false, features = ["dns", "gossipsub", "kad", "macros", "noise", "request-response", "serde", "tcp", "tokio", "yamux"] } +libp2p-identity = "0.1.0" +log = "0.4.14" +lru = "0.10.0" +nym-config = { path = "../common/config" } +nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } +pretty_env_logger = "0.4" +refinery = { version = "0.8.7", features = ["rusqlite"], optional = true } +reqwest = { version = "0.11.6", features = ["json"] } +rocksdb = { version = "0.20.1", optional = true } +rusqlite = { version = "0.27.0", features = ["bundled"], optional = true } +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0.149" +serde_json = "1.0.91" +thiserror = "1.0.37" +tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] } +tokio-tungstenite = "0.18.0" +tokio-util = { version = "0.7.4", features = ["full"] } +toml = "0.7.0" +unsigned-varint = "0.7.1" +utoipa = { version = "3.0.1", features = ["actix_extras"] } +utoipa-swagger-ui = { version = "3.0.2", features = ["actix-web"] } +uuid = { version = "1.2.2", features = ["v4"] } + +# Temporary fix to https://github.com/bluejekyll/trust-dns/issues/1946 +enum-as-inner = "=0.5.1" + +[dev-dependencies] +assert_matches = "1.5.0" +rand = "0.8.5" + +[features] +default = ["sqlite_storage"] +rocksdb_storage = ["rocksdb"] +sqlite_storage = ["rusqlite", "refinery"] diff --git a/ephemera/README.md b/ephemera/README.md new file mode 100644 index 0000000000..5d4a9a6de7 --- /dev/null +++ b/ephemera/README.md @@ -0,0 +1,142 @@ +# Ephemera - reliable broadcast protocol implementation + +Ephemera does reliable broadcast for blocks. + +## Short Overview + +All Ephemera nodes accept messages submitted by clients. Node then gossips these to other nodes in the cluster. After certain interval, +a node collects messages and produces a block. Then it does reliable broadcast for the block with other nodes in the cluster. + +Ephemera doesn't have the concept of (decentralised) leader at the moment. It's up to an _Application_ to decide which block to use. +For example in case of Nym-Api, it is the first block submitted to a "Smart Contract". + +At the same time, the purpose of blocks is to reach consensus about which messages are included. It's just that Ephemera doesn't make the final decision, +instead it leaves that to an _Application_. + +## Main concepts + +- **Node** - a single instance of Ephemera. +- **Cluster** - a set of nodes participating in reliable broadcast. +- **EphemeraMessage** - a message submitted by a client. +- **Block** - a set of messages collected by a node. +- **Application(ABCI)** - a trait which Ephemera users implement to accept messages and blocks. + - check_tx + - check_block + - accept_block + +## How to run + +[README](../scripts/README.md) + +## HTTP API + +See [Rust](src/api/http/mod.rs) + +### Endpoints + +**NODE** +- `/ephemera/node/health` +- `/ephemera/node/config` + +**BLOCKS** +- `/ephemera/broadcast/block/{hash}` +- `/ephemera/broadcast/block/height/{height}` +- `/ephemera/broadcast/blocks/last` +- `/ephemera/broadcast/block/certificates/{hash}` +- `/ephemera/broadcast/block/broadcast_info/{hash}` + +**GROUP** +- `/ephemera/broadcast/group/info` + +**MESSAGES** +- `/ephemera/broadcast/submit_message` + +**DHT** +- `/ephemera/dht/query/{key}` +- `/ephemera/dht/store` + +## Rust API + +Almost identical to HTTP API. + +See [Rust](src/api/mod.rs) + +## Application(Ephemera ABCI) + +Cosmos style ABCI application hook +- `check_tx` +- `check_block` +- `deliver_block` + +See [Rust](src/api/application.rs) + +## Examples + +### Ephemera HTTP and WS external interfaces example/tests + +See [README.md](../examples/http-ws-sync/README.md) + +### Nym Api simulation + +See [README.md](../examples/nym-api/README.md) + +### http API example/tests + +See [README.md](../examples/cluster-http-api/README.md) + +### Membership over HTTP API example/tests + +See [README.md](../examples/members_provider_http/README.md) + +## About reliable broadcast and consensus + +In blockchain technology blocks have two main purposes: +1. To maintain chain of blocks, so that the validity of each block can be cryptographically verified by the previous blocks +2. As a unit of consensus, each block contains a set of transactions/messages/actions that are agreed upon by + the network. This set of transactions is chosen from the global set of all possible transactions that are pending. + We call the set of transactions in a block consensus because the set of nodes trying to achieve global shared state + agreed on this particular set of transactions. + +Ephemera is not a blockchain. But it uses blocks to agree on the set of transactions in a block. +But at the same time it doesn't behave like a blockchain consensus algorithm. +We may say that it allows each application that uses Ephemera to "propose" something what can be +afterwards to be used to achieve consensus. + +### In Summary + +1. Ephemera provides functionality to reach agreement on a single value between a set of nodes. +2. Ephemera also provides the concept of a block, which application can take advantage of to reach consensus externally. + +### Reliable broadcast, consensus and blocks + +In distributed systems(including byzantine), we try to solve the problem of reaching to a commons state between a set of nodes. + +One way to define this problem is using the following properties: +1. + 1) Agreement: All nodes agree on the same value.(TODO clarify) + 2) Consensus: All nodes agree on the same value.(TODO clarify) +2. Validity: All nodes agree on a value that is valid. +3. Termination: All nodes eventually agree on a value. + +Reliable broadcast ensures the properties of 1.1 and 1.2. It's left to a particular consensus algorithm to ensure the termination property. + +One important feature of consensus in blockchain is that it guarantees total ordering of transactions. +Reliable broadcast with blocks helps to ensure this total ordering. + +### Ephemera specific properties + +Because Ephemera doesn't use the idea of leader, we can say that it solves consensus partially. +It allows each instance to create a block. And then it's up to an application to decide which block to use. + +Also, as it doesn't implement a full consensus algorithm, it doesn't ensure the termination. +There's no algorithm in place what tries to reach a consensus about a single block globally and sequentially +in time. + +When a block contains a single message, then it's semantically equivalent to a reliable broadcast. + +But when a block contains multiple messages, then it can be part of a consensus process. Except that in Ephemera each node +can create a block. To achieve consensus in a more traditional sense, it needs an application help if more strict +consensus is required. + +For example, Nym-Api allows each node to create a block but uses external coordinator(a smart contract) +to decide which block to use. \ No newline at end of file diff --git a/ephemera/bin/main.rs b/ephemera/bin/main.rs new file mode 100644 index 0000000000..5f7d12976c --- /dev/null +++ b/ephemera/bin/main.rs @@ -0,0 +1,12 @@ +use clap::Parser; + +use ephemera::cli::Cli; +use ephemera::logging; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logging::init(); + + Cli::parse().execute().await?; + Ok(()) +} diff --git a/ephemera/migrations/V1__block.sql b/ephemera/migrations/V1__block.sql new file mode 100644 index 0000000000..512f9c3af1 --- /dev/null +++ b/ephemera/migrations/V1__block.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS blocks ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + block_hash TEXT NOT NULL UNIQUE, + height TEXT NOT NULL UNIQUE, + block BLOB NOT NULL +); + +CREATE TABLE IF NOT EXISTS block_certificates ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + block_hash TEXT NOT NULL UNIQUE, + certificates BLOB NOT NULL +); + +CREATE TABLE IF NOT EXISTS block_broadcast_group ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + block_hash TEXT NOT NULL UNIQUE, + members BLOB NOT NULL +); + +CREATE TABLE IF NOT EXISTS block_merkle_tree ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + block_hash TEXT NOT NULL UNIQUE, + merkle_tree BLOB NOT NULL +); \ No newline at end of file diff --git a/ephemera/src/api/application.rs b/ephemera/src/api/application.rs new file mode 100644 index 0000000000..4d605273f0 --- /dev/null +++ b/ephemera/src/api/application.rs @@ -0,0 +1,104 @@ +use log::trace; +use thiserror::Error; + +use crate::api::types::{ApiBlock, ApiEphemeraMessage}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RemoveMessages { + /// Remove all messages from the mempool + All, + /// Remove only inclued messages from the mempool + Selected(Vec), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CheckBlockResult { + /// Accept the block + Accept, + /// Reject the block with a reason. + Reject, + /// Reject the block and remove messages from the mempool + RejectAndRemoveMessages(RemoveMessages), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CheckBlockResponse { + pub accept: bool, + pub reason: Option, +} + +#[derive(Error, Debug)] +pub enum Error { + //Just a placeholder for now + #[error("ApplicationError: {0}")] + Application(#[from] anyhow::Error), +} + +pub type Result = std::result::Result; + +/// Cosmos style ABCI application hook +/// +/// These functions should be relatively fast, as they are called synchronously by Ephemera main loop. +pub trait Application { + /// It's called when receiving a new message from network before adding it to the mempool. + /// It's up to the application to decide whether the message is valid or not. + /// Basic check could be for example signature verification. + /// + /// # Arguments + /// * `message` - message to be checked + /// + /// # Returns + /// * `true` - if the message is valid + /// * `false` - if the message is invalid + /// + /// # Errors + /// * `Error::General` - if there was an error during validation + fn check_tx(&self, message: ApiEphemeraMessage) -> Result; + + /// Ephemera produces new blocks with configured interval. + /// Application can decide whether to accept the block or not. + /// For example, if the block doesn't contain any transactions, it can be rejected. + /// + /// # Arguments + /// * `block` - block to be checked + /// + /// # Returns + /// * `CheckBlockResult::Accept` - if the block is valid + /// * `CheckBlockResult::Reject` - if the block is invalid + /// * `CheckBlockResult::RejectAndRemoveMessages` - if the block is invalid and some messages should be removed from the mempool + /// + /// # Errors + /// * `Error::General` - if there was an error during validation + fn check_block(&self, block: &ApiBlock) -> Result; + + /// Deliver Block is called after block is confirmed by Ephemera and persisted to the storage. + /// + /// # Arguments + /// * `block` - block to be delivered + /// + /// # Errors + /// * `Error::General` - if there was an error during validation + fn deliver_block(&self, block: ApiBlock) -> Result<()>; +} + +/// Dummy application which doesn't do any validation. +/// Might be useful for testing. +#[derive(Default)] +pub struct Dummy; + +impl Application for Dummy { + fn check_tx(&self, tx: ApiEphemeraMessage) -> Result { + trace!("check_tx: {tx:?}"); + Ok(true) + } + + fn check_block(&self, block: &ApiBlock) -> Result { + trace!("accept_block: {block:?}"); + Ok(CheckBlockResult::Accept) + } + + fn deliver_block(&self, block: ApiBlock) -> Result<()> { + trace!("deliver_block: {block:?}"); + Ok(()) + } +} diff --git a/ephemera/src/api/http/client.rs b/ephemera/src/api/http/client.rs new file mode 100644 index 0000000000..593ee5d0f6 --- /dev/null +++ b/ephemera/src/api/http/client.rs @@ -0,0 +1,497 @@ +use std::time::Duration; + +use thiserror::Error; + +use crate::api::types::{ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiHealth}; +use crate::ephemera_api::{ + ApiBlock, ApiCertificate, ApiDhtQueryRequest, ApiDhtQueryResponse, ApiDhtStoreRequest, + ApiEphemeraConfig, ApiEphemeraMessage, ApiVerifyMessageInBlock, +}; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Internal(#[from] reqwest::Error), + #[error("Unexpected response: {status} {body}")] + UnexpectedResponse { + status: reqwest::StatusCode, + body: String, + }, +} + +pub type Result = std::result::Result; + +/// Client to interact with the node over HTTP api. +pub struct Client { + pub(crate) client: reqwest::Client, + pub(crate) url: String, +} + +impl Client { + /// Create a http new client. + /// + /// # Arguments + /// * `url` - The url of the node api endpoint. + #[must_use] + pub fn new(url: String) -> Self { + let client = reqwest::Client::new(); + Self { client, url } + } + + /// Create a new client. + /// + /// # Arguments + /// * `url` - The url of the node api endpoint. + /// * `timeout_sec` - Request timeout in seconds. + /// + /// # Panics + /// If the client cannot be created. + #[must_use] + pub fn new_with_timeout(url: String, timeout_sec: u64) -> Self { + let client = reqwest::ClientBuilder::new() + .timeout(Duration::from_secs(timeout_sec)) + .build() + .unwrap(); + Self { client, url } + } + + /// Get the health of the node. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::{Client, ApiHealth}; + /// + /// #[tokio::main] + ///async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000".to_string()); + /// let health = client.health().await.unwrap(); + /// Ok(()) + /// } + /// ``` + /// + /// # Returns + /// * [`ApiHealth`] - The health of the node. + /// + /// # Errors + /// If the request fails. + pub async fn health(&self) -> Result { + self.query("ephemera/node/health").await + } + + /// Get the block by hash. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::{ApiBlock, Client}; + /// + /// #[tokio::main] + ///async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000".to_string()); + /// let block = client.get_block_by_hash("9D2LaY17rbnxfgKUbvcsJ5cB2BRHEd8fPJwsBnDHNGBX").await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Returns + /// * Option<[`ApiBlock`]> - The block. + /// + /// # Errors + /// If the request fails. + pub async fn get_block_by_hash(&self, hash: &str) -> Result> { + let url = format!("ephemera/broadcast/block/{hash}",); + self.query_optional(&url).await + } + + /// Get the block certificates by hash. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::{ApiCertificate, Client}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000".to_string()); + /// let certificates = client.get_block_certificates("9D2LaY17rbnxfgKUbvcsJ5cB2BRHEd8fPJwsBnDHNGBX").await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Arguments + /// * `hash` - The hash of the block. + /// + /// # Returns + /// * Option> - The block certificates. + /// + /// # Errors + /// If the request fails. + pub async fn get_block_certificates(&self, hash: &str) -> Result>> { + let url = format!("ephemera/broadcast/block/certificates/{hash}",); + self.query_optional(&url).await + } + + /// Get the block by height. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::{ApiBlock, Client}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let block = client.get_block_by_height(1).await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Arguments + /// * `height` - The height of the block. + /// + /// # Returns + /// * Option<[`ApiBlock`]> - The block. + /// + /// # Errors + /// If the request fails. + pub async fn get_block_by_height(&self, height: u64) -> Result> { + let url = format!("ephemera/broadcast/block/height/{height}",); + self.query_optional(&url).await + } + + /// Get the last block. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::{ApiBlock, Client}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let block = client.get_last_block().await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Returns + /// * [`ApiBlock`] - The last block. + /// + /// # Errors + /// If the request fails. + pub async fn get_last_block(&self) -> Result { + self.query("ephemera/broadcast/blocks/last").await + } + + /// Get the node configuration. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::{ApiEphemeraConfig, Client}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let config = client.get_ephemera_config().await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Returns + /// * [`ApiEphemeraConfig`] - The node configuration. + /// + /// # Errors + /// If the request fails. + pub async fn get_ephemera_config(&self) -> Result { + self.query("ephemera/node/config").await + } + + /// Submit a message to the node. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::{ApiEphemeraMessage, Client}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let message = unimplemented!("See how to create a ApiEphemeraMessage"); + /// client.submit_message(message).await?; + /// Ok(()) + /// } + /// + /// ``` + /// + /// # Arguments + /// * `message` - The message to submit. + /// + /// # Errors + /// If the request fails. + pub async fn submit_message(&self, message: ApiEphemeraMessage) -> Result<()> { + let url = format!("{}/{}", self.url, "ephemera/broadcast/submit_message"); + let response = self.client.post(&url).json(&message).send().await?; + if response.status().is_success() { + Ok(()) + } else { + Err(Error::UnexpectedResponse { + status: response.status(), + body: response.text().await?, + }) + } + } + + ///Store Key Value pair in the DHT. + /// + /// # Example + ///```no_run + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// use ephemera::ephemera_api::Client; + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let request = unimplemented!("See how to create a ApiDhtStoreRequest"); + /// client.store_in_dht(request).await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Arguments + /// * `request` - Key Value pair to store. + /// + /// # Errors + /// If the request fails. + pub async fn store_in_dht(&self, request: ApiDhtStoreRequest) -> Result<()> { + let url = format!("{}/{}", self.url, "ephemera/dht/store"); + let response = self.client.post(&url).json(&request).send().await?; + if response.status().is_success() { + Ok(()) + } else { + Err(Error::UnexpectedResponse { + status: response.status(), + body: response.text().await?, + }) + } + } + + ///Store Key Value pair in the DHT. + /// + /// # Example + ///```no_run + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// use ephemera::ephemera_api::Client; + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let key = &[1, 2, 3]; + /// let value = &[4, 5, 6]; + /// client.store_in_dht_key_value(key, value).await?; + /// Ok(()) + /// } + /// ``` + /// # Arguments + /// * `key` - Key to use to store the value. + /// * `value` - Value to store. + /// + /// # Errors + /// If the request fails. + pub async fn store_in_dht_key_value(&self, key: &[u8], value: &[u8]) -> Result<()> { + let request = ApiDhtStoreRequest::new(key, value); + self.store_in_dht(request).await + } + + /// Query the DHT for a given key. + /// + /// # Example + ///```no_run + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// use ephemera::ephemera_api::{ApiDhtQueryRequest, Client}; + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let request = ApiDhtQueryRequest::new(&[1, 2, 3]); + /// let response = client.query_dht(request).await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Arguments + /// * `request` - Key to query. + /// + /// # Returns + /// * Option<[`ApiDhtQueryResponse`]> - The value stored in the DHT for the given key. + /// + /// # Errors + /// If the request fails. + pub async fn query_dht( + &self, + request: ApiDhtQueryRequest, + ) -> Result> { + let url = format!("ephemera/dht/query/{}", request.key_encoded()); + self.query_optional(&url).await + } + + /// Query the DHT for a given key. + /// + /// # Example + /// ```no_run + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// use ephemera::ephemera_api::Client; + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let key = &[1, 2, 3]; + /// let response = client.query_dht_key(key).await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Arguments + /// * `key` - Key to query. + /// + /// # Returns + /// * Option<[`ApiDhtQueryResponse`]> - The value stored in the DHT for the given key. + /// + /// # Errors + /// If the request fails. + pub async fn query_dht_key(&self, key: &[u8]) -> Result> { + let request = ApiDhtQueryRequest::new(key); + self.query_dht(request).await + } + + /// Get broadcast group info. + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::Client; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let info = client.broadcast_info().await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Returns + /// * [`ApiBroadcastInfo`] - The broadcast group info. + /// + /// # Errors + /// If the request fails. + pub async fn broadcast_info(&self) -> Result { + self.query("ephemera/broadcast/group/info").await + } + + /// Get block broadcast info + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::Client; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let info = client.get_block_broadcast_info("hash").await?; + /// Ok(()) + /// } + ///``` + /// + /// # Arguments + /// * `hash` - Hash of the block to query. + /// + /// # Returns + /// * Some([`ApiBlockBroadcastInfo`]) - The block broadcast info. + /// * None - If the block is not found. + /// + /// # Errors + /// If the request fails. + pub async fn get_block_broadcast_info( + &self, + hash: &str, + ) -> Result> { + let url = format!("ephemera/broadcast/block/broadcast_info/{hash}",); + self.query_optional(&url).await + } + + /// Verifies if given message is in block identified by block hash + /// + /// # Example + /// ```no_run + /// use ephemera::ephemera_api::Client; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let client = Client::new("http://localhost:7000/".to_string()); + /// let is_in_block = client.verify_message_in_block("block_hash", "message_hash", 0).await?; + /// Ok(()) + /// } + /// ``` + /// + /// # Arguments + /// * `block_hash` - Hash of the block to query. + /// * `message_hash` - Hash of the message to query. + /// * `message_index` - Index of the message in the block. + /// + /// # Returns + /// * bool - True if the message is in the block, false otherwise. + /// + /// # Errors + /// If the request fails. + pub async fn verify_message_in_block( + &self, + block_hash: &str, + message_hash: &str, + message_index: usize, + ) -> Result { + let request = ApiVerifyMessageInBlock::new( + block_hash.to_string(), + message_hash.to_string(), + message_index, + ); + + let url = format!("{}/{}", self.url, "ephemera/messages/verify"); + let response = self.client.post(&url).json(&request).send().await?; + if response.status().is_success() { + let body = response.json::().await?; + Ok(body) + } else { + Err(Error::UnexpectedResponse { + status: response.status(), + body: response.text().await?, + }) + } + } + + async fn query_optional serde::Deserialize<'de>>( + &self, + path: &str, + ) -> Result> { + let url = format!("{}/{}", self.url, path); + match self.client.get(&url).send().await { + Ok(response) => { + if response.status().is_success() { + let body = response.json::().await?; + Ok(Some(body)) + } else if response.status() == reqwest::StatusCode::NOT_FOUND { + Ok(None) + } else { + return Err(Error::UnexpectedResponse { + status: response.status(), + body: response.text().await?, + }); + } + } + Err(err) => Err(err.into()), + } + } + + async fn query serde::Deserialize<'de>>(&self, path: &str) -> Result { + let url = format!("{}/{}", self.url, path); + match self.client.get(&url).send().await { + Ok(response) => { + if response.status().is_success() { + let body = response.json::().await?; + Ok(body) + } else { + Err(Error::UnexpectedResponse { + status: response.status(), + body: response.text().await?, + }) + } + } + Err(err) => Err(err.into()), + } + } +} diff --git a/ephemera/src/api/http/mod.rs b/ephemera/src/api/http/mod.rs new file mode 100644 index 0000000000..010e397aec --- /dev/null +++ b/ephemera/src/api/http/mod.rs @@ -0,0 +1,88 @@ +use actix_web::{dev::Server, http::KeepAlive, web::Data, App, HttpServer}; +use log::info; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::api::CommandExecutor; +use crate::core::builder::NodeInfo; + +pub(crate) mod client; +pub(crate) mod query; +pub(crate) mod submit; + +/// Starts the HTTP server. +pub(crate) fn init(node_info: &NodeInfo, api: CommandExecutor) -> anyhow::Result { + print_startup_messages(node_info); + + let server = HttpServer::new(move || { + App::new() + .app_data(Data::new(api.clone())) + .service(query::health) + .service(query::block_by_hash) + .service(query::block_certificates) + .service(query::block_by_height) + .service(query::block_broadcast_group) + .service(query::last_block) + .service(query::node_config) + .service(query::query_dht) + .service(query::broadcast_info) + .service(submit::submit_message) + .service(submit::store_in_dht) + .service(submit::verify_message_in_block) + .service(swagger_ui()) + }) + .keep_alive(KeepAlive::Os) + .bind((node_info.ip.as_str(), node_info.initial_config.http.port))? + .run(); + Ok(server) +} + +/// Builds the Swagger UI. +/// +/// Note that all routes you want Swagger docs for must be in the `paths` annotation. +fn swagger_ui() -> SwaggerUi { + use crate::api::types; + #[derive(OpenApi)] + #[openapi( + paths( + query::health, + query::block_by_hash, + query::block_certificates, + query::block_by_height, + query::last_block, + query::block_broadcast_group, + query::node_config, + query::query_dht, + query::broadcast_info, + submit::submit_message, + submit::store_in_dht, + submit::verify_message_in_block + ), + components(schemas( + types::ApiBlock, + types::ApiEphemeraMessage, + types::ApiCertificate, + types::ApiSignature, + types::ApiPublicKey, + types::ApiHealth, + types::HealthStatus, + types::ApiEphemeraConfig, + types::ApiDhtStoreRequest, + types::ApiDhtQueryRequest, + types::ApiDhtQueryResponse, + types::ApiBroadcastInfo, + types::ApiVerifyMessageInBlock, + )) + )] + struct ApiDoc; + SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-doc/openapi.json", ApiDoc::openapi()) +} + +/// Prints messages saying which ports HTTP is running on, and some helpful pointers +/// `OpenAPI` and `Swagger UI` endpoints. +fn print_startup_messages(info: &NodeInfo) { + let http_root = info.api_address_http(); + info!("Server running on {}", http_root); + info!("Swagger UI: {}/swagger-ui/", http_root); + info!("OpenAPI spec is at: {}/api-doc/openapi.json", http_root); +} diff --git a/ephemera/src/api/http/query.rs b/ephemera/src/api/http/query.rs new file mode 100644 index 0000000000..cb7f53288e --- /dev/null +++ b/ephemera/src/api/http/query.rs @@ -0,0 +1,182 @@ +use actix_web::{get, web, HttpResponse, Responder}; +use log::error; + +use crate::{ + api::{types::ApiHealth, types::HealthStatus::Healthy, CommandExecutor}, + ephemera_api::{ApiDhtQueryRequest, ApiDhtQueryResponse}, +}; + +#[utoipa::path( +responses( +(status = 200, description = "Endpoint to check if the server is running")), +)] +#[get("/ephemera/node/health")] +#[allow(clippy::unused_async)] +pub(crate) async fn health() -> impl Responder { + HttpResponse::Ok().json(ApiHealth { status: Healthy }) +} + +#[utoipa::path( +responses( +(status = 200, description = "Get current broadcast group"), +(status = 500, description = "Server failed to process request")), +)] +#[get("/ephemera/broadcast/group/info")] +pub(crate) async fn broadcast_info(api: web::Data) -> impl Responder { + match api.get_broadcast_info().await { + Ok(group) => HttpResponse::Ok().json(group), + Err(err) => { + error!("Failed to get current broadcast group: {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +responses( +(status = 200, description = "GET block by hash"), +(status = 404, description = "Block not found"), +(status = 500, description = "Server failed to process request")), +params(("hash", description = "Block hash")), +)] +#[get("/ephemera/broadcast/block/{hash}")] +pub(crate) async fn block_by_hash( + hash: web::Path, + api: web::Data, +) -> impl Responder { + match api.get_block_by_id(hash.into_inner()).await { + Ok(Some(block)) => HttpResponse::Ok().json(block), + Ok(_) => HttpResponse::NotFound().json("Block not found"), + Err(err) => { + error!("Failed to get block by hash: {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +responses( +(status = 200, description = "Get block signatures"), +(status = 404, description = "Certificates not found"), +(status = 500, description = "Server failed to process request")), +params(("hash", description = "Block hash")), +)] +#[get("/ephemera/broadcast/block/certificates/{hash}")] +pub(crate) async fn block_certificates( + hash: web::Path, + api: web::Data, +) -> impl Responder { + let id = hash.into_inner(); + match api.get_block_certificates(id.clone()).await { + Ok(Some(signatures)) => HttpResponse::Ok().json(signatures), + Ok(_) => HttpResponse::NotFound().json("Certificates not found"), + Err(err) => { + error!("Failed to get signatures {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +responses( +(status = 200, description = "Get block by height"), +(status = 404, description = "Block not found"), +(status = 500, description = "Server failed to process request")), +params(("height", description = "Block height")), +)] +#[get("/ephemera/broadcast/block/height/{height}")] +pub(crate) async fn block_by_height( + height: web::Path, + api: web::Data, +) -> impl Responder { + match api.get_block_by_height(height.into_inner()).await { + Ok(Some(block)) => HttpResponse::Ok().json(block), + Ok(_) => HttpResponse::NotFound().json("Block not found"), + Err(err) => { + error!("Failed to get block {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +responses( +(status = 200, description = "Get last block"), +(status = 500, description = "Server failed to process request")), +)] +//Need to use plural(blocks), otherwise overlaps with block_by_id route +#[get("/ephemera/broadcast/blocks/last")] +pub(crate) async fn last_block(api: web::Data) -> impl Responder { + match api.get_last_block().await { + Ok(block) => HttpResponse::Ok().json(block), + Err(err) => { + error!("Failed to get block {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +responses( +(status = 200, description = "Get block broadcast group"), +(status = 404, description = "Block not found"), +(status = 500, description = "Server failed to process request")), +params(("hash", description = "Block hash")), +)] +#[get("/ephemera/broadcast/block/broadcast_info/{hash}")] +pub(crate) async fn block_broadcast_group( + hash: web::Path, + api: web::Data, +) -> impl Responder { + let hash = hash.into_inner(); + match api.get_block_broadcast_info(hash).await { + Ok(Some(group)) => HttpResponse::Ok().json(group), + Ok(_) => HttpResponse::NotFound().json("Block not found"), + Err(err) => { + error!("Failed to get block broadcast group {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +responses( +(status = 200, description = "Get node config"), +(status = 500, description = "Server failed to process request")), +)] +#[get("/ephemera/node/config")] +pub(crate) async fn node_config(api: web::Data) -> impl Responder { + match api.get_node_config().await { + Ok(config) => HttpResponse::Ok().json(config), + Err(err) => { + error!("Failed to get node config {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +responses( +(status = 200, description = "Query dht"), +(status = 500, description = "Server failed to process request")), +params(("query", description = "Dht query")), +)] +#[get("/ephemera/dht/query/{key}")] +pub(crate) async fn query_dht( + api: web::Data, + key: web::Path, +) -> impl Responder { + let key = ApiDhtQueryRequest::parse_key(key.into_inner().as_str()); + + match api.query_dht(key).await { + Ok(Some((key, value))) => { + let response = ApiDhtQueryResponse::new(key, value); + HttpResponse::Ok().json(response) + } + Ok(_) => HttpResponse::NotFound().json("Not found"), + Err(err) => { + error!("Failed to query dht {err}",); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} diff --git a/ephemera/src/api/http/submit.rs b/ephemera/src/api/http/submit.rs new file mode 100644 index 0000000000..7142c2ad9b --- /dev/null +++ b/ephemera/src/api/http/submit.rs @@ -0,0 +1,88 @@ +use actix_web::{post, web, HttpResponse}; +use log::{debug, error}; + +use crate::api::types::ApiVerifyMessageInBlock; +use crate::api::{ + types::{ApiDhtStoreRequest, ApiEphemeraMessage}, + ApiError, CommandExecutor, +}; + +#[utoipa::path( +request_body = ApiEphemeraMessage, +responses( +(status = 200, description = "Send a message to an Ephemera node which will be broadcast to the network"), +(status = 500, description = "Server failed to process request")), +params(("message", description = "Message to send")) +)] +#[post("/ephemera/broadcast/submit_message")] +pub(crate) async fn submit_message( + message: web::Json, + api: web::Data, +) -> HttpResponse { + match api.send_ephemera_message(message.into_inner()).await { + Ok(_) => HttpResponse::Ok().json("Message submitted"), + Err(err) => { + if let ApiError::DuplicateMessage = err { + debug!("Message already submitted {err:?}"); + HttpResponse::BadRequest().json("Message already submitted") + } else { + error!("Error submitting message: {}", err); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } + } +} + +#[utoipa::path( +request_body = ApiDhtStoreRequest, +responses( +(status = 200, description = "Request to store a value in the DHT"), +(status = 500, description = "Server failed to process request")), +params( +("request", description = "Dht store request") +) +)] +#[post("/ephemera/dht/store")] +pub(crate) async fn store_in_dht( + request: web::Json, + api: web::Data, +) -> HttpResponse { + let request = request.into_inner(); + + let key = request.key(); + let value = request.value(); + + match api.store_in_dht(key, value).await { + Ok(_) => HttpResponse::Ok().json("Store request submitted"), + Err(err) => { + error!("Error storing in dht: {}", err); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} + +#[utoipa::path( +request_body = ApiVerifyMessageInBlock, +responses( +(status = 200, description = "Verifies if given message is in block identified by block hash.\ +Returns true if message is in block, false otherwise. False can also mean that block or message \ +does not exist in that block."), +(status = 500, description = "Server failed to process request")), +params( +("request", description = "Verify message request") +) +)] +#[post("/ephemera/messages/verify")] +pub(crate) async fn verify_message_in_block( + request: web::Json, + api: web::Data, +) -> HttpResponse { + let request = request.into_inner(); + match api.verify_message_in_block(request).await { + Ok(valid) => HttpResponse::Ok().json(valid), + Err(err) => { + error!("Error verifying message: {}", err); + HttpResponse::InternalServerError().json("Server failed to process request") + } + } +} diff --git a/ephemera/src/api/mod.rs b/ephemera/src/api/mod.rs new file mode 100644 index 0000000000..ae2b4f6de4 --- /dev/null +++ b/ephemera/src/api/mod.rs @@ -0,0 +1,309 @@ +//! # Ephemera API +//! +//! This module contains all the types and functions available as part of Ephemera public API. +//! +//! This API is also available over HTTP. + +use std::fmt::Display; + +use log::{error, trace}; +use tokio::sync::{ + mpsc::{channel, Receiver, Sender}, + oneshot, +}; + +use crate::api::types::{ + ApiBlock, ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiCertificate, ApiEphemeraConfig, + ApiEphemeraMessage, ApiError, ApiVerifyMessageInBlock, +}; + +pub(crate) mod application; +pub(crate) mod http; +pub(crate) mod types; + +/// Kademlia DHT key +pub(crate) type DhtKey = Vec; + +/// Kademlia DHT value +pub(crate) type DhtValue = Vec; + +/// Kademlia DHT key/value pair +pub(crate) type DhtKV = (DhtKey, DhtValue); + +pub(crate) type Result = std::result::Result; + +#[derive(Debug)] +pub(crate) enum ToEphemeraApiCmd { + SubmitEphemeraMessage(Box, oneshot::Sender>), + QueryBlockByHeight(u64, oneshot::Sender>>), + QueryBlockByHash(String, oneshot::Sender>>), + QueryLastBlock(oneshot::Sender>), + QueryBlockCertificates(String, oneshot::Sender>>>), + QueryDht(DhtKey, oneshot::Sender>>), + StoreInDht(DhtKey, DhtValue, oneshot::Sender>), + QueryEphemeraConfig(oneshot::Sender>), + QueryBroadcastGroup(oneshot::Sender>), + QueryBlockBroadcastInfo( + String, + oneshot::Sender>>, + ), + VerifyMessageInBlock(String, String, usize, oneshot::Sender>), +} + +impl Display for ToEphemeraApiCmd { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ToEphemeraApiCmd::SubmitEphemeraMessage(message, _) => { + write!(f, "SubmitEphemeraMessage({message})",) + } + ToEphemeraApiCmd::QueryBlockByHeight(height, _) => { + write!(f, "QueryBlockByHeight({height})",) + } + ToEphemeraApiCmd::QueryBlockByHash(hash, _) => write!(f, "QueryBlockByHash({hash})",), + ToEphemeraApiCmd::QueryLastBlock(_) => write!(f, "QueryLastBlock"), + ToEphemeraApiCmd::QueryBlockCertificates(id, _) => { + write!(f, "QueryBlockSignatures{id}") + } + ToEphemeraApiCmd::QueryDht(_, _) => { + write!(f, "QueryDht") + } + ToEphemeraApiCmd::StoreInDht(_, _, _) => { + write!(f, "StoreInDht") + } + ToEphemeraApiCmd::QueryEphemeraConfig(_) => { + write!(f, "EphemeraConfig") + } + ToEphemeraApiCmd::QueryBroadcastGroup(_) => { + write!(f, "BroadcastGroup") + } + ToEphemeraApiCmd::QueryBlockBroadcastInfo(hash, ..) => { + write!(f, "BlockBroadcastInfo({hash})") + } + ToEphemeraApiCmd::VerifyMessageInBlock(block_id, message_id, height, _) => { + write!( + f, + "VerifyMessageInBlock({block_id}, {message_id}, {height})", + ) + } + } + } +} + +pub(crate) struct ApiListener { + pub(crate) messages_rcv: Receiver, +} + +impl ApiListener { + pub(crate) fn new(messages_rcv: Receiver) -> Self { + Self { messages_rcv } + } +} + +#[derive(Clone)] +pub struct CommandExecutor { + pub(crate) commands_channel: Sender, +} + +impl CommandExecutor { + pub(crate) fn new() -> (CommandExecutor, ApiListener) { + let (commands_channel, signed_messages_rcv) = channel(100); + let api_listener = ApiListener::new(signed_messages_rcv); + let api = CommandExecutor { commands_channel }; + (api, api_listener) + } + + /// Returns block with given id if it exists + /// + /// # Arguments + /// * `block_id` - Block id + /// + /// # Returns + /// * `ApiBlock` - Block + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn get_block_by_id(&self, block_id: String) -> Result> { + trace!("get_block_by_id({:?})", block_id); + self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockByHash(block_id, tx)) + .await + } + + /// Returns block with given height if it exists + /// + /// # Arguments + /// * `height` - Block height + /// + /// # Returns + /// * `ApiBlock` - Block + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn get_block_by_height(&self, height: u64) -> Result> { + trace!("get_block_by_height({:?})", height); + self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockByHeight(height, tx)) + .await + } + + /// Returns last block. Which has maximum height and is stored in database + /// + /// # Returns + /// * `ApiBlock` - Last block + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn get_last_block(&self) -> Result { + trace!("get_last_block()"); + self.send_and_wait_response(ToEphemeraApiCmd::QueryLastBlock) + .await + } + + /// Returns signatures for given block id + /// + /// # Arguments + /// * `block_hash` - Block id + /// + /// # Returns + /// * `Vec` - Certificates + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn get_block_certificates( + &self, + block_hash: String, + ) -> Result>> { + trace!("get_block_certificates({block_hash:?})",); + self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockCertificates(block_hash, tx)) + .await + } + + /// Queries DHT for given key + /// + /// # Arguments + /// * `key` - DHT key + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + /// + /// # Returns + /// * `Some((key, value))` - If key is found + /// * `None` - If key is not found + pub async fn query_dht(&self, key: DhtKey) -> Result> { + trace!("get_dht({key:?})"); + //TODO: this needs timeout(somewhere around dht query functionality) + self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryDht(key, tx)) + .await + } + + /// Stores given key-value pair in DHT + /// + /// # Arguments + /// * `key` - DHT key + /// * `value` - DHT value + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn store_in_dht(&self, key: DhtKey, value: DhtValue) -> Result<()> { + trace!("store_in_dht({key:?}, {value:?})"); + self.send_and_wait_response(|tx| ToEphemeraApiCmd::StoreInDht(key, value, tx)) + .await + } + + /// Returns node configuration + /// + /// # Returns + /// * `ApiEphemeraConfig` - Node configuration + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn get_node_config(&self) -> Result { + trace!("get_node_config()"); + self.send_and_wait_response(ToEphemeraApiCmd::QueryEphemeraConfig) + .await + } + + /// Returns broadcast group + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + /// + /// # Return + /// * `ApiBroadcastInfo` - Broadcast group + pub async fn get_broadcast_info(&self) -> Result { + trace!("get_broadcast_group()"); + self.send_and_wait_response(ToEphemeraApiCmd::QueryBroadcastGroup) + .await + } + + /// Returns block broadcast info. + /// + /// # Arguments + /// * `block_hash` - Block hash + /// + /// # Return + /// * `ApiBlockBroadcastInfo` - Block broadcast info + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn get_block_broadcast_info( + &self, + block_hash: String, + ) -> Result> { + trace!("get_broadcast_group()"); + self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockBroadcastInfo(block_hash, tx)) + .await + } + + /// Send a message to Ephemera which should then be included in mempool and broadcast to all peers + /// + /// # Arguments + /// * `message` - Message to be sent + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn send_ephemera_message(&self, message: ApiEphemeraMessage) -> Result<()> { + trace!("send_ephemera_message({message})",); + self.send_and_wait_response(|tx| { + ToEphemeraApiCmd::SubmitEphemeraMessage(message.into(), tx) + }) + .await + } + + /// Verifies if given message is in block identified by block hash + /// Returns true if message is in block, false otherwise. False can also mean that block or message + /// does not exist. + /// + /// # Arguments + /// * `request` - Message and block hash + /// + /// # Errors + /// * `ApiError::InternalError` - If there is an internal error + pub async fn verify_message_in_block(&self, request: ApiVerifyMessageInBlock) -> Result { + trace!("verify_message_in_block({request})",); + let block_hash = request.block_hash; + let message_hash = request.message_hash; + let index = request.message_index; + self.send_and_wait_response(|tx| { + ToEphemeraApiCmd::VerifyMessageInBlock(block_hash, message_hash, index, tx) + }) + .await + } + + async fn send_and_wait_response(&self, f: F) -> Result + where + F: FnOnce(oneshot::Sender>) -> ToEphemeraApiCmd, + R: Send + 'static, + { + let (tx, rcv) = oneshot::channel(); + let cmd = f(tx); + if let Err(e) = self.commands_channel.send(cmd).await { + error!("Failed to send command to Ephemera: {e:?}",); + return Err(ApiError::Internal( + "Failed to receive response from Ephemera".to_string(), + )); + } + rcv.await.map_err(|e| { + error!("Failed to receive response from Ephemera: {e:?}",); + ApiError::Internal("Failed to receive response from Ephemera".to_string()) + })? + } +} diff --git a/ephemera/src/api/types.rs b/ephemera/src/api/types.rs new file mode 100644 index 0000000000..cfd99a300d --- /dev/null +++ b/ephemera/src/api/types.rs @@ -0,0 +1,659 @@ +//! This module contains all the types that are used in the API. +//! +//! - `ApiEphemeraMessage` +//! - `RawApiEphemeraMessage` +//! - `ApiBlock` +//! - `ApiCertificate` +//! - `Health` +//! - `ApiError` +//! - `ApiEphemeraConfig` +//! - `ApiDhtQueryRequest` +//! - `ApiDhtQueryResponse` +//! - `ApiDhtStoreRequest` +//! - `ApiBroadcastInfo` +//! - `ApiBlockBroadcastInfo` +//! - `ApiVerifyMessageInBlock` + +use std::collections::HashSet; +use std::fmt::Display; + +use array_bytes::{bytes2hex, hex2bytes}; +use log::error; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use utoipa::ToSchema; + +use crate::peer::{PeerId, ToPeerId}; +use crate::utilities::codec::{Codec, DecodingError, EncodingError, EphemeraCodec}; +use crate::{ + block::types::{block::Block, block::BlockHeader, message::EphemeraMessage}, + codec::{Decode, Encode}, + crypto::{Keypair, PublicKey}, + ephemera_api, + utilities::{ + crypto::{Certificate, Signature}, + time::EphemeraTime, + }, +}; + +#[derive(Error, Debug)] +pub enum ApiError { + #[error("Application rejected ephemera message")] + ApplicationRejectedMessage, + #[error("Duplicate message")] + DuplicateMessage, + #[error("Invalid hash: {0}")] + InvalidHash(String), + #[error("ApplicationError: {0}")] + Application(#[from] ephemera_api::ApplicationError), + #[error("Internal error: {0}")] + Internal(String), +} + +/// # Ephemera message. +/// +/// A message submitted to an Ephemera node will be gossiped to other nodes. +/// And will be eventually included in a Ephemera block. +/// +/// It needs to signed by the sender. The signature is included in the certificate. +/// +/// The fields of the message what are signed: +/// - timestamp +/// - label +/// - data +/// +/// Currently it's up provided [`ephemera_api::application::Application`] to verify the signature. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiEphemeraMessage { + /// The timestamp of the message. + pub timestamp: u64, + /// The label of the message. It can be used to identify the type of a message for example. + pub label: String, + /// The data of the message. It is application specific. + pub data: Vec, + /// The certificate of the message. All messages are required to be signed. + pub certificate: ApiCertificate, +} + +impl ApiEphemeraMessage { + #[must_use] + pub fn new(raw_message: RawApiEphemeraMessage, certificate: ApiCertificate) -> Self { + Self { + timestamp: raw_message.timestamp, + label: raw_message.label, + data: raw_message.data, + certificate, + } + } + + /// Generates the message hash. + /// + /// # Errors + /// - If internal hash function fails. + pub fn hash(&self) -> anyhow::Result { + let em = EphemeraMessage::from(self.clone()); + let hash = em.hash_with_default_hasher()?.to_string(); + Ok(hash) + } +} + +/// `RawApiEphemeraMessage` contains the fields of the `ApiEphemeraMessage` that are signed. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct RawApiEphemeraMessage { + /// The timestamp of the message. It's initialized when the message is created. + /// It uses UTC time. + pub timestamp: u64, + /// The label of the message. It can be used to identify the type of a message without decoding full data. + pub label: String, + /// The data of the message. It is application specific. + pub data: Vec, +} + +impl RawApiEphemeraMessage { + #[must_use] + pub fn new(label: String, data: Vec) -> Self { + Self { + timestamp: EphemeraTime::now(), + label, + data, + } + } + + /// Signs the message with the given keypair. + /// + /// # Signing example + /// + /// ``` + /// use ephemera::codec::Encode; + /// use ephemera::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair}; + /// use ephemera::ephemera_api::{ApiEphemeraMessage, RawApiEphemeraMessage}; + /// + /// let keypair = Keypair::generate(None); + /// let raw_message = RawApiEphemeraMessage::new("test".to_string(), vec![]); + /// + /// let signed_message:ApiEphemeraMessage = raw_message.sign(&keypair).unwrap(); + /// + /// assert_eq!(signed_message.certificate.public_key, keypair.public_key().into()); + /// + /// let bytes = raw_message.encode().unwrap(); + /// assert!(keypair.public_key().verify(&bytes, &signed_message.certificate.signature.into())); + /// ``` + /// + /// # Errors + /// - If the message can't be encoded. + /// - If the message can't be signed. + pub fn sign(&self, keypair: &Keypair) -> anyhow::Result { + let certificate = Certificate::prepare(keypair, &self)?; + let message = ApiEphemeraMessage::new(self.clone(), certificate.into()); + Ok(message) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ApiBlockHeader { + /// The timestamp of the block. It's initialized when the block is created. + /// It uses UTC time. + pub timestamp: u64, + /// The PeerId of the block producer instance. + pub creator: PeerId, + /// The height of the block. + pub height: u64, + /// The hash of the current block. + pub hash: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiBlock { + pub header: ApiBlockHeader, + pub messages: Vec, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ApiRawBlock { + pub(crate) header: ApiBlockHeader, + pub(crate) messages: Vec, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiCertificate { + pub signature: ApiSignature, + pub public_key: ApiPublicKey, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiSignature(pub(crate) Signature); + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiPublicKey(pub(crate) PublicKey); + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiEphemeraConfig { + /// The address of the node. It's the address what Ephemera instance uses to communicate with other nodes. + pub protocol_address: String, + /// The HTTP API address of the node. + pub api_address: String, + /// The WebSocket address of the node. + pub websocket_address: String, + /// Node's public key. + /// + /// # Converting to string and back example + /// ``` + /// use ephemera::crypto::{EphemeraKeypair, Keypair, PublicKey}; + /// + /// let keypair = Keypair::generate(None); + /// let public_key = keypair.public_key().to_string(); + /// + /// let from_str = public_key.parse::().unwrap(); + /// + /// assert_eq!(keypair.public_key(), from_str); + /// ``` + pub public_key: String, + /// True if the node is a block producer. It's a configuration option. + pub block_producer: bool, + /// The interval of block creation in seconds. It's a configuration option. + pub block_creation_interval_sec: u64, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiDhtQueryRequest { + /// The key to query for in hex format. + key: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiDhtQueryResponse { + /// The key that was queried for in hex format. + key: String, + /// The value that was stored under the queried key in hex format. + value: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub enum HealthStatus { + Healthy, + Unhealthy, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiHealth { + pub(crate) status: HealthStatus, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiDhtStoreRequest { + /// The key to store the value under in hex format. + key: String, + /// The value to store in hex format. + value: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiBroadcastInfo { + /// The PeerId of the local node. + pub local_peer_id: PeerId, + /// The list of the current members of the network. + pub current_members: HashSet, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiBlockBroadcastInfo { + pub local_peer_id: PeerId, + pub broadcast_group: Vec, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct ApiVerifyMessageInBlock { + pub block_hash: String, + pub message_hash: String, + pub message_index: usize, +} + +impl ApiVerifyMessageInBlock { + #[must_use] + pub fn new(block_hash: String, message_hash: String, message_index: usize) -> Self { + Self { + block_hash, + message_hash, + message_index, + } + } +} + +impl Display for ApiVerifyMessageInBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ block_hash: {}, message_hash: {} }}", + self.block_hash, self.message_hash + ) + } +} + +impl ApiBlockBroadcastInfo { + pub(crate) fn new(local_peer_id: PeerId, broadcast_group: Vec) -> Self { + Self { + local_peer_id, + broadcast_group, + } + } +} + +impl ApiBroadcastInfo { + pub(crate) fn new(current_members: HashSet, local_peer_id: PeerId) -> Self { + Self { + local_peer_id, + current_members, + } + } +} + +impl Display for ApiBroadcastInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let current_members = self.current_members.iter().map(ToString::to_string); + write!( + f, + "{{ local_peer_id: {}, current_members: {current_members:?} }}", + self.local_peer_id, + ) + } +} + +impl Display for ApiEphemeraMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ApiEphemeraMessage(timestamp: {}, label: {})", + self.timestamp, self.label, + ) + } +} + +impl From for RawApiEphemeraMessage { + fn from(message: ApiEphemeraMessage) -> Self { + RawApiEphemeraMessage { + timestamp: message.timestamp, + label: message.label, + data: message.data, + } + } +} + +impl From for EphemeraMessage { + fn from(message: ApiEphemeraMessage) -> Self { + Self { + timestamp: message.timestamp, + label: message.label, + data: message.data, + certificate: message.certificate.into(), + } + } +} + +impl Decode for RawApiEphemeraMessage { + type Output = Self; + + fn decode(bytes: &[u8]) -> Result { + Codec::decode(bytes) + } +} + +impl Encode for RawApiEphemeraMessage { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(self) + } +} + +impl Encode for &RawApiEphemeraMessage { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(self) + } +} + +impl Display for ApiBlockHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ApiBlockHeader(timestamp: {}, creator: {}, height: {}, hash: {})", + self.timestamp, self.creator, self.height, self.hash, + ) + } +} + +impl ApiBlock { + #[must_use] + pub fn as_raw_block(&self) -> ApiRawBlock { + ApiRawBlock { + header: self.header.clone(), + messages: self.messages.clone(), + } + } + + #[must_use] + pub fn message_count(&self) -> usize { + self.messages.len() + } + + #[must_use] + pub fn hash(&self) -> String { + self.header.hash.clone() + } + + /// # Errors + /// - If the block is invalid. + /// - If the block's certificate is invalid. + /// - If the block's certificate is not signed by the block's creator. + pub fn verify(&self, certificate: &ApiCertificate) -> Result { + let block: Block = self.clone().try_into()?; + let valid = block.verify(&(certificate.clone()).into()).map_err(|e| { + error!("Failed to verify block: {}", e); + ApiError::Internal("Failed to verify block certificate".to_string()) + })?; + Ok(valid) + } +} + +impl Display for ApiBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ApiBlock(header: {}, message_count: {})", + self.header, + self.message_count() + ) + } +} + +impl ApiRawBlock { + pub fn new(header: ApiBlockHeader, messages: Vec) -> Self { + Self { header, messages } + } +} + +impl ApiCertificate { + /// # Errors + /// - `EncodingError` if the message cannot be encoded. + /// - `KeyPairError` if the message cannot be signed. + pub fn prepare(key_pair: &Keypair, data: &D) -> anyhow::Result { + Certificate::prepare(key_pair, data).map(Into::into) + } + + /// # Errors + /// -`EncodingError` if the message cannot be encoded. + pub fn verify(&self, data: &D) -> anyhow::Result { + let certificate: Certificate = (self.clone()).into(); + Certificate::verify(&certificate, data) + } +} + +impl From for ApiEphemeraMessage { + fn from(ephemera_message: EphemeraMessage) -> Self { + Self { + timestamp: ephemera_message.timestamp, + label: ephemera_message.label, + data: ephemera_message.data, + certificate: ApiCertificate { + signature: ephemera_message.certificate.signature.into(), + public_key: ephemera_message.certificate.public_key.into(), + }, + } + } +} + +impl From for ApiCertificate { + fn from(signature: Certificate) -> Self { + Self { + signature: signature.signature.into(), + public_key: signature.public_key.into(), + } + } +} + +impl From for Certificate { + fn from(value: ApiCertificate) -> Self { + Certificate { + signature: value.signature.into(), + public_key: value.public_key.into(), + } + } +} + +impl From<&Block> for &ApiBlock { + fn from(block: &Block) -> Self { + let api_block: ApiBlock = block.clone().into(); + Box::leak(Box::new(api_block)) + } +} + +impl From for ApiSignature { + fn from(signature: Signature) -> Self { + Self(signature) + } +} + +impl From for Signature { + fn from(signature: ApiSignature) -> Self { + signature.0 + } +} + +impl ApiPublicKey { + pub fn peer_id(&self) -> String { + self.0.peer_id().to_string() + } +} + +impl From for ApiPublicKey { + fn from(public_key: PublicKey) -> Self { + Self(public_key) + } +} + +impl From for PublicKey { + fn from(public_key: ApiPublicKey) -> Self { + public_key.0 + } +} + +impl From for ApiBlock { + fn from(block: Block) -> Self { + Self { + header: ApiBlockHeader { + timestamp: block.header.timestamp, + creator: block.header.creator, + height: block.header.height, + hash: block.header.hash.to_string(), + }, + messages: block.messages.into_iter().map(Into::into).collect(), + } + } +} + +impl TryFrom for Block { + type Error = ApiError; + + fn try_from(api_block: ApiBlock) -> Result { + let messages: Vec = api_block + .messages + .into_iter() + .map(Into::into) + .collect::>(); + Ok(Self { + header: BlockHeader { + timestamp: api_block.header.timestamp, + creator: api_block.header.creator, + height: api_block.header.height, + hash: api_block.header.hash.parse().map_err(|e| { + error!("Failed to parse block hash: {}", e); + ApiError::Internal("Failed to parse block hash".to_string()) + })?, + }, + messages, + }) + } +} + +impl ApiDhtStoreRequest { + #[must_use] + pub fn new(key: &[u8], value: &[u8]) -> Self { + let key = bytes2hex("0x", key); + let value = bytes2hex("0x", value); + Self { key, value } + } + + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn key(&self) -> Vec { + //We can unwrap here because the key is always valid. + hex2bytes(&self.key).unwrap() + } + + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn value(&self) -> Vec { + //We can unwrap here because the value is always valid. + hex2bytes(&self.value).unwrap() + } +} + +impl ApiDhtQueryRequest { + #[must_use] + pub fn new(key: &[u8]) -> Self { + let key = bytes2hex("0x", key); + Self { key } + } + + #[must_use] + pub fn key_encoded(&self) -> String { + self.key.clone() + } + + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn key(&self) -> Vec { + //We can unwrap here because the value is always valid. + hex2bytes(&self.key).unwrap() + } + + pub(crate) fn parse_key(key: &str) -> Vec { + hex2bytes(key).unwrap() + } +} + +impl ApiDhtQueryResponse { + pub(crate) fn new(key: Vec, value: Vec) -> Self { + let key = bytes2hex("0x", key); + let value = bytes2hex("0x", value); + Self { key, value } + } + + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn key(&self) -> Vec { + //We can unwrap here because the key is always valid. + hex2bytes(&self.key).unwrap() + } + + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn value(&self) -> Vec { + //We can unwrap here because the value is always valid. + hex2bytes(&self.value).unwrap() + } +} + +#[cfg(test)] +mod test { + use crate::crypto::EphemeraKeypair; + use crate::crypto::Keypair; + + use super::*; + + #[test] + fn test_message_sign_ok() { + let message_signing_keypair = Keypair::generate(None); + + let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]); + let signed_message = message + .sign(&message_signing_keypair) + .expect("Failed to sign message"); + + let certificate = signed_message.certificate; + + assert!(certificate.verify(&message).unwrap()); + } + + #[test] + fn test_message_sign_fail() { + let message_signing_keypair = Keypair::generate(None); + + let message = RawApiEphemeraMessage::new("test1".to_string(), vec![1, 2, 3]); + let signed_message = message + .sign(&message_signing_keypair) + .expect("Failed to sign message"); + + let certificate = signed_message.certificate; + + let modified_message = RawApiEphemeraMessage::new("test2".to_string(), vec![1, 2, 3]); + assert!(!certificate.verify(&modified_message).unwrap()); + } +} diff --git a/ephemera/src/block/builder.rs b/ephemera/src/block/builder.rs new file mode 100644 index 0000000000..e74530ae81 --- /dev/null +++ b/ephemera/src/block/builder.rs @@ -0,0 +1,74 @@ +use std::collections::HashSet; +use std::{sync::Arc, time::Duration}; + +use log::{debug, info}; + +use crate::block::manager::State; +use crate::peer::ToPeerId; +use crate::{ + block::{ + manager::{BlockChainState, BlockManager}, + message_pool::MessagePool, + producer::BlockProducer, + types::block::Block, + }, + broadcast::signing::BlockSigner, + config::BlockManagerConfiguration, + crypto::Keypair, + storage::EphemeraDatabase, +}; + +pub(crate) struct BlockManagerBuilder { + config: BlockManagerConfiguration, + block_producer: BlockProducer, + keypair: Arc, +} + +impl BlockManagerBuilder { + pub(crate) fn new(config: BlockManagerConfiguration, keypair: Arc) -> Self { + let block_producer = BlockProducer::new(keypair.peer_id()); + Self { + config, + block_producer, + keypair, + } + } + + pub(crate) fn build( + self, + storage: &mut D, + ) -> anyhow::Result { + let mut most_recent_block = storage.get_last_block()?; + if most_recent_block.is_none() { + //Although Ephemera is not a blockchain(chain of historically dependent blocks), + //it's helpful to have some sort of notion of progress in time. So we use the concept of height. + //The genesis block helps to define the start of it. + + info!("No last block found in database. Creating genesis block."); + + let genesis_block = Block::new_genesis_block(self.block_producer.peer_id); + storage.store_block(&genesis_block, HashSet::new(), HashSet::new())?; + most_recent_block = Some(genesis_block); + } + + let last_created_block = most_recent_block.expect("Block should be present"); + debug!("Most recent block: {:?}", last_created_block); + + let block_signer = BlockSigner::new(self.keypair.clone()); + let message_pool = MessagePool::new(); + let block_chain_state = BlockChainState::new(last_created_block); + let block_creation_interval = + tokio::time::interval(Duration::from_secs(self.config.creation_interval_sec)); + + Ok(BlockManager { + config: self.config, + block_producer: self.block_producer, + block_signer, + message_pool, + block_chain_state, + state: State::Paused, + backoff: None, + block_creation_interval, + }) + } +} diff --git a/ephemera/src/block/manager.rs b/ephemera/src/block/manager.rs new file mode 100644 index 0000000000..481cd92418 --- /dev/null +++ b/ephemera/src/block/manager.rs @@ -0,0 +1,688 @@ +use std::collections::HashSet; +use std::future::Future; +use std::task::Poll; +use std::time::Duration; +use std::{ + num::NonZeroUsize, + pin::Pin, + task, + task::Poll::{Pending, Ready}, +}; + +use anyhow::anyhow; +use futures::Stream; +use futures_util::FutureExt; +use log::{debug, error, info, trace}; +use lru::LruCache; +use thiserror::Error; +use tokio::time; +use tokio::time::{Instant, Interval}; + +use crate::network::PeerId; +use crate::peer::ToPeerId; +use crate::{ + api::application::RemoveMessages, + block::{ + message_pool::MessagePool, + producer::BlockProducer, + types::{block::Block, message::EphemeraMessage}, + }, + broadcast::signing::BlockSigner, + config::BlockManagerConfiguration, + utilities::{crypto::Certificate, hash::Hash}, +}; + +pub(crate) type Result = std::result::Result; + +#[derive(Error, Debug)] +pub(crate) enum BlockManagerError { + #[error("Message is already in pool: {0}")] + DuplicateMessage(String), + //Just a placeholder for now + #[error("BlockManagerError: {0}")] + BlockManager(#[from] anyhow::Error), +} + +/// It helps to use atomic state management for new blocks. +pub(crate) struct BlockChainState { + pub(crate) last_blocks: LruCache, + /// Last block that we created. + /// It's not Option because we always have genesis block + last_produced_block: Option, + /// Last block that we accepted + /// It's not Option because we always have genesis block + last_committed_block: Block, +} + +impl BlockChainState { + pub(crate) fn new(last_committed_block: Block) -> Self { + Self { + //1000 is just a "big enough". + last_blocks: LruCache::new(NonZeroUsize::new(1000).unwrap()), + last_produced_block: None, + last_committed_block, + } + } + + fn mark_last_produced_block_as_committed(&mut self) { + self.last_committed_block = self + .last_produced_block + .take() + .expect("Block should be present"); + } + + fn is_last_produced_block(&self, hash: Hash) -> bool { + match self.last_produced_block.as_ref() { + Some(block) => block.get_hash() == hash, + None => false, + } + } + + fn is_last_produced_block_is_pending(&self) -> bool { + self.last_produced_block.is_some() + } + + fn next_block_height(&self) -> u64 { + self.last_committed_block.get_height() + 1 + } + + fn remove_last_produced_block(&mut self) -> Block { + self.last_produced_block + .take() + .expect("Block should be present") + } +} + +pub(crate) enum State { + Paused, + Running, +} + +#[derive(Debug)] +pub(crate) struct BackOffInterval { + /// Maximum number of attempts before this backoff expires. + maximum_times: u32, + /// Number of attempts that have been made so far. + nr_of_attempts: u32, + /// Backoff rate. Previous delay is multiplied by this rate to get next delay. + backoff_rate: u32, + /// Delay between before next attempt. + delay: Interval, +} + +impl BackOffInterval { + fn new(maximum_times: u32, backoff_rate: u32, initial_wait: Duration) -> Self { + let delay = time::interval_at(Instant::now() + initial_wait, initial_wait); + Self { + maximum_times, + nr_of_attempts: 0, + backoff_rate, + delay, + } + } + + fn is_expired(&self) -> bool { + self.nr_of_attempts >= self.maximum_times + } +} + +impl Future for BackOffInterval { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { + if self.nr_of_attempts >= self.maximum_times { + debug!("Backoff expired after {} attempts", self.nr_of_attempts); + return Pending; + } + + match Pin::new(&mut self.delay).poll_tick(cx) { + Ready(_) => { + self.nr_of_attempts += 1; + let next_tick = Instant::now() + + self.delay.period() * self.backoff_rate.pow(self.nr_of_attempts); + debug!("Backoff attempt: {}", self.nr_of_attempts); + self.delay = time::interval_at(next_tick, self.delay.period()); + Ready(()) + } + Pending => Pending, + } + } +} + +pub(crate) struct BlockManager { + pub(crate) config: BlockManagerConfiguration, + /// Block producer. Simple helper that creates blocks + pub(crate) block_producer: BlockProducer, + /// Message pool. Contains all messages that we received from the network and not included in any(committed) block yet. + pub(crate) message_pool: MessagePool, + /// Delay between block creation attempts. + pub(crate) block_creation_interval: Interval, + /// Backoff between block creation attempts. When `last_produced_block` is not committed during + /// certain time window, and normal delay is not passed yet, we use backoff delay to try again. + pub(crate) backoff: Option, + /// Signs and verifies blocks + pub(crate) block_signer: BlockSigner, + /// State management for new blocks + pub(crate) block_chain_state: BlockChainState, + /// Current state of the block manager + pub(crate) state: State, +} + +impl BlockManager { + pub(crate) fn on_new_message(&mut self, msg: EphemeraMessage) -> Result<()> { + trace!("Message received: {:?}", msg); + + let message_hash = msg.hash_with_default_hasher()?; + if self.message_pool.contains(&message_hash) { + return Err(BlockManagerError::DuplicateMessage( + message_hash.to_string(), + )); + } + + self.message_pool.add_message(msg)?; + Ok(()) + } + + pub(crate) fn on_block( + &mut self, + sender: &PeerId, + block: &Block, + certificate: &Certificate, + ) -> Result<()> { + let hash = block.hash_with_default_hasher()?; + + trace!( + "Received block: {:?} from peer {sender:?}", + block.get_hash() + ); + + //Reject blocks with invalid hash + if block.header.hash != hash { + return Err(anyhow!("Block hash is invalid: {} != {hash}", block.header.hash).into()); + } + + //Block signer should be also its sender + let signer_peer_id = certificate.public_key.peer_id(); + if *sender != signer_peer_id { + return Err(anyhow!( + "Block signer is not the block sender: {sender:?} != {signer_peer_id:?}", + ) + .into()); + } + + //Verify that block signature is valid + if self.block_signer.verify_block(block, certificate).is_err() { + return Err(anyhow!("Block signature is invalid: {hash}").into()); + } + + self.block_chain_state.last_blocks.put(hash, block.clone()); + Ok(()) + } + + pub(crate) fn sign_block(&mut self, block: &Block) -> Result { + let hash = block.hash_with_default_hasher()?; + + trace!("Signing block: {block}"); + + let certificate = self.block_signer.sign_block(block, &hash)?; + + trace!("Block certificate: {certificate:?}",); + + Ok(certificate) + } + + pub(crate) fn on_application_rejected_block( + &mut self, + messages_to_remove: RemoveMessages, + ) -> Result<()> { + debug!("Application rejected last created block"); + + let last_produced_block = self.block_chain_state.remove_last_produced_block(); + match messages_to_remove { + RemoveMessages::All => { + let messages = last_produced_block + .messages + .into_iter() + .map(Into::into) + .collect::>(); + + debug!("Removing block messages from pool: all: {messages:?}",); + self.message_pool.remove_messages(&messages)?; + } + RemoveMessages::Selected(messages) => { + debug!("Removing block messages from pool: selected: {messages:?}",); + let messages = messages.into_iter().map(Into::into).collect::>(); + self.message_pool.remove_messages(messages.as_slice())?; + } + }; + Ok(()) + } + + /// After a block gets committed, clear up mempool from its messages + pub(crate) fn on_block_committed(&mut self, block: &Block) -> Result<()> { + info!("Block committed: {}", block); + + let hash = &block.header.hash; + + if !self.block_chain_state.is_last_produced_block(*hash) { + let last_produced_block = self + .block_chain_state + .last_produced_block + .as_ref() + .expect("Last produced block should be present"); + log::error!( + "Received unexpected committed block: {hash}, was expecting: {}", + last_produced_block.get_hash() + ); + panic!("Received committed block which isn't last produced block, this is a bug!"); + } + + match self.message_pool.remove_messages(&block.messages) { + Ok(_) => { + self.block_chain_state + .mark_last_produced_block_as_committed(); + } + Err(e) => { + return Err(anyhow!("Failed to remove messages from mempool: {}", e).into()); + } + } + Ok(()) + } + + pub(crate) fn get_block_by_hash(&mut self, block_id: &Hash) -> Option { + self.block_chain_state.last_blocks.get(block_id).cloned() + } + + pub(crate) fn get_block_certificates(&mut self, hash: &Hash) -> Option<&HashSet> { + self.block_signer.get_block_certificates(hash) + } + + pub(crate) fn stop(&mut self) { + debug!("Stopping block creation"); + self.state = State::Paused; + self.backoff = None; + } + + pub(crate) fn start(&mut self) { + if !self.config.producer { + return; + } + if let State::Running = self.state { + return; + } + debug!("Starting block creation"); + self.state = State::Running; + self.block_creation_interval = + tokio::time::interval(Duration::from_secs(self.config.creation_interval_sec)); + } +} + +//Produces blocks at a predefined interval. +//If blocks will be actually broadcast depends on the application. +impl Stream for BlockManager { + type Item = (Block, Certificate); + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context) -> Poll> { + //Optionally it is possible to turn off block production and let the node behave just as voter. + //For example for testing purposes. + if !self.config.producer { + return Pending; + } + + //It is dynamically turned off when node is not part of most recent broadcast group. + if let State::Paused = self.state { + return Pending; + } + + let is_previous_pending = self.block_chain_state.is_last_produced_block_is_pending(); + if !is_previous_pending { + self.backoff = None; + } + + if self.block_creation_interval.poll_tick(cx).is_pending() { + if let Some(mut backoff) = self.backoff.take() { + if backoff.is_expired() { + return Pending; + } + if backoff.poll_unpin(cx).is_pending() { + self.backoff = Some(backoff); + return Pending; + } + self.backoff = Some(backoff); + } else { + return Pending; + } + } else { + self.backoff = None; + } + + //If backoff is expired and we still don't have previous block committed + let repeat_previous = is_previous_pending && self.config.repeat_last_block_messages; + + let pending_messages = if repeat_previous { + let block = self + .block_chain_state + .last_produced_block + .clone() + .expect("Block should be present"); + + //Use only previous block messages but create new block with new timestamp. + debug!("Producing block with previous messages"); + block.messages + } else { + debug!("Producing block with new messages"); + self.message_pool.get_messages() + }; + + let new_height = self.block_chain_state.next_block_height(); + let created_block = self + .block_producer + .create_block(new_height, pending_messages); + + if let Ok(block) = created_block { + info!("Created block: {}", block); + + let hash = block.get_hash(); + self.block_chain_state.last_produced_block = Some(block.clone()); + self.block_chain_state.last_blocks.put(hash, block.clone()); + + let certificate = self + .block_signer + .sign_block(&block, &hash) + .expect("Failed to sign block"); + + if self.backoff.is_none() { + let backoff = BackOffInterval::new(100, 2, Duration::from_secs(10)); + self.backoff = Some(backoff); + } + + Ready(Some((block, certificate))) + } else { + error!("Error producing block: {:?}", created_block); + Pending + } + } +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + use std::time::Duration; + + use assert_matches::assert_matches; + use futures_util::StreamExt; + + use crate::crypto::{EphemeraKeypair, Keypair}; + use crate::ephemera_api::RawApiEphemeraMessage; + + use super::*; + + #[tokio::test] + async fn test_add_message() { + let (mut manager, _) = block_manager_with_defaults(); + + let signed_message = message("test"); + let hash = signed_message.hash_with_default_hasher().unwrap(); + + manager.on_new_message(signed_message).unwrap(); + + assert!(manager.message_pool.contains(&hash)); + } + + #[tokio::test] + async fn test_add_duplicate_message() { + let (mut manager, _) = block_manager_with_defaults(); + + let signed_message = message("test"); + + manager.on_new_message(signed_message.clone()).unwrap(); + + assert_matches!( + manager.on_new_message(signed_message), + Err(BlockManagerError::DuplicateMessage(_)) + ); + } + + #[tokio::test] + async fn test_accept_valid_block() { + let (mut manager, peer_id) = block_manager_with_defaults(); + + let block = block(); + let certificate = manager.sign_block(&block).unwrap(); + + let result = manager.on_block(&peer_id, &block, &certificate); + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_reject_invalid_sender() { + let (mut manager, _) = block_manager_with_defaults(); + + let block = block(); + let certificate = manager.sign_block(&block).unwrap(); + + let invalid_peer_id = PeerId::from_public_key(&Keypair::generate(None).public_key()); + let result = manager.on_block(&invalid_peer_id, &block, &certificate); + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_reject_invalid_hash() { + let (mut manager, peer_id) = block_manager_with_defaults(); + + let mut block = block(); + let certificate = manager.sign_block(&block).unwrap(); + + block.header.hash = Hash::new([0; 32]); + let result = manager.on_block(&peer_id, &block, &certificate); + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_reject_invalid_signature() { + let (mut manager, peer_id) = block_manager_with_defaults(); + + let correct_block = block(); + let fake_block = block(); + + let fake_certificate = manager.sign_block(&fake_block).unwrap(); + let correct_certificate = manager.sign_block(&correct_block).unwrap(); + + let result = manager.on_block(&peer_id, &correct_block, &fake_certificate); + assert!(result.is_err()); + + let result = manager.on_block(&peer_id, &fake_block, &correct_certificate); + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_next_block_empty() { + let (mut manager, _) = block_manager_with_defaults(); + + let (block, _) = manager.next().await.unwrap(); + assert_eq!(block.header.height, 1); + assert!(block.messages.is_empty()); + } + + #[tokio::test] + async fn test_next_block_with_message() { + let (mut manager, _) = block_manager_with_defaults(); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + match manager.next().await { + Some((block, _)) => { + assert_eq!(block.header.height, 1); + assert_eq!(block.messages.len(), 1); + } + None => { + panic!("No block produced"); + } + } + } + + #[tokio::test] + async fn test_next_block_previous_not_committed_repeat() { + let (mut manager, _) = block_manager_with_defaults(); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + let (block1, _) = manager.next().await.unwrap(); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + let (block2, _) = manager.next().await.unwrap(); + + assert_eq!(block1.messages.len(), block2.messages.len()); + assert_eq!(block1.header.height, block2.header.height); + } + + #[tokio::test] + async fn test_next_block_previous_not_committed_repeat_false() { + let config = BlockManagerConfiguration::new(true, 0, false); + let (mut manager, _) = block_manager_with_config(config); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + let (block1, _) = manager.next().await.unwrap(); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + let (block2, _) = manager.next().await.unwrap(); + + assert_eq!(block1.messages.len(), 1); + assert_eq!(block2.messages.len(), 2); + //We create new block but don't leave gap + assert_eq!(block1.header.height, block2.header.height); + } + + #[tokio::test] + async fn test_on_committed_with_correct_pending_block() { + let (mut manager, _) = block_manager_with_defaults(); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + let (block, _) = manager.next().await.unwrap(); + + let result = manager.on_block_committed(&block); + + assert!(result.is_ok()); + assert!(manager.message_pool.get_messages().is_empty()); + } + + #[tokio::test] + #[should_panic] + async fn test_on_committed_with_invalid_pending_block() { + let (mut manager, _) = block_manager_with_defaults(); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + manager.next().await.unwrap(); + + //Create invalid block + let wrong_block = block(); + + //This shouldn't remove messages from the pool + manager.on_block_committed(&wrong_block).unwrap(); + } + + #[tokio::test] + async fn application_rejected_messages_all() { + let (mut manager, _) = block_manager_with_defaults(); + + //Add messages to pool + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + let signed_message = message("test"); + manager.on_new_message(signed_message).unwrap(); + + //Produce new block + manager.next().await.unwrap(); + + //Application Rejects the block with ALL messages + manager + .on_application_rejected_block(RemoveMessages::All) + .unwrap(); + + assert!(manager.message_pool.get_messages().is_empty()); + } + + #[tokio::test] + async fn application_rejected_messages_selected() { + let (mut manager, _) = block_manager_with_defaults(); + + //Add messages to pool + let signed_message1 = message("test"); + manager.on_new_message(signed_message1.clone()).unwrap(); + + let signed_message2 = message("test"); + manager.on_new_message(signed_message2.clone()).unwrap(); + + //Produce new block + manager.next().await.unwrap(); + + //Application Rejects the block with ALL messages + manager + .on_application_rejected_block(RemoveMessages::Selected(vec![signed_message2.into()])) + .unwrap(); + + assert_eq!(manager.message_pool.get_messages().len(), 1); + let message = manager + .message_pool + .get_messages() + .into_iter() + .next() + .unwrap(); + assert_eq!(message, signed_message1); + } + + fn block_manager_with_defaults() -> (BlockManager, PeerId) { + let config = BlockManagerConfiguration::new(true, 0, true); + block_manager_with_config(config) + } + + fn block_manager_with_config(config: BlockManagerConfiguration) -> (BlockManager, PeerId) { + let keypair: Arc = Keypair::generate(None).into(); + let peer_id = keypair.public_key().peer_id(); + let genesis_block = Block::new_genesis_block(peer_id); + let block_chain_state = BlockChainState::new(genesis_block); + ( + BlockManager { + config, + block_producer: BlockProducer::new(peer_id), + message_pool: MessagePool::new(), + block_creation_interval: tokio::time::interval(Duration::from_millis(1)), + backoff: None, + block_signer: BlockSigner::new(keypair), + block_chain_state, + state: State::Running, + }, + peer_id, + ) + } + + fn block() -> Block { + let keypair: Arc = Keypair::generate(None).into(); + let peer_id = keypair.public_key().peer_id(); + let mut producer = BlockProducer::new(peer_id); + producer.create_block(1, vec![]).unwrap() + } + + fn message(label: &str) -> EphemeraMessage { + let message1 = RawApiEphemeraMessage::new(label.into(), vec![1, 2, 3]); + let keypair = Keypair::generate(None); + let signed_message1 = message1.sign(&keypair).expect("Failed to sign message"); + signed_message1.into() + } +} diff --git a/ephemera/src/block/message_pool.rs b/ephemera/src/block/message_pool.rs new file mode 100644 index 0000000000..bf1e062be2 --- /dev/null +++ b/ephemera/src/block/message_pool.rs @@ -0,0 +1,87 @@ +//! Message pool for Ephemera messages +//! +//! It stores pending Ephemera messages which will be added to a future block. +//! It doesn't have any other logic than just storing messages. +//! +//! It's up to the user provided [`crate::ephemera_api::Application::check_tx`] to decide which messages to include. + +use std::collections::HashMap; + +use log::{trace, warn}; + +use crate::block::types::message::EphemeraMessage; +use crate::utilities::hash::Hash; + +pub(crate) struct MessagePool { + pending_messages: HashMap, +} + +impl MessagePool { + pub(super) fn new() -> Self { + Self { + pending_messages: HashMap::default(), + } + } + + pub(crate) fn contains(&self, hash: &Hash) -> bool { + self.pending_messages.contains_key(hash) + } + + pub(super) fn add_message(&mut self, msg: EphemeraMessage) -> anyhow::Result<()> { + trace!("Adding message to pool: {:?}", msg); + + let msg_hash = msg.hash_with_default_hasher()?; + + self.pending_messages.insert(msg_hash, msg); + + trace!("Message pool size: {:?}", self.pending_messages.len()); + Ok(()) + } + + pub(super) fn remove_messages(&mut self, messages: &[EphemeraMessage]) -> anyhow::Result<()> { + trace!( + "Mempool size before removing messages {}", + self.pending_messages.len() + ); + for msg in messages { + let hash = msg.hash_with_default_hasher()?; + if self.pending_messages.remove(&hash).is_none() { + warn!("Message not found in pool: {:?}", msg); + } + } + trace!( + "Mempool size after removing messages {}", + self.pending_messages.len() + ); + Ok(()) + } + + /// Returns a `Vec` of all `EphemeraMessage`s in the message pool. + /// The message pool is not cleared. + pub(super) fn get_messages(&self) -> Vec { + self.pending_messages.values().cloned().collect() + } +} + +#[cfg(test)] +mod test { + use crate::block::message_pool::MessagePool; + use crate::block::types::message::EphemeraMessage; + use crate::crypto::{EphemeraKeypair, Keypair}; + use crate::ephemera_api::RawApiEphemeraMessage; + + #[test] + fn test_add_remove() { + let keypair = Keypair::generate(None); + + let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]); + let signed_message = message.sign(&keypair).expect("Failed to sign message"); + let signed_message: EphemeraMessage = signed_message.into(); + + let mut pool = MessagePool::new(); + pool.add_message(signed_message.clone()).unwrap(); + pool.remove_messages(&[signed_message]).unwrap(); + + assert_eq!(pool.get_messages().len(), 0); + } +} diff --git a/ephemera/src/block/mod.rs b/ephemera/src/block/mod.rs new file mode 100644 index 0000000000..b6d3684d80 --- /dev/null +++ b/ephemera/src/block/mod.rs @@ -0,0 +1,26 @@ +//! # Block manager +//! +//! Block manager is quite simple. It keeps pending messages in memory and puts all of them into a block +//! at predefined intervals. That's all it does. +//! +//! If the block actually will be broadcast or not is decided by the application. If not, it will produce next block with +//! the same messages plus the new ones. +//! +//! When application shuts down, pending messages are lost. +//! +//! When a block gets accepted by reliable broadcast then Block Manager will remove all messages included in the block from the +//! pending messages queue. +//! +//! # Synchronization and duplicate messages in sequence of blocks +//! +//! When previous block hasn't been accepted yet, then the next block will contain the same messages as the previous one. +//! One way to solve this is that an application itself keeps track of duplicate messages and discards them if necessary. +//! +//! But it seems a reasonable assumption that in general duplicate messages are unwanted. Therefore, Ephemera solves this +//! by dropping previous blocks which get Finalised/Committed after a new block has been created. + +pub(crate) mod builder; +pub(crate) mod manager; +pub(crate) mod message_pool; +pub(crate) mod producer; +pub(crate) mod types; diff --git a/ephemera/src/block/producer.rs b/ephemera/src/block/producer.rs new file mode 100644 index 0000000000..20f82bc328 --- /dev/null +++ b/ephemera/src/block/producer.rs @@ -0,0 +1,92 @@ +use crate::block::{ + types::block::{Block, RawBlock, RawBlockHeader}, + types::message::EphemeraMessage, +}; +use crate::peer::PeerId; +use log::trace; + +pub(crate) struct BlockProducer { + pub(crate) peer_id: PeerId, +} + +impl BlockProducer { + pub(super) fn new(peer_id: PeerId) -> Self { + Self { peer_id } + } + + pub(super) fn create_block( + &mut self, + height: u64, + pending_messages: Vec, + ) -> anyhow::Result { + trace!("Pending messages for new block: {:?}", pending_messages); + let block = self.new_block(height, pending_messages)?; + Ok(block) + } + + fn new_block(&self, height: u64, mut messages: Vec) -> anyhow::Result { + //Ordering is fundamental for block hash. Simple sort is fine for now. + messages.sort(); + + let raw_header = RawBlockHeader::new(self.peer_id, height); + let raw_block = RawBlock::new(raw_header, messages); + + //Better idea is probably combine header hash with Merkle tree root hash + let block_hash = raw_block.hash_with_default_hasher()?; + + let block = Block::new(raw_block, block_hash); + Ok(block) + } +} + +#[cfg(test)] +mod test { + use crate::crypto::{EphemeraKeypair, Keypair}; + use crate::ephemera_api::RawApiEphemeraMessage; + use std::cmp::Ordering; + + use super::*; + + #[test] + fn test_produce_block() { + let peer_id = PeerId::random(); + + let mut block_producer = BlockProducer::new(peer_id); + + let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]); + let signed_message = message + .sign(&Keypair::generate(None)) + .expect("Failed to sign message"); + let signed_message1: EphemeraMessage = signed_message.into(); + + let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]); + let signed_message = message + .sign(&Keypair::generate(None)) + .expect("Failed to sign message"); + let signed_message2: EphemeraMessage = signed_message.into(); + + let messages = vec![signed_message1.clone(), signed_message2.clone()]; + + let block = block_producer.create_block(1, messages).unwrap(); + + assert_eq!(block.header.height, 1); + assert_eq!(block.header.creator, peer_id); + assert_eq!(block.messages.len(), 2); + + //Nondeterministic because of timestamp + match signed_message1.cmp(&signed_message2) { + Ordering::Less => { + assert_eq!(block.messages[0], signed_message1); + assert_eq!(block.messages[1], signed_message2); + } + Ordering::Greater => { + assert_eq!(block.messages[0], signed_message2); + assert_eq!(block.messages[1], signed_message1); + } + + Ordering::Equal => { + panic!("Messages are equal"); + } + } + } +} diff --git a/ephemera/src/block/types/block.rs b/ephemera/src/block/types/block.rs new file mode 100644 index 0000000000..7b1538a5a5 --- /dev/null +++ b/ephemera/src/block/types/block.rs @@ -0,0 +1,327 @@ +use std::fmt::{Debug, Display}; + +use serde::{Deserialize, Serialize}; + +use crate::utilities::merkle::MerkleTree; +use crate::{ + block::types::message::EphemeraMessage, + codec::{Decode, Encode}, + crypto::Keypair, + peer::PeerId, + utilities::{ + codec::{Codec, DecodingError, EncodingError, EphemeraCodec}, + crypto::Certificate, + hash::{EphemeraHash, EphemeraHasher}, + hash::{Hash, Hasher}, + time::EphemeraTime, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub(crate) struct BlockHeader { + pub(crate) timestamp: u64, + pub(crate) creator: PeerId, + pub(crate) height: u64, + pub(crate) hash: Hash, +} + +impl BlockHeader { + pub(crate) fn new(raw_header: &RawBlockHeader, hash: Hash) -> Self { + Self { + timestamp: raw_header.timestamp, + creator: raw_header.creator, + height: raw_header.height, + hash, + } + } +} + +impl Display for BlockHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hash = &self.hash; + let time = self.timestamp; + let creator = &self.creator; + let height = self.height; + write!( + f, + "hash: {hash}, timestamp: {time}, creator: {creator}, height: {height}", + ) + } +} + +impl Encode for BlockHeader { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(&self) + } +} + +impl Decode for BlockHeader { + type Output = Self; + + fn decode(bytes: &[u8]) -> Result { + Codec::decode(bytes) + } +} + +impl EphemeraHash for BlockHeader { + fn hash(&self, state: &mut H) -> anyhow::Result<()> { + let bytes = Codec::encode(&self)?; + state.update(&bytes); + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub(crate) struct RawBlockHeader { + pub(crate) timestamp: u64, + pub(crate) creator: PeerId, + pub(crate) height: u64, +} + +impl RawBlockHeader { + pub(crate) fn new(creator: PeerId, height: u64) -> Self { + Self { + timestamp: EphemeraTime::now(), + creator, + height, + } + } + + pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result { + let mut hasher = Hasher::default(); + self.hash(&mut hasher)?; + let header_hash = hasher.finish().into(); + Ok(header_hash) + } +} + +impl Display for RawBlockHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let creator = &self.creator; + let height = self.height; + write!(f, "creator: {creator}, height: {height}",) + } +} + +impl From for RawBlockHeader { + fn from(block_header: BlockHeader) -> Self { + Self { + timestamp: block_header.timestamp, + creator: block_header.creator, + height: block_header.height, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub(crate) struct Block { + pub(crate) header: BlockHeader, + pub(crate) messages: Vec, +} + +impl Block { + pub(crate) fn new(raw_block: RawBlock, block_hash: Hash) -> Self { + let header = BlockHeader::new(&raw_block.header, block_hash); + Self { + header, + messages: raw_block.messages, + } + } + + pub(crate) fn get_hash(&self) -> Hash { + self.header.hash + } + + pub(crate) fn get_height(&self) -> u64 { + self.header.height + } + + pub(crate) fn new_genesis_block(creator: PeerId) -> Self { + let mut block = Self { + header: BlockHeader { + timestamp: EphemeraTime::now(), + creator, + height: 0, + hash: Hash::new([0; 32]), + }, + messages: Vec::new(), + }; + + let hash = block + .hash_with_default_hasher() + .expect("Failed to hash genesis block"); + block.header.hash = hash; + block + } + + pub(crate) fn sign(&self, keypair: &Keypair) -> anyhow::Result { + let raw_block: RawBlock = self.clone().into(); + let certificate = Certificate::prepare(keypair, &raw_block)?; + Ok(certificate) + } + + pub(crate) fn verify(&self, certificate: &Certificate) -> anyhow::Result { + let raw_block: RawBlock = self.clone().into(); + certificate.verify(&raw_block) + } + + pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result { + let raw_block: RawBlock = self.clone().into(); + raw_block.hash_with_default_hasher() + } + + pub(crate) fn merkle_tree(&self) -> anyhow::Result { + merkle_tree(&self.messages) + } +} + +impl Display for Block { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let header = &self.header; + write!(f, "{header}, nr of messages: {}", self.messages.len()) + } +} + +impl Encode for Block { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(&self) + } +} + +impl Decode for Block { + type Output = Block; + + fn decode(bytes: &[u8]) -> Result { + Codec::decode(bytes) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub(crate) struct RawBlock { + pub(crate) header: RawBlockHeader, + pub(crate) messages: Vec, +} + +impl RawBlock { + pub(crate) fn new(header: RawBlockHeader, messages: Vec) -> Self { + Self { header, messages } + } + + pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result { + let header_hash = self.header.hash_with_default_hasher()?; + let merkle_root = merkle_tree(&self.messages)?.root_hash(); + let block_hash = Hasher::digest(&[header_hash.inner(), merkle_root.inner()].concat()); + Ok(block_hash.into()) + } +} + +impl From for RawBlock { + fn from(block: Block) -> Self { + Self { + header: block.header.into(), + messages: block.messages, + } + } +} + +impl Encode for RawBlockHeader { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(&self) + } +} + +impl Decode for RawBlockHeader { + type Output = RawBlockHeader; + + fn decode(bytes: &[u8]) -> Result { + Codec::decode(bytes) + } +} + +impl Encode for RawBlock { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(&self) + } +} + +impl Decode for RawBlock { + type Output = RawBlock; + + fn decode(bytes: &[u8]) -> Result { + Codec::decode(bytes) + } +} + +impl EphemeraHash for RawBlockHeader { + fn hash(&self, state: &mut H) -> anyhow::Result<()> { + state.update(&self.encode()?); + Ok(()) + } +} + +impl EphemeraHash for RawBlock { + fn hash(&self, state: &mut H) -> anyhow::Result<()> { + self.header.hash(state)?; + for message in &self.messages { + message.hash(state)?; + } + Ok(()) + } +} + +pub(crate) fn merkle_tree(messages: &[EphemeraMessage]) -> anyhow::Result { + let message_hashes = messages + .iter() + .map(EphemeraMessage::hash_with_default_hasher) + .collect::>>()?; + let merkle_tree = MerkleTree::build_tree(&message_hashes); + Ok(merkle_tree) +} + +#[cfg(test)] +mod test { + use crate::block::types::message::RawEphemeraMessage; + use crate::crypto::EphemeraKeypair; + + use super::*; + + #[test] + fn test_block_hash_no_messages() { + let block = Block::new_genesis_block(PeerId::random()); + let block_hash = block.hash_with_default_hasher().unwrap(); + assert_eq!(block_hash, block.get_hash()); + } + + #[test] + fn test_block_hash_with_messages() { + let messages = create_ephemera_messages(10); + let message_hashes = messages + .iter() + .map(EphemeraMessage::hash_with_default_hasher) + .collect::>>() + .unwrap(); + + let raw_block = RawBlock::new(RawBlockHeader::new(PeerId::random(), 0), messages); + let block_hash = raw_block.hash_with_default_hasher().unwrap(); + + let header_hash = raw_block.header.hash_with_default_hasher().unwrap(); + let merkle_root = MerkleTree::build_tree(&message_hashes).root_hash(); + let expected_block_hash = + Hasher::digest(&[header_hash.inner(), merkle_root.inner()].concat()); + + assert_eq!(block_hash, expected_block_hash.into()); + } + + fn create_ephemera_messages(n: usize) -> Vec { + let keypair = Keypair::generate(None); + let mut messages = Vec::new(); + for i in 0..n { + let label = format!("test {i}",); + let message = RawEphemeraMessage::new(label, vec![0; 32]); + let certificate = Certificate::prepare(&keypair, &message).unwrap(); + let ephemera_message = EphemeraMessage::new(message, certificate); + messages.push(ephemera_message); + } + messages + } +} diff --git a/ephemera/src/block/types/message.rs b/ephemera/src/block/types/message.rs new file mode 100644 index 0000000000..0cf271ba46 --- /dev/null +++ b/ephemera/src/block/types/message.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +use crate::utilities::codec::{Codec, DecodingError, EncodingError, EphemeraCodec}; +use crate::{ + codec::{Decode, Encode}, + utilities::{ + crypto::Certificate, + hash::{EphemeraHash, EphemeraHasher}, + hash::{Hash, Hasher}, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub(crate) struct EphemeraMessage { + ///Timestamp of the message + pub(crate) timestamp: u64, + ///Application specific logical identifier of the message + pub(crate) label: String, + ///Application specific data + pub(crate) data: Vec, + ///Signature of the raw message + pub(crate) certificate: Certificate, +} + +impl EphemeraMessage { + #[cfg(test)] + pub(crate) fn new(raw_message: RawEphemeraMessage, certificate: Certificate) -> Self { + Self { + timestamp: raw_message.timestamp, + label: raw_message.label, + data: raw_message.data, + certificate, + } + } + + pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result { + let mut hasher = Hasher::default(); + self.hash(&mut hasher)?; + let hash = hasher.finish().into(); + Ok(hash) + } +} + +impl Encode for EphemeraMessage { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(&self) + } +} + +impl Decode for EphemeraMessage { + type Output = Self; + + fn decode(bytes: &[u8]) -> Result { + Codec::decode(bytes) + } +} + +impl EphemeraHash for EphemeraMessage { + fn hash(&self, state: &mut H) -> anyhow::Result<()> { + state.update(&self.encode()?); + Ok(()) + } +} + +/// Raw message represents all the data what will be signed. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub(crate) struct RawEphemeraMessage { + pub(crate) timestamp: u64, + pub(crate) label: String, + pub(crate) data: Vec, +} + +impl RawEphemeraMessage { + #[cfg(test)] + pub(crate) fn new(label: String, data: Vec) -> Self { + use crate::utilities::time::EphemeraTime; + Self { + timestamp: EphemeraTime::now(), + label, + data, + } + } +} + +impl From for RawEphemeraMessage { + fn from(message: EphemeraMessage) -> Self { + Self { + timestamp: message.timestamp, + label: message.label, + data: message.data, + } + } +} + +impl Encode for RawEphemeraMessage { + fn encode(&self) -> Result, EncodingError> { + Codec::encode(&self) + } +} diff --git a/ephemera/src/block/types/mod.rs b/ephemera/src/block/types/mod.rs new file mode 100644 index 0000000000..21c274a325 --- /dev/null +++ b/ephemera/src/block/types/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod block; +pub(crate) mod message; diff --git a/ephemera/src/broadcast/bracha/broadcast.rs b/ephemera/src/broadcast/bracha/broadcast.rs new file mode 100644 index 0000000000..19e3e00d4b --- /dev/null +++ b/ephemera/src/broadcast/bracha/broadcast.rs @@ -0,0 +1,320 @@ +use std::num::NonZeroUsize; + +use log::{debug, trace}; +use lru::LruCache; + +use crate::broadcast::bracha::quorum::BrachaMessageType; +use crate::peer::PeerId; +use crate::{ + block::types::block::Block, + broadcast::{ + bracha::quorum::Quorum, + MessageType::{Echo, Vote}, + ProtocolContext, RawRbMsg, + }, + utilities::hash::Hash, +}; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub(crate) enum BroadcastResponse { + Broadcast(RawRbMsg), + Deliver(Hash), + Drop(Hash), +} + +pub(crate) struct Broadcaster { + /// Local peer id + local_peer_id: PeerId, + /// We keep a context for each block we are processing. + contexts: LruCache, + /// Current cluster size + cluster_size: usize, +} + +impl Broadcaster { + pub fn new(peer_id: PeerId) -> Broadcaster { + Broadcaster { + //At any given time we are processing in parallel about n messages, where n is the number of peers in the group. + //This is just large enough buffer. + contexts: LruCache::new(NonZeroUsize::new(1000).unwrap()), + cluster_size: 0, + local_peer_id: peer_id, + } + } + + pub(crate) fn new_broadcast(&mut self, block: Block) -> anyhow::Result { + debug!("Starting broadcast for new block {:?}", block.get_hash()); + self.handle(&RawRbMsg::new(block, self.local_peer_id)) + } + + pub(crate) fn handle(&mut self, rb_msg: &RawRbMsg) -> anyhow::Result { + trace!("Processing new broadcast message: {:?}", rb_msg); + + let block = rb_msg.block(); + let hash = block.hash_with_default_hasher()?; + + let ctx = self.contexts.get_or_insert(hash, || { + ProtocolContext::new(hash, self.local_peer_id, Quorum::new(self.cluster_size)) + }); + + if ctx.delivered { + trace!("Block {hash:?} already delivered"); + return Ok(BroadcastResponse::Drop(hash)); + } + + match rb_msg.message_type { + Echo(_) => { + trace!("Processing ECHO {:?}", rb_msg.id); + Ok(self.process_echo(rb_msg, hash)) + } + Vote(_) => { + trace!("Processing VOTE {:?}", rb_msg.id); + Ok(self.process_vote(rb_msg, hash)) + } + } + } + + fn process_echo(&mut self, rb_msg: &RawRbMsg, hash: Hash) -> BroadcastResponse { + let ctx = self.contexts.get_mut(&hash).expect("Context not found"); + + if self.local_peer_id != rb_msg.original_sender { + trace!("Adding echo from {:?}", rb_msg.original_sender); + ctx.add_echo(rb_msg.original_sender); + } + + if !ctx.echoed() { + ctx.add_echo(self.local_peer_id); + + trace!("Sending echo reply for {hash:?}",); + return BroadcastResponse::Broadcast( + rb_msg.echo_reply(self.local_peer_id, rb_msg.block()), + ); + } + + if !ctx.voted() + && ctx + .quorum + .check_threshold(ctx, BrachaMessageType::Echo) + .is_vote() + { + ctx.add_vote(self.local_peer_id); + + trace!("Sending vote reply for {hash:?}",); + return BroadcastResponse::Broadcast( + rb_msg.vote_reply(self.local_peer_id, rb_msg.block()), + ); + } + + BroadcastResponse::Drop(hash) + } + + fn process_vote(&mut self, rb_msg: &RawRbMsg, hash: Hash) -> BroadcastResponse { + let block = rb_msg.block(); + let ctx = self.contexts.get_mut(&hash).expect("Context not found"); + + if self.local_peer_id != rb_msg.original_sender { + trace!("Adding vote from {:?}", rb_msg.original_sender); + ctx.add_vote(rb_msg.original_sender); + } + + if ctx + .quorum + .check_threshold(ctx, BrachaMessageType::Vote) + .is_vote() + { + ctx.add_vote(self.local_peer_id); + + trace!("Sending vote reply for {hash:?}",); + return BroadcastResponse::Broadcast(rb_msg.vote_reply(self.local_peer_id, block)); + } + + if ctx + .quorum + .check_threshold(ctx, BrachaMessageType::Vote) + .is_deliver() + { + trace!("Commit complete for {:?}", rb_msg.id); + + ctx.delivered = true; + + return BroadcastResponse::Deliver(hash); + } + + BroadcastResponse::Drop(hash) + } + + pub(crate) fn group_updated(&mut self, size: usize) { + self.cluster_size = size; + } +} + +#[cfg(test)] +mod tests { + + //1.make sure before voting enough echo messages are received + //2.make sure before delivering enough vote messages are received + //a)Either f + 1 + //b)Or n - f + + //3.make sure that duplicate messages doesn't have impact + + //4. "Ideally" make sure that when group changes, the ongoing broadcast can deal with it + + use std::iter; + + use assert_matches::assert_matches; + + use crate::broadcast::bracha::broadcast::BroadcastResponse; + use crate::peer::PeerId; + use crate::utilities::hash::Hash; + use crate::{ + block::types::block::{Block, RawBlock, RawBlockHeader}, + broadcast::{self, bracha::broadcast::Broadcaster, RawRbMsg}, + }; + + #[test] + fn test_state_transitions_from_start_to_end() { + let peers: Vec = iter::repeat_with(PeerId::random).take(10).collect(); + let local_peer_id = peers[0]; + let block_creator_peer_id = peers[1]; + + let mut broadcaster = Broadcaster::new(local_peer_id); + broadcaster.group_updated(peers.len()); + + let (block_hash, block) = create_block(block_creator_peer_id); + + //After this echo set contains local and block creator(msg sender) + receive_echo_first_message(&mut broadcaster, &block, block_creator_peer_id); + + let ctx = broadcaster.contexts.get(&block_hash).unwrap(); + assert_eq!(ctx.echo.len(), 2); + assert!(ctx.echoed()); + assert!(!ctx.voted()); + + receive_nr_of_echo_messages_below_vote_threshold(&mut broadcaster, &block, &peers[2..6]); + + let ctx = broadcaster.contexts.get(&block_hash).unwrap(); + assert_eq!(ctx.echo.len(), 6); + assert!(ctx.echoed()); + assert!(!ctx.voted()); + + receive_echo_threshold_message(&mut broadcaster, &block, *peers.get(7).unwrap()); + + let ctx = broadcaster.contexts.get(&block_hash).unwrap(); + assert_eq!(ctx.echo.len(), 7); + assert_eq!(ctx.vote.len(), 1); + assert!(ctx.echoed()); + assert!(ctx.voted()); + + receive_nr_of_vote_messages_below_deliver_threshold(&mut broadcaster, &block, &peers[2..7]); + + let ctx = broadcaster.contexts.get(&block_hash).unwrap(); + assert_eq!(ctx.echo.len(), 7); + assert_eq!(ctx.vote.len(), 6); + assert!(ctx.echoed()); + assert!(ctx.voted()); + + receive_threshold_vote_message_for_deliver( + &mut broadcaster, + &block, + *peers.get(8).unwrap(), + ); + } + + fn receive_threshold_vote_message_for_deliver( + broadcaster: &mut Broadcaster, + block: &Block, + peer_id: PeerId, + ) { + let rb_msg = RawRbMsg::new(block.clone(), PeerId::random()); + let rb_msg = rb_msg.vote_reply(peer_id, block.clone()); + + let response = handle_double(broadcaster, &rb_msg); + + assert_matches!(response, BroadcastResponse::Deliver(_)); + } + + fn receive_nr_of_echo_messages_below_vote_threshold( + broadcaster: &mut Broadcaster, + block: &Block, + peers: &[PeerId], + ) { + for peer_id in peers { + let rb_msg = RawRbMsg::new(block.clone(), *peer_id); + + let response = handle_double(broadcaster, &rb_msg); + + assert_matches!(response, BroadcastResponse::Drop(_)); + } + } + + fn receive_nr_of_vote_messages_below_deliver_threshold( + broadcaster: &mut Broadcaster, + block: &Block, + peers: &[PeerId], + ) { + for peer_id in peers { + let rb_msg = RawRbMsg::new(block.clone(), PeerId::random()); + let rb_msg = rb_msg.vote_reply(*peer_id, block.clone()); + + let response = handle_double(broadcaster, &rb_msg); + assert_matches!(response, BroadcastResponse::Drop(_)); + } + } + + fn receive_echo_first_message( + broadcaster: &mut Broadcaster, + block: &Block, + block_creator: PeerId, + ) { + let rb_msg = RawRbMsg::new(block.clone(), block_creator); + let response = handle_double(broadcaster, &rb_msg); + + assert_matches!( + response, + BroadcastResponse::Broadcast(RawRbMsg { + id: _, + request_id: _, + original_sender: _, + timestamp: _, + message_type: broadcast::MessageType::Echo(_), + }) + ); + } + + fn receive_echo_threshold_message( + broadcaster: &mut Broadcaster, + block: &Block, + peer_id: PeerId, + ) { + let rb_msg = RawRbMsg::new(block.clone(), peer_id); + + let response = handle_double(broadcaster, &rb_msg); + assert_matches!( + response, + BroadcastResponse::Broadcast(RawRbMsg { + id: _, + request_id: _, + original_sender: _, + timestamp: _, + message_type: broadcast::MessageType::Vote(_), + }) + ); + } + + fn create_block(block_creator_peer_id: PeerId) -> (Hash, Block) { + let header = RawBlockHeader::new(block_creator_peer_id, 0); + let raw_block = RawBlock::new(header, vec![]); + let block_hash = raw_block.hash_with_default_hasher().unwrap(); + let block = Block::new(raw_block, block_hash); + (block_hash, block) + } + + //make sure that duplicate messages doesn't have impact + fn handle_double(broadcaster: &mut Broadcaster, rb_msg: &RawRbMsg) -> BroadcastResponse { + let response = broadcaster.handle(rb_msg).unwrap(); + broadcaster.handle(rb_msg).unwrap(); + response + } +} diff --git a/ephemera/src/broadcast/bracha/mod.rs b/ephemera/src/broadcast/bracha/mod.rs new file mode 100644 index 0000000000..ddbf36eb64 --- /dev/null +++ b/ephemera/src/broadcast/bracha/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod broadcast; +pub(crate) mod quorum; diff --git a/ephemera/src/broadcast/bracha/quorum.rs b/ephemera/src/broadcast/bracha/quorum.rs new file mode 100644 index 0000000000..35bb935ae6 --- /dev/null +++ b/ephemera/src/broadcast/bracha/quorum.rs @@ -0,0 +1,254 @@ +use log::trace; + +use crate::broadcast::{MessageType, ProtocolContext}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BrachaMessageType { + Echo, + Vote, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BrachaAction { + Vote, + Deliver, + Ignore, +} + +impl BrachaAction { + pub(crate) fn is_vote(self) -> bool { + matches!(self, BrachaAction::Vote) + } + + pub(crate) fn is_deliver(self) -> bool { + matches!(self, BrachaAction::Deliver) + } +} + +impl From for BrachaMessageType { + fn from(message_type: MessageType) -> Self { + match message_type { + MessageType::Echo(_) => BrachaMessageType::Echo, + MessageType::Vote(_) => BrachaMessageType::Vote, + } + } +} + +const MAX_FAULTY_RATIO: f64 = 1.0 / 3.0; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct Quorum { + pub(crate) cluster_size: usize, + pub(crate) max_faulty_nodes: usize, +} + +impl Quorum { + pub fn new(cluster_size: usize) -> Self { + let max_faulty_nodes = Quorum::max_faulty_nodes(cluster_size); + Self { + cluster_size, + max_faulty_nodes, + } + } + + pub(crate) fn check_threshold( + &self, + ctx: &ProtocolContext, + phase: BrachaMessageType, + ) -> BrachaAction { + if self.cluster_size == 0 { + return BrachaAction::Ignore; + } + + match phase { + BrachaMessageType::Echo => { + if ctx.echo.len() >= self.cluster_size - self.max_faulty_nodes { + trace!( + "Echo threshold reached: Echoed:{} / Threshold:{} for Block:{}", + ctx.echo.len(), + self.cluster_size - self.max_faulty_nodes, + ctx.hash + ); + BrachaAction::Vote + } else { + trace!( + "Echo threshold not reached: Echoed:{} / Threshold:{} for Block:{}", + ctx.echo.len(), + self.cluster_size - self.max_faulty_nodes, + ctx.hash + ); + BrachaAction::Ignore + } + } + BrachaMessageType::Vote => { + if !ctx.voted() { + // f + 1 votes are enough to send our vote + if ctx.vote.len() >= self.max_faulty_nodes { + trace!( + "Vote send threshold reached: Voted:{} / Threshold:{} for Block:{}", + ctx.vote.len(), + self.max_faulty_nodes + 1, + ctx.hash + ); + return BrachaAction::Vote; + } + } + + if ctx.voted() { + // n-f votes are enough to deliver the value + if ctx.vote.len() >= self.cluster_size - self.max_faulty_nodes { + trace!( + "Deliver threshold reached: Voted:{} / Threshold:{} for Block:{}", + ctx.vote.len(), + self.cluster_size - self.max_faulty_nodes, + ctx.hash + ); + return BrachaAction::Deliver; + } + } + + trace!( + "Vote threshold not reached: Voted:{} / Threshold:{} for Block:{}", + ctx.vote.len(), + self.max_faulty_nodes + 1, + ctx.hash + ); + BrachaAction::Ignore + } + } + } + + pub(crate) fn cluster_size_info(cluster_size: usize) -> String { + let max_faulty_nodes = Quorum::max_faulty_nodes(cluster_size); + format!("Cluster size: {cluster_size} / Max faulty nodes: {max_faulty_nodes}",) + } + + #[allow( + clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::cast_possible_truncation + )] + fn max_faulty_nodes(cluster_size: usize) -> usize { + (cluster_size as f64 * MAX_FAULTY_RATIO).floor() as usize + } +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use crate::broadcast::{ + bracha::quorum::{BrachaAction, BrachaMessageType, Quorum}, + ProtocolContext, + }; + use crate::peer::PeerId; + + #[test] + fn test_max_faulty_nodes() { + let quorum = Quorum::new(10); + assert_eq!(quorum.max_faulty_nodes, 3); + } + + #[test] + fn test_vote_threshold_from_n_minus_f_peers() { + let quorum = Quorum::new(10); + + let ctx = ctx_with_nr_echoes(0); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Echo), + BrachaAction::Ignore + ); + + let ctx = ctx_with_nr_echoes(3); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Echo), + BrachaAction::Ignore + ); + + let ctx = ctx_with_nr_echoes(8); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Echo), + BrachaAction::Vote + ); + } + + #[test] + fn test_vote_threshold_from_f_plus_one_peers() { + let quorum = Quorum::new(10); + + let ctx = ctx_with_nr_votes(0, None); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Vote), + BrachaAction::Ignore + ); + + let ctx = ctx_with_nr_votes(2, None); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Vote), + BrachaAction::Ignore + ); + + let ctx = ctx_with_nr_votes(5, None); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Vote), + BrachaAction::Vote + ); + } + + #[test] + fn test_deliver_threshold_from_n_minus_f_peers() { + let quorum = Quorum::new(10); + + let local_peer_id = PeerId::random(); + let ctx = ctx_with_nr_votes(0, local_peer_id.into()); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Vote), + BrachaAction::Ignore + ); + + let ctx = ctx_with_nr_votes(3, local_peer_id.into()); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Vote), + BrachaAction::Ignore + ); + + let ctx = ctx_with_nr_votes(7, local_peer_id.into()); + assert_eq!( + quorum.check_threshold(&ctx, BrachaMessageType::Vote), + BrachaAction::Deliver + ); + } + + fn ctx_with_nr_echoes(n: usize) -> ProtocolContext { + let mut ctx = ProtocolContext { + local_peer_id: PeerId::random(), + hash: [0; 32].into(), + echo: HashSet::default(), + vote: HashSet::default(), + quorum: Quorum::new(10), + delivered: false, + }; + for _ in 0..n { + ctx.echo.insert(PeerId::random()); + } + ctx + } + + fn ctx_with_nr_votes(n: usize, local_peer_id: Option) -> ProtocolContext { + let mut ctx = ProtocolContext { + local_peer_id: local_peer_id.unwrap_or(PeerId::random()), + hash: [0; 32].into(), + echo: HashSet::default(), + vote: HashSet::default(), + quorum: Quorum::new(10), + delivered: false, + }; + for _ in 0..n { + ctx.vote.insert(PeerId::random()); + } + if let Some(id) = local_peer_id { + ctx.vote.insert(id); + } + ctx + } +} diff --git a/ephemera/src/broadcast/group.rs b/ephemera/src/broadcast/group.rs new file mode 100644 index 0000000000..d8483b8aaf --- /dev/null +++ b/ephemera/src/broadcast/group.rs @@ -0,0 +1,249 @@ +use std::collections::HashSet; +use std::num::NonZeroUsize; + +use log::warn; +use lru::LruCache; + +use crate::peer::PeerId; +use crate::utilities::hash::Hash; + +pub(crate) struct BroadcastGroup { + /// The id of current group. Incremented every time a new snapshot is added. + pub(crate) current_id: u64, + /// A cache of the group snapshots. + pub(crate) snapshots: LruCache>, + /// A cache of the groups for each block. + pub(crate) broadcast_groups: LruCache, +} + +impl BroadcastGroup { + pub(crate) fn new() -> BroadcastGroup { + let mut snapshots = LruCache::new(NonZeroUsize::new(100).unwrap()); + snapshots.put(0, HashSet::new()); + BroadcastGroup { + current_id: 0, + snapshots, + broadcast_groups: LruCache::new(NonZeroUsize::new(100).unwrap()), + } + } + + pub(crate) fn add_snapshot(&mut self, snapshot: HashSet) { + self.current_id += 1; + self.snapshots.put(self.current_id, snapshot); + } + + pub(crate) fn is_member(&mut self, id: u64, peer_id: &PeerId) -> bool { + self.snapshots + .get(&id) + .map_or(false, |s| s.contains(peer_id)) + } + + pub(crate) fn is_empty(&mut self) -> bool { + self.snapshots + .get(&self.current_id) + .map_or(true, HashSet::is_empty) + } + + // Returns empty snapshots(inserted in 'new' fn) if we haven't received any yet. + pub(crate) fn current(&mut self) -> &HashSet { + self.snapshots + .get(&self.current_id) + .expect("Current group should always exist") + } + + // Checks if creator and sender are part of the expected group. + // If we see hash first time, it checks against the current group. And if check passes, it + // associates the hash with the current group. + pub(crate) fn check_membership( + &mut self, + hash: Hash, + block_creator: &PeerId, + message_sender: &PeerId, + ) -> bool { + //We see this block first time + if !self.broadcast_groups.contains(&hash) { + //This can happen at startup for example when node is not ready yet(caught up with the network) + if self.is_empty() { + warn!( + "Received new block {:?} but current group is empty, rejecting the block", + hash + ); + return false; + } + } + + //Make sure that the sender peer_id and block peer_id are part of the block initial group + //1. If the block is new, the group is the current one + //2. If the block is old, the group is the one that was used when the block was created + + //It's needed to make sure that + //1. The peer is authenticated(part of the network) + //2. Block processing is consistent regarding the group across rounds + + let membership_id = *self.broadcast_groups.get(&hash).unwrap_or(&self.current_id); + + //Node is excluded from group for some reason(for example health checks failed) + if !self.is_member(membership_id, message_sender) { + warn!( + "Received new block {} but sender {} is not part of the current group", + hash, message_sender + ); + return false; + } + + //Node is excluded from group for some reason(for example health checks failed) + if !self.is_member(membership_id, block_creator) { + warn!( + "Received new block {} but sender {} is not part of the current group", + hash, message_sender + ); + return false; + } + + self.broadcast_groups.put(hash, membership_id); + + true + } + + pub(crate) fn get_group_by_block_hash(&mut self, hash: Hash) -> Option<&HashSet> { + let membership_id = *self.broadcast_groups.get(&hash)?; + self.snapshots.get(&membership_id) + } +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use crate::broadcast::group::BroadcastGroup; + use crate::peer::PeerId; + use crate::utilities::hash::Hash; + + #[test] + fn test_no_snapshot() { + let group = BroadcastGroup::new(); + assert_eq!(group.current_id, 0); + //Including initial default snapshot + assert_eq!(group.snapshots.len(), 1); + } + + #[test] + fn test_multiple_snapshots() { + let (mut group, snapshots) = group_with_snapshots(10); + assert_eq!(group.current_id, 10); + //Including initial default snapshot + assert_eq!(group.snapshots.len(), 11); + + for (i, sn) in snapshots + .iter() + .enumerate() + .map(|(i, sn)| ((i + 1) as u64, sn)) + { + let gsn = group.snapshots.get(&i).unwrap(); + assert_eq!(sn, gsn); + } + } + + #[test] + fn check_membership_empty_group() { + let mut group = BroadcastGroup::new(); + let hash = Hash::new([0; 32]); + assert!(!group.check_membership(hash, &PeerId::random(), &PeerId::random())); + assert!(!group.broadcast_groups.contains(&hash)); + } + + #[test] + fn check_membership_creator_nor_sender_not_member() { + let (mut group, _snapshots) = group_with_snapshots(1); + assert!(!group.check_membership(Hash::new([0; 32]), &PeerId::random(), &PeerId::random())); + assert!(!group.broadcast_groups.contains(&Hash::new([0; 32]))); + } + + #[test] + fn check_membership_creator_not_member() { + let (mut group, snapshots) = group_with_snapshots(1); + let sender = snapshots[0].clone().into_iter().next().unwrap(); + + let hash = Hash::new([0; 32]); + assert!(!group.check_membership(hash, &PeerId::random(), &sender)); + assert!(!group.broadcast_groups.contains(&hash)); + } + + #[test] + fn check_membership_sender_not_member() { + let (mut group, snapshots) = group_with_snapshots(1); + let creator = snapshots[0].clone().into_iter().next().unwrap(); + let hash = Hash::new([0; 32]); + assert!(!group.check_membership(hash, &creator, &PeerId::random())); + assert!(!group.broadcast_groups.contains(&hash)); + } + + #[test] + fn check_snapshot_membership_both_are_members() { + let (mut group, snapshots) = group_with_snapshots(1); + let creator = snapshots[0].clone().into_iter().next().unwrap(); + let sender = creator; + let hash = Hash::new([0; 32]); + assert!(group.check_membership(hash, &creator, &sender)); + assert!(group.broadcast_groups.contains(&hash)); + } + + #[test] + fn check_snapshot_membership_of_current_snapshot() { + let (mut group, snapshots) = group_with_snapshots(2); + let current_snapshot = snapshots[1].clone(); + let creator = current_snapshot.into_iter().next().unwrap(); + let sender = creator; + + let hash = Hash::new([0; 32]); + assert!(group.check_membership(hash, &creator, &sender)); + assert!(group.broadcast_groups.contains(&hash)); + + //Remove the current snapshot + group.snapshots.pop(&group.current_id); + + //Membership should fail + assert!(!group.check_membership(hash, &creator, &sender)); + } + + #[test] + fn check_snapshot_membership_of_previous_snapshot() { + let mut group = BroadcastGroup::new(); + let first_snapshot = create_snapshot(); + group.add_snapshot(first_snapshot.clone()); + + let creator = first_snapshot.into_iter().next().unwrap(); + let sender = creator; + let hash = Hash::new([0; 32]); + assert!(group.check_membership(hash, &creator, &sender)); + assert!(group.broadcast_groups.contains(&hash)); + + //Add second snapshot + group.add_snapshot(create_snapshot()); + + //Remove the current snapshot + group.snapshots.pop(&group.current_id); + + //Membership should still pass + assert!(group.broadcast_groups.contains(&hash)); + assert!(group.check_membership(hash, &creator, &sender)); + } + + fn group_with_snapshots(count: usize) -> (BroadcastGroup, Vec>) { + let mut group = BroadcastGroup::new(); + let mut snapshots = Vec::new(); + for _ in 0..count { + let snapshot = create_snapshot(); + snapshots.push(snapshot.clone()); + group.add_snapshot(snapshot); + } + (group, snapshots) + } + + fn create_snapshot() -> HashSet { + let mut snapshot = HashSet::new(); + let peer_id = PeerId::random(); + snapshot.insert(peer_id); + snapshot + } +} diff --git a/ephemera/src/broadcast/mod.rs b/ephemera/src/broadcast/mod.rs new file mode 100644 index 0000000000..ca930c451d --- /dev/null +++ b/ephemera/src/broadcast/mod.rs @@ -0,0 +1,191 @@ +//! Simple reliable broadcast protocol(Bracha) implementation +//! +use std::collections::HashSet; +use std::fmt::{Debug, Display}; + +use serde_derive::{Deserialize, Serialize}; + +use crate::broadcast::bracha::quorum::Quorum; +use crate::{ + block::types::block::Block, + peer::PeerId, + utilities::{ + crypto::Certificate, + hash::Hash, + id::{EphemeraId, EphemeraIdentifier}, + time::EphemeraTime, + }, +}; + +pub(crate) mod bracha; +pub(crate) mod group; +pub(crate) mod signing; + +/// Context keeps the broadcast state for a block +#[derive(Debug, Clone)] +pub(crate) struct ProtocolContext { + pub(crate) local_peer_id: PeerId, + /// Block hash + pub(crate) hash: Hash, + /// Peers that sent prepare message(this peer included) + pub(crate) echo: HashSet, + /// Peers that sent commit message(this peer included) + pub(crate) vote: HashSet, + /// Quorum logic for Bracha protocol + pub(crate) quorum: Quorum, + /// Flag indicating if the message was delivered to the client + pub(crate) delivered: bool, +} + +impl ProtocolContext { + pub(crate) fn new(hash: Hash, local_peer_id: PeerId, quorum: Quorum) -> ProtocolContext { + ProtocolContext { + local_peer_id, + hash, + echo: HashSet::new(), + vote: HashSet::new(), + quorum, + delivered: false, + } + } + + fn add_echo(&mut self, peer: PeerId) { + self.echo.insert(peer); + } + + fn add_vote(&mut self, peer: PeerId) { + self.vote.insert(peer); + } + + fn echoed(&self) -> bool { + self.echo.contains(&self.local_peer_id) + } + + fn voted(&self) -> bool { + self.vote.contains(&self.local_peer_id) + } +} + +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)] +pub(crate) struct RbMsg { + ///Unique id of the message which stays the same throughout the protocol + pub(crate) id: EphemeraId, + ///Distinct id of the message which changes when the message is rebroadcast + pub(crate) request_id: EphemeraId, + ///Id of the peer that CREATED the message(not necessarily the one that sent it, with gossip it can come through a different peer) + pub(crate) original_sender: PeerId, + ///When the message was created by the sender. + pub(crate) timestamp: u64, + ///Current phase of the protocol(Echo, Vote) + pub(crate) phase: MessageType, + ///Signature of the message + pub(crate) certificate: Certificate, +} + +impl RbMsg { + pub(crate) fn new(raw: RawRbMsg, signature: Certificate) -> RbMsg { + RbMsg { + id: raw.id, + request_id: raw.request_id, + original_sender: raw.original_sender, + timestamp: raw.timestamp, + phase: raw.message_type, + certificate: signature, + } + } + + pub(crate) fn block(&self) -> &Block { + match &self.phase { + MessageType::Echo(block) | MessageType::Vote(block) => block, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub(crate) struct RawRbMsg { + pub(crate) id: EphemeraId, + pub(crate) request_id: EphemeraId, + pub(crate) original_sender: PeerId, + pub(crate) timestamp: u64, + pub(crate) message_type: MessageType, +} + +impl RawRbMsg { + pub(crate) fn new(block: Block, original_sender: PeerId) -> RawRbMsg { + RawRbMsg { + id: EphemeraId::generate(), + request_id: EphemeraId::generate(), + original_sender, + timestamp: EphemeraTime::now(), + message_type: MessageType::Echo(block), + } + } + + pub(crate) fn block(&self) -> Block { + match &self.message_type { + MessageType::Echo(block) | MessageType::Vote(block) => block.clone(), + } + } + + pub(crate) fn reply(&self, local_id: PeerId, phase: MessageType) -> Self { + RawRbMsg { + id: self.id.clone(), + request_id: EphemeraId::generate(), + original_sender: local_id, + timestamp: EphemeraTime::now(), + message_type: phase, + } + } + + pub(crate) fn echo_reply(&self, local_id: PeerId, data: Block) -> Self { + self.reply(local_id, MessageType::Echo(data)) + } + + pub(crate) fn vote_reply(&self, local_id: PeerId, data: Block) -> Self { + self.reply(local_id, MessageType::Vote(data)) + } +} + +impl From for RawRbMsg { + fn from(msg: RbMsg) -> Self { + RawRbMsg { + id: msg.id, + request_id: msg.request_id, + original_sender: msg.original_sender, + timestamp: msg.timestamp, + message_type: msg.phase, + } + } +} + +impl Display for RbMsg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[id: {}, peer: {}, block: {}, phase: {:?}]", + self.id, + self.original_sender, + self.block().get_hash(), + self.phase + ) + } +} + +impl Debug for RbMsg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[id: {}, peer: {}, block: {}, phase: {:?}]", + self.id, + self.original_sender, + self.block().get_hash(), + self.phase + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub(crate) enum MessageType { + Echo(Block), + Vote(Block), +} diff --git a/ephemera/src/broadcast/signing.rs b/ephemera/src/broadcast/signing.rs new file mode 100644 index 0000000000..c3003365f3 --- /dev/null +++ b/ephemera/src/broadcast/signing.rs @@ -0,0 +1,150 @@ +use std::collections::HashSet; +use std::num::NonZeroUsize; +use std::sync::Arc; + +use log::trace; +use lru::LruCache; + +use crate::{ + block::types::block::{Block, RawBlock}, + crypto::Keypair, + utilities::{codec::Encode, crypto::Certificate, crypto::EphemeraPublicKey, hash::Hash}, +}; + +pub(crate) struct BlockSigner { + /// All signatures of the last blocks that we received from the network(+ our own) + verified_signatures: LruCache>, + /// Our own keypair + signing_keypair: Arc, +} + +impl BlockSigner { + pub fn new(keypair: Arc) -> Self { + Self { + verified_signatures: LruCache::new(NonZeroUsize::new(1000).unwrap()), + signing_keypair: keypair, + } + } + + pub(crate) fn get_block_certificates( + &mut self, + block_id: &Hash, + ) -> Option<&HashSet> { + self.verified_signatures.get(block_id) + } + + pub(crate) fn sign_block(&mut self, block: &Block, hash: &Hash) -> anyhow::Result { + trace!("Signing block: {:?}", block); + + let certificate = block.sign(self.signing_keypair.as_ref())?; + self.add_certificate(hash, certificate.clone()); + Ok(certificate) + } + + /// This verification is part of reliable broadcast and verifies only the + /// signature of the sender. + pub(crate) fn verify_block( + &mut self, + block: &Block, + certificate: &Certificate, + ) -> anyhow::Result<()> { + trace!("Verifying block: {block:?} against certificate {certificate:?}"); + + let raw_block: RawBlock = (*block).clone().into(); + let raw_block = raw_block.encode()?; + + if certificate + .public_key + .verify(&raw_block, &certificate.signature) + { + self.add_certificate(&block.header.hash, certificate.clone()); + Ok(()) + } else { + anyhow::bail!("Invalid block certificate"); + } + } + + fn add_certificate(&mut self, hash: &Hash, certificate: Certificate) { + trace!("Adding certificate to block: {}", hash); + self.verified_signatures + .get_or_insert_mut(*hash, HashSet::new) + .insert(certificate); + } +} + +#[cfg(test)] +mod test { + use crate::block::types::block::RawBlockHeader; + use crate::block::types::message::{EphemeraMessage, RawEphemeraMessage}; + use crate::crypto::EphemeraKeypair; + use crate::peer::ToPeerId; + + use super::*; + + #[test] + fn test_sign_verify_block_ok() { + let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None))); + + let message_signing_keypair = Keypair::generate(None); + + let block = new_block(&message_signing_keypair, "label1"); + let hash = block.hash_with_default_hasher().unwrap(); + + let certificate = signer.sign_block(&block, &hash).unwrap(); + + assert!(signer.verify_block(&block, &certificate).is_ok()); + } + + #[test] + fn test_sign_signatures_cached_correctly() { + let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None))); + + let block = new_block(&Keypair::generate(None), "label1"); + let hash = block.hash_with_default_hasher().unwrap(); + + //Signed by node 1 + let certificate1 = block.sign(&Keypair::generate(None)).unwrap(); + signer.verify_block(&block, &certificate1).unwrap(); + //Signed by node 2 + let certificate2 = block.sign(&Keypair::generate(None)).unwrap(); + signer.verify_block(&block, &certificate2).unwrap(); + + let block_certificates = signer.get_block_certificates(&hash).unwrap(); + assert_eq!(block_certificates.len(), 2); + } + + #[test] + fn test_sign_verify_block_fail() { + let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None))); + let message_signing_keypair = Keypair::generate(None); + + let block = new_block(&message_signing_keypair, "label1"); + let certificate = block.sign(&message_signing_keypair).unwrap(); + + let modified_block = new_block(&message_signing_keypair, "label2"); + + assert!(signer.verify_block(&modified_block, &certificate).is_err()); + } + + fn new_block(keypair: &Keypair, message_label: &str) -> Block { + let peer_id = keypair.public_key().peer_id(); + + let raw_ephemera_message = + RawEphemeraMessage::new(message_label.to_string(), "payload".as_bytes().to_vec()); + + let message_certificate = Certificate::prepare(keypair, &raw_ephemera_message).unwrap(); + let messages = vec![EphemeraMessage::new( + raw_ephemera_message, + message_certificate, + )]; + + let raw_block_header = RawBlockHeader::new(peer_id, 0); + let raw_block = RawBlock::new(raw_block_header, messages); + + let block_hash = raw_block + .hash_with_default_hasher() + .expect("Hashing failed"); + + Block::new(raw_block, block_hash) + } +} diff --git a/ephemera/src/cli/config.rs b/ephemera/src/cli/config.rs new file mode 100644 index 0000000000..1fca81afcc --- /dev/null +++ b/ephemera/src/cli/config.rs @@ -0,0 +1,132 @@ +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; + +use clap::Parser; +use log::{error, info}; +use toml::{Table, Value}; + +use crate::config::Configuration; + +#[derive(Debug, Clone, Parser)] +pub struct UpdateConfigCmd { + #[clap(long)] + pub config_path: String, + #[clap(long)] + pub property: String, + #[clap(long)] + pub value: String, +} + +impl UpdateConfigCmd { + /// # Panics + /// Panics if the config file does not exist. + pub fn execute(self) { + let path: PathBuf = self.config_path.clone().into(); + match Configuration::try_load(path.clone()) { + Ok(_) => { + info!("Updating config: {:?}", self); + + let toml_str = fs::read_to_string(path.clone()).unwrap(); + let table = toml_str.parse::().unwrap(); + + let keys = self.property.split('.').collect::>(); + + if !table.contains_key(keys[0]) { + println!("Key '{}' does not exist", keys[0]); + return; + } + + let mut visitor = ConfigVisitor::new(keys, self.value); + let table = visitor.process(table); + + let toml_str = toml::to_string(&table).unwrap(); + fs::write(path, toml_str).unwrap(); + } + Err(err) => { + error!("Error loading configuration file: {err:?}"); + } + } + } +} + +struct ConfigVisitor<'a> { + keys: Vec<&'a str>, + value: String, + in_array: bool, + data_in_array: Vec, +} + +impl<'a> ConfigVisitor<'a> { + fn new(keys: Vec<&'a str>, value: String) -> Self { + Self { + keys, + value, + in_array: false, + data_in_array: vec![], + } + } + + #[allow(clippy::needless_pass_by_value)] + fn process(&mut self, table: Table) -> Table { + let key = self.keys[0]; + let value = table.get(key).unwrap(); + self.process_value(table.clone(), value.clone(), 0) + } + + fn process_value(&mut self, mut table: Table, value: Value, key_index: usize) -> Table { + let root_key = self.keys[key_index]; + match value { + Value::String(str) => { + println!("Old value: {str}",); + let value = String::from(&self.value); + println!("New value: {value}",); + table.insert(root_key.to_string(), Value::String(value)); + } + Value::Integer(i) => { + println!("Old value: {i}",); + let value = i64::from_str(&self.value).unwrap(); + println!("New value: {value}",); + table.insert(root_key.to_string(), Value::Integer(value)); + } + Value::Float(f) => { + println!("Old value: {f}",); + let value = f64::from_str(&self.value).unwrap(); + println!("New value: {value}",); + table.insert(root_key.to_string(), Value::Float(value)); + } + Value::Boolean(b) => { + println!("Old value: {b}",); + let value = bool::from_str(&self.value).unwrap(); + println!("New value: {value}",); + table.insert(root_key.to_string(), Value::Boolean(value)); + } + Value::Datetime(_) => { + println!("Datetime not supported"); + } + Value::Array(ar) => { + self.in_array = true; + for value in ar { + self.process_value(table.clone(), value, key_index); + } + table.remove(root_key); + table.insert( + root_key.to_string(), + Value::Array(self.data_in_array.clone()), + ); + self.data_in_array.clear(); + self.in_array = false; + } + Value::Table(t) => { + let value = t.get(self.keys[key_index + 1]).cloned().unwrap(); + let updated_table = self.process_value(t, value, key_index + 1); + if self.in_array { + self.data_in_array.push(updated_table.into()); + return table.clone(); + } + table.insert(root_key.to_string(), updated_table.into()); + } + } + table + } +} diff --git a/ephemera/src/cli/crypto.rs b/ephemera/src/cli/crypto.rs new file mode 100644 index 0000000000..fc6f3e989b --- /dev/null +++ b/ephemera/src/cli/crypto.rs @@ -0,0 +1,15 @@ +use crate::crypto::{EphemeraKeypair, Keypair}; +use clap::Parser; + +use crate::utilities::crypto::EphemeraPublicKey; + +#[derive(Debug, Clone, Parser)] +pub struct GenerateKeypairCmd; + +impl GenerateKeypairCmd { + pub fn execute() { + let keypair = Keypair::generate(None); + println!("Keypair: {:>5}", keypair.to_base58()); + println!("Public key: {:>5}", keypair.public_key().to_base58()); + } +} diff --git a/ephemera/src/cli/init.rs b/ephemera/src/cli/init.rs new file mode 100644 index 0000000000..60d245d279 --- /dev/null +++ b/ephemera/src/cli/init.rs @@ -0,0 +1,165 @@ +use clap::{Args, Parser}; +use serde::{Deserialize, Serialize}; + +use crate::config::{ + BlockManagerConfiguration, Configuration, DatabaseConfiguration, HttpConfiguration, + Libp2pConfiguration, MembershipKind as ConfigMembershipKind, NodeConfiguration, + WebsocketConfiguration, +}; +use crate::crypto::{EphemeraKeypair, Keypair}; + +//network settings +const DEFAULT_LISTEN_ADDRESS: &str = "127.0.0.1"; +const DEFAULT_PROT_LISTEN_PORT: u16 = 3000; +const DEFAULT_WS_LISTEN_PORT: u16 = 3001; +const DEFAULT_HTTP_LISTEN_PORT: u16 = 3002; + +//libp2p settings +const DEFAULT_MESSAGES_TOPIC_NAME: &str = "nym-ephemera-proposed"; +const DEFAULT_HEARTBEAT_INTERVAL_SEC: u64 = 1; + +#[derive(Args, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[group(required = true, multiple = false)] +pub struct MembershipKind { + /// Requires the threshold of peers returned by membership provider to be online + #[clap(long)] + threshold: Option, + /// Requires that all peers returned by membership provider peers to be online + #[clap(long)] + all: bool, + /// Membership is just all online peers from the set returned by membership provider + #[clap(long)] + any: bool, +} + +impl Default for MembershipKind { + fn default() -> Self { + MembershipKind { + threshold: None, + all: false, + any: true, + } + } +} + +impl From for ConfigMembershipKind { + fn from(kind: MembershipKind) -> Self { + match (kind.threshold, kind.all, kind.any) { + //FIXME: use threshold value + (Some(_), false, false) => ConfigMembershipKind::Threshold, + (None, true, false) => ConfigMembershipKind::AllOnline, + (None, false, true) => ConfigMembershipKind::AnyOnline, + _ => panic!("Invalid membership kind"), + } + } +} + +#[derive(Parser, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Cmd { + #[clap(long)] + /// The IP address to listen on + pub ephemera_ip: Option, + /// The port which Ephemera uses for peer to peer communication + #[clap(long)] + pub ephemera_protocol_port: Option, + /// The port which Ephemera listens on for websocket subscriptions + #[clap(long)] + pub ephemera_websocket_port: Option, + /// The port which Ephemera listens on for http api + #[clap(long)] + pub ephemera_http_api_port: Option, + /// Either this node produces blocks or not + #[clap(skip)] + pub block_producer: bool, + /// At which interval to produce blocks + #[clap(skip)] + pub block_creation_interval_sec: u64, + /// When next block is created before previous one is finished, should we repeat it with the same messages + #[clap(skip)] + pub repeat_last_block_messages: bool, + /// The interval at which Ephemera requests the list of members + #[clap(skip)] + pub members_provider_delay_sec: u64, + /// A rule how to choose members based on their online status + #[clap(skip)] + pub membership_kind: MembershipKind, +} + +impl Default for Cmd { + fn default() -> Self { + Cmd { + ephemera_ip: Some(String::from(DEFAULT_LISTEN_ADDRESS)), + ephemera_protocol_port: Some(DEFAULT_PROT_LISTEN_PORT), + ephemera_websocket_port: Some(DEFAULT_WS_LISTEN_PORT), + ephemera_http_api_port: Some(DEFAULT_HTTP_LISTEN_PORT), + block_producer: true, + block_creation_interval_sec: 30, + repeat_last_block_messages: false, + members_provider_delay_sec: 60 * 60, + membership_kind: MembershipKind::default(), + } + } +} + +impl Cmd { + /// # Panics + /// Panics if the config file already exists. + pub fn execute(self, id: Option<&str>) { + assert!( + Configuration::try_load_from_home_dir().is_err(), + "Configuration file already exists", + ); + + let path = Configuration::ephemera_root_dir(id).unwrap(); + println!("Creating ephemera node configuration in: {path:?}",); + + let db_dir = path.join("db"); + let rocksdb_path = db_dir.join("rocksdb"); + let sqlite_path = db_dir.join("ephemera.sqlite"); + std::fs::create_dir_all(&rocksdb_path).unwrap(); + std::fs::File::create(&sqlite_path).unwrap(); + + let keypair = Keypair::generate(None); + let private_key = keypair.to_base58(); + + let default_cfg = Self::default(); + + let configuration = Configuration { + node: NodeConfiguration { + ip: self.ephemera_ip.unwrap_or(default_cfg.ephemera_ip.unwrap()), + private_key, + }, + libp2p: Libp2pConfiguration { + port: self + .ephemera_protocol_port + .unwrap_or(default_cfg.ephemera_protocol_port.unwrap()), + ephemera_msg_topic_name: DEFAULT_MESSAGES_TOPIC_NAME.to_string(), + heartbeat_interval_sec: DEFAULT_HEARTBEAT_INTERVAL_SEC, + members_provider_delay_sec: self.members_provider_delay_sec, + membership_kind: self.membership_kind.into(), + }, + storage: DatabaseConfiguration { + rocksdb_path: rocksdb_path.as_os_str().to_str().unwrap().to_string(), + sqlite_path: sqlite_path.as_os_str().to_str().unwrap().to_string(), + create_if_not_exists: true, + }, + websocket: WebsocketConfiguration { + port: self + .ephemera_websocket_port + .unwrap_or(default_cfg.ephemera_websocket_port.unwrap()), + }, + http: HttpConfiguration { + port: self + .ephemera_http_api_port + .unwrap_or(default_cfg.ephemera_http_api_port.unwrap()), + }, + block_manager: BlockManagerConfiguration { + producer: self.block_producer, + creation_interval_sec: self.block_creation_interval_sec, + repeat_last_block_messages: self.repeat_last_block_messages, + }, + }; + + configuration.try_write_home_dir(id).ok(); + } +} diff --git a/ephemera/src/cli/mod.rs b/ephemera/src/cli/mod.rs new file mode 100644 index 0000000000..1828d89347 --- /dev/null +++ b/ephemera/src/cli/mod.rs @@ -0,0 +1,49 @@ +use crate::cli::crypto::GenerateKeypairCmd; +use clap::Parser; + +pub mod config; +mod crypto; +pub mod init; +pub mod peers; +pub mod run_node; + +pub const PEERS_CONFIG_FILE: &str = "peers.toml"; + +#[derive(Parser)] +#[command()] +pub struct Cli { + #[command(subcommand)] + pub subcommand: Subcommand, +} + +#[derive(clap::Subcommand)] +pub enum Subcommand { + InitConfig(init::Cmd), + InitLocalPeersConfig(peers::CreateLocalPeersConfiguration), + RunNode(run_node::RunExternalNodeCmd), + GenerateKeypair(crypto::GenerateKeypairCmd), + UpdateConfig(config::UpdateConfigCmd), +} + +impl Cli { + /// # Errors + /// Returns an error if the subcommand fails. + pub async fn execute(self) -> anyhow::Result<()> { + match self.subcommand { + Subcommand::InitConfig(init) => { + init.execute(None); + } + Subcommand::InitLocalPeersConfig(add_local_peers) => { + add_local_peers.execute(); + } + Subcommand::RunNode(run_node) => run_node.execute().await?, + Subcommand::GenerateKeypair(_) => { + GenerateKeypairCmd::execute(); + } + Subcommand::UpdateConfig(update_config) => { + update_config.execute(); + } + } + Ok(()) + } +} diff --git a/ephemera/src/cli/peers.rs b/ephemera/src/cli/peers.rs new file mode 100644 index 0000000000..c77f4974e2 --- /dev/null +++ b/ephemera/src/cli/peers.rs @@ -0,0 +1,65 @@ +//! This is used to create local peers configuration file. Useful for local cluster development. + +use crate::cli::PEERS_CONFIG_FILE; +use clap::Parser; + +use crate::config::Configuration; +use crate::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair}; +use crate::membership::PeerSetting; +use crate::network::members::ConfigPeers; + +#[derive(Debug, Clone, Parser)] +pub struct CreateLocalPeersConfiguration; + +impl CreateLocalPeersConfiguration { + /// # Panics + /// Panics if the configuration file cannot be written. + pub fn execute(self) { + let peers = Self::from_ephemera_dev_cluster_conf().unwrap(); + let config_peers = ConfigPeers::new(peers); + + let peers_conf_path = Configuration::ephemera_root_dir(None) + .unwrap() + .join(PEERS_CONFIG_FILE); + + config_peers.try_write(peers_conf_path).unwrap(); + } + + //LOCAL DEV CLUSTER ONLY + //Get peers from dev Ephemera cluster config files + pub(crate) fn from_ephemera_dev_cluster_conf() -> anyhow::Result> { + let ephemera_root_dir = Configuration::ephemera_root_dir(None).unwrap(); + + let mut peers = vec![]; + + let home_dir = std::fs::read_dir(ephemera_root_dir)?; + for entry in home_dir { + let path = entry?.path(); + if path.is_dir() { + let cosmos_address = path.file_name().unwrap().to_str().unwrap(); + + println!("Reading peer info config from node {cosmos_address}",); + + let conf = Configuration::try_load_from_home_dir().unwrap_or_else(|_| { + panic!("Error loading configuration for node {cosmos_address}") + }); + + let node_info = conf.node; + + let keypair = bs58::decode(&node_info.private_key).into_vec().unwrap(); + let keypair = Keypair::from_bytes(&keypair).unwrap(); + + let peer = PeerSetting { + cosmos_address: cosmos_address.to_string(), + address: format!("/ip4/{}/tcp/{}", node_info.ip, conf.libp2p.port), + public_key: keypair.public_key().to_base58(), + }; + peers.push(peer); + + println!("Loaded config for node {cosmos_address}",); + } + } + + Ok(peers) + } +} diff --git a/ephemera/src/cli/run_node.rs b/ephemera/src/cli/run_node.rs new file mode 100644 index 0000000000..a33854f52d --- /dev/null +++ b/ephemera/src/cli/run_node.rs @@ -0,0 +1,140 @@ +use anyhow::anyhow; +use std::str::FromStr; +use std::sync::Arc; + +use clap::Parser; +use log::trace; +use nym_task::TaskManager; +use reqwest::Url; + +use crate::ephemera_api::ApplicationResult; +use crate::utilities::codec::{Codec, EphemeraCodec}; +use crate::{ + api::application::CheckBlockResult, + cli::PEERS_CONFIG_FILE, + config::Configuration, + crypto::EphemeraKeypair, + crypto::Keypair, + ephemera_api::{ApiBlock, ApiEphemeraMessage, Application, Dummy, RawApiEphemeraMessage}, + network::members::ConfigMembersProvider, + EphemeraStarterInit, +}; + +#[derive(Clone, Debug)] +pub struct HttpMembersProviderArg { + pub url: Url, +} + +impl FromStr for HttpMembersProviderArg { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(HttpMembersProviderArg { url: s.parse()? }) + } +} + +#[derive(Parser)] +pub struct RunExternalNodeCmd { + #[clap(short, long)] + pub config_file: String, + #[clap(short, long)] + pub peers_config: String, +} + +impl RunExternalNodeCmd { + /// # Errors + /// If the members provider cannot be created. + /// + /// # Panics + /// If the ephemera cannot be created. + pub async fn execute(&self) -> anyhow::Result<()> { + let ephemera_conf = match Configuration::try_load(self.config_file.clone()) { + Ok(conf) => conf, + Err(err) => anyhow::bail!("Error loading configuration file: {err:?}"), + }; + + let members_provider = Self::config_members_provider_with_path(self.peers_config.clone())?; + let ephemera = EphemeraStarterInit::new(ephemera_conf.clone()) + .unwrap() + .with_application(Dummy) + .with_members_provider(members_provider)? + .build(); + + let shutdown = TaskManager::new(10); + + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(ephemera.run(shutdown_listener)); + + if let Err(err) = shutdown.catch_interrupt().await { + Err(anyhow!("Shutdown error {:?}", err)) + } else { + Ok(()) + } + } + + #[allow(dead_code)] + fn config_members_provider() -> anyhow::Result { + let peers_conf_path = Configuration::ephemera_root_dir(None) + .unwrap() + .join(PEERS_CONFIG_FILE); + + let peers_conf = match ConfigMembersProvider::init(peers_conf_path) { + Ok(conf) => conf, + Err(err) => anyhow::bail!("Error loading peers file: {err:?}"), + }; + Ok(peers_conf) + } + + #[allow(dead_code)] + fn config_members_provider_with_path( + peers_conf_path: String, + ) -> anyhow::Result { + let peers_conf = match ConfigMembersProvider::init(peers_conf_path) { + Ok(conf) => conf, + Err(err) => anyhow::bail!("Error loading peers file: {err:?}"), + }; + Ok(peers_conf) + } +} + +pub struct SignatureVerificationApplication { + keypair: Arc, +} + +impl SignatureVerificationApplication { + #[must_use] + pub fn new(keypair: Arc) -> Self { + Self { keypair } + } + + pub(crate) fn verify_message(&self, msg: ApiEphemeraMessage) -> anyhow::Result<()> { + let signature = msg.certificate.clone(); + let raw_message: RawApiEphemeraMessage = msg.into(); + let encoded_message = Codec::encode(&raw_message)?; + if self + .keypair + .verify(&encoded_message, &signature.signature.into()) + { + Ok(()) + } else { + anyhow::bail!("Invalid signature") + } + } +} + +impl Application for SignatureVerificationApplication { + fn check_tx(&self, tx: ApiEphemeraMessage) -> ApplicationResult { + trace!("SignatureVerificationApplicationHook::check_tx"); + self.verify_message(tx)?; + Ok(true) + } + + fn check_block(&self, _block: &ApiBlock) -> ApplicationResult { + Ok(CheckBlockResult::Accept) + } + + fn deliver_block(&self, _block: ApiBlock) -> ApplicationResult<()> { + trace!("SignatureVerificationApplicationHook::deliver_block"); + Ok(()) + } +} diff --git a/ephemera/src/config/mod.rs b/ephemera/src/config/mod.rs new file mode 100644 index 0000000000..bf30850928 --- /dev/null +++ b/ephemera/src/config/mod.rs @@ -0,0 +1,334 @@ +//! Configuration for Ephemeris node. It contains mandatory settings for a node to start. +//! +//! Default location for the configuration file is `~/.nym/ephemera/ephemera.toml`. +//! Or relative to a node specific directory `~/.nym/ephemera//ephemera.toml`. + +use std::io::Write; +use std::path::PathBuf; + +use config::ConfigError; +use log::{error, info}; +use nym_config::{DEFAULT_NYM_APIS_DIR, NYM_DIR}; +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; + +//TODO - validate configuration at load time +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Configuration { + /// Configuration related to node instance identity + pub node: NodeConfiguration, + /// Configuration for libp2p network + pub libp2p: Libp2pConfiguration, + /// Configuration for Ephemera embedded database + pub storage: DatabaseConfiguration, + /// Configuration for websocket + pub websocket: WebsocketConfiguration, + /// Configuration for embedded http API server + pub http: HttpConfiguration, + /// Configuration related to block creation + pub block_manager: BlockManagerConfiguration, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct NodeConfiguration { + /// Node IP shared by all Ephemera services like libp2p, websocket, http. + /// If separate IP/DNS is needed for each service, it should be configured outside of Ephemera. + pub ip: String, + //FIXME: dev only + /// If private key is stored in configuration as plain text, read it from here. + /// Private key is mandatory for a node to be able to function in the network. + /// It is used to signe protocol messages and identify node in the network. + pub private_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum MembershipKind { + /// Mandatory minimum membership size is defined by threshold of all peers returned by membership provider. + /// Threshold value is defined the ratio of peers that need to be available. + /// For example, if the threshold is 0.5, then at least 50% of the peers need to be available. + Threshold, + /// Mandatory minimum membership size is all peers who are online. + AnyOnline, + /// Mandatory minimum membership size is all peers returned by membership provider. + AllOnline, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Libp2pConfiguration { + /// Port to listen on for libp2p internal connections + pub port: u16, + /// Gossipsub topic to gossip Ephemera messages between peers. Ephemera listens messages + /// only from this topic. Invalid topic configuration means that Ephemera is not able to + /// reach messages from other peers. + pub ephemera_msg_topic_name: String, + /// Gossipsub interval to check its mesh health. + pub heartbeat_interval_sec: u64, + /// How often Ephemera checks its membership rendezvous endpoint. It's configurable with second granularity. + /// But in general it's up to the user and depends how rendezvous endpoint is configured. + /// + /// Ephemera uses rendezvous endpoint as authority to tell which nodes are authorized to participate. + /// So it should be configured and implemented in a manner that nodes always have the most up to date and + /// accurate information. + pub members_provider_delay_sec: u64, + /// Defines how the actual membership is decided. See `[ephemera:]` for more details. + pub membership_kind: MembershipKind, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DatabaseConfiguration { + /// Path to the RocksDb database directory + pub rocksdb_path: String, + /// Path to the SQLite database file + pub sqlite_path: String, + /// If to create database if it does not exist + pub create_if_not_exists: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct WebsocketConfiguration { + /// Port to listen on for WebSocket subscriptions. + pub port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct HttpConfiguration { + /// Port to listen on for HTTP API requests + pub port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BlockManagerConfiguration { + /// By default every node is block producer. + /// + /// But in trusted settings it might be useful configure only one or selected nodes to be block producer. + /// The rest of nodes still participate in the message gossiping and reliable broadcast. + pub producer: bool, + /// Interval in seconds between block creation + /// Blocks are "proposed" at this interval. + pub creation_interval_sec: u64, + /// Ephemera creates blocks at fixed interval and doesn't have any consensus algorithm to make progress + /// if the most recent block fails to go through reliable broadcast. + /// This flag tells what to do when at next interval previous block is not yet delivered. + /// + /// If true, Ephemera will repeat messages from the previous block. Otherwise it will take all messages + /// from mempool as normally. + pub repeat_last_block_messages: bool, +} + +impl BlockManagerConfiguration { + pub fn new(producer: bool, creation_interval_sec: u64, repeat_last_block: bool) -> Self { + BlockManagerConfiguration { + producer, + creation_interval_sec, + repeat_last_block_messages: repeat_last_block, + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + /// This is returned if configuration file exists and user tries to create new one. + #[error("Configuration file exists: '{0}'")] + Exists(String), + /// This is returned if configuration file does not exist and user tries to load it. + #[error("Configuration file does not exists: '{0}'")] + NotFound(String), + #[error("Configuration file does not exist")] + /// This is returned if IoError happens during configuration file read/write. + IoError(#[from] std::io::Error), + /// This is returned if configuration file is invalid. + #[error("Configuration file is invalid: '{0}'")] + InvalidFormat(String), + /// This is returned if configuration file path is invalid. + #[error("Configuration file path is invalid: '{0}'")] + InvalidPath(String), + /// Technical error happens during parsing. + #[error("{}", .0)] + Other(String), +} + +impl From for Error { + fn from(err: ConfigError) -> Self { + match err { + ConfigError::NotFound(err) => Error::NotFound(err), + ConfigError::PathParse(err) => { + Error::InvalidPath(format!("Invalid path to configuration file: {err:?}",)) + } + ConfigError::FileParse { uri, cause } => { + Error::InvalidFormat(format!("Invalid configuration file: {uri:?}: {cause:?}",)) + } + _ => Error::Other(err.to_string()), + } + } +} + +const EPHEMERA_DIR_NAME: &str = "ephemera"; +const EPHEMERA_CONFIG_FILE: &str = "ephemera.toml"; + +type Result = std::result::Result; + +impl Configuration { + /// Tries to read Ephemera node configuration file (`ephemera.toml`) from the given path. + /// + /// # Arguments + /// * `path` - Path to the configuration file. + /// + /// # Errors + /// Returns an error if the configuration file does not exist or is invalid. + /// + /// # Example + /// ```no_run + /// use ephemera::configuration::Configuration; + /// + /// let config = Configuration::try_load("/ephemera/ephemera.toml"); + /// ``` + pub fn try_load>(path: P) -> Result { + let buf = path.into(); + log::debug!("Loading configuration from {:?}", buf); + let config = config::Config::builder() + .add_source(config::File::from(buf)) + .build() + .map_err(Error::from)?; + + config.try_deserialize().map_err(Error::from) + } + + /// Tries to read Ephemera node configuration from default + /// default Ephemera configuration directory(`~.nym/ephemera`). Full path resolves + /// as `~/.nym/ephemera//`. + /// + /// # Arguments + /// * `node_name` - Name of the node. + /// * `file` - Name of the configuration file. + /// + /// # Errors + /// Returns an error if the configuration file does not exist or is invalid. + pub fn try_load_node_from_home_dir(file: &str) -> Result { + let file_path = Self::ephemera_node_dir(None)?.join(file); + Configuration::try_load(file_path) + } + + /// Tries to read Ephemera node configuration from default Ephemera configuration directory(`~.nym/ephemera`). + /// Full path resolves as `~/.nym/ephemera//ephemera.toml`. + /// + /// # Arguments + /// * `node_name` - Name of the node. + /// + /// # Errors + /// Returns an error if the configuration file does not exist or is invalid. + pub fn try_load_from_home_dir() -> Result { + let file_path = Configuration::ephemera_config_file_home(None)?; + let config = config::Config::builder() + .add_source(config::File::from(file_path)) + .build() + .map_err(Error::from)?; + + config.try_deserialize().map_err(Error::from) + } + + /// Tries to write(create) Ephemera node configuration file (`ephemera.toml`) relative to default + /// Ephemera configuration directory(`~.nym/ephemera`). Full path resolves as `~/.nym/ephemera//ephemera.toml`. + /// + /// # Arguments + /// * `id` - Id of the node. + /// + /// # Errors + /// Returns an error if the configuration file already exists. + /// + /// # Panics + /// Panics if the configuration file cannot be written. + pub fn try_write_home_dir(&self, id: Option<&str>) -> Result<()> { + let conf_path = Configuration::ephemera_node_dir(id)?; + if !conf_path.exists() { + std::fs::create_dir_all(conf_path)?; + } + + let file_path = Configuration::ephemera_config_file_home(id)?; + if file_path.exists() { + return Err(Error::Exists(file_path.to_str().unwrap().to_string())); + } + + self.write(&file_path)?; + Ok(()) + } + + /// Tries to write(update) Ephemera node configuration file (`ephemera.toml`) relative to default + /// Ephemera configuration directory(`~.nym/ephemera`). Full path resolves as `~/.nym/ephemera//ephemera.toml`. + /// If the file does not exist, update will be refused. + /// + /// # Arguments + /// * `node_name` - Name of the node. + /// + /// # Errors + /// Returns an error if the configuration file does not exist. + /// + /// # Panics + /// Panics if the configuration file cannot be written. + pub fn try_update_home_dir(&self) -> Result<()> { + let file_path = Configuration::ephemera_config_file_home(None)?; + if !file_path.exists() { + error!( + "Configuration file does not exist {}", + file_path.to_str().unwrap() + ); + return Err(Error::NotFound(file_path.to_str().unwrap().to_string())); + } + self.write(&file_path)?; + Ok(()) + } + + /// Returns node configuration file path relative to default Ephemera configuration directory(`~.nym/ephemera`). + /// Full path resolves as `~/.nym/ephemera//ephemera.toml`. + /// + /// # Arguments + /// * `id` - Id of the node. + /// + /// # Errors + /// Returns an error if the configuration file path cannot be resolved. + pub fn ephemera_config_file_home(id: Option<&str>) -> Result { + Ok(Self::ephemera_node_dir(id)?.join(EPHEMERA_CONFIG_FILE)) + } + + /// Returns default Ephemera configuration directory(`~.nym/ephemera`). + /// + /// # Errors + /// Returns an error if the configuration directory cannot be resolved. + pub fn ephemera_root_dir(id: Option<&str>) -> Result { + let id = id.unwrap_or_default(); + dirs::home_dir() + .map(|home| { + home.join(NYM_DIR) + .join(DEFAULT_NYM_APIS_DIR) + .join(id) + .join(EPHEMERA_DIR_NAME) + }) + .ok_or(Error::Other("Could not find home directory".to_string())) + } + + pub(crate) fn ephemera_node_dir(id: Option<&str>) -> Result { + Self::ephemera_root_dir(id) + } + + fn write(&self, file_path: &PathBuf) -> Result<()> { + //TODO: use toml or config crate, not both + let config = toml::to_string(&self).map_err(|e| { + Error::InvalidFormat(format!("Failed to serialize configuration: {e}",)) + })?; + + let config = format!( + "#This file is generated by cli and automatically overwritten every time when cli is run\n{config}", + ); + + if file_path.exists() { + info!("Updating configuration file: '{}'", file_path.display()); + } else { + info!("Writing configuration to file: '{}'", file_path.display()); + } + + let mut file = std::fs::File::create(file_path)?; + file.write_all(config.as_bytes())?; + + Ok(()) + } +} diff --git a/ephemera/src/core/api_cmd.rs b/ephemera/src/core/api_cmd.rs new file mode 100644 index 0000000000..2d5647adec --- /dev/null +++ b/ephemera/src/core/api_cmd.rs @@ -0,0 +1,390 @@ +use std::num::NonZeroUsize; + +use log::{debug, error, trace}; +use lru::LruCache; +use tokio::sync::oneshot::Sender; + +use crate::api::types::{ApiBlockBroadcastInfo, ApiBroadcastInfo}; +use crate::api::{DhtKV, DhtKey, DhtValue}; +use crate::ephemera_api::ApiEphemeraMessage; +use crate::peer::ToPeerId; +use crate::{ + api::{ + self, + application::Application, + types::{ApiBlock, ApiCertificate, ApiError}, + ToEphemeraApiCmd, + }, + block::{manager::BlockManagerError, types::message}, + crypto::EphemeraKeypair, + ephemera_api::ApiEphemeraConfig, + network::libp2p::ephemera_sender::EphemeraEvent, + Ephemera, +}; + +type DhtPendingQueryReply = Sender, Vec)>, ApiError>>; + +pub(crate) struct ApiCmdProcessor { + pub(crate) dht_query_cache: LruCache, Vec>, +} + +impl ApiCmdProcessor { + pub(crate) fn new() -> Self { + Self { + dht_query_cache: LruCache::new(NonZeroUsize::new(1000).unwrap()), + } + } + + pub(crate) async fn process_api_requests( + ephemera: &mut Ephemera, + cmd: ToEphemeraApiCmd, + ) -> api::Result<()> { + trace!("Processing API request: {:?}", cmd); + match cmd { + ToEphemeraApiCmd::SubmitEphemeraMessage(api_msg, reply) => { + // Ask application to decide if we should accept this message. + Self::submit_message(ephemera, api_msg, reply).await?; + } + + ToEphemeraApiCmd::QueryBlockByHash(block_hash, reply) => { + Self::query_block_by_hash(ephemera, &block_hash, reply).await; + } + + ToEphemeraApiCmd::QueryBlockByHeight(height, reply) => { + Self::query_block_by_height(ephemera, height, reply).await; + } + + ToEphemeraApiCmd::QueryLastBlock(reply) => { + Self::query_last_block(ephemera, reply).await; + } + + ToEphemeraApiCmd::QueryBlockCertificates(block_id, reply) => { + Self::query_block_certificates(ephemera, &block_id, reply).await; + } + + ToEphemeraApiCmd::QueryDht(key, reply) => { + Self::query_dht(ephemera, key, reply).await; + } + + ToEphemeraApiCmd::StoreInDht(key, value, reply) => { + Self::store_in_dht(ephemera, key, value, reply).await; + } + + ToEphemeraApiCmd::QueryEphemeraConfig(reply) => { + Self::ephemera_config(ephemera, reply); + } + + ToEphemeraApiCmd::QueryBroadcastGroup(reply) => { + Self::broadcast_group(ephemera, reply); + } + ToEphemeraApiCmd::QueryBlockBroadcastInfo(hash, reply) => { + Self::query_block_broadcast_info(ephemera, &hash, reply).await; + } + ToEphemeraApiCmd::VerifyMessageInBlock(block_hash, message_hash, index, reply) => { + Self::verify_message_in_block(ephemera, block_hash, message_hash, index, reply) + .await; + } + } + Ok(()) + } + + fn broadcast_group( + ephemera: &mut Ephemera, + reply: Sender>, + ) { + let group_peers = ephemera.broadcast_group.current(); + + let bc = ApiBroadcastInfo::new(group_peers.clone(), ephemera.node_info.peer_id); + reply + .send(Ok(bc)) + .expect("Error sending BroadcastGroup response to api"); + } + + fn ephemera_config( + ephemera: &mut Ephemera, + reply: Sender>, + ) { + let node_info = ephemera.node_info.clone(); + let api_config = ApiEphemeraConfig { + protocol_address: node_info.protocol_address(), + api_address: node_info.api_address_http(), + websocket_address: node_info.ws_address_ws(), + public_key: node_info.keypair.public_key().to_string(), + block_producer: node_info.initial_config.block_manager.producer, + block_creation_interval_sec: node_info + .initial_config + .block_manager + .creation_interval_sec, + }; + reply + .send(Ok(api_config)) + .expect("Error sending EphemeraConfig response to api"); + } + + async fn store_in_dht( + ephemera: &mut Ephemera, + key: DhtKey, + value: DhtValue, + reply: Sender>, + ) { + let response = match ephemera + .to_network + .send_ephemera_event(EphemeraEvent::StoreInDht { key, value }) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("Error sending StoreInDht to network: {:?}", err); + Err(ApiError::Internal("Failed to store in DHT".to_string())) + } + }; + reply + .send(response) + .expect("Error sending StoreInDht response to api"); + } + + async fn query_dht( + ephemera: &mut Ephemera, + key: DhtKey, + reply: Sender>>, + ) { + match ephemera + .to_network + .send_ephemera_event(EphemeraEvent::QueryDht { key: key.clone() }) + .await + { + Ok(_) => { + //Save the reply channel in a map and send the reply when we get the response from the network + ephemera + .api_cmd_processor + .dht_query_cache + .get_or_insert_mut(key, Vec::new) + .push(reply); + } + Err(err) => { + error!("Error sending QueryDht to network: {:?}", err); + reply + .send(Err(ApiError::Internal("Failed to query DHT".to_string()))) + .expect("Error sending QueryDht response to api"); + } + }; + } + + async fn query_block_certificates( + ephemera: &mut Ephemera, + block_id: &str, + reply: Sender>>>, + ) { + let response = match ephemera + .storage + .lock() + .await + .get_block_certificates(block_id) + { + Ok(signatures) => { + let certificates = signatures.map(|s| { + s.into_iter() + .map(Into::into) + .collect::>() + }); + Ok(certificates) + } + Err(err) => { + error!("Error querying block certificates: {:?}", err); + Err(ApiError::Internal( + "Failed to query block certificates".to_string(), + )) + } + }; + reply + .send(response) + .expect("Error sending QueryBlockSignatures response to api"); + } + + async fn query_last_block( + ephemera: &mut Ephemera, + reply: Sender>, + ) { + let response = match ephemera.storage.lock().await.get_last_block() { + Ok(Some(block)) => Ok(block.into()), + Ok(None) => Err(ApiError::Internal( + "No blocks found, this is a bug!".to_string(), + )), + Err(err) => { + error!("Error querying last block: {:?}", err); + Err(ApiError::Internal("Failed to query last block".to_string())) + } + }; + reply + .send(response) + .expect("Error sending QueryLastBlock response to api"); + } + + async fn query_block_by_height( + ephemera: &mut Ephemera, + height: u64, + reply: Sender>>, + ) { + let response = match ephemera.storage.lock().await.get_block_by_height(height) { + Ok(Some(block)) => { + let api_block: ApiBlock = block.into(); + Ok(api_block.into()) + } + Ok(None) => Ok(None), + Err(err) => { + error!("Error querying block by height: {:?}", err); + Err(ApiError::Internal( + "Failed to query block by height".to_string(), + )) + } + }; + reply + .send(response) + .expect("Error sending QueryBlockByHeight response to api"); + } + + async fn query_block_by_hash( + ephemera: &mut Ephemera, + block_hash: &str, + reply: Sender>>, + ) { + let response = match ephemera.storage.lock().await.get_block_by_hash(block_hash) { + Ok(Some(block)) => { + let api_block: ApiBlock = block.into(); + Ok(api_block.into()) + } + Ok(None) => Ok(None), + Err(err) => { + error!("Error querying block by id: {:?}", err); + Err(ApiError::Internal( + "Failed to query block by id".to_string(), + )) + } + }; + reply + .send(response) + .expect("Error sending QueryBlockByHash response to api"); + } + + async fn submit_message( + ephemera: &mut Ephemera, + api_msg: Box, + reply: Sender>, + ) -> api::Result<()> { + let response = match ephemera.application.check_tx(*api_msg.clone()) { + Ok(true) => { + trace!("Application accepted ephemera message: {:?}", api_msg); + + // Send to BlockManager to verify it and put into memory pool + let ephemera_msg: message::EphemeraMessage = (*api_msg).into(); + match ephemera.block_manager.on_new_message(ephemera_msg.clone()) { + Ok(_) => { + //Gossip to network for other nodes to receive + match ephemera + .to_network + .send_ephemera_event(EphemeraEvent::EphemeraMessage( + ephemera_msg.into(), + )) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("Error sending EphemeraMessage to network: {:?}", err); + Err(ApiError::Internal("Failed to submit message".to_string())) + } + } + } + Err(err) => match err { + BlockManagerError::DuplicateMessage(_) => Err(ApiError::DuplicateMessage), + BlockManagerError::BlockManager(err) => { + error!("Error submitting message to block manager: {:?}", err); + Err(ApiError::Internal("Failed to submit message".to_string())) + } + }, + } + } + Ok(false) => { + debug!("Application rejected ephemera message: {:?}", api_msg); + Err(ApiError::ApplicationRejectedMessage) + } + Err(err) => { + error!("Application rejected ephemera message: {:?}", err); + Err(ApiError::Application(err)) + } + }; + reply + .send(response) + .expect("Error sending SubmitEphemeraMessage response to api"); + Ok(()) + } + + async fn query_block_broadcast_info( + ephemera: &mut Ephemera, + block_id: &str, + reply: Sender>>, + ) { + let response = match ephemera + .storage + .lock() + .await + .get_block_broadcast_group(block_id) + { + Ok(Some(peers)) => { + let local_peer = ephemera.node_info.keypair.peer_id(); + Ok(Some(ApiBlockBroadcastInfo::new(local_peer, peers))) + } + Ok(None) => Ok(None), + Err(err) => { + error!("Error querying block broadcast info: {:?}", err); + Err(ApiError::Internal( + "Failed to query block broadcast info".to_string(), + )) + } + }; + reply + .send(response) + .expect("Error sending QueryBlockBroadcastGroup response to api"); + } + async fn verify_message_in_block( + ephemera: &mut Ephemera, + block_hash: String, + message_hash: String, + index: usize, + reply: Sender>, + ) { + let message_hash_hash = message_hash.parse(); + if message_hash_hash.is_err() { + reply + .send(Err(ApiError::InvalidHash( + "Failed to parse message hash".to_string(), + ))) + .expect("Error sending VerifyMessageInBlock response to api"); + return; + } + + let message_hash_hash = message_hash_hash.unwrap(); + + let storage = ephemera.storage.lock().await; + match storage.get_block_merkle_tree(&block_hash) { + Ok(Some(tree)) => { + let result = tree.verify_leaf_at_index(message_hash_hash, index); + reply + .send(Ok(result)) + .expect("Error sending VerifyMessageInBlock response to api"); + } + Ok(None) => { + reply + .send(Ok(false)) + .expect("Error sending VerifyMessageInBlock response to api"); + } + Err(err) => { + error!("Error querying block merkle tree: {:?}", err); + reply + .send(Err(ApiError::Internal( + "Failed to verify message".to_string(), + ))) + .expect("Error sending VerifyMessageInBlock response to api"); + } + } + } +} diff --git a/ephemera/src/core/builder.rs b/ephemera/src/core/builder.rs new file mode 100644 index 0000000000..db9b3e8254 --- /dev/null +++ b/ephemera/src/core/builder.rs @@ -0,0 +1,413 @@ +use std::fmt::Display; +use std::future::Future; +use std::sync::Arc; + +use futures_util::future::BoxFuture; +use futures_util::FutureExt; +use log::{debug, error, info}; +use tokio::sync::Mutex; + +use crate::core::shutdown::Shutdown; +#[cfg(feature = "rocksdb_storage")] +use crate::storage::rocksdb::RocksDbStorage; +#[cfg(feature = "sqlite_storage")] +use crate::storage::sqlite::SqliteStorage; +use crate::{ + api::{application::Application, http, ApiListener, CommandExecutor}, + block::{builder::BlockManagerBuilder, manager::BlockManager}, + broadcast::bracha::broadcast::Broadcaster, + broadcast::group::BroadcastGroup, + config::Configuration, + core::{ + api_cmd::ApiCmdProcessor, + shutdown::{Handle, ShutdownManager}, + }, + crypto::Keypair, + membership, + membership::PeerInfo, + network::libp2p::{ + ephemera_sender::EphemeraToNetworkSender, network_sender::NetCommunicationReceiver, + swarm_network::SwarmNetwork, + }, + peer::{PeerId, ToPeerId}, + storage::EphemeraDatabase, + utilities::crypto::key_manager::KeyManager, + websocket::ws_manager::{WsManager, WsMessageBroadcaster}, + Ephemera, +}; + +#[derive(Clone)] +pub(crate) struct NodeInfo { + pub(crate) ip: String, + pub(crate) protocol_port: u16, + pub(crate) http_port: u16, + pub(crate) ws_port: u16, + pub(crate) peer_id: PeerId, + pub(crate) keypair: Arc, + pub(crate) initial_config: Configuration, +} + +impl NodeInfo { + pub(crate) fn new(config: Configuration) -> anyhow::Result { + let keypair = KeyManager::read_keypair_from_str(&config.node.private_key)?; + let info = Self { + ip: config.node.ip.clone(), + protocol_port: config.libp2p.port, + http_port: config.http.port, + ws_port: config.websocket.port, + peer_id: keypair.peer_id(), + keypair, + initial_config: config, + }; + Ok(info) + } + + pub(crate) fn protocol_address(&self) -> String { + format!("/ip4/{}/tcp/{}", self.ip, self.protocol_port) + } + + pub(crate) fn api_address_http(&self) -> String { + format!("http://{}:{}", self.ip, self.http_port) + } + + pub(crate) fn ws_address_ws(&self) -> String { + format!("ws://{}:{}", self.ip, self.ws_port) + } + + pub(crate) fn ws_address_ip_port(&self) -> String { + format!("{}:{}", self.ip, self.ws_port) + } +} + +impl Display for NodeInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "NodeInfo {{ ip: {}, protocol_port: {}, http_port: {}, ws_port: {}, peer_id: {}, keypair: {} }}", + self.ip, + self.protocol_port, + self.http_port, + self.ws_port, + self.peer_id, + self.keypair + ) + } +} + +#[derive(Clone)] +pub struct EphemeraHandle { + /// Ephemera API + pub api: CommandExecutor, + /// Allows to send shutdown signal to the node + pub shutdown: Handle, +} + +pub struct EphemeraStarterInit { + config: Configuration, + node_info: NodeInfo, + broadcaster: Broadcaster, + api_listener: ApiListener, + api: CommandExecutor, +} + +impl EphemeraStarterInit { + /// Initialize Ephemera builder + /// + /// # Arguments + /// * `config` - [Configuration] + /// + /// # Returns + /// [`EphemeraStarterInit`] + /// + /// # Errors + /// * If the node configuration is invalid + pub fn new(config: Configuration) -> anyhow::Result { + let instance_info = NodeInfo::new(config.clone())?; + let broadcaster = Broadcaster::new(instance_info.peer_id); + let (api, api_listener) = CommandExecutor::new(); + + let builder = EphemeraStarterInit { + config, + node_info: instance_info, + broadcaster, + api_listener, + api, + }; + Ok(builder) + } + + pub fn with_application( + self, + application: A, + ) -> EphemeraStarterWithApplication { + EphemeraStarterWithApplication { + init: self, + application, + } + } +} + +#[derive(Default)] +struct ServiceInfo { + ws_message_broadcast: Option, + from_network: Option, + to_network: Option, +} + +pub struct EphemeraStarterWithApplication { + init: EphemeraStarterInit, + application: A, +} + +impl EphemeraStarterWithApplication { + /// Initialize Ephemera with the given application. + /// It also tries to open the database connection. + /// + /// # Arguments + /// * `application` - [Application] to be used + /// + /// # Returns + /// [`EphemeraStarterWithApplication`] + /// + /// # Errors + /// * If the node configuration is invalid or the database connection cannot be opened + pub fn with_members_provider< + P: Future>> + Send + Unpin + 'static, + >( + mut self, + provider: P, + ) -> anyhow::Result> { + cfg_if::cfg_if! { + if #[cfg(feature = "sqlite_storage")] { + let mut storage = self.connect_sqlite()?; + debug!("Connected to sqlite database") + } else if #[cfg(feature = "rocksdb_storage")] { + let mut storage = self.connect_rocksdb()?; + debug!("Connected to rocksdb database") + } else { + compile_error!("Must enable either sqlite or rocksdb feature"); + } + } + + let block_manager = self.init_block_manager(&mut storage)?; + + let (mut shutdown_manager, shutdown_handle) = ShutdownManager::init(); + + let mut service_data = ServiceInfo::default(); + let services = self.init_services(&mut service_data, &mut shutdown_manager, provider)?; + + Ok(EphemeraStarterWithProvider { + with_application: self, + block_manager: Some(block_manager), + service_data, + services, + storage: Some(Box::new(storage)), + shutdown_manager: Some(shutdown_manager), + shutdown_handle: Some(shutdown_handle), + }) + } + + //allocate database connection + #[cfg(feature = "rocksdb_storage")] + fn connect_rocksdb(&self) -> anyhow::Result { + info!("Opening database..."); + RocksDbStorage::open(self.init.config.storage.clone()) + .map_err(|e| anyhow::anyhow!("Failed to open database: {}", e)) + } + + #[cfg(feature = "sqlite_storage")] + fn connect_sqlite(&mut self) -> anyhow::Result { + info!("Opening database..."); + SqliteStorage::open(self.init.config.storage.clone()) + .map_err(|e| anyhow::anyhow!("Failed to open database: {}", e)) + } + + fn init_block_manager( + &mut self, + db: &mut D, + ) -> anyhow::Result { + let block_manager_configuration = self.init.config.block_manager.clone(); + let keypair = self.init.node_info.keypair.clone(); + let builder = BlockManagerBuilder::new(block_manager_configuration, keypair); + builder.build(db) + } + + fn init_services< + P: Future>> + Send + Unpin + 'static, + >( + &mut self, + service_data: &mut ServiceInfo, + shutdown_manager: &mut ShutdownManager, + provider: P, + ) -> anyhow::Result>>> { + let services = vec![ + self.init_libp2p(service_data, shutdown_manager.subscribe(), provider)?, + self.init_http(shutdown_manager.subscribe())?, + self.init_websocket(service_data, shutdown_manager.subscribe()), + ]; + Ok(services) + } + + fn init_websocket( + &mut self, + service_data: &mut ServiceInfo, + mut shutdown: Shutdown, + ) -> BoxFuture<'static, anyhow::Result<()>> { + let (mut websocket, ws_message_broadcast) = + WsManager::new(self.init.node_info.ws_address_ip_port()); + + service_data.ws_message_broadcast = Some(ws_message_broadcast); + + async move { + websocket.listen().await?; + + tokio::select! { + _ = shutdown.shutdown_signal_rcv.recv() => { + info!("Shutting down websocket manager"); + } + ws_stopped = websocket.run() => { + match ws_stopped { + Ok(_) => info!("Websocket stopped unexpectedly"), + Err(e) => error!("Websocket stopped with error: {}", e), + } + } + } + info!("Websocket task finished"); + Ok(()) + } + .boxed() + } + + fn init_http( + &mut self, + mut shutdown: Shutdown, + ) -> anyhow::Result>> { + let http = http::init(&self.init.node_info, self.init.api.clone())?; + + let fut = async move { + let server_handle = http.handle(); + tokio::select! { + _ = shutdown.shutdown_signal_rcv.recv() => { + info!("Shutting down http server"); + server_handle.stop(true).await; + } + http_stopped = http => { + match http_stopped { + Ok(_) => info!("Http server stopped unexpectedly"), + Err(e) => error!("Http server stopped with error: {}", e), + } + } + } + info!("Http task finished"); + Ok(()) + } + .boxed(); + Ok(fut) + } + + fn init_libp2p< + P: Future>> + Send + Unpin + 'static, + >( + &mut self, + service_data: &mut ServiceInfo, + mut shutdown: Shutdown, + provider: P, + ) -> anyhow::Result>> { + info!("Starting network...",); + + let (mut network, from_network, to_network) = + SwarmNetwork::new(self.init.node_info.clone(), provider)?; + + service_data.from_network = Some(from_network); + service_data.to_network = Some(to_network); + + let libp2p = async move { + network.listen()?; + + tokio::select! { + _ = shutdown.shutdown_signal_rcv.recv() => { + info!("Shutting down network"); + } + nw_stopped = network.start() => { + match nw_stopped { + Ok(_) => info!("Network stopped unexpectedly"), + Err(e) => error!("Network stopped with error: {e}",), + } + } + } + info!("Network task finished"); + Ok(()) + } + .boxed(); + Ok(libp2p) + } +} + +pub struct EphemeraStarterWithProvider +where + A: Application + 'static, +{ + with_application: EphemeraStarterWithApplication, + block_manager: Option, + service_data: ServiceInfo, + storage: Option>, + services: Vec>>, + shutdown_manager: Option, + shutdown_handle: Option, +} + +impl EphemeraStarterWithProvider +where + A: Application + 'static, +{ + pub fn build(self) -> Ephemera { + self.ephemera() + } + + fn ephemera(mut self) -> Ephemera { + let ephemera_handle = EphemeraHandle { + api: self.with_application.init.api, + shutdown: self.shutdown_handle.take().unwrap(), + }; + + let node_info = self.with_application.init.node_info; + let application = self.with_application.application; + let block_manager = self.block_manager.expect("Block manager not initialized"); + let broadcaster = self.with_application.init.broadcaster; + let from_network = self + .service_data + .from_network + .expect("From network not initialized"); + let to_network = self + .service_data + .to_network + .expect("To network not initialized"); + let storage = self.storage.expect("Storage not initialized"); + let ws_message_broadcast = self + .service_data + .ws_message_broadcast + .expect("WS message broadcast not initialized"); + let api_listener = self.with_application.init.api_listener; + let shutdown_manager = self + .shutdown_manager + .expect("Shutdown manager not initialized"); + let services = self.services; + + Ephemera { + node_info, + block_manager, + broadcaster, + from_network, + to_network, + broadcast_group: BroadcastGroup::new(), + storage: Arc::new(Mutex::new(storage)), + ws_message_broadcast, + api_listener, + api_cmd_processor: ApiCmdProcessor::new(), + application: application.into(), + ephemera_handle, + shutdown_manager, + services, + } + } +} diff --git a/ephemera/src/core/ephemera.rs b/ephemera/src/core/ephemera.rs new file mode 100644 index 0000000000..9c970b9962 --- /dev/null +++ b/ephemera/src/core/ephemera.rs @@ -0,0 +1,421 @@ +use std::sync::Arc; + +use anyhow::anyhow; +use futures_util::future::BoxFuture; +use futures_util::StreamExt; +use log::{debug, error, info, trace}; +use nym_task::TaskClient; +use thiserror::Error; +use tokio::sync::Mutex; + +use crate::broadcast::bracha::quorum::Quorum; +use crate::storage::DatabaseError; +use crate::{ + api::{application::Application, application::CheckBlockResult, ApiListener}, + block::{manager::BlockManager, types::block::Block}, + broadcast::{ + bracha::broadcast::BroadcastResponse, bracha::broadcast::Broadcaster, + group::BroadcastGroup, RbMsg, + }, + core::{ + api_cmd::ApiCmdProcessor, + builder::{EphemeraHandle, NodeInfo}, + shutdown::ShutdownManager, + }, + network::{ + libp2p::network_sender::GroupChangeEvent, + libp2p::{ + ephemera_sender::{EphemeraEvent, EphemeraToNetworkSender}, + network_sender::{NetCommunicationReceiver, NetworkEvent}, + }, + }, + storage::EphemeraDatabase, + utilities::crypto::Certificate, + websocket::ws_manager::WsMessageBroadcaster, +}; + +#[derive(Error, Debug)] +enum EphemeraCoreError { + #[error("DatabaseFailure: {0}")] + DatabaseFailure(DatabaseError), + //Just a placeholder now + #[error("EphemeraCore: {0}")] + EphemeraCore(#[from] anyhow::Error), +} + +type Result = std::result::Result; + +pub struct Ephemera { + /// Node info + pub(crate) node_info: NodeInfo, + + /// Block manager responsibility includes: + /// - Block production and signing + /// - Block verification for externally received blocks + /// - Message verification sent by clients and gossiped other nodes + pub(crate) block_manager: BlockManager, + + /// Broadcaster is making sure that blocks are deterministically agreed by all nodes. + pub(crate) broadcaster: Broadcaster, + + /// A component which receives messages from network. + pub(crate) from_network: NetCommunicationReceiver, + + /// A component which sends messages to network. + pub(crate) to_network: EphemeraToNetworkSender, + + /// A component which keeps track of broadcast group over time. + pub(crate) broadcast_group: BroadcastGroup, + + /// A component which has mutable access to database. + pub(crate) storage: Arc>>, + + /// A component which broadcasts messages to websocket clients. + pub(crate) ws_message_broadcast: WsMessageBroadcaster, + + /// A component which listens API requests. + pub(crate) api_listener: ApiListener, + + /// A component which processes API requests. + pub(crate) api_cmd_processor: ApiCmdProcessor, + + /// An implementation of Application trait. Provides callbacks to broadcast. + pub(crate) application: Arc, + + ///Interface to external Rust code + pub(crate) ephemera_handle: EphemeraHandle, + + /// A component which handles shutdown. + pub(crate) shutdown_manager: ShutdownManager, + + /// A list of services which are running in background. + pub(crate) services: Vec>>, +} + +impl Ephemera { + ///Provides external api for Rust code to interact with ephemera node. + #[must_use] + pub fn handle(&self) -> EphemeraHandle { + self.ephemera_handle.clone() + } + + /// Main loop of ephemera node. + /// 1. Block manager generates new blocks or receives blocks from network. + /// 2. Run reliable broadcast. + /// 3. Process reliable broadcast result. + /// 4. Process http api request + /// 5. Process rust api request + /// 6. Publish(gossip) messages to network + /// 7. Publish blocks to network + /// 8. Broadcast messages to websocket clients + pub async fn run(mut self, mut shutdown: TaskClient) { + info!("Starting ephemera services"); + for service in self.services.drain(..) { + let handle = tokio::spawn(service); + self.shutdown_manager.add_handle(handle); + } + + info!("Starting ephemera main loop"); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("UpdateHandler: Received shutdown"); + self.shutdown_manager.stop().await; + break; + } + + // GENERATING NEW BLOCKS + Some((new_block, certificate)) = self.block_manager.next() => { + if let Err(err) = self.process_new_local_block(new_block, certificate).await{ + error!("Error processing new block: {:?}", err); + } + } + + // PROCESSING NETWORK EVENTS + Some(net_event) = self.from_network.net_event_rcv.recv() => { + if let Err(err) = self.process_network_event(net_event).await{ + error!("Error processing network event: {:?}", err); + if let EphemeraCoreError::DatabaseFailure(_) = err { + info!("Database failure. Shutting down ephemera"); + self.shutdown_manager.stop().await; + break; + } + } + } + + //PROCESSING EXTERNAL API REQUESTS + api = self.api_listener.messages_rcv.recv() => { + match api { + Some(api_msg) => { + if let Err(err) = ApiCmdProcessor::process_api_requests(&mut self, api_msg).await{ + error!("Error processing api request: {:?}", err); + } + } + None => { + error!("Error: Api listener channel closed"); + //TODO: handle shutdown + } + } + } + } + } + info!("Ephemera main loop finished"); + } + + async fn process_network_event(&mut self, net_event: NetworkEvent) -> Result<()> { + trace!("New network event: {:?}", net_event); + + match net_event { + NetworkEvent::EphemeraMessage(em) => { + let api_msg = (*em.clone()).into(); + trace!("New ephemera message from network: {:?}", api_msg); + + //Only Application checks if messages are valid(possibly message origin). + //For messages we don't check if sender belongs to group. + + // Ask application to decide if we should accept this message. + match self.application.check_tx(api_msg) { + Ok(true) => { + trace!("Application accepted message: {:?}", em); + + // Send to BlockManager to store in mempool. + if let Err(err) = self.block_manager.on_new_message(*em) { + error!("Error sending signed message to block manager: {:?}", err); + } + } + Ok(false) => { + trace!("Application rejected message: {:?}", em); + } + Err(err) => { + error!("Application check_tx failed: {:?}", err); + } + } + } + NetworkEvent::BroadcastMessage(rb_msg) => { + self.process_block_from_network(*rb_msg).await?; + } + NetworkEvent::GroupUpdate(event) => { + self.process_group_update(event); + } + NetworkEvent::QueryDhtResponse { key, value } => { + match self.api_cmd_processor.dht_query_cache.pop(&key) { + Some(replies) => { + for reply in replies { + let response = Ok(Some((key.clone(), value.clone()))); + if let Err(err) = reply.send(response) { + error!("Error sending dht query response: {:?}", err); + } + } + } + None => { + trace!( + "No dht query cache found for key: {:?}", + String::from_utf8(key) + ); + } + } + } + } + Ok(()) + } + + fn process_group_update(&mut self, event: GroupChangeEvent) { + match event { + GroupChangeEvent::PeersUpdated(peers) => { + info!("New group: {:?}", peers); + info!("{}", Quorum::cluster_size_info(peers.len())); + self.broadcaster.group_updated(peers.len()); + self.broadcast_group.add_snapshot(peers); + self.block_manager.start(); + } + GroupChangeEvent::LocalPeerRemoved(peers) | GroupChangeEvent::NotEnoughPeers(peers) => { + info!("New group: {:?}", peers); + info!("Group update: Local peer removed or not enough peers"); + self.broadcaster.group_updated(0); + self.broadcast_group.add_snapshot(peers); + self.block_manager.stop(); + } + } + } + + async fn process_new_local_block( + &mut self, + new_block: Block, + certificate: Certificate, + ) -> Result<()> { + debug!("New block from block manager: {:?}", new_block.get_hash()); + + let hash = new_block.header.hash; + let block_creator = &self.node_info.peer_id; + let sender = &self.node_info.peer_id; + + // Check if block matches group membership. + if !self + .broadcast_group + .check_membership(hash, block_creator, sender) + { + debug!("Membership check rejected block: {:?}", new_block); + return Ok(()); + } + + //Ephemera ABCI + match self.application.check_block(&new_block.clone().into()) { + Ok(response) => match response { + CheckBlockResult::Accept => { + debug!("Application accepted new block: {hash:?}",); + } + CheckBlockResult::Reject => { + debug!("Application rejected block: {hash:?}",); + return Ok(()); + } + CheckBlockResult::RejectAndRemoveMessages(messages_to_remove) => { + debug!("Application rejected block: {:?}", messages_to_remove); + self.block_manager + .on_application_rejected_block(messages_to_remove) + .map_err(|err| { + anyhow!("Error rejecting block from block manager: {:?}", err) + })?; + } + }, + Err(err) => { + return Err(anyhow!("Application check_block failed: {:?}", err).into()); + } + } + + //Block manager generated new block that nobody hasn't seen yet. + //We start reliable broadcaster protocol to broadcaster it to other nodes. + match self.broadcaster.new_broadcast(new_block) { + Ok(resp) => { + if let BroadcastResponse::Broadcast(msg) = resp { + trace!("Broadcasting new block: {:?}", msg); + + let rb_msg = RbMsg::new(msg, certificate); + self.to_network + .send_ephemera_event(EphemeraEvent::ProtocolMessage(rb_msg.into())) + .await?; + } + } + Err(err) => { + error!("Error starting new broadcast: {:?}", err); + } + } + Ok(()) + } + + //TODO: should we accept more blocks(certificates) from peers after its committed? + async fn process_block_from_network(&mut self, msg: RbMsg) -> Result<()> { + let msg_id = msg.id.clone(); + let block = msg.block(); + let block_creator = &block.header.creator; + let sender = &msg.original_sender; + let hash = block.header.hash; + let certificate = msg.certificate.clone(); + + trace!("New broadcast message from network: {:?}", msg); + + if !self + .broadcast_group + .check_membership(hash, block_creator, sender) + { + return Err(anyhow!("Block doesn't match broacast group").into()); + } + + if let Err(err) = self.block_manager.on_block(sender, block, &certificate) { + return Err(anyhow!("Error sending block to block manager: {:?}", err).into()); + } + let raw_mgs = msg.into(); + match self.broadcaster.handle(&raw_mgs) { + Ok(resp) => { + match resp { + BroadcastResponse::Broadcast(msg) => { + trace!("Broadcasting block to network: {:?}", msg); + + match self.block_manager.sign_block(&msg.block()) { + Ok(certificate) => { + let rb_msg = RbMsg::new(msg, certificate); + self.to_network + .send_ephemera_event(EphemeraEvent::ProtocolMessage( + rb_msg.into(), + )) + .await?; + } + Err(err) => { + return Err(anyhow!("Error signing block: {:?}", err).into()); + } + } + } + BroadcastResponse::Deliver(hash) => { + trace!("Block broadcast complete: {hash:?}",); + let block = self.block_manager.get_block_by_hash(&hash); + match block { + Some(block) => { + if block.header.creator == self.node_info.peer_id { + info!("Block committed, ready to deliver...: {hash:?}",); + + //BlockManager + self.block_manager.on_block_committed(&block).map_err(|e| { + anyhow!( + "Error: BlockManager failed to process block: {e:?}", + ) + })?; + + //Save to database + let certificates = self + .block_manager + .get_block_certificates(&block.header.hash) + .ok_or(anyhow!( + "Error: Block certificates not found for block: {hash:?}" + ))?; + let members = self + .broadcast_group + .get_group_by_block_hash(block.get_hash()) + .ok_or(anyhow!( + "Error: Group not found for block: {hash:?}" + ))?; + + if let Err(e) = self.storage.lock().await.store_block( + &block, + certificates.clone(), + members.clone(), + ) { + return Err(EphemeraCoreError::DatabaseFailure(e)); + } + + // It is open question how much Application `deliver_block` failure should affect + // continuing with next block. + //Application(ABCI) + self.application + .deliver_block(Into::into(block.clone())) + .map_err(|e| { + anyhow!( + "Error: Deliver block to Application failed: {e:?}", + ) + })?; + + //WS + self.ws_message_broadcast.send_block(&block)?; + info!("Block broadcast complete: {hash:?}",); + } + } + None => { + return Err( + anyhow!("Error: Block not found in block manager").into() + ); + } + } + } + BroadcastResponse::Drop(hash) => { + trace!("Ignoring broadcast message {:?}[block {:?}]", msg_id, hash); + return Ok(()); + } + } + } + Err(err) => { + error!("Error handling broadcast message: {:?}", err); + } + } + Ok(()) + } +} diff --git a/ephemera/src/core/mod.rs b/ephemera/src/core/mod.rs new file mode 100644 index 0000000000..f6bd82aa49 --- /dev/null +++ b/ephemera/src/core/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod api_cmd; +pub(crate) mod builder; +pub(crate) mod ephemera; +pub(crate) mod shutdown; diff --git a/ephemera/src/core/shutdown.rs b/ephemera/src/core/shutdown.rs new file mode 100644 index 0000000000..ee2e9c3c70 --- /dev/null +++ b/ephemera/src/core/shutdown.rs @@ -0,0 +1,77 @@ +use log::info; + +use tokio::sync::broadcast; +use tokio::task::JoinHandle; + +pub(crate) struct ShutdownManager { + pub(crate) shutdown_tx: broadcast::Sender<()>, + pub(crate) _shutdown_rcv: broadcast::Receiver<()>, + handles: Vec>>, +} + +pub(crate) struct Shutdown { + pub(crate) shutdown_signal_rcv: broadcast::Receiver<()>, +} + +#[derive(Clone)] +pub struct Handle { + pub(crate) shutdown_started: bool, +} + +impl Handle { + /// Shutdown the node. + /// This will send a shutdown signal to all tasks and wait for them to finish. + /// + /// # Errors + /// This will return an error if shutdown signal can't be sent. + /// + /// # Panics + /// This will panic if shutdown signal can't be sent. + pub fn shutdown(&mut self) { + self.shutdown_started = true; + } +} + +impl ShutdownManager { + pub(crate) fn init() -> (ShutdownManager, Handle) { + let (shutdown_tx, shutdown_rcv) = broadcast::channel(1); + let shutdown_handle = Handle { + shutdown_started: false, + }; + let manager = Self { + shutdown_tx, + _shutdown_rcv: shutdown_rcv, + handles: vec![], + }; + (manager, shutdown_handle) + } + + pub async fn stop(self) { + info!("Starting Ephemera shutdown"); + self.shutdown_tx.send(()).unwrap(); + info!("Waiting for tasks to finish"); + for (i, handle) in self + .handles + .into_iter() + .enumerate() + .map(|(i, h)| (i + 1, h)) + { + match handle.await.unwrap() { + Ok(_) => info!("Task {i} finished successfully"), + Err(e) => info!("Task {i} finished with error: {e}",), + } + } + info!("All tasks finished"); + } + + pub(crate) fn subscribe(&self) -> Shutdown { + let shutdown = self.shutdown_tx.subscribe(); + Shutdown { + shutdown_signal_rcv: shutdown, + } + } + + pub(crate) fn add_handle(&mut self, handle: JoinHandle>) { + self.handles.push(handle); + } +} diff --git a/ephemera/src/lib.rs b/ephemera/src/lib.rs new file mode 100644 index 0000000000..db506a96b3 --- /dev/null +++ b/ephemera/src/lib.rs @@ -0,0 +1,118 @@ +//! # Ephemera Node + +//! An Ephemera node does reliable broadcast of inbound messages to all other Ephemera nodes in the cluster. +//! +//! Each node has a unique ID, and each message is signed by the node that first received it. +//! Messages are then re-broadcast and re-signed by all other nodes in the cluster. +//! +//! At the end of the process, each message is signed by every node in the cluster, and each node has also +//! signed all messages that were broadcast by other nodes. This means that nodes are unable to repudiate messages +//! once they are seen and signed, so there is a strong guarantee of message integrity within the cluster. +//! +//! # Why would I want this? +//! +//! Let's say you have blockchain system that needs to ship large amounts of information around, but the information +//! is relatively short-lived. You could use a blockchain to store the information, but that would be expensive, +//! slow, and wasteful. Instead, you could use Ephemera to broadcast the information to all nodes in the cluster, +//! and then store only a cryptographic commitment in the blockchain's data store. +//! +//! Ephemera nodes then keep messages around for inspection in a data availability layer (accessible over HTTP) +//! so that interested parties can verify correctness. Ephemeral information can then be automatically discarded +//! once it's no longer useful. +//! +//! This gives very similar guarantees to a blockchain, but without incurring the permanent storage costs. +//! +//! Note that it *requires* a blockchain to be present. + +//'Denying' everything and allowing exceptions seems better than other way around. +#![deny(clippy::pedantic)] + +// PUBLIC MODULES + +pub use crate::core::builder::{ + EphemeraStarterInit, EphemeraStarterWithApplication, EphemeraStarterWithProvider, +}; +pub use crate::core::ephemera::Ephemera; +pub use crate::core::shutdown::Handle as ShutdownHandle; + +/// Ephemera API. Public interface and types. +pub mod ephemera_api { + pub use crate::api::{ + application::{ + Application, CheckBlockResult, Dummy, Error as ApplicationError, RemoveMessages, + Result as ApplicationResult, + }, + http::client::{Client, Error as HttpClientError, Result as HttpClientResult}, + types::{ + ApiBlock, ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiCertificate, ApiDhtQueryRequest, + ApiDhtQueryResponse, ApiDhtStoreRequest, ApiEphemeraConfig, ApiEphemeraMessage, + ApiError, ApiHealth, ApiVerifyMessageInBlock, RawApiEphemeraMessage, + }, + CommandExecutor, + }; +} + +/// Peer identification +#[allow(clippy::module_name_repetitions)] +pub mod peer { + pub use super::network::{PeerId, PeerIdError, ToPeerId}; +} + +/// Ephemera membership. How to find other nodes in the cluster. +pub mod membership { + pub use super::network::members::{ + ConfigMembersProvider, DummyMembersProvider, PeerInfo, PeerSetting, ProviderError, Result, + }; +} + +/// Ephemera keypair and public key +pub mod crypto { + pub use super::utilities::crypto::{ + EphemeraKeypair, EphemeraPublicKey, KeyPairError, Keypair, PublicKey, + }; +} + +/// Ephemera codec to encode and decode messages +pub mod codec { + pub use super::utilities::codec::{Decode, Encode}; +} + +/// Ephemera node configuration +pub mod configuration { + pub use super::config::Configuration; +} + +/// Ephemera CLI. Helpers for creating configuration, running node, etc. +pub mod cli; + +/// Utilities to set up logging. +pub mod logging; + +// PRIVATE MODULES + +/// External interface for Ephemera +mod api; + +/// Block creation code +mod block; + +/// Ephemera reliable broadcast +mod broadcast; + +/// Ephemera configuration +mod config; + +/// Ephemera core. Ephemera builder and instance. +mod core; + +/// Ephemera networking with peers +mod network; + +/// Ephemera storage. Block storage and certificate storage. +mod storage; + +/// Ephemera utilities. Crypto, codec, etc. +mod utilities; + +/// Ephemera websocket. Websocket server where external clients can subscribe. +mod websocket; diff --git a/ephemera/src/logging/mod.rs b/ephemera/src/logging/mod.rs new file mode 100644 index 0000000000..79968347b0 --- /dev/null +++ b/ephemera/src/logging/mod.rs @@ -0,0 +1,19 @@ +pub fn init() { + if let Ok(directives) = ::std::env::var("RUST_LOG") { + println!("Logging enabled with directives: {directives}",); + pretty_env_logger::formatted_timed_builder() + .parse_filters(&directives) + .format_timestamp_millis() + .init(); + } else { + println!("Logging disabled"); + } +} + +pub fn init_with_directives(directives: &str) { + println!("Logging enabled with directives: {directives}",); + pretty_env_logger::formatted_timed_builder() + .parse_filters(directives) + .format_timestamp_millis() + .init(); +} diff --git a/ephemera/src/network/libp2p/behaviours/membership/behaviour.rs b/ephemera/src/network/libp2p/behaviours/membership/behaviour.rs new file mode 100644 index 0000000000..65c3a36dc8 --- /dev/null +++ b/ephemera/src/network/libp2p/behaviours/membership/behaviour.rs @@ -0,0 +1,556 @@ +//! # Membership behaviour. +//! +//! Ephemera `reliable broadcast` needs to know the list of peers who participate in the protocol. +//! Also it's not enough to have just the list but to make sure that they are actually online. +//! When the list of available peers changes, `reliable broadcast` needs to be notified so that it can adjust accordingly. +//! +//! This behaviour is responsible for keeping membership up to date. +//! +//! `MembersProviderFuture`: `Future>> + Send + 'static`. +//! +//! User provides a [`MembersProviderFuture`] implementation to the [Behaviour] which is responsible for fetching the list of peers. +//! +//! [Behaviour] accepts only peers that are actually online. +//! +//! When peers become available or unavailable, [Behaviour] adjusts the list of connected peers accordingly and notifies `reliable broadcast` +//! about the membership change. +//! +//! It is configurable what `threshold` of peers(from the total list provided by [`MembersProviderFuture`]) should be available at any given time. +//! Or if just to use all peers who are online. See [`MembershipKind`] for more details. +//! +//! Ideally [`MembersProviderFuture`] can depend on a resource that gives reliable results. Some kind of registry which itself keeps track of actually online nodes. +//! As Ephemera uses only peers provided by [`MembersProviderFuture`], it depends on its accuracy. +//! At the same time it tries to be flexible and robust to handle less reliable [`MembersProviderFuture`] implementations. + +// When peer gets disconnected, we try to dial it and if that fails, we update group. +// (it may connect us meanwhile). +// Although we can retry to connect to disconnected peers, it's simpler if we just assume that when +// they come online again, they will connect us. + +//So the main goal is to remove offline peers from the group so that rb can make progress. + +//a)when peer disconnects, we try to dial it and if that fails, we update group. +//b)when peer connects, we will update the group. + +use std::future::Future; +use std::time::Duration; +use std::{ + collections::HashMap, + collections::HashSet, + fmt::Debug, + task::{Context, Poll}, +}; + +use futures_util::FutureExt; +use libp2p::core::Endpoint; +use libp2p::swarm::{ConnectionDenied, NotifyHandler, THandler}; +use libp2p::{ + swarm::ToSwarm, + swarm::{ + behaviour::ConnectionEstablished, + dial_opts::{DialOpts, PeerCondition}, + ConnectionClosed, ConnectionId, DialFailure, FromSwarm, NetworkBehaviour, PollParameters, + THandlerInEvent, THandlerOutEvent, + }, + Multiaddr, +}; +use libp2p_identity::PeerId; +use log::{debug, error, trace, warn}; +use tokio::time; +use tokio::time::{Instant, Interval}; + +use crate::network::libp2p::behaviours::membership::handler::ToHandler; +use crate::network::libp2p::behaviours::membership::{Membership, MEMBERSHIP_SYNC_INTERVAL_SEC}; +use crate::network::Peer; +use crate::{ + membership, + network::{ + libp2p::behaviours::{ + membership::connections::ConnectedPeers, + membership::protocol::ProtocolMessage, + membership::{handler::Handler, MAX_DIAL_ATTEMPT_ROUNDS}, + membership::{MembershipKind, Memberships}, + }, + members::PeerInfo, + }, +}; + +/// [`MembersProviderFuture`] state when we are trying to connect to new peers. +/// +/// We try to connect few times before giving up. Generally speaking an another peer is either online or offline +/// at any given time. But it has been helpful for testing when whole cluster comes up around the same time. +#[derive(Debug, Default)] +struct PendingPeersUpdate { + /// Peers that we are haven't tried to connect to yet. + waiting_to_dial: HashSet, + /// Number of dial attempts per round. + dial_attempts: usize, + /// How long we wait between dial attempts. + interval_between_dial_attempts: Option, +} + +#[derive(Debug, Default)] +struct SyncPeers { + pending_peers: Vec, +} + +impl SyncPeers { + fn new(pending_peers: Vec) -> Self { + Self { pending_peers } + } +} + +/// Behaviour states. +enum State { + /// Waiting for new peers from the members provider trait. + WaitingPeers, + /// Trying to connect to new peers. + WaitingDial(PendingPeersUpdate), + /// We have finished trying to connect to new peers and going to report it. + NotifyPeersUpdated, + /// Notify other members that we have updated members. + SyncPeers(SyncPeers), +} + +/// Events that can be emitted by the `Behaviour`. +pub(crate) enum Event { + /// We have received new peers from the members provider trait. + /// We are going to try to connect to them. + PeerUpdatePending, + /// We have finished trying to connect to new peers and going to report it. + PeersUpdated(HashSet), + /// MembersProviderFuture reported us new peers and this set doesn't contain our local peer. + LocalRemoved(HashSet), + /// MembersProviderFuture reported us new peers and we failed to connect to enough of them. + NotEnoughPeers(HashSet), +} + +pub(crate) struct Behaviour

+where + P: Future>> + Send + 'static, +{ + /// All peers that are part of the current group. + memberships: Memberships, + /// Local peer id. + local_peer_id: PeerId, + /// Future that provides new peers. + members_provider: P, + /// Interval between requesting new peers from the members provider. + members_provider_interval: Option, + /// Delay between dial attempts. + members_provider_delay: Duration, + /// Current behaviour state. + state: State, + /// Current state of all incoming and outgoing connections. + all_connections: ConnectedPeers, + /// Membership kind. + membership_kind: MembershipKind, + /// Last time we broadcast SYNC + last_sync_time: Instant, + /// Minimum time between members provider updates. + minimum_time_between_sync: Duration, +} + +impl

Behaviour

+where + P: Future>> + Send + Unpin + 'static, +{ + pub(crate) fn new( + members_provider: P, + members_provider_delay: Duration, + local_peer_id: PeerId, + membership_kind: MembershipKind, + ) -> Self { + let initial_delay = Instant::now() + Duration::from_secs(5); + let delay = tokio::time::interval_at(initial_delay, members_provider_delay); + Behaviour { + memberships: Memberships::new(), + local_peer_id, + members_provider, + members_provider_interval: Some(delay), + members_provider_delay, + state: State::WaitingPeers, + all_connections: ConnectedPeers::default(), + membership_kind, + last_sync_time: Instant::now(), + minimum_time_between_sync: Duration::from_secs(MEMBERSHIP_SYNC_INTERVAL_SEC), + } + } + + /// Returns the list of peers that are part of current group. + pub(crate) fn active_peer_ids(&mut self) -> &HashSet { + self.memberships.current().connected_peers() + } + + pub(crate) fn active_peer_ids_with_local(&mut self) -> HashSet { + self.memberships.current().connected_peer_ids_with_local() + } + + fn waiting_peers(&mut self, cx: &mut Context) -> Poll> { + if let Some(mut tick) = self.members_provider_interval.take() { + if !tick.poll_tick(cx).is_ready() { + self.members_provider_interval = Some(tick); + return Poll::Pending; + } + } + let peers = match self.members_provider.poll_unpin(cx) { + Poll::Ready(peers) => { + let wait_time = Instant::now() + self.members_provider_delay; + self.members_provider_interval = + time::interval_at(wait_time, self.members_provider_delay).into(); + self.last_sync_time = Instant::now(); + peers + } + Poll::Pending => { + return Poll::Pending; + } + }; + + match peers { + Ok(peers) => { + if peers.is_empty() { + //Not sure what to do here. Tempted to think that if this happens + //we should ignore it and assume that this is probably a bug in the membership service. + + warn!("Received empty peers from provider. To try again before preconfigured interval, please restart the node."); + return Poll::Ready(ToSwarm::GenerateEvent(Event::NotEnoughPeers( + HashSet::default(), + ))); + } + + let mut new_peers = HashMap::new(); + + for peer_info in peers { + match >::try_into(peer_info) { + Ok(peer) => { + debug!( + "Received peer: {:?}, {:?}", + peer.peer_id.inner(), + peer.cosmos_address + ); + new_peers.insert(*peer.peer_id.inner(), peer); + } + Err(err) => { + error!("Error while converting peer info to peer: {}", err); + } + } + } + + //If we are not part of the new membership, notify immediately + if !new_peers.contains_key(&self.local_peer_id) { + debug!( + "Local peer {:?} is not part of the new membership. Notifying immediately.", + self.local_peer_id + ); + let pending_membership = Membership::new(new_peers.clone()); + self.memberships.set_pending(pending_membership); + self.state = State::NotifyPeersUpdated; + return Poll::Pending; + } + + let mut pending_membership = + Membership::new_with_local(new_peers.clone(), self.local_peer_id); + let mut pending_update = PendingPeersUpdate::default(); + + for peer_id in new_peers.keys() { + if self.all_connections.is_peer_connected(peer_id) { + pending_membership.peer_connected(*peer_id); + } else { + pending_update.waiting_to_dial.insert(*peer_id); + } + } + + self.memberships.set_pending(pending_membership); + + //It seems that all peers from updated membership set are already connected + if pending_update.waiting_to_dial.is_empty() { + self.state = State::NotifyPeersUpdated; + Poll::Pending + } else { + self.state = State::WaitingDial(pending_update); + + //Just let the rest of the system to know that we are in the middle of updating membership + Poll::Ready(ToSwarm::GenerateEvent(Event::PeerUpdatePending)) + } + } + Err(err) => { + error!("Error while getting peers from provider: {:?}", err); + Poll::Ready(ToSwarm::GenerateEvent(Event::NotEnoughPeers( + HashSet::default(), + ))) + } + } + } + + fn waiting_dial( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + if let State::WaitingDial(PendingPeersUpdate { + waiting_to_dial, + dial_attempts, + interval_between_dial_attempts, + }) = &mut self.state + { + //Refresh the list of connected peers + for peer_id in self.all_connections.all_connected_peers_ref() { + waiting_to_dial.remove(peer_id); + } + + //FIXME + waiting_to_dial.remove(&self.local_peer_id); + + let pending_membership = self + .memberships + .pending() + .expect("Pending membership should be set"); + + //With each 'poll' we can tell Swarm to dial one peer + let next_waiting = waiting_to_dial.iter().next().copied(); + + if let Some(peer_id) = next_waiting { + waiting_to_dial.remove(&peer_id); + + let address = pending_membership + .peer_address(&peer_id) + .expect("Peer should exist"); + + trace!("Dialing peer: {:?} {:?}", peer_id, address); + + let opts = DialOpts::peer_id(peer_id) + .condition(PeerCondition::NotDialing) + .addresses(vec![address.clone()]) + .build(); + + Poll::Ready(ToSwarm::Dial { opts }) + } else { + let all_peers = pending_membership.all_peer_ids(); + let connected_peers = pending_membership.connected_peers(); + + //Exclude local peer + let all_connected = connected_peers.len() == all_peers.len() - 1; + + if all_connected || *dial_attempts >= MAX_DIAL_ATTEMPT_ROUNDS { + interval_between_dial_attempts.take(); + self.state = State::NotifyPeersUpdated; + return Poll::Pending; + } + //Try again few times before notifying the rest of the system about membership update. + //Dialing attempt + if let Some(interval) = interval_between_dial_attempts { + if interval.poll_tick(cx) == Poll::Pending { + return Poll::Pending; + } + *dial_attempts += 1; + trace!("Next attempt({dial_attempts:?}) to dial failed peers"); + } else { + let start_at = Instant::now() + Duration::from_secs(5); + *interval_between_dial_attempts = + Some(time::interval_at(start_at, Duration::from_secs(10))); + } + if *dial_attempts > 0 { + waiting_to_dial.extend(all_peers.difference(connected_peers).copied()); + } + + Poll::Pending + } + } else { + unreachable!() + } + } + + fn notify_peers_updated(&mut self) -> Poll> { + if let Some(membership) = self.memberships.remove_pending() { + self.memberships.update(membership); + } + + let membership = self.memberships.current(); + let membership_connected_peers = membership.connected_peer_ids(); + + let event = if membership.includes_local() { + if self.membership_kind.accept(membership) { + debug!("Membership accepted by kind: {:?}", self.membership_kind); + Event::PeersUpdated(membership_connected_peers) + } else { + debug!("Membership rejected by kind: {:?}", self.membership_kind); + Event::NotEnoughPeers(membership_connected_peers) + } + } else { + debug!("Membership does not include local peer"); + Event::LocalRemoved(membership_connected_peers) + }; + + //TODO: this list should also include "old" peers(peers who aren't part of new membership). + let connected_peers = membership + .connected_peer_ids() + .into_iter() + .collect::>(); + self.state = State::SyncPeers(SyncPeers::new(connected_peers)); + Poll::Ready(ToSwarm::GenerateEvent(event)) + } + + fn sync_peers(&mut self) -> Poll> { + if let State::SyncPeers(SyncPeers { pending_peers }) = &mut self.state { + match pending_peers.pop() { + None => { + self.state = State::WaitingPeers; + Poll::Pending + } + Some(peer_id) => { + debug!("Notifying {peer_id:?} about membership update",); + Poll::Ready(ToSwarm::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event: ToHandler::Message(ProtocolMessage::Sync), + }) + } + } + } else { + unreachable!("State should be SyncPeers") + } + } +} + +impl

NetworkBehaviour for Behaviour

+where + P: Future>> + Send + Unpin + 'static, +{ + type ConnectionHandler = Handler; + type OutEvent = Event; + + fn handle_pending_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result<(), ConnectionDenied> { + //TODO: we can refuse connections from peers that are not part of the current membership. + Ok(()) + } + + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + peer: PeerId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + trace!("Established inbound connection with peer: {:?}", peer); + Ok(Handler::new()) + } + + fn handle_pending_outbound_connection( + &mut self, + _connection_id: ConnectionId, + maybe_peer: Option, + _addresses: &[Multiaddr], + _effective_role: Endpoint, + ) -> Result, ConnectionDenied> { + //FIXME: deprecated + #[allow(deprecated)] + match maybe_peer { + Some(peer_id) => Ok(self.addresses_of_peer(&peer_id)), + None => Ok(vec![]), + } + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + peer: PeerId, + addr: &Multiaddr, + _role_override: Endpoint, + ) -> Result, ConnectionDenied> { + trace!( + "Established outbound connection with peer: {:?} {:?}", + peer, + addr + ); + Ok(Handler::new()) + } + + ///Membership behaviour is responsible for providing addresses to another Swarm behaviours. + fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { + self.memberships + .current() + .peer_address(peer_id) + .cloned() + .map_or(vec![], |addr| vec![addr]) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + match event { + FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id, + connection_id: _, + endpoint, + failed_addresses: _, + other_established: _, + }) => { + self.all_connections + .insert(peer_id, endpoint.clone().into()); + if let Some(pending) = self.memberships.pending_mut() { + pending.peer_connected(peer_id); + } + trace!("{:?}", self.all_connections); + } + + FromSwarm::ConnectionClosed(ConnectionClosed { + peer_id, + connection_id: _, + endpoint, + handler: _h, + remaining_established: _, + }) => { + self.all_connections + .remove(&peer_id, &endpoint.clone().into()); + if let Some(pending) = self.memberships.pending_mut() { + pending.peer_disconnected(&peer_id); + } + debug!("{:?}", self.all_connections); + } + FromSwarm::DialFailure(DialFailure { + peer_id: Some(peer_id), + error, + connection_id: _, + }) => { + trace!("Dial failure: {:?} {:?}", peer_id, error); + } + _ => {} + } + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + _connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + trace!( + "Received event from connection handler: {:?} from peer: {:?}", + event, + peer_id + ); + + //TODO: we may need to check who sent the update: probably we should accept only updates from members who we already know + if let State::WaitingPeers = self.state { + if self.last_sync_time + self.minimum_time_between_sync < Instant::now() { + self.members_provider_interval = None; + debug!("Received sync notification from peer {peer_id:?}, requesting membership update"); + } + } + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + _params: &mut impl PollParameters, + ) -> Poll>> { + match &mut self.state { + State::WaitingPeers => self.waiting_peers(cx), + State::WaitingDial(_) => self.waiting_dial(cx), + State::NotifyPeersUpdated => self.notify_peers_updated(), + State::SyncPeers(_) => self.sync_peers(), + } + } +} diff --git a/ephemera/src/network/libp2p/behaviours/membership/connections.rs b/ephemera/src/network/libp2p/behaviours/membership/connections.rs new file mode 100644 index 0000000000..1e5bab908d --- /dev/null +++ b/ephemera/src/network/libp2p/behaviours/membership/connections.rs @@ -0,0 +1,88 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; + +use libp2p::core::ConnectedPoint; +use libp2p::Multiaddr; +use serde::Serialize; + +#[derive(PartialEq, Eq, Debug, Clone, Hash, Serialize)] +pub(crate) enum Endpoint { + Dialer { + address: Multiaddr, + }, + Listener { + local_address: Multiaddr, + remote_address: Multiaddr, + }, +} + +impl From for Endpoint { + fn from(connected_point: ConnectedPoint) -> Self { + match connected_point { + ConnectedPoint::Dialer { address, .. } => Endpoint::Dialer { address }, + ConnectedPoint::Listener { + local_addr, + send_back_addr, + } => Endpoint::Listener { + local_address: local_addr, + remote_address: send_back_addr, + }, + } + } +} + +#[derive(Debug, Default, Serialize)] +pub(crate) struct Connections { + dialer: HashSet, + listener: HashSet, +} + +impl Connections { + fn insert(&mut self, connected_point: Endpoint) { + match connected_point { + Endpoint::Dialer { .. } => { + self.dialer.insert(connected_point); + } + Endpoint::Listener { .. } => { + self.listener.insert(connected_point); + } + } + } + + fn remove(&mut self, connected_point: &Endpoint) { + match connected_point { + Endpoint::Dialer { .. } => { + self.dialer.remove(connected_point); + } + Endpoint::Listener { .. } => { + self.listener.remove(connected_point); + } + } + } +} + +#[derive(Debug, Default, Serialize)] +pub(crate) struct ConnectedPeers { + connections: HashMap, +} + +impl ConnectedPeers { + pub(crate) fn is_peer_connected(&self, peer_id: &libp2p_identity::PeerId) -> bool { + self.connections.contains_key(peer_id) + } + + pub(crate) fn all_connected_peers_ref(&self) -> Vec<&libp2p_identity::PeerId> { + self.connections.keys().collect() + } + + pub(crate) fn insert(&mut self, peer_id: libp2p_identity::PeerId, connected_point: Endpoint) { + let connections = self.connections.entry(peer_id).or_default(); + connections.insert(connected_point); + } + + pub(crate) fn remove(&mut self, peer_id: &libp2p_identity::PeerId, connected_point: &Endpoint) { + if let Some(connections) = self.connections.get_mut(peer_id) { + connections.remove(connected_point); + } + } +} diff --git a/ephemera/src/network/libp2p/behaviours/membership/handler.rs b/ephemera/src/network/libp2p/behaviours/membership/handler.rs new file mode 100644 index 0000000000..9735d5cea9 --- /dev/null +++ b/ephemera/src/network/libp2p/behaviours/membership/handler.rs @@ -0,0 +1,340 @@ +//! Because each Ephemera instance requests peers at arbitrary time, a node needs to notify other +//! peers when it has just requested an update. That helps to keep the whole cluster in sync and avoid +//! nodes' membership diverging. +//! +//! Overall this synchronizes all nodes' view of the membership. +//! +//! Current approach is a bit 'burst'. It makes all nodes to request membership info at the same time. +//! +//! TODO +//! Because we actually can verify peers' membership, it would be possible that one peer(or subset of peers) requests the +//! peers from a rendezvous point and then sends the list to the other peers. Or possibly only the difference. +use std::pin::Pin; +use std::task::{Context, Poll}; + +use asynchronous_codec::Framed; +use futures::Sink; +use futures_util::StreamExt; +use libp2p::{ + swarm::handler::{ + DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound, ListenUpgradeError, + }, + swarm::NegotiatedSubstream, + swarm::{ + handler::ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, KeepAlive, + SubstreamProtocol, + }, +}; +use log::{debug, error}; +use thiserror::Error; + +use crate::network::libp2p::behaviours::membership::protocol::{ + MembershipCodec, Protocol, ProtocolMessage, +}; + +#[derive(Error, Debug)] +pub(crate) enum Error { + #[error("HandlerError: {0}")] + Handler(#[from] anyhow::Error), +} + +//Because we keep long lived connections, we need to restrict number of substream attempts. +//Here we don't need more than 1 because it's a 'quiet' protocol. +const MAX_SUBSTREAM_ATTEMPTS: usize = 1; + +enum InboundSubstreamState { + WaitingInput(Framed), + Closing(Framed), +} + +enum OutboundSubstreamState { + WaitingOutput(Framed), + PendingSend( + Framed, + ProtocolMessage, + ), + PendingFlush(Framed), +} + +pub(crate) struct Handler { + outbound_substream: Option, + inbound_substream: Option, + send_queue: Vec, + outbound_substream_establishing: bool, + outbound_substream_attempts: usize, + inbound_substream_attempts: usize, +} + +impl Handler { + pub(crate) fn new() -> Self { + Self { + outbound_substream: None, + inbound_substream: None, + send_queue: vec![], + outbound_substream_establishing: false, + outbound_substream_attempts: 0, + inbound_substream_attempts: 0, + } + } + + //Process inbound stream messages + //WAITING_INPUT + // - if message received, send it to behaviour + // - if receive error or None, close substream + // + //CLOSING + // - Wait buffer to be flushed + // - Close substream + fn process_inbound_stream( + &mut self, + cx: &mut Context, + ) -> Option>> { + loop { + match std::mem::take(&mut self.inbound_substream) { + // inbound idle state + Some(InboundSubstreamState::WaitingInput(mut substream)) => { + match substream.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(message))) => { + self.inbound_substream = + Some(InboundSubstreamState::WaitingInput(substream)); + + let from_handler = FromHandler::Message(message); + + return Poll::Ready(ConnectionHandlerEvent::Custom(from_handler)) + .into(); + } + Poll::Ready(Some(Err(err))) => { + error!("Failed to read from substream: {err}",); + self.inbound_substream = + Some(InboundSubstreamState::Closing(substream)); + } + Poll::Ready(None) => { + debug!("Inbound stream closed by remote"); + self.inbound_substream = + Some(InboundSubstreamState::Closing(substream)); + } + Poll::Pending => { + self.inbound_substream = + Some(InboundSubstreamState::WaitingInput(substream)); + break; + } + } + } + Some(InboundSubstreamState::Closing(mut substream)) => { + match Sink::poll_close(Pin::new(&mut substream), cx) { + Poll::Ready(res) => { + if let Err(e) = res { + error!("Inbound substream error while closing: {e}"); + } + self.inbound_substream = None; + break; + } + Poll::Pending => { + self.inbound_substream = + Some(InboundSubstreamState::Closing(substream)); + break; + } + } + } + None => { + self.inbound_substream = None; + break; + } + } + } + None + } + + //Process outbound stream messages + //WAITING_OUTPUT + // - if send queue is not empty, go to PENDING_SEND + // + //PENDING_SEND + // - send message to substream + // - if send error, mark substream as Closing + // + //PENDING_FLUSH + // - flush substream + // - if flush error, mark substream as Closing + fn process_outbound_stream(&mut self, cx: &mut Context) { + loop { + match std::mem::take(&mut self.outbound_substream) { + // outbound idle state + Some(OutboundSubstreamState::WaitingOutput(substream)) => { + if let Some(message) = self.send_queue.pop() { + self.outbound_substream = + Some(OutboundSubstreamState::PendingSend(substream, message)); + continue; + } + + self.outbound_substream = + Some(OutboundSubstreamState::WaitingOutput(substream)); + break; + } + Some(OutboundSubstreamState::PendingSend(mut substream, message)) => { + match Sink::poll_ready(Pin::new(&mut substream), cx) { + Poll::Ready(Ok(())) => { + match Sink::start_send(Pin::new(&mut substream), message) { + Ok(()) => { + self.outbound_substream = + Some(OutboundSubstreamState::PendingFlush(substream)); + } + Err(e) => { + debug!("Failed to send message on outbound stream: {e}"); + self.outbound_substream = None; + break; + } + } + } + Poll::Ready(Err(e)) => { + debug!("Failed to send message on outbound stream: {e}"); + self.outbound_substream = None; + break; + } + Poll::Pending => { + self.outbound_substream = + Some(OutboundSubstreamState::PendingSend(substream, message)); + break; + } + } + } + Some(OutboundSubstreamState::PendingFlush(mut substream)) => { + match Sink::poll_flush(Pin::new(&mut substream), cx) { + Poll::Ready(Ok(())) => { + self.outbound_substream = + Some(OutboundSubstreamState::WaitingOutput(substream)); + } + Poll::Ready(Err(e)) => { + debug!("Failed to flush outbound stream: {e}"); + self.outbound_substream = None; + break; + } + Poll::Pending => { + self.outbound_substream = + Some(OutboundSubstreamState::PendingFlush(substream)); + break; + } + } + } + None => { + self.outbound_substream = None; + break; + } + } + } + } +} + +#[derive(Debug)] +pub(crate) enum FromHandler { + Message(ProtocolMessage), +} + +#[derive(Debug)] +pub(crate) enum ToHandler { + Message(ProtocolMessage), +} + +impl ConnectionHandler for Handler { + type InEvent = ToHandler; + type OutEvent = FromHandler; + type Error = Error; + type InboundProtocol = Protocol; + type OutboundProtocol = Protocol; + type InboundOpenInfo = (); + type OutboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + SubstreamProtocol::new(Protocol, ()) + } + + fn connection_keep_alive(&self) -> KeepAlive { + //we could add idle timeout here + KeepAlive::Yes + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ConnectionHandlerEvent< + Self::OutboundProtocol, + Self::OutboundOpenInfo, + Self::OutEvent, + Self::Error, + >, + > { + // poll STATE_MACHINE + //- Request outbound substream if neccessary + //- poll inbound substream + //- poll outbound substream + + //Establish new connection when behaviour wants to send a message and we don't have an outbound substream yet + if !self.send_queue.is_empty() + && self.outbound_substream.is_none() + && !self.outbound_substream_establishing + { + self.outbound_substream_establishing = true; + return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new(Protocol, ()), + }); + } + + if let Some(res) = self.process_inbound_stream(cx) { + return res; + } + + self.process_outbound_stream(cx); + + Poll::Pending + } + + fn on_behaviour_event(&mut self, event: Self::InEvent) { + match event { + ToHandler::Message(message) => { + self.send_queue.push(message); + } + } + } + + fn on_connection_event( + &mut self, + event: ConnectionEvent< + Self::InboundProtocol, + Self::OutboundProtocol, + Self::InboundOpenInfo, + Self::OutboundOpenInfo, + >, + ) { + match event { + ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound { + protocol: stream, + info: _, + }) => { + if self.inbound_substream_attempts > MAX_SUBSTREAM_ATTEMPTS { + log::warn!("Too many inbound substream attempts, refusing stream"); + return; + } + self.inbound_substream_attempts += 1; + self.inbound_substream = Some(InboundSubstreamState::WaitingInput(stream)); + } + ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound { + protocol, + info: _, + }) => { + if self.outbound_substream_attempts > MAX_SUBSTREAM_ATTEMPTS { + log::warn!("Too many outbound substream attempts, refusing stream"); + return; + } + self.outbound_substream = Some(OutboundSubstreamState::WaitingOutput(protocol)); + } + ConnectionEvent::DialUpgradeError(DialUpgradeError { info, error }) => { + error!("DialUpgradeError: info: {:?}, error: {:?}", info, error); + } + ConnectionEvent::ListenUpgradeError(ListenUpgradeError { info, error }) => { + error!("ListenUpgradeError: info: {:?}, error: {:?}", info, error); + } + ConnectionEvent::AddressChange(_) => {} + } + } +} diff --git a/ephemera/src/network/libp2p/behaviours/membership/mod.rs b/ephemera/src/network/libp2p/behaviours/membership/mod.rs new file mode 100644 index 0000000000..6a4f6e6178 --- /dev/null +++ b/ephemera/src/network/libp2p/behaviours/membership/mod.rs @@ -0,0 +1,190 @@ +//! In Ephemera, membership of reliable broadcast protocol is decided by membership provider. +//! Only peers who are returned by [`crate::membership::MembersProviderFut`] are allowed to participate. + +use std::collections::{HashMap, HashSet}; +use std::num::NonZeroUsize; + +use libp2p_identity::PeerId; +use lru::LruCache; + +use crate::network::Peer; + +pub(crate) mod behaviour; +mod connections; +mod handler; +mod protocol; + +const MAX_DIAL_ATTEMPT_ROUNDS: usize = 6; + +/// Minimum percentage of available nodes to consider the network healthy. +//TODO: make this configurable +const MEMBERSHIP_MINIMUM_AVAILABLE_NODES_RATIO: f64 = 0.8; + +/// Minimum time between syncs of membership. +const MEMBERSHIP_SYNC_INTERVAL_SEC: u64 = 60; + +/// Maximum percentage of nodes that can change in a single membership update. +/// In general it should be considered a security risk if it has changed too much. +/// //TODO: make this configurable +const _MEMBERSHIP_MAXIMUM_ALLOWED_CHANGE_RATIO: f64 = 0.2; + +/// Membership provider returns list of peers. But it is up to the Ephemera user to decide +/// how reliable the list is. For example, it can contain peers who are offline. + +/// This enum defines how the actual membership is decided. +#[derive(Debug)] +pub(crate) enum MembershipKind { + /// Mandatory minimum membership size is defined by threshold of all peers returned by membership provider. + Threshold(f64), + /// Mandatory minimum membership size is all peers who are online. + AnyOnline, + /// Mandatory minimum membership size is all peers returned by membership provider. + AllOnline, +} + +impl MembershipKind { + #[allow( + clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::cast_possible_truncation + )] + pub(crate) fn accept(&self, membership: &Membership) -> bool { + let total_number_of_peers = membership.all_members.len(); + let connected_peers = membership.connected_peers_ids.len(); + match self { + MembershipKind::Threshold(threshold) => { + let minimum_available_nodes = (total_number_of_peers as f64 * threshold) as usize; + connected_peers >= minimum_available_nodes + } + MembershipKind::AnyOnline => connected_peers > 0, + MembershipKind::AllOnline => connected_peers == total_number_of_peers, + } + } +} + +pub(crate) struct Memberships { + snapshots: LruCache, + current: u64, + /// This is set when we get new peers set from [crate::membership::MembersProviderFut] + /// but haven't yet activated it. + pending_membership: Option, +} + +impl Memberships { + pub(crate) fn new() -> Self { + let mut snapshots = LruCache::new(NonZeroUsize::new(1000).unwrap()); + snapshots.put(0, Membership::new(HashMap::default())); + Self { + snapshots, + current: 0, + pending_membership: None, + } + } + + pub(crate) fn current(&mut self) -> &Membership { + //Unwrap is safe because we always have current membership + self.snapshots.get(&self.current).unwrap() + } + + pub(crate) fn update(&mut self, membership: Membership) { + self.current += 1; + self.snapshots.put(self.current, membership); + } + + pub(crate) fn set_pending(&mut self, membership: Membership) { + self.pending_membership = Some(membership); + } + + pub(crate) fn remove_pending(&mut self) -> Option { + self.pending_membership.take() + } + + pub(crate) fn pending(&self) -> Option<&Membership> { + self.pending_membership.as_ref() + } + + pub(crate) fn pending_mut(&mut self) -> Option<&mut Membership> { + self.pending_membership.as_mut() + } +} + +#[derive(Debug)] +pub(crate) struct Membership { + local_peer_id: PeerId, + all_members: HashMap, + all_peers_ids: HashSet, + connected_peers_ids: HashSet, +} + +impl Membership { + pub(crate) fn new_with_local( + all_members: HashMap, + local_peer_id: PeerId, + ) -> Self { + let all_peers_ids = all_members.keys().copied().collect(); + Self { + local_peer_id, + all_members, + all_peers_ids, + connected_peers_ids: HashSet::new(), + } + } + + pub(crate) fn new(all_members: HashMap) -> Self { + let all_peers_ids = all_members.keys().copied().collect(); + Self { + local_peer_id: PeerId::random(), + all_members, + all_peers_ids, + connected_peers_ids: HashSet::new(), + } + } + + pub(crate) fn includes_local(&self) -> bool { + self.all_members.contains_key(&self.local_peer_id) + } + + pub(crate) fn peer_connected(&mut self, peer_id: PeerId) { + self.connected_peers_ids.insert(peer_id); + } + + pub(crate) fn peer_disconnected(&mut self, peer_id: &PeerId) { + self.connected_peers_ids.remove(peer_id); + } + + pub(crate) fn all_peer_ids(&self) -> &HashSet { + &self.all_peers_ids + } + + pub(crate) fn connected_peer_ids(&self) -> HashSet { + self.connected_peers_ids.clone() + } + + pub(crate) fn connected_peer_ids_with_local(&self) -> HashSet { + let mut active_peers = self.connected_peers_ids.clone(); + active_peers.insert(self.local_peer_id); + active_peers + } + + pub(crate) fn connected_peers(&self) -> &HashSet { + &self.connected_peers_ids + } + + pub(crate) fn peer_address(&self, peer_id: &PeerId) -> Option<&libp2p::Multiaddr> { + self.all_members + .get(peer_id) + .map(|peer| peer.address.inner()) + } +} + +impl From for MembershipKind { + fn from(kind: crate::config::MembershipKind) -> Self { + match kind { + crate::config::MembershipKind::Threshold => { + MembershipKind::Threshold(MEMBERSHIP_MINIMUM_AVAILABLE_NODES_RATIO) + } + crate::config::MembershipKind::AnyOnline => MembershipKind::AnyOnline, + crate::config::MembershipKind::AllOnline => MembershipKind::AllOnline, + } + } +} diff --git a/ephemera/src/network/libp2p/behaviours/membership/protocol.rs b/ephemera/src/network/libp2p/behaviours/membership/protocol.rs new file mode 100644 index 0000000000..cb2c1d631c --- /dev/null +++ b/ephemera/src/network/libp2p/behaviours/membership/protocol.rs @@ -0,0 +1,100 @@ +use std::future::Future; +use std::iter; +use std::pin::Pin; + +use asynchronous_codec::{Decoder, Encoder, Framed}; +use bytes::BytesMut; +use futures::{AsyncRead, AsyncWrite}; +use futures_util::future; +use libp2p::core::UpgradeInfo; +use libp2p::{InboundUpgrade, OutboundUpgrade}; +use log::trace; +use serde::{Deserialize, Serialize}; + +use crate::utilities::codec::varint_bytes::{read_length_prefixed, write_length_prefixed}; + +pub const PROTOCOL_NAME: &[u8] = b"/ephemera/membership/1.0.0"; + +pub(crate) struct Protocol; + +impl UpgradeInfo for Protocol { + type Info = &'static [u8]; + type InfoIter = iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + iter::once(PROTOCOL_NAME) + } +} + +impl InboundUpgrade for Protocol +where + C: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Output = Framed; + type Error = anyhow::Error; + type Future = Pin> + Send>>; + + fn upgrade_inbound(self, socket: C, _: Self::Info) -> Self::Future { + trace!( + "Inbound upgrade for protocol: {}", + String::from_utf8_lossy(PROTOCOL_NAME) + ); + Box::pin(future::ok(Framed::new(socket, MembershipCodec {}))) + } +} + +impl OutboundUpgrade for Protocol +where + C: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Output = Framed; + type Error = anyhow::Error; + type Future = Pin> + Send>>; + + fn upgrade_outbound(self, socket: C, _: Self::Info) -> Self::Future { + trace!( + "Outbound upgrade for protocol: {}", + String::from_utf8_lossy(PROTOCOL_NAME) + ); + Box::pin(future::ok(Framed::new(socket, MembershipCodec {}))) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum ProtocolMessage { + Sync, +} + +pub(crate) struct MembershipCodec {} + +impl Encoder for MembershipCodec { + type Item = ProtocolMessage; + type Error = anyhow::Error; + + fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { + //FIXME: switch to binary + let data = serde_json::to_vec(&item).unwrap(); + write_length_prefixed(dst, data); + Ok(()) + } +} + +impl Decoder for MembershipCodec { + type Item = ProtocolMessage; + type Error = anyhow::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + if src.is_empty() { + return Ok(None); + } + let data = read_length_prefixed(src, 1024 * 1024)?; + match data { + None => Ok(None), + Some(data) => { + //FIXME: switch to binary + let msg = serde_json::from_slice(&data)?; + Ok(msg) + } + } + } +} diff --git a/ephemera/src/network/libp2p/behaviours/mod.rs b/ephemera/src/network/libp2p/behaviours/mod.rs new file mode 100644 index 0000000000..fdd1b4bfa5 --- /dev/null +++ b/ephemera/src/network/libp2p/behaviours/mod.rs @@ -0,0 +1,183 @@ +use std::future::Future; +use std::{iter, sync::Arc, time::Duration}; + +use libp2p::{ + core::{muxing::StreamMuxerBox, transport::Boxed}, + dns, gossipsub, + gossipsub::{IdentTopic as Topic, MessageAuthenticity, ValidationMode}, + kad, noise, request_response as libp2p_request_response, + swarm::NetworkBehaviour, + tcp::{tokio::Transport as TokioTransport, Config as TokioConfig}, + yamux, PeerId as Libp2pPeerId, Transport, +}; +use log::info; + +use crate::membership::PeerInfo; +use crate::network::libp2p::behaviours::membership::MembershipKind; +use crate::{ + broadcast::RbMsg, + crypto::Keypair, + network::libp2p::behaviours::request_response::{ + RbMsgMessagesCodec, RbMsgProtocol, RbMsgResponse, + }, + peer::{PeerId, ToPeerId}, + utilities::hash::{EphemeraHasher, Hasher}, +}; + +pub(crate) mod membership; +pub(crate) mod request_response; + +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "GroupBehaviourEvent")] +pub(crate) struct GroupNetworkBehaviour

+where + P: Future>> + Send + 'static, +{ + pub(crate) members_provider: membership::behaviour::Behaviour

, + pub(crate) gossipsub: gossipsub::Behaviour, + pub(crate) request_response: libp2p_request_response::Behaviour, + pub(crate) kademlia: kad::Kademlia, +} + +#[allow(clippy::large_enum_variant)] +pub(crate) enum GroupBehaviourEvent { + Gossipsub(gossipsub::Event), + RequestResponse(libp2p_request_response::Event), + Membership(membership::behaviour::Event), + Kademlia(kad::KademliaEvent), +} + +impl From for GroupBehaviourEvent { + fn from(event: gossipsub::Event) -> Self { + GroupBehaviourEvent::Gossipsub(event) + } +} + +impl From> for GroupBehaviourEvent { + fn from(event: libp2p_request_response::Event) -> Self { + GroupBehaviourEvent::RequestResponse(event) + } +} + +impl From for GroupBehaviourEvent { + fn from(event: membership::behaviour::Event) -> Self { + GroupBehaviourEvent::Membership(event) + } +} + +impl From for GroupBehaviourEvent { + fn from(event: kad::KademliaEvent) -> Self { + GroupBehaviourEvent::Kademlia(event) + } +} + +//Create combined behaviour. +//Gossipsub takes care of message delivery semantics +//Membership takes care of providing peers who are part of the reliable broadcast group +//Kademlia takes provides closest neighbours and general DHT functionality +pub(crate) fn create_behaviour

( + keypair: &Arc, + ephemera_msg_topic: &Topic, + members_provider: P, + members_provider_delay: Duration, + membership_kind: MembershipKind, +) -> GroupNetworkBehaviour

+where + P: Future>> + Send + Unpin + 'static, +{ + //TODO: review behaviours config(eg. gossipsub minimum peers, kademlia ttl, request-response timeouts etc.) + let local_peer_id = keypair.peer_id(); + let gossipsub = create_gossipsub(keypair, ephemera_msg_topic); + let request_response = create_request_response(); + let rendezvous_behaviour = create_membership( + members_provider, + members_provider_delay, + membership_kind, + local_peer_id, + ); + let kademlia = create_kademlia(keypair); + + GroupNetworkBehaviour { + members_provider: rendezvous_behaviour, + gossipsub, + request_response, + kademlia, + } +} + +// Configure networking messaging stack(Gossipsub) +pub(crate) fn create_gossipsub(local_key: &Arc, topic: &Topic) -> gossipsub::Behaviour { + let gossipsub_config = gossipsub::ConfigBuilder::default() + //TODO: settings from config + .heartbeat_interval(Duration::from_secs(5)) + .message_id_fn(|msg: &gossipsub::Message| Hasher::digest(&msg.data).into()) + .validation_mode(ValidationMode::Strict) + .build() + .expect("Valid config"); + + let mut behaviour = gossipsub::Behaviour::new( + MessageAuthenticity::Signed(local_key.inner().clone()), + gossipsub_config, + ) + .expect("Correct configuration"); + + info!("Subscribing to topic: {}", topic); + behaviour.subscribe(topic).expect("Valid topic"); + behaviour +} + +pub(crate) fn create_request_response() -> libp2p_request_response::Behaviour { + let config = libp2p_request_response::Config::default(); + libp2p_request_response::Behaviour::new( + RbMsgMessagesCodec, + iter::once(( + RbMsgProtocol, + libp2p_request_response::ProtocolSupport::Full, + )), + config, + ) +} + +pub(crate) fn create_membership

( + members_provider: P, + members_provider_delay: Duration, + membership_kind: MembershipKind, + local_peer_id: PeerId, +) -> membership::behaviour::Behaviour

+where + P: Future>> + Send + Unpin + 'static, +{ + membership::behaviour::Behaviour::new( + members_provider, + members_provider_delay, + local_peer_id.into(), + membership_kind, + ) +} + +pub(super) fn create_kademlia(local_key: &Arc) -> kad::Kademlia { + let peer_id = local_key.peer_id(); + let mut cfg = kad::KademliaConfig::default(); + cfg.set_query_timeout(Duration::from_secs(5 * 60)); + let store = kad::store::MemoryStore::new(peer_id.0); + kad::Kademlia::with_config(*peer_id.inner(), store, cfg) +} + +//Configure networking connection stack(Tcp, Noise, Yamux) +//Tcp protocol for networking +//Noise protocol for encryption +//Yamux protocol for multiplexing +pub(crate) fn create_transport( + local_key: &Arc, +) -> anyhow::Result> { + let transport = TokioTransport::new(TokioConfig::default().nodelay(true)); + let transport = dns::TokioDnsConfig::system(transport)?; + + let noise_config = noise::Config::new(local_key.inner())?; + Ok(transport + .upgrade(libp2p::core::upgrade::Version::V1) + .authenticate(noise_config) + .multiplex(yamux::Config::default()) + .timeout(Duration::from_secs(20)) + .boxed()) +} diff --git a/ephemera/src/network/libp2p/behaviours/request_response/mod.rs b/ephemera/src/network/libp2p/behaviours/request_response/mod.rs new file mode 100644 index 0000000000..ca70b7b788 --- /dev/null +++ b/ephemera/src/network/libp2p/behaviours/request_response/mod.rs @@ -0,0 +1,101 @@ +use async_trait::async_trait; +use futures::{AsyncRead, AsyncWrite}; +use libp2p::request_response; +use log::trace; +use serde::{Deserialize, Serialize}; + +use crate::broadcast::RbMsg; +use crate::utilities::codec::varint_async::{read_length_prefixed, write_length_prefixed}; +use crate::utilities::id::EphemeraId; + +#[derive(Clone)] +pub(crate) struct RbMsgMessagesCodec; + +impl RbMsgMessagesCodec {} + +#[derive(Clone)] +pub(crate) struct RbMsgProtocol; + +impl request_response::ProtocolName for RbMsgProtocol { + fn protocol_name(&self) -> &[u8] { + "/ephemera/reliable_broadcast/1.0.0".as_bytes() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct RbMsgResponse { + pub(crate) id: EphemeraId, +} + +impl RbMsgResponse { + pub(crate) fn new(id: EphemeraId) -> Self { + Self { id } + } +} + +#[async_trait] +impl request_response::Codec for RbMsgMessagesCodec { + type Protocol = RbMsgProtocol; + type Request = RbMsg; + type Response = RbMsgResponse; + + async fn read_request( + &mut self, + _: &Self::Protocol, + io: &mut T, + ) -> Result + where + T: AsyncRead + Unpin + Send, + { + //FIXME: max size + let data = read_length_prefixed(io, 1024 * 1024).await?; + //FIXME: switch to binary + let msg = serde_json::from_slice(&data)?; + trace!("Received request {:?}", msg); + Ok(msg) + } + + async fn read_response( + &mut self, + _: &Self::Protocol, + io: &mut T, + ) -> std::io::Result + where + T: AsyncRead + Unpin + Send, + { + //FIXME: max size + let response = read_length_prefixed(io, 1024 * 1024).await?; + //FIXME: switch to binary + let response = serde_json::from_slice(&response)?; + trace!("Received response {:?}", response); + Ok(response) + } + + async fn write_request( + &mut self, + _: &Self::Protocol, + io: &mut T, + req: Self::Request, + ) -> Result<(), std::io::Error> + where + T: AsyncWrite + Unpin + Send, + { + let data = serde_json::to_vec(&req).unwrap(); + write_length_prefixed(io, data).await?; + Ok(()) + } + + async fn write_response( + &mut self, + _: &Self::Protocol, + io: &mut T, + response: Self::Response, + ) -> std::io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + let response = serde_json::to_vec(&response).unwrap(); + write_length_prefixed(io, response).await?; + Ok(()) + } +} diff --git a/ephemera/src/network/libp2p/ephemera_sender.rs b/ephemera/src/network/libp2p/ephemera_sender.rs new file mode 100644 index 0000000000..c117a15539 --- /dev/null +++ b/ephemera/src/network/libp2p/ephemera_sender.rs @@ -0,0 +1,58 @@ +use log::trace; +use tokio::sync::mpsc; + +use crate::block::types::message::EphemeraMessage; +use crate::broadcast::RbMsg; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum EphemeraEvent { + EphemeraMessage(Box), + ProtocolMessage(Box), + StoreInDht { key: Vec, value: Vec }, + QueryDht { key: Vec }, +} + +pub(crate) struct EphemeraToNetwork; + +impl EphemeraToNetwork { + pub(crate) fn init() -> (EphemeraToNetworkSender, EphemeraToNetworkReceiver) { + let (net_event_tx, net_event_rcv) = mpsc::channel(1000); + + let receiver = EphemeraToNetworkReceiver::new(net_event_rcv); + let sender = EphemeraToNetworkSender::new(net_event_tx); + + (sender, receiver) + } +} + +//Receives messages from the network +pub(crate) struct EphemeraToNetworkReceiver { + pub(crate) net_event_rcv: mpsc::Receiver, +} + +impl EphemeraToNetworkReceiver { + pub(crate) fn new(net_event_rcv: mpsc::Receiver) -> Self { + Self { net_event_rcv } + } +} + +//Sends messages to the network +pub(crate) struct EphemeraToNetworkSender { + pub(crate) network_event_sender_tx: mpsc::Sender, +} + +impl EphemeraToNetworkSender { + pub(crate) fn new(network_event_sender_tx: mpsc::Sender) -> Self { + Self { + network_event_sender_tx, + } + } + + pub(crate) async fn send_ephemera_event(&mut self, event: EphemeraEvent) -> anyhow::Result<()> { + trace!("Network event: {:?}", event); + self.network_event_sender_tx + .send(event) + .await + .map_err(|e| anyhow::anyhow!(e)) + } +} diff --git a/ephemera/src/network/libp2p/mod.rs b/ephemera/src/network/libp2p/mod.rs new file mode 100644 index 0000000000..bbd1c5b5ad --- /dev/null +++ b/ephemera/src/network/libp2p/mod.rs @@ -0,0 +1,4 @@ +mod behaviours; +pub(crate) mod ephemera_sender; +pub(crate) mod network_sender; +pub(crate) mod swarm_network; diff --git a/ephemera/src/network/libp2p/network_sender.rs b/ephemera/src/network/libp2p/network_sender.rs new file mode 100644 index 0000000000..e00782f6a0 --- /dev/null +++ b/ephemera/src/network/libp2p/network_sender.rs @@ -0,0 +1,67 @@ +use log::trace; +use std::collections::HashSet; +use tokio::sync::mpsc; + +use crate::block::types::message::EphemeraMessage; +use crate::broadcast::RbMsg; +use crate::peer::PeerId; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum GroupChangeEvent { + PeersUpdated(HashSet), + LocalPeerRemoved(HashSet), + NotEnoughPeers(HashSet), +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum NetworkEvent { + EphemeraMessage(Box), + BroadcastMessage(Box), + GroupUpdate(GroupChangeEvent), + QueryDhtResponse { key: Vec, value: Vec }, +} + +pub(crate) struct EphemeraNetworkCommunication; + +impl EphemeraNetworkCommunication { + pub(crate) fn init() -> (NetCommunicationSender, NetCommunicationReceiver) { + let (net_event_tx, net_event_rcv) = mpsc::channel(1000); + + let receiver = NetCommunicationReceiver::new(net_event_rcv); + let sender = NetCommunicationSender::new(net_event_tx); + + (sender, receiver) + } +} + +//Receives messages from the network +pub(crate) struct NetCommunicationReceiver { + pub(crate) net_event_rcv: mpsc::Receiver, +} + +impl NetCommunicationReceiver { + pub(crate) fn new(net_event_rcv: mpsc::Receiver) -> Self { + Self { net_event_rcv } + } +} + +//Sends messages to the network +pub(crate) struct NetCommunicationSender { + pub(crate) network_event_sender_tx: mpsc::Sender, +} + +impl NetCommunicationSender { + pub(crate) fn new(network_event_sender_tx: mpsc::Sender) -> Self { + Self { + network_event_sender_tx, + } + } + + pub(crate) async fn send_network_event(&mut self, event: NetworkEvent) -> anyhow::Result<()> { + trace!("Network event: {:?}", event); + self.network_event_sender_tx + .send(event) + .await + .map_err(|e| anyhow::anyhow!(e)) + } +} diff --git a/ephemera/src/network/libp2p/swarm_network.rs b/ephemera/src/network/libp2p/swarm_network.rs new file mode 100644 index 0000000000..3e9dc04776 --- /dev/null +++ b/ephemera/src/network/libp2p/swarm_network.rs @@ -0,0 +1,604 @@ +use std::collections::HashSet; +use std::future::Future; +use std::str::FromStr; + +use futures::StreamExt; +use libp2p::kad::{GetClosestPeersResult, GetRecordResult}; +use libp2p::swarm::{NetworkBehaviour, SwarmBuilder}; +use libp2p::{ + gossipsub, gossipsub::IdentTopic as Topic, kad, request_response, swarm::SwarmEvent, Multiaddr, + Swarm, +}; +use log::{debug, error, info, trace}; + +use crate::membership::PeerInfo; +use crate::{ + block::types::message::EphemeraMessage, + broadcast::RbMsg, + codec::Encode, + core::builder::NodeInfo, + network::libp2p::behaviours, + network::libp2p::{ + behaviours::{ + create_behaviour, create_transport, request_response::RbMsgResponse, + GroupBehaviourEvent, GroupNetworkBehaviour, + }, + ephemera_sender::{ + EphemeraEvent, EphemeraToNetwork, EphemeraToNetworkReceiver, EphemeraToNetworkSender, + }, + network_sender::{ + EphemeraNetworkCommunication, GroupChangeEvent, + GroupChangeEvent::{LocalPeerRemoved, NotEnoughPeers}, + NetCommunicationReceiver, NetCommunicationSender, NetworkEvent, + }, + }, +}; + +pub(crate) type InitSwarm

= ( + SwarmNetwork

, + NetCommunicationReceiver, + EphemeraToNetworkSender, +); + +pub struct SwarmNetwork

+where + P: Future>> + Send + Unpin + 'static, +{ + node_info: NodeInfo, + swarm: Swarm>, + from_ephemera_rcv: EphemeraToNetworkReceiver, + to_ephemera_tx: NetCommunicationSender, + ephemera_msg_topic: Topic, +} + +impl

SwarmNetwork

+where + P: Future>> + Send + Unpin + 'static, +{ + pub(crate) fn new(node_info: NodeInfo, members_provider: P) -> anyhow::Result> + where + P: Future>> + Send + 'static, + { + let (from_ephemera_tx, from_ephemera_rcv) = EphemeraToNetwork::init(); + let (to_ephemera_tx, to_ephemera_rcv) = EphemeraNetworkCommunication::init(); + + let libp2p_configuration = node_info.initial_config.libp2p.clone(); + + let local_key = node_info.keypair.clone(); + let peer_id = node_info.peer_id; + let ephemera_msg_topic = Topic::new(&libp2p_configuration.ephemera_msg_topic_name); + + let transport = create_transport(&local_key)?; + + let members_provider_delay = + std::time::Duration::from_secs(libp2p_configuration.members_provider_delay_sec); + let behaviour = create_behaviour( + &local_key, + &ephemera_msg_topic, + members_provider, + members_provider_delay, + libp2p_configuration.membership_kind.into(), + ); + + let swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, peer_id.into()).build(); + + let network = SwarmNetwork { + node_info, + swarm, + from_ephemera_rcv, + to_ephemera_tx, + ephemera_msg_topic, + }; + + Ok((network, to_ephemera_rcv, from_ephemera_tx)) + } + + pub(crate) fn listen(&mut self) -> anyhow::Result<()> { + let address = + Multiaddr::from_str(&self.node_info.protocol_address()).expect("Invalid multi-address"); + self.swarm.listen_on(address.clone())?; + + info!("Listening on {address:?}"); + Ok(()) + } + + pub(crate) async fn start(mut self) -> anyhow::Result<()> { + loop { + tokio::select! { + swarm_event = self.swarm.next() => { + match swarm_event{ + Some(event) => { + if let Err(err) = self.handle_incoming_messages(event).await{ + error!("Error handling swarm event: {:?}", err); + } + } + None => { + anyhow::bail!("Swarm event channel closed"); + } + } + }, + Some(event) = self.from_ephemera_rcv.net_event_rcv.recv() => { + self.process_ephemera_events(event); + } + } + } + } + + fn process_ephemera_events(&mut self, event: EphemeraEvent) { + match event { + EphemeraEvent::EphemeraMessage(em) => { + self.send_ephemera_message(em.as_ref()); + } + EphemeraEvent::ProtocolMessage(pm) => { + self.send_broadcast_message(pm.as_ref()); + } + EphemeraEvent::StoreInDht { key, value } => { + let record = kad::Record::new(key, value); + let quorum = kad::Quorum::One; + match self + .swarm + .behaviour_mut() + .kademlia + .put_record(record, quorum) + { + Ok(ok) => { + trace!("StoreDht: {:?}", ok); + } + Err(err) => { + error!("StoreDht: {:?}", err); + } + } + } + EphemeraEvent::QueryDht { key } => { + let kad_key = kad::record::Key::new::>(key.as_ref()); + let query_id = self.swarm.behaviour_mut().kademlia.get_record(kad_key); + trace!("QueryDht: {:?}", query_id); + } + } + } + + async fn handle_incoming_messages( + &mut self, + swarm_event: SwarmEvent, + ) -> anyhow::Result<()> { + if let SwarmEvent::Behaviour(b) = swarm_event { + if let Err(err) = self.process_group_behaviour_event(b).await { + error!("Error handling behaviour event: {:?}", err); + } + } else { + Self::process_other_swarm_events(swarm_event); + } + Ok(()) + } + + async fn process_group_behaviour_event( + &mut self, + event: GroupBehaviourEvent, + ) -> anyhow::Result<()> { + match event { + GroupBehaviourEvent::Gossipsub(gs) => { + if let Err(err) = self.process_gossipsub_event(gs).await { + error!("Error processing gossipsub event: {:?}", err); + } + } + GroupBehaviourEvent::RequestResponse(request_response) => { + if let Err(err) = self.process_request_response(request_response).await { + error!("Error processing request response: {:?}", err); + } + } + + GroupBehaviourEvent::Membership(event) => { + if let Err(err) = self.process_members_provider_event(event).await { + error!("Error processing rendezvous event: {:?}", err); + } + } + GroupBehaviourEvent::Kademlia(ev) => { + if let Err(err) = self.process_kad_event(ev).await { + error!("Error processing kademlia event: {:?}", err); + } + } // GroupBehaviourEvent::Ping(_) => {} + } + Ok(()) + } + + async fn process_gossipsub_event(&mut self, event: gossipsub::Event) -> anyhow::Result<()> { + match event { + gossipsub::Event::Message { + propagation_source: _, + message_id: _, + message, + } => { + let msg: EphemeraMessage = serde_json::from_slice(&message.data[..])?; + self.to_ephemera_tx + .send_network_event(NetworkEvent::EphemeraMessage(msg.into())) + .await?; + } + + gossipsub::Event::Subscribed { peer_id, topic } => { + trace!("Peer {peer_id:?} subscribed to topic {topic:?}"); + } + gossipsub::Event::Unsubscribed { peer_id, topic } => { + trace!("Peer {peer_id:?} unsubscribed from topic {topic:?}"); + } + gossipsub::Event::GossipsubNotSupported { peer_id } => { + trace!("Peer {peer_id:?} does not support gossipsub"); + } + } + Ok(()) + } + + async fn process_request_response( + &mut self, + event: request_response::Event, + ) -> anyhow::Result<()> { + match event { + request_response::Event::Message { peer, message } => match message { + request_response::Message::Request { + request_id: _, + request, + channel, + } => { + let rb_id = request.id.clone(); + trace!("Received request {:?}", request); + self.to_ephemera_tx + .send_network_event(NetworkEvent::BroadcastMessage(request.into())) + .await?; + if let Err(err) = self + .swarm + .behaviour_mut() + .request_response + .send_response(channel, RbMsgResponse::new(rb_id)) + { + error!("Error sending response: {:?}", err); + } + } + request_response::Message::Response { + request_id, + response, + } => { + trace!("Received response {response:?} from peer: {peer:?}, request_id: {request_id:?}",); + } + }, + request_response::Event::OutboundFailure { + peer, + request_id, + error, + } => { + error!("Outbound failure: {error:?}, peer:{peer:?}, request_id:{request_id:?}",); + } + request_response::Event::InboundFailure { + peer, + request_id, + error, + } => { + error!("Inbound failure: {error:?}, peer:{peer:?}, request_id:{request_id:?}",); + } + request_response::Event::ResponseSent { peer, request_id } => { + trace!("Response sent to peer: {peer:?}, {request_id:?}",); + } + } + Ok(()) + } + + async fn process_members_provider_event( + &mut self, + event: behaviours::membership::behaviour::Event, + ) -> anyhow::Result<()> { + match event { + behaviours::membership::behaviour::Event::PeersUpdated(peers_ids) => { + info!("Peers updated: {:?}", peers_ids); + + let local_peer_id = *self.swarm.local_peer_id(); + for peer_id in peers_ids { + if peer_id == local_peer_id { + continue; + } + //FIXME: deprecated + #[allow(deprecated)] + let address = self + .swarm + .behaviour_mut() + .members_provider + .addresses_of_peer(&peer_id); + if let Some(address) = address.first() { + self.swarm + .behaviour_mut() + .kademlia + .add_address(&peer_id, address.clone()); + } + } + + let query_id = self + .swarm + .behaviour_mut() + .kademlia + .get_closest_peers(libp2p::PeerId::random()); + debug!("Neighbours: {:?}", query_id); + } + behaviours::membership::behaviour::Event::PeerUpdatePending => { + info!("Peer update pending"); + } + behaviours::membership::behaviour::Event::LocalRemoved(peers_ids) => { + //TODO: should pause all network block and message activities...? + let peers_ids = peers_ids.into_iter().map(Into::into).collect(); + let update = NetworkEvent::GroupUpdate(LocalPeerRemoved(peers_ids)); + self.to_ephemera_tx.send_network_event(update).await?; + } + behaviours::membership::behaviour::Event::NotEnoughPeers(peers_ids) => { + //TODO: should pause all network block and message activities...? + let peers_ids = peers_ids.into_iter().map(Into::into).collect(); + let update = NetworkEvent::GroupUpdate(NotEnoughPeers(peers_ids)); + self.to_ephemera_tx.send_network_event(update).await?; + } + } + Ok(()) + } + + async fn process_kad_event(&mut self, event: kad::KademliaEvent) -> anyhow::Result<()> { + match event { + kad::KademliaEvent::OutboundQueryProgressed { + id, + result, + stats, + step, + } => { + trace!( + "Outbound query progressed: id:{:?}, result:{:?}, stats:{:?}, step:{:?}", + id, + result, + stats, + step + ); + match result { + kad::QueryResult::GetClosestPeers(gcp) => { + self.process_closest_peers(gcp).await?; + } + kad::QueryResult::GetRecord(get_res) => { + self.process_get_record(get_res).await?; + } + kad::QueryResult::Bootstrap(bt) => { + trace!("Bootstrap: {:?}", bt); + } + kad::QueryResult::GetProviders(gp) => { + trace!("GetProviders: {:?}", gp); + } + kad::QueryResult::StartProviding(sp) => { + trace!("StartProviding: {:?}", sp); + } + kad::QueryResult::RepublishProvider(rp) => { + trace!("RepublishProvider: {:?}", rp); + } + kad::QueryResult::PutRecord(pr) => { + trace!("PutRecord: {:?}", pr); + } + kad::QueryResult::RepublishRecord(rr) => { + trace!("RepublishRecord: {:?}", rr); + } + } + } + + kad::KademliaEvent::InboundRequest { request } => { + trace!("Inbound request: {:?}", request); + } + + kad::KademliaEvent::RoutingUpdated { + peer: peer_id, + is_new_peer: _, + addresses, + bucket_range: _, + old_peer: _, + } => { + trace!("Routing updated: peer:{peer_id}, addresses:{addresses:?}",); + } + kad::KademliaEvent::UnroutablePeer { peer } => { + trace!("Unroutable peer: {:?}", peer); + } + kad::KademliaEvent::RoutablePeer { peer, address } => { + trace!("Routable peer: {:?}, address: {:?}", peer, address); + } + kad::KademliaEvent::PendingRoutablePeer { peer, address } => { + trace!("Pending routable peer: {:?}, address: {:?}", peer, address); + } + } + Ok(()) + } + + async fn process_get_record(&mut self, get_res: GetRecordResult) -> anyhow::Result<()> { + trace!("GetRecord: {:?}", get_res); + match get_res { + Ok(ok) => match ok { + kad::GetRecordOk::FoundRecord(fr) => { + let record = fr.record; + let event = NetworkEvent::QueryDhtResponse { + key: record.key.to_vec(), + value: record.value, + }; + self.to_ephemera_tx.send_network_event(event).await?; + } + kad::GetRecordOk::FinishedWithNoAdditionalRecord { .. } => { + trace!("FinishedWithNoAdditionalRecord"); + } + }, + Err(err) => { + trace!("Not getting record: {:?}", err); + } + } + Ok(()) + } + + async fn process_closest_peers(&mut self, gcp: GetClosestPeersResult) -> anyhow::Result<()> { + trace!("GetClosestPeers: {:?}", gcp); + //TODO: we need also to make sure that we have enough peers + // (Repeat if not enough, may need to wait network to stabilize) + match gcp { + Ok(cp) => { + if cp.peers.is_empty() { + log::warn!("No peers found"); + return Ok(()); + } + + let gossipsub = &mut self.swarm.behaviour_mut().gossipsub; + for peer_id in cp.peers { + gossipsub.add_explicit_peer(&peer_id); + } + + let active_peers = self + .swarm + .behaviour_mut() + .members_provider + .active_peer_ids_with_local(); + let active_peers = active_peers + .into_iter() + .map(Into::into) + .collect::>(); + let group_update = + NetworkEvent::GroupUpdate(GroupChangeEvent::PeersUpdated(active_peers)); + self.to_ephemera_tx.send_network_event(group_update).await?; + } + Err(err) => { + error!("Error getting closest peers: {:?}", err); + } + } + Ok(()) + } + + fn send_broadcast_message(&mut self, msg: &RbMsg) { + trace!("Sending broadcast message: {:?}", msg); + let local_peer_id = *self.swarm.local_peer_id(); + let behaviours = self.swarm.behaviour_mut(); + for peer in behaviours.members_provider.active_peer_ids() { + trace!("Sending broadcast message: {:?} to peer: {peer:?}", msg.id,); + if *peer == local_peer_id { + continue; + } + behaviours.request_response.send_request(peer, msg.clone()); + } + } + + fn send_ephemera_message(&mut self, msg: &EphemeraMessage) { + trace!("Sending Ephemera message: {:?}", msg); + match msg.encode() { + Ok(vec) => { + let topic = self.ephemera_msg_topic.clone(); + if let Err(err) = self.swarm.behaviour_mut().gossipsub.publish(topic, vec) { + error!("Error publishing message: {}", err); + } + } + Err(err) => { + error!("Error serializing message: {}", err); + } + } + } + + //Just logging + #[allow(clippy::too_many_lines)] + fn process_other_swarm_events(swarm_event: SwarmEvent) { + match swarm_event { + SwarmEvent::ConnectionEstablished { + peer_id, + endpoint, + num_established, + concurrent_dial_errors, + established_in, + } => { + trace!("Connection established: peer_id:{:?}, endpoint:{:?}, num_established:{:?}, concurrent_dial_errors:{:?}, established_in:{:?}", peer_id, endpoint, num_established, concurrent_dial_errors, established_in); + } + SwarmEvent::ConnectionClosed { + peer_id, + endpoint, + num_established, + cause: _, + } => { + trace!( + "Connection closed: peer_id:{:?}, endpoint:{:?}, num_established:{:?}", + peer_id, + endpoint, + num_established + ); + } + SwarmEvent::IncomingConnection { + local_addr, + send_back_addr, + } => { + trace!( + "Incoming connection: local_addr:{:?}, send_back_addr:{:?}", + local_addr, + send_back_addr + ); + } + SwarmEvent::IncomingConnectionError { + local_addr, + send_back_addr, + error, + } => { + trace!( + "Incoming connection error: local_addr:{:?}, send_back_addr:{:?}, error:{:?}", + local_addr, + send_back_addr, + error + ); + } + SwarmEvent::OutgoingConnectionError { peer_id, error } => { + trace!( + "Outgoing connection error: peer_id:{:?}, error:{:?}", + peer_id, + error + ); + } + #[allow(deprecated)] + SwarmEvent::BannedPeer { peer_id, endpoint } => { + trace!( + "Banned peer: peer_id:{:?}, endpoint:{:?}", + peer_id, + endpoint + ); + } + SwarmEvent::NewListenAddr { + listener_id, + address, + } => { + trace!( + "New listen address: listener_id:{:?}, address:{:?}", + listener_id, + address + ); + } + SwarmEvent::ExpiredListenAddr { + listener_id, + address, + } => { + trace!( + "Expired listen address: listener_id:{:?}, address:{:?}", + listener_id, + address + ); + } + SwarmEvent::ListenerClosed { + listener_id, + addresses, + reason, + } => { + trace!( + "Listener closed: listener_id:{:?}, addresses:{:?}, reason:{:?}", + listener_id, + addresses, + reason + ); + } + SwarmEvent::ListenerError { listener_id, error } => { + trace!( + "Listener error: listener_id:{:?}, error:{:?}", + listener_id, + error + ); + } + SwarmEvent::Dialing(peer_id) => { + trace!("Dialing: {peer_id:?}",); + } + + SwarmEvent::Behaviour(_) => { + trace!("Unexpected behaviour event"); + } + } + } +} diff --git a/ephemera/src/network/members/mod.rs b/ephemera/src/network/members/mod.rs new file mode 100644 index 0000000000..efb99c802a --- /dev/null +++ b/ephemera/src/network/members/mod.rs @@ -0,0 +1,252 @@ +use std::fmt::Display; +use std::future::Future; +use std::io::Write; +use std::path::PathBuf; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::{future, FutureExt}; +use log::error; +use nym_ephemera_common::types::JsonPeerInfo; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::crypto::PublicKey; +use crate::network::{Address, Peer}; +use crate::peer::PeerId; + +/// Information about an Ephemera peer. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct PeerInfo { + /// The cosmos address of the peer, used in interacting with the chain. + pub cosmos_address: String, + /// The address of the peer. + /// Expected formats: + /// 1. `:` + /// 2. `/ip4//tcp/` - this is the format used by libp2p multiaddr + pub address: String, + /// The public key of the peer. It uniquely identifies the peer. + /// Public key is used to derive the peer id. + pub pub_key: PublicKey, +} + +impl Display for PeerInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "cosmos address {}, address {}, public key {}", + self.cosmos_address, self.address, self.pub_key + ) + } +} + +impl TryFrom for Peer { + type Error = anyhow::Error; + + fn try_from(value: PeerInfo) -> std::result::Result { + let address: Address = value.address.parse()?; + let public_key = value.pub_key; + Ok(Self { + cosmos_address: value.cosmos_address, + address, + public_key: public_key.clone(), + peer_id: PeerId::from_public_key(&public_key), + }) + } +} + +#[derive(Error, Debug)] +pub enum ProviderError { + #[error("ResourceUnavailable: {0}")] + ResourceUnavailable(String), + #[error("MembersProvider: {0}")] + MembersProvider(#[from] anyhow::Error), + #[error("Could not get peers - {0}")] + GetPeers(String), +} + +pub type Result = std::result::Result; + +/// A membership provider that does nothing. +/// Might be useful for testing. +pub struct DummyMembersProvider; + +#[allow(clippy::missing_errors_doc, clippy::unused_async)] +impl DummyMembersProvider { + pub async fn empty_peers_list() -> Result> { + Ok(vec![]) + } +} + +#[derive(Error, Debug)] +pub enum ConfigMembersProviderError { + #[error("ConfigDoesNotExist: '{0}'")] + NotExist(String), + #[error("ParsingFailed: {0}")] + ParsingFailed(#[from] config::ConfigError), + #[error("TomlError: {0}")] + TomlError(#[from] toml::ser::Error), + #[error("IoError: {0}")] + IoError(#[from] std::io::Error), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct PeerSetting { + /// The cosmos address of the peer, used in interacting with the chain. + pub cosmos_address: String, + /// The address of the peer. + /// Expected formats: + /// 1. `:` + /// 2. `/ip4//tcp/` - this is the format used by libp2p multiaddr + pub address: String, + ///Serialized public key. + /// + /// # Converting to string and back example + ///``` + /// use ephemera::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair, PublicKey}; + /// + /// let public_key = Keypair::generate(None).public_key(); + /// + /// let public_key_str = public_key.to_string(); + /// + /// let public_key_parsed = public_key_str.parse::().unwrap(); + /// + /// assert_eq!(public_key, public_key_parsed); + /// ``` + pub public_key: String, +} + +impl TryFrom for PeerInfo { + type Error = anyhow::Error; + + fn try_from(setting: PeerSetting) -> std::result::Result { + let pub_key = setting.public_key.parse::()?; + Ok(PeerInfo { + cosmos_address: setting.cosmos_address, + address: setting.address, + pub_key, + }) + } +} + +///[`ProviderFut`] that reads the peers from a toml config file. +/// +/// # Configuration example +/// ```toml +/// [[peers]] +/// name = "node1" +/// address = "/ip4/127.0.0.1/tcp/3000" +/// pub_key = "4XTTMEghav9LZThm6opUaHrdGEEYUkrfkakVg4VAetetBZDWJ" +/// +/// [[peers]] +/// name = "node2" +/// address = "/ip4/127.0.0.1/tcp/3001" +/// pub_key = "4XTTMFQt2tgNRmwRgEAaGQe2NXygsK6Vr3pkuBfYezhDfoVty" +/// ``` +pub struct ConfigMembersProvider { + config_location: PathBuf, +} + +impl ConfigMembersProvider { + /// Creates a new [`ConfigMembersProvider`] instance. + /// + /// # Arguments + /// * `path` - Path to the peers toml config file. + /// + /// # Errors + /// Returns [`ConfigMembersProviderError::NotExist`] if the file does not exist. + /// Returns [`ConfigMembersProviderError::ParsingFailed`] if the file is not a valid members file. + pub fn init>( + path: I, + ) -> std::result::Result { + let path_buf = path.into(); + if !path_buf.exists() { + return Err(ConfigMembersProviderError::NotExist( + path_buf.to_string_lossy().to_string(), + )); + } + + let provider = Self { + config_location: path_buf, + }; + + if provider.read_config().is_err() { + return Err(ConfigMembersProviderError::ParsingFailed( + config::ConfigError::Message("Failed to parse config".to_string()), + )); + } + + Ok(provider) + } + + pub(crate) fn read_config(&self) -> Result> { + let config_peers = ConfigPeers::try_load(self.config_location.clone()) + .map_err(|err| anyhow::anyhow!(err))?; + + let peers = config_peers + .peers + .iter() + .map(|peer| PeerInfo::try_from(peer.clone())) + .collect::>>()?; + Ok(peers) + } +} + +impl Future for ConfigMembersProvider { + type Output = Result>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + future::ready(self.read_config()).poll_unpin(cx) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct ConfigPeers { + peers: Vec, +} + +impl ConfigPeers { + pub(crate) fn new(peers: Vec) -> Self { + Self { peers } + } + + pub(crate) fn try_load>( + path: I, + ) -> std::result::Result { + let path = path.into(); + let config = config::Config::builder() + .add_source(config::File::from(path)) + .build()?; + + config.try_deserialize().map_err(Into::into) + } + + pub(crate) fn try_write>( + &self, + path: I, + ) -> std::result::Result<(), ConfigMembersProviderError> { + let config = toml::to_string(&self)?; + + let config = format!( + "#This file is generated by cli and automatically overwritten every time when cli is §\n{config}", + ); + + let mut file = std::fs::File::create(path.into())?; + file.write_all(config.as_bytes())?; + + Ok(()) + } +} + +impl TryFrom for PeerInfo { + type Error = anyhow::Error; + + fn try_from(json_peer_info: JsonPeerInfo) -> std::result::Result { + let pub_key = json_peer_info.public_key.parse::()?; + Ok(PeerInfo { + cosmos_address: json_peer_info.cosmos_address.to_string(), + address: json_peer_info.ip_address, + pub_key, + }) + } +} diff --git a/ephemera/src/network/mod.rs b/ephemera/src/network/mod.rs new file mode 100644 index 0000000000..205e147903 --- /dev/null +++ b/ephemera/src/network/mod.rs @@ -0,0 +1,237 @@ +use std::fmt::Display; +use std::net::IpAddr; +use std::str::FromStr; + +use ::libp2p::{multiaddr::Protocol, Multiaddr}; +use libp2p_identity::PeerId as Libp2pPeerId; +use log::info; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::crypto::PublicKey; + +pub(crate) mod libp2p; +pub(crate) mod members; + +pub(crate) type PeerIdType = Libp2pPeerId; + +#[derive(Debug, Error)] +pub enum PeerIdError { + #[error("Invalid peer ID: {0}")] + InvalidPeerId(String), +} + +/// Unique identifier of a peer. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PeerId(pub(crate) PeerIdType); + +impl PeerId { + #[must_use] + pub fn random() -> Self { + Self(PeerIdType::random()) + } + + /// Returns the internal representation of the peer ID. + pub(crate) fn inner(&self) -> &PeerIdType { + &self.0 + } + + /// Returns a raw representation of the peer ID. + #[must_use] + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes() + } + + /// Returns a peer ID from a raw representation. + /// + /// # Returns + /// A `PeerId` if the bytes are valid. + /// + /// # Errors + /// An error if input has wrong format. This function is reverse to [`PeerId::to_bytes`]. + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Self(PeerIdType::from_bytes(bytes).map_err(|e| { + PeerIdError::InvalidPeerId(format!("Invalid peer ID: {e}")) + })?)) + } + + /// Builds a `PeerId` from a public key. + #[must_use] + pub fn from_public_key(public_key: &PublicKey) -> Self { + Self(PeerIdType::from_public_key(public_key.inner())) + } +} + +impl From for libp2p_identity::PeerId { + fn from(peer_id: PeerId) -> Self { + peer_id.0 + } +} + +impl From for PeerId { + fn from(peer_id: libp2p_identity::PeerId) -> Self { + Self(peer_id) + } +} + +impl Display for PeerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +pub trait ToPeerId { + fn peer_id(&self) -> PeerId; +} + +/// A peer of the network. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct Peer { + /// The peer's ID. It identifies the peer uniquely and is derived from its public key. + /// + /// # Deriving PeerId from PublicKey example + /// + /// ``` + /// use ephemera::crypto::{EphemeraKeypair, Keypair, PublicKey}; + /// use ephemera::peer::{PeerId, ToPeerId}; + /// + /// let public_key = Keypair::generate(None).public_key(); + /// + /// let peer_id = PeerId::from_public_key(&public_key); + /// + /// assert_eq!(peer_id, public_key.peer_id()); + /// + /// ``` + pub peer_id: PeerId, + /// The peer's public key. It matches PeerId. + pub public_key: PublicKey, + /// The peer's address. + pub address: Address, + /// The cosmos address of the peer, used in interacting with the chain. + pub cosmos_address: String, +} + +#[derive(Error, Debug)] +pub enum AddressError { + #[error("Failed to parse address: {0}")] + ParsingError(String), +} + +/// Ephemera node address. +/// +/// Supported formats: +/// 1. `:` +/// 2. `/ip4//tcp/` - this is format used by libp2p multiaddr. +/// 3. `/dns4//tcp/` - this is format used by libp2p multiaddr. +/// See [libp2p/multiaddress](https://github.com/libp2p/specs/blob/master/addressing/README.md) for more details. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Address(pub Multiaddr); + +impl Address { + pub fn inner(&self) -> &Multiaddr { + &self.0 + } +} + +impl From for Address { + fn from(multiaddr: Multiaddr) -> Self { + Self(multiaddr) + } +} + +impl FromStr for Address { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let address: Option = match Multiaddr::from_str(s) { + Ok(multiaddr) => Some(multiaddr), + Err(err) => { + info!("Failed to parse multiaddr: {}", err); + None + } + }; + + let multi_address = address.or_else(|| match std::net::SocketAddr::from_str(s) { + Ok(sa) => { + let mut multiaddr = Multiaddr::empty(); + match sa { + std::net::SocketAddr::V4(v4) => { + multiaddr.push(Protocol::Ip4(*v4.ip())); + multiaddr.push(Protocol::Tcp(v4.port())); + } + std::net::SocketAddr::V6(v6) => { + multiaddr.push(Protocol::Ip6(*v6.ip())); + multiaddr.push(Protocol::Tcp(v6.port())); + } + } + + Some(multiaddr) + } + Err(err) => { + info!("Failed to parse socket addr: {err}"); + None + } + }); + + match multi_address { + Some(multi_address) => Ok(Self(multi_address)), + None => Err(AddressError::ParsingError(s.to_string())), + } + } +} + +impl TryFrom

for (IpAddr, u16) { + type Error = std::io::Error; + + fn try_from(addr: Address) -> Result { + let mut multiaddr = addr.0; + if let Some(Protocol::Tcp(port)) = multiaddr.pop() { + if let Some(Protocol::Ip4(ip)) = multiaddr.pop() { + return Ok((IpAddr::V4(ip), port)); + } + } + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "invalid address", + )) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_multiaddr() { + "/ip4/127.0.0.1/tcp/1234".parse::
().unwrap(); + } + + #[test] + fn test_parse_ip_port() { + "127.0.0.1:1234".parse::
().unwrap(); + } + + #[test] + fn test_fail_parse_multiaddr_without_port() { + let result = "/ip4/127.0.0.1/tcp/".parse::
(); + assert!(matches!(result, Err(AddressError::ParsingError(_)))); + } + + #[test] + fn test_fail_parse_multiaddr_without_ip() { + let result = "/ip4//tcp/1234".parse::
(); + assert!(matches!(result, Err(AddressError::ParsingError(_)))); + } + + #[test] + fn test_fail_parse_ip_port_without_port() { + let result = "127.0.0.1".parse::
(); + assert!(matches!(result, Err(AddressError::ParsingError(_)))); + } + + #[test] + fn test_fail_parse_ip_port_without_ip() { + let result = "1234".parse::
(); + assert!(matches!(result, Err(AddressError::ParsingError(_)))); + } +} diff --git a/ephemera/src/storage/mod.rs b/ephemera/src/storage/mod.rs new file mode 100644 index 0000000000..2342625d19 --- /dev/null +++ b/ephemera/src/storage/mod.rs @@ -0,0 +1,65 @@ +//! # Database +//! +//! It supports `SqlLite` and `RocksDB`. +//! +//! ## `RocksDB` +//! +//! `SqlLite` is used by default. +//! +//! ## `SqlLite` +//! +//! To use `SqlLite`, you need to compile with the `sqlite_storage` feature and with `--no-default-features` flag. + +use std::collections::HashSet; + +use thiserror::Error; + +use crate::block::types::block::Block; +use crate::peer::PeerId; +use crate::utilities::crypto::Certificate; +use crate::utilities::merkle::MerkleTree; + +#[cfg(feature = "rocksdb_storage")] +pub(crate) mod rocksdb; + +#[cfg(feature = "sqlite_storage")] +pub(crate) mod sqlite; + +pub(crate) type Result = std::result::Result; + +#[derive(Error, Debug)] +pub(crate) enum DatabaseError { + //Pessimistically assume that most database errors are fatal and unrecoverable. + #[error("Database failure: {0}")] + DatabaseFailure(#[from] anyhow::Error), +} + +pub(crate) trait EphemeraDatabase: Send { + /// Returns block by its id. Block ids are generated by Ephemera + fn get_block_by_hash(&self, block_hash: &str) -> Result>; + + /// Returns last committed/finalised block. + fn get_last_block(&self) -> Result>; + + /// Returns block by its height + fn get_block_by_height(&self, height: u64) -> Result>; + + /// Returns block certificates. + /// + /// Certificates were created as part of broadcast protocol and signed by peers who participated. + fn get_block_certificates(&self, block_hash: &str) -> Result>>; + + /// Returns peers who participated in block broadcast. + fn get_block_broadcast_group(&self, block_hash: &str) -> Result>>; + + /// Stores block and its signatures + fn store_block( + &mut self, + block: &Block, + certificates: HashSet, + members: HashSet, + ) -> Result<()>; + + /// Returns block merkle tree + fn get_block_merkle_tree(&self, block_hash: &str) -> Result>; +} diff --git a/ephemera/src/storage/rocksdb/mod.rs b/ephemera/src/storage/rocksdb/mod.rs new file mode 100644 index 0000000000..dc86a4bd7f --- /dev/null +++ b/ephemera/src/storage/rocksdb/mod.rs @@ -0,0 +1,125 @@ +use std::collections::HashSet; +use std::sync::Arc; + +use log::info; +use rocksdb::{TransactionDB, TransactionDBOptions}; + +use crate::block::types::block::Block; +use crate::config::DatabaseConfiguration; +use crate::peer::PeerId; +use crate::storage::rocksdb::query::Database; +use crate::storage::rocksdb::store::DbStore; +use crate::storage::EphemeraDatabase; +use crate::storage::Result; +use crate::utilities::crypto::Certificate; +use crate::utilities::merkle::MerkleTree; + +pub(crate) mod query; +pub(crate) mod store; + +pub(crate) struct RocksDbStorage { + pub(crate) db_store: DbStore, + pub(crate) db_query: Database, +} + +const PREFIX_LAST_BLOCK_KEY: &str = "last_block"; +const PREFIX_BLOCK_HASH: &str = "block_hash"; +const PREFIX_BLOCK_HEIGHT: &str = "block_height"; +const PREFIX_CERTIFICATES: &str = "block_certificates"; +const PREFIX_MEMBERS: &str = "block_members"; +const MERKLE_TREE: &str = "merkle_tree"; + +impl RocksDbStorage { + pub fn open(db_conf: &DatabaseConfiguration) -> Result { + info!("Opening RocksDB database at {}", db_conf.rocksdb_path); + + let mut options = rocksdb::Options::default(); + options.create_if_missing(db_conf.create_if_not_exists); + + let db = TransactionDB::open( + &options, + &TransactionDBOptions::default(), + db_conf.rocksdb_path.clone(), + ) + .map_err(|err| anyhow::anyhow!(err))?; + + let db = Arc::new(db); + let db_store = DbStore::new(db.clone()); + let db_query = Database::new(db); + let storage = Self { db_store, db_query }; + + info!("Opened RocksDB database at {}", db_conf.rocksdb_path); + Ok(storage) + } +} + +impl EphemeraDatabase for RocksDbStorage { + fn get_block_by_hash(&self, block_id: &str) -> Result> { + self.db_query + .get_block_by_hash(block_id) + .map_err(Into::into) + } + + fn get_last_block(&self) -> Result> { + self.db_query.get_last_block().map_err(Into::into) + } + + fn get_block_by_height(&self, height: u64) -> Result> { + self.db_query + .get_block_by_height(height) + .map_err(Into::into) + } + + fn get_block_certificates(&self, block_id: &str) -> Result>> { + self.db_query + .get_block_certificates(block_id) + .map_err(Into::into) + } + + fn get_block_broadcast_group(&self, block_id: &str) -> Result>> { + self.db_query + .get_block_broadcast_group(block_id) + .map_err(Into::into) + } + + fn store_block( + &mut self, + block: &Block, + certificates: HashSet, + members: HashSet, + ) -> Result<()> { + self.db_store + .store_block(block, certificates, members) + .map_err(Into::into) + } + + fn get_block_merkle_tree(&self, block_hash: &str) -> Result> { + self.db_query + .get_block_merkle_tree(block_hash) + .map_err(Into::into) + } +} + +fn block_hash_key(block_hash: &str) -> String { + format!("{PREFIX_BLOCK_HASH}:{block_hash}") +} + +fn block_height_key(height: &u64) -> String { + format!("{PREFIX_BLOCK_HEIGHT}:{height}") +} + +fn last_block_key() -> String { + PREFIX_LAST_BLOCK_KEY.to_string() +} + +fn certificates_key(block_hash: &str) -> String { + format!("{PREFIX_CERTIFICATES}:{block_hash}",) +} + +fn members_key(block_hash: &str) -> String { + format!("{PREFIX_MEMBERS}:{block_hash}",) +} + +fn merkle_tree_key(block_hash: &str) -> String { + format!("{MERKLE_TREE}:{block_hash}",) +} diff --git a/ephemera/src/storage/rocksdb/query.rs b/ephemera/src/storage/rocksdb/query.rs new file mode 100644 index 0000000000..a2586557e5 --- /dev/null +++ b/ephemera/src/storage/rocksdb/query.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use log::trace; +use rocksdb::TransactionDB; + +use crate::block::types::block::Block; +use crate::network::PeerId; +use crate::storage::rocksdb::{ + block_hash_key, block_height_key, certificates_key, last_block_key, members_key, + merkle_tree_key, +}; +use crate::utilities::crypto::Certificate; +use crate::utilities::merkle::MerkleTree; + +pub struct Database { + database: Arc, +} + +impl Database { + #[allow(dead_code)] + pub fn new(db: Arc) -> Database { + Database { database: db } + } + + pub(crate) fn get_block_by_hash(&self, block_hash: &str) -> anyhow::Result> { + trace!("Getting block by id: {:?}", block_hash); + + let block_hash_key = block_hash_key(block_hash); + + let block = if let Some(block) = self.database.get(block_hash_key)? { + let block = serde_json::from_slice::(&block)?; + trace!("Found block: {}", block.header); + Some(block) + } else { + trace!("Didn't find block"); + None + }; + Ok(block) + } + + pub(crate) fn get_last_block(&self) -> anyhow::Result> { + trace!("Getting last block"); + + if let Some(block_hash) = self.database.get(last_block_key())? { + let block_hash = String::from_utf8(block_hash)?; + self.get_block_by_hash(&block_hash) + } else { + trace!("Unable to get last block"); + Ok(None) + } + } + + pub(crate) fn get_block_by_height(&self, height: u64) -> anyhow::Result> { + trace!("Getting block by height: {}", height); + + if let Some(block_hash) = self.database.get(block_height_key(&height))? { + let block_hash = String::from_utf8(block_hash)?; + self.get_block_by_hash(&block_hash) + } else { + trace!("Didn't find block"); + Ok(None) + } + } + + pub(crate) fn get_block_certificates( + &self, + block_hash: &str, + ) -> anyhow::Result>> { + trace!("Getting block signatures: {}", block_hash); + + let certificates_key = certificates_key(block_hash); + + if let Some(certificates) = self.database.get(certificates_key)? { + let certificates: Vec = serde_json::from_slice(&certificates)?; + trace!("Found certificates: {:?}", certificates); + Ok(Some(certificates)) + } else { + trace!("Didn't find signatures"); + Ok(None) + } + } + + pub(crate) fn get_block_broadcast_group( + &self, + block_hash: &str, + ) -> anyhow::Result>> { + trace!("Getting block broadcast group: {}", block_hash); + + let members_key = members_key(block_hash); + + if let Some(members) = self.database.get(members_key)? { + let members: Vec = serde_json::from_slice(&members)?; + trace!("Found members: {:?}", members); + Ok(Some(members)) + } else { + trace!("Didn't find members"); + Ok(None) + } + } + + pub(crate) fn get_block_merkle_tree( + &self, + block_hash: &str, + ) -> anyhow::Result> { + trace!("Getting block merkle tree: {}", block_hash); + + let merkle_tree_key = merkle_tree_key(block_hash); + + if let Some(merkle_tree) = self.database.get(merkle_tree_key)? { + let merkle_tree: MerkleTree = serde_json::from_slice(&merkle_tree)?; + trace!("Found merkle tree: {:?}", merkle_tree); + Ok(Some(merkle_tree)) + } else { + trace!("Didn't find merkle tree"); + Ok(None) + } + } +} diff --git a/ephemera/src/storage/rocksdb/store.rs b/ephemera/src/storage/rocksdb/store.rs new file mode 100644 index 0000000000..66e1f6d2de --- /dev/null +++ b/ephemera/src/storage/rocksdb/store.rs @@ -0,0 +1,79 @@ +use std::collections::HashSet; +use std::sync::Arc; + +use crate::block::types::block::Block; +use crate::network::PeerId; +use crate::storage::rocksdb::{ + block_hash_key, block_height_key, certificates_key, last_block_key, members_key, + merkle_tree_key, +}; +use log::{debug, trace}; +use rocksdb::{TransactionDB, WriteBatchWithTransaction}; + +use crate::utilities::crypto::Certificate; + +pub struct DbStore { + connection: Arc, +} + +impl DbStore { + pub fn new(db: Arc) -> DbStore { + DbStore { connection: db } + } + + pub(crate) fn store_block( + &self, + block: &Block, + certificates: HashSet, + members: HashSet, + ) -> anyhow::Result<()> { + debug!("Storing block: {}", block.header); + trace!("Storing block certificates: {}", certificates.len()); + + let hash_str = block.header.hash.to_string(); + + let block_id_key = block_hash_key(&hash_str); + let certificates_key = certificates_key(&hash_str); + let height_key = block_height_key(&block.header.height); + let members_key = members_key(&hash_str); + let merkle_tree_key = merkle_tree_key(&hash_str); + + // Check UNIQUE constraints + let existing_id = self.connection.get(&block_id_key)?; + if existing_id.is_some() { + return Err(anyhow::anyhow!("Block already exists")); + } + + let mut batch = WriteBatchWithTransaction::::default(); + + //Store last block id(without prefix!) + //May want to check that height is incremented by 1 + batch.put(last_block_key(), hash_str.clone()); + + // Store block height + batch.put(height_key.as_bytes(), hash_str); + + // Store block(without signature) + let block_bytes = serde_json::to_vec::(block)?; + batch.put(block_id_key.as_bytes(), block_bytes); + + // Store block certificates + let certificates_bytes = + serde_json::to_vec(&certificates.into_iter().collect::>()) + .map_err(|e| anyhow::anyhow!(e))?; + batch.put(certificates_key.as_bytes(), certificates_bytes); + + // Store block members + let members_bytes = serde_json::to_vec(&members.into_iter().collect::>()) + .map_err(|e| anyhow::anyhow!(e))?; + batch.put(members_key.as_bytes(), members_bytes); + + //Store Merkle Tree + let merkle_tree = block.merkle_tree()?; + let merkle_tree_bytes = serde_json::to_vec(&merkle_tree).map_err(|e| anyhow::anyhow!(e))?; + batch.put(merkle_tree_key.as_bytes(), merkle_tree_bytes); + + self.connection.write(batch)?; + Ok(()) + } +} diff --git a/ephemera/src/storage/sqlite/mod.rs b/ephemera/src/storage/sqlite/mod.rs new file mode 100644 index 0000000000..798e9eb501 --- /dev/null +++ b/ephemera/src/storage/sqlite/mod.rs @@ -0,0 +1,107 @@ +use log::{error, info}; +use rusqlite::Connection; +use std::collections::HashSet; + +use crate::block::types::block::Block; +use crate::config::DatabaseConfiguration; +use crate::peer::PeerId; +use crate::storage::sqlite::query::DbQuery; +use crate::storage::sqlite::store::Database; +use crate::storage::EphemeraDatabase; +use crate::storage::Result; +use crate::utilities::crypto::Certificate; +use crate::utilities::merkle::MerkleTree; + +pub(crate) mod query; +pub(crate) mod store; + +mod migrations { + use refinery::embed_migrations; + + embed_migrations!("migrations"); +} + +pub(crate) struct SqliteStorage { + pub(crate) db_store: Database, + pub(crate) db_query: DbQuery, +} + +impl SqliteStorage { + pub(crate) fn open(db_conf: DatabaseConfiguration) -> Result { + let mut flags = rusqlite::OpenFlags::default(); + if !db_conf.create_if_not_exists { + flags.remove(rusqlite::OpenFlags::SQLITE_OPEN_CREATE); + } + + let mut connection = Connection::open_with_flags(db_conf.sqlite_path.clone(), flags) + .map_err(|err| anyhow::anyhow!(err))?; + Self::run_migrations(&mut connection)?; + + info!("Starting db backend with path: {}", db_conf.sqlite_path); + let db_store = Database::open(db_conf.clone(), flags)?; + let db_query = DbQuery::open(db_conf, flags)?; + let storage = Self { db_store, db_query }; + Ok(storage) + } + + pub(crate) fn run_migrations(connection: &mut Connection) -> Result<()> { + info!("Running database migrations"); + match migrations::migrations::runner().run(connection) { + Ok(ok) => { + info!("Database migrations completed:{:?} ", ok); + Ok(()) + } + Err(err) => { + error!("Database migrations failed: {}", err); + Err(anyhow::anyhow!(err).into()) + } + } + } +} + +impl EphemeraDatabase for SqliteStorage { + fn get_block_by_hash(&self, block_id: &str) -> Result> { + self.db_query + .get_block_by_hash(block_id) + .map_err(Into::into) + } + + fn get_last_block(&self) -> Result> { + self.db_query.get_last_block().map_err(Into::into) + } + + fn get_block_by_height(&self, height: u64) -> Result> { + self.db_query + .get_block_by_height(height) + .map_err(Into::into) + } + + fn get_block_certificates(&self, block_id: &str) -> Result>> { + self.db_query + .get_block_certificates(block_id) + .map_err(Into::into) + } + + fn get_block_broadcast_group(&self, block_id: &str) -> Result>> { + self.db_query + .get_block_broadcast_group(block_id) + .map_err(Into::into) + } + + fn store_block( + &mut self, + block: &Block, + certificates: HashSet, + members: HashSet, + ) -> Result<()> { + self.db_store + .store_block(block, certificates, members) + .map_err(Into::into) + } + + fn get_block_merkle_tree(&self, block_hash: &str) -> Result> { + self.db_query + .get_block_merkle_tree(block_hash) + .map_err(Into::into) + } +} diff --git a/ephemera/src/storage/sqlite/query.rs b/ephemera/src/storage/sqlite/query.rs new file mode 100644 index 0000000000..df3b1cd342 --- /dev/null +++ b/ephemera/src/storage/sqlite/query.rs @@ -0,0 +1,167 @@ +use log::{error, trace}; +use rusqlite::{params, Connection, OpenFlags, OptionalExtension, Row}; + +use crate::block::types::block::Block; +use crate::config::DatabaseConfiguration; +use crate::peer::PeerId; +use crate::utilities::crypto::Certificate; +use crate::utilities::merkle::MerkleTree; + +pub(crate) struct DbQuery { + pub(crate) connection: Connection, +} + +impl DbQuery { + pub(crate) fn open(db_conf: DatabaseConfiguration, flags: OpenFlags) -> anyhow::Result { + let connection = Connection::open_with_flags(db_conf.sqlite_path, flags)?; + let query = Self { connection }; + Ok(query) + } + + pub(crate) fn get_block_by_hash(&self, block_hash: &str) -> anyhow::Result> { + let mut stmt = self + .connection + .prepare_cached("SELECT block FROM blocks WHERE block_hash = ?1")?; + let block = stmt + .query_row(params![block_hash], Self::map_block()) + .optional()?; + + if let Some(block) = &block { + trace!("Found block: {}", block.header); + } else { + trace!("Block not found: {}", block_hash); + }; + + Ok(block) + } + + pub(crate) fn get_last_block(&self) -> anyhow::Result> { + let mut stmt = self + .connection + .prepare_cached("SELECT block FROM blocks where id = (select max(id) from blocks)")?; + + let block = stmt.query_row(params![], Self::map_block()).optional()?; + + if let Some(block) = &block { + trace!("Found last block: {}", block.header); + } else { + trace!("Last block not found"); + }; + + Ok(block) + } + + pub(crate) fn get_block_by_height(&self, height: u64) -> anyhow::Result> { + let mut stmt = self + .connection + .prepare_cached("SELECT block FROM blocks WHERE height = ?1")?; + let block = stmt + .query_row(params![height], Self::map_block()) + .optional()?; + + if let Some(block) = &block { + trace!("Found block: {}", block.header); + } else { + trace!("Block not found: {}", height); + }; + + Ok(block) + } + + pub(crate) fn get_block_certificates( + &self, + block_hash: &str, + ) -> anyhow::Result>> { + let mut stmt = self + .connection + .prepare_cached("SELECT certificates FROM block_certificates where block_hash = ?1")?; + + let signatures = stmt + .query_row(params![block_hash], |row| { + let certificates: Vec = row.get(0)?; + let certificates = serde_json::from_slice::>(&certificates) + .map_err(|e| { + error!("Error deserializing certificates: {}", e); + rusqlite::Error::InvalidQuery {} + })?; + Ok(certificates) + }) + .optional()?; + + if signatures.is_some() { + trace!("Found block {} certificates", block_hash); + } else { + trace!("Certificates not found"); + }; + + Ok(signatures) + } + + pub(crate) fn get_block_broadcast_group( + &self, + block_hash: &str, + ) -> anyhow::Result>> { + let mut stmt = self + .connection + .prepare_cached("SELECT members FROM block_broadcast_group where block_hash = ?1")?; + + let members = stmt + .query_row(params![block_hash], |row| { + let members: Vec = row.get(0)?; + let members = serde_json::from_slice::>(&members).map_err(|e| { + error!("Error deserializing members: {}", e); + rusqlite::Error::InvalidQuery {} + })?; + Ok(members) + }) + .optional()?; + + if members.is_some() { + trace!("Found block {} members", block_hash); + } else { + trace!("Members not found"); + }; + + Ok(members) + } + + pub(crate) fn get_block_merkle_tree( + &self, + block_hash: &str, + ) -> anyhow::Result> { + let mut stmt = self + .connection + .prepare_cached("SELECT merkle_tree FROM block_merkle_tree where block_hash = ?1")?; + + let merkle_tree = stmt + .query_row(params![block_hash], |row| { + let merkle_tree: Vec = row.get(0)?; + let merkle_tree = + serde_json::from_slice::(&merkle_tree).map_err(|e| { + error!("Error deserializing merkle_tree: {}", e); + rusqlite::Error::InvalidQuery {} + })?; + Ok(merkle_tree) + }) + .optional()?; + + if merkle_tree.is_some() { + trace!("Found block {} merkle_tree", block_hash); + } else { + trace!("Merkle_tree not found"); + }; + + Ok(merkle_tree) + } + + fn map_block() -> impl FnOnce(&Row) -> Result { + |row| { + let body: Vec = row.get(0)?; + let block = serde_json::from_slice::(&body).map_err(|e| { + error!("Error deserializing block: {}", e); + rusqlite::Error::InvalidQuery {} + })?; + Ok(block) + } + } +} diff --git a/ephemera/src/storage/sqlite/store.rs b/ephemera/src/storage/sqlite/store.rs new file mode 100644 index 0000000000..29cf181d9f --- /dev/null +++ b/ephemera/src/storage/sqlite/store.rs @@ -0,0 +1,71 @@ +use crate::block::types::block::Block; +use anyhow::Result; +use log::debug; +use rusqlite::{params, Connection, OpenFlags}; +use std::collections::HashSet; + +use crate::config::DatabaseConfiguration; +use crate::network::PeerId; +use crate::utilities::crypto::Certificate; + +pub struct Database { + connection: Connection, +} + +impl Database { + pub fn open(db_conf: DatabaseConfiguration, flags: OpenFlags) -> Result { + let connection = Connection::open_with_flags(db_conf.sqlite_path, flags)?; + Ok(Database { connection }) + } + + pub(crate) fn store_block( + &mut self, + block: &Block, + certificates: HashSet, + members: HashSet, + ) -> Result<()> { + debug!("Storing block: {}", block.header); + + let hash = block.header.hash.to_string(); + let height = block.header.height; + let block_bytes = serde_json::to_vec::(block).map_err(|e| anyhow::anyhow!(e))?; + let certificates_bytes = + serde_json::to_vec(&certificates.into_iter().collect::>()) + .map_err(|e| anyhow::anyhow!(e))?; + let members_bytes = serde_json::to_vec(&members.into_iter().collect::>()) + .map_err(|e| anyhow::anyhow!(e))?; + let merkle_tree = block.merkle_tree()?; + let merkle_tree_bytes = serde_json::to_vec(&merkle_tree).map_err(|e| anyhow::anyhow!(e))?; + + let tx = self.connection.transaction()?; + { + let mut statement = tx.prepare_cached( + "INSERT INTO blocks (block_hash, height, block) VALUES (?1, ?2, ?3)", + )?; + statement.execute(params![&hash, &height, &block_bytes,])?; + + let mut statement = tx.prepare_cached( + "INSERT INTO block_certificates (block_hash, certificates) VALUES (?1, ?2)", + )?; + + statement.execute(params![&hash, &certificates_bytes,])?; + + let mut statement = tx.prepare_cached( + "INSERT INTO block_broadcast_group (block_hash, members) VALUES (?1, ?2)", + )?; + + statement.execute(params![&hash, &members_bytes])?; + + //store Merkle Tree + let mut statement = tx.prepare_cached( + "INSERT INTO block_merkle_tree (block_hash, merkle_tree) VALUES (?1, ?2)", + )?; + + statement.execute(params![&hash, &merkle_tree_bytes])?; + } + + tx.commit()?; + + Ok(()) + } +} diff --git a/ephemera/src/utilities/codec/mod.rs b/ephemera/src/utilities/codec/mod.rs new file mode 100644 index 0000000000..69905b5440 --- /dev/null +++ b/ephemera/src/utilities/codec/mod.rs @@ -0,0 +1,102 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +pub(crate) mod varint_async; +pub(crate) mod varint_bytes; + +pub(crate) type Codec = SerdeCodec; + +#[derive(Debug, Error)] +pub enum DecodingError { + #[error("Decoding error: {0}")] + DecodingError(#[from] serde_json::Error), +} + +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Error)] +pub enum EncodingError { + #[error("Encoding error: {0}")] + EncodingError(#[from] serde_json::Error), +} + +/// Simple trait for encoding +pub(crate) trait EphemeraCodec { + /// Encodes a message into a vector of bytes + /// + /// # Arguments + /// + /// * `data` - data to encode + /// + /// # Returns + /// + /// * `Result, EncodingError>` - encoded data or error + /// + /// # Errors + /// + /// * `EncodingError` - if encoding fails + fn encode(data: &M) -> Result, EncodingError>; + + /// Decodes a message from a vector of bytes + fn decode serde::Deserialize<'de>>(bytes: &[u8]) -> Result; +} + +pub(crate) struct SerdeCodec; + +impl EphemeraCodec for SerdeCodec { + fn encode(data: &M) -> Result, EncodingError> { + let bytes = serde_json::to_vec(data)?; + Ok(bytes) + } + + fn decode Deserialize<'de>>(bytes: &[u8]) -> Result { + let decoded = serde_json::from_slice(bytes)?; + Ok(decoded) + } +} + +/// Trait which types can implement to provide their own encoding +pub trait Encode { + /// Encodes itself into a vector of bytes + /// + /// # Returns + /// + /// * `Result, EncodingError>` - encoded data or error + /// + /// # Errors + /// + /// * `EncodingError` - if encoding fails + fn encode(&self) -> Result, EncodingError>; +} + +/// Trait which types can implement to provide their own decoding +pub trait Decode { + type Output: for<'de> serde::Deserialize<'de>; + + /// Decodes itself from a vector of bytes + /// + /// # Arguments + /// + /// * `bytes` - bytes to decode + /// + /// # Returns + /// + /// * `Result` - decoded data or error + /// + /// # Errors + /// + /// * `DecodingError` - if decoding fails + fn decode(bytes: &[u8]) -> Result; +} + +#[cfg(test)] +mod test { + use crate::utilities::codec::EphemeraCodec; + + #[test] + fn test_encode_decode() { + let data = vec![1, 2, 3, 4, 5]; + let encoded = super::SerdeCodec::encode(&data).unwrap(); + let decoded = super::SerdeCodec::decode::>(&encoded).unwrap(); + assert_eq!(data, decoded); + } +} diff --git a/ephemera/src/utilities/codec/varint_async.rs b/ephemera/src/utilities/codec/varint_async.rs new file mode 100644 index 0000000000..d097ce95c9 --- /dev/null +++ b/ephemera/src/utilities/codec/varint_async.rs @@ -0,0 +1,100 @@ +use futures::{AsyncRead, AsyncWrite}; +use futures_util::{AsyncReadExt, AsyncWriteExt}; +use log::error; + +#[allow(clippy::cast_possible_truncation)] + +pub(crate) async fn write_length_prefixed, I: AsyncWrite + Unpin>( + io: &mut I, + data: D, +) -> Result<(), std::io::Error> { + write_varint(io, data.as_ref().len() as u32).await?; + io.write_all(data.as_ref()).await?; + io.flush().await?; + + Ok(()) +} + +async fn write_varint(io: &mut I, len: u32) -> Result<(), std::io::Error> { + let mut len_data = unsigned_varint::encode::u32_buffer(); + let encoded_len = unsigned_varint::encode::u32(len, &mut len_data).len(); + io.write_all(&len_data[..encoded_len]).await?; + + Ok(()) +} + +async fn read_varint(io: &mut I) -> Result { + let mut buffer = unsigned_varint::encode::u32_buffer(); + let mut buffer_len = 0; + + loop { + //read 1 byte at time because we don't know how it compacted 32 bit integer + io.read_exact(&mut buffer[buffer_len..=buffer_len]).await?; + buffer_len += 1; + match unsigned_varint::decode::u32(&buffer[..buffer_len]) { + Ok((len, _)) => { + return Ok(len); + } + Err(unsigned_varint::decode::Error::Overflow) => { + error!("Invalid varint received"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid varint", + )); + } + Err(unsigned_varint::decode::Error::Insufficient) => { + continue; + } + Err(_) => { + error!("Varint decoding error: #[non_exhaustive]"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid varint", + )); + } + } + } +} + +pub(crate) async fn read_length_prefixed( + io: &mut I, + max_size: u32, +) -> Result, std::io::Error> { + let len = read_varint(io).await?; + if len > max_size { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Message too large", + )); + } + + let mut buf = vec![0; len as usize]; + io.read_exact(&mut buf).await?; + Ok(buf) +} + +#[cfg(test)] +mod test { + + use futures_util::io::Cursor; + + use super::*; + + #[tokio::test] + async fn test_read_write() { + let data = "hello world".to_string(); + + let mut buf = Vec::with_capacity(data.len() + 1); + let mut cursor = Cursor::new(&mut buf); + + write_length_prefixed(&mut cursor, data.as_bytes()) + .await + .unwrap(); + cursor.set_position(0); + + let vec = read_length_prefixed(&mut cursor, 100).await.unwrap(); + let result = String::from_utf8(vec).unwrap(); + + assert_eq!(result, data); + } +} diff --git a/ephemera/src/utilities/codec/varint_bytes.rs b/ephemera/src/utilities/codec/varint_bytes.rs new file mode 100644 index 0000000000..9a7d9d08ea --- /dev/null +++ b/ephemera/src/utilities/codec/varint_bytes.rs @@ -0,0 +1,85 @@ +use bytes::{Buf, BytesMut}; + +use thiserror::Error; +use unsigned_varint::{decode, encode}; + +#[derive(Debug, Error)] +pub(crate) enum VarintError { + #[error("InvalidInput: {0}")] + InvalidInput(String), + #[error("varint error: {0}")] + Varint(#[from] decode::Error), + #[error("TooLarge")] + TooLarge, +} + +#[allow(clippy::cast_possible_truncation)] + +pub(crate) fn write_length_prefixed>(dst: &mut BytesMut, data: D) { + write_varint(dst, data.as_ref().len() as u32); + dst.extend_from_slice(data.as_ref()); +} + +fn write_varint(dst: &mut BytesMut, len: u32) { + let mut len_data = encode::u32_buffer(); + let encoded_len = encode::u32(len, &mut len_data).len(); + dst.extend_from_slice(&len_data[..encoded_len]); +} + +pub(crate) fn read_length_prefixed( + bytes: &mut BytesMut, + max_size: u32, +) -> Result>, VarintError> { + let len = read_varint(bytes)?; + if len > max_size { + return Err(VarintError::TooLarge); + } + + if bytes.remaining() < len as usize { + return Ok(None); + } + + let vec = bytes.to_vec(); + bytes.advance(len as usize); + Ok(Some(vec)) +} + +fn read_varint(bytes: &mut BytesMut) -> Result { + let mut buffer = encode::u32_buffer(); + + for (i, byte) in bytes.iter().enumerate() { + buffer[i] = *byte; + match decode::u32(&buffer[..i]) { + Ok((len, _)) => { + bytes.advance(i); + return Ok(len); + } + Err(decode::Error::Insufficient) => continue, + Err(err) => Err(err)?, + } + } + + Err(VarintError::InvalidInput( + "Unable to read varint".to_string(), + )) +} + +#[cfg(test)] +mod test { + use bytes::BytesMut; + + use super::*; + + #[test] + fn test_read_write() { + let data = "hello world".to_string(); + + let mut encoded = BytesMut::with_capacity(0); + write_length_prefixed(&mut encoded, data.clone()); + let vec = read_length_prefixed(&mut encoded, 100).unwrap().unwrap(); + let result = String::from_utf8(vec).unwrap(); + + assert_eq!(result, data); + assert_eq!(encoded.remaining(), 0); + } +} diff --git a/ephemera/src/utilities/crypto/ed25519.rs b/ephemera/src/utilities/crypto/ed25519.rs new file mode 100644 index 0000000000..9b92304c2d --- /dev/null +++ b/ephemera/src/utilities/crypto/ed25519.rs @@ -0,0 +1,186 @@ +use std::fmt::Display; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use crate::crypto::EphemeraKeypair; +use crate::peer::{PeerId, ToPeerId}; +use crate::utilities::crypto::keypair::KeyPairError; +use crate::utilities::crypto::{EphemeraPublicKey, Signature}; + +// Internally uses libp2p Keypair for now +pub struct Keypair(pub(crate) libp2p::identity::Keypair); + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct PublicKey(libp2p::identity::PublicKey); + +impl PublicKey { + pub(crate) fn inner(&self) -> &libp2p::identity::PublicKey { + &self.0 + } + + pub(crate) fn to_bytes(&self) -> Vec { + self.0.encode_protobuf() + } +} + +impl Keypair { + pub(crate) fn inner(&self) -> &libp2p::identity::Keypair { + &self.0 + } +} + +impl Display for Keypair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Public key: {}, Secret key: .........", + self.public_key().to_base58() + ) + } +} + +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_base58()) + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + PublicKey::from_base58(&s).map_err(serde::de::Error::custom) + } +} + +impl Display for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_base58()) + } +} + +impl FromStr for PublicKey { + type Err = KeyPairError; + + fn from_str(s: &str) -> Result { + PublicKey::from_base58(s) + } +} + +/// A wrapper around the libp2p Keypair type. +/// libp2p internally supports different key types, we only use Ed25519. +impl EphemeraKeypair for Keypair { + type Signature = Signature; + type PublicKey = PublicKey; + + fn generate(_seed: Option>) -> Self { + let keypair = libp2p::identity::Keypair::generate_ed25519(); + Keypair(keypair) + } + + fn sign>(&self, msg: &M) -> Result { + self.inner() + .sign(msg.as_ref()) + .map_err(|err| KeyPairError::Signing(err.to_string())) + .map(Signature) + } + + fn verify>(&self, msg: &M, signature: &Self::Signature) -> bool { + self.0.public().verify(msg.as_ref(), signature.as_ref()) + } + + fn to_bytes(&self) -> Vec { + self.inner().to_protobuf_encoding().unwrap() + } + + fn from_bytes(raw: &[u8]) -> Result + where + Self: Sized, + { + let keypair = libp2p::identity::Keypair::from_protobuf_encoding(raw) + .map_err(|err| KeyPairError::Decoding(err.to_string()))?; + Ok(Keypair(keypair)) + } + + fn public_key(&self) -> Self::PublicKey { + PublicKey(self.0.public()) + } +} + +impl EphemeraPublicKey for PublicKey { + type Signature = Signature; + + fn to_bytes(&self) -> Vec { + self.0.encode_protobuf() + } + + fn from_bytes(raw: &[u8]) -> Result + where + Self: Sized, + { + let public_key = libp2p::identity::PublicKey::try_decode_protobuf(raw) + .map_err(|err| KeyPairError::Decoding(err.to_string()))?; + Ok(PublicKey(public_key)) + } + + fn verify>(&self, msg: &M, signature: &Self::Signature) -> bool { + self.0.verify(msg.as_ref(), signature.as_ref()) + } +} + +impl ToPeerId for Keypair { + fn peer_id(&self) -> PeerId { + PeerId(self.0.public().to_peer_id()) + } +} + +impl ToPeerId for PublicKey { + fn peer_id(&self) -> PeerId { + PeerId(self.0.to_peer_id()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::crypto::EphemeraKeypair; + use crate::peer::ToPeerId; + use crate::utilities::crypto::keypair::KeyPairError; + use crate::utilities::crypto::EphemeraPublicKey; + use assert_matches::assert_matches; + use std::str::FromStr; + + #[test] + fn test_keypair() { + let keypair = Keypair::generate(None); + let public_key = keypair.public_key(); + let peer_id = keypair.peer_id(); + let peer_id_from_public_key = public_key.peer_id(); + assert_eq!(peer_id, peer_id_from_public_key); + + let msg = "Message to sign"; + let signature = keypair.sign(&msg).unwrap(); + assert!(public_key.verify(&msg, &signature)); + assert!(keypair.verify(&msg, &signature)); + + let initial = keypair.to_bytes(); + let parsed = Keypair::from_bytes(&initial).unwrap(); + assert_eq!(initial, parsed.to_bytes()); + + let initial = public_key.to_bytes(); + let parsed = PublicKey::from_bytes(&initial).unwrap(); + assert_eq!(initial, parsed.to_bytes()); + + let public_key_from_str = PublicKey::from_str(&public_key.to_base58()).unwrap(); + assert_eq!(public_key, public_key_from_str); + + let public_key_from_str = PublicKey::from_str(&keypair.to_base58()); + assert_matches!(public_key_from_str, Err(KeyPairError::Decoding(_))); + } +} diff --git a/ephemera/src/utilities/crypto/key_manager.rs b/ephemera/src/utilities/crypto/key_manager.rs new file mode 100644 index 0000000000..b27cb9020d --- /dev/null +++ b/ephemera/src/utilities/crypto/key_manager.rs @@ -0,0 +1,14 @@ +use std::sync::Arc; + +use crate::crypto::{EphemeraKeypair, Keypair}; + +pub(crate) struct KeyManager; + +impl KeyManager { + pub(crate) fn read_keypair_from_str(private_key: &str) -> anyhow::Result> { + let keypair: Arc = Keypair::from_base58(private_key) + .map_err(|e| anyhow::anyhow!("Failed to parse private key from config. Error: {e:?}"))? + .into(); + Ok(keypair) + } +} diff --git a/ephemera/src/utilities/crypto/keypair.rs b/ephemera/src/utilities/crypto/keypair.rs new file mode 100644 index 0000000000..fa609ae7e4 --- /dev/null +++ b/ephemera/src/utilities/crypto/keypair.rs @@ -0,0 +1,158 @@ +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum KeyPairError { + #[error("Failed to encode: {0}")] + Encoding(String), + #[error("Failed to decode: {0}")] + Decoding(String), + #[error("Invalid signature")] + Signature(String), + #[error("Signing failed: {0}")] + Signing(String), +} + +pub trait EphemeraPublicKey { + type Signature: AsRef<[u8]>; + + /// Returns raw bytes of the public key + /// + /// # Returns + /// * `Vec` - raw bytes of the public key + fn to_bytes(&self) -> Vec; + + /// Parses public key from raw bytes + /// + /// # Arguments + /// * `raw` - raw bytes of the public key + /// + /// # Returns + /// * `Result` - public key or error + /// + /// # Errors + /// * `KeyPairError::Decoding` - if bytes are not valid + fn from_bytes(bytes: &[u8]) -> Result + where + Self: Sized; + + /// Verifies the signature of the message using the public key + /// + /// # Arguments + /// * `msg` - message to verify + /// * `signature` - signature of the message + /// + /// # Returns + /// * `bool` - true if the signature is valid, false otherwise + fn verify>(&self, msg: &M, signature: &Self::Signature) -> bool; + + /// Returns base58 encoded public key + /// + /// # Returns + /// * `String` - base58 encoded public key + fn to_base58(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } + + /// Parses public key from base58 encoded string + /// + /// # Arguments + /// * `base58` - base58 encoded public key + /// + /// # Returns + /// * `Result` - public key or error + /// + /// # Errors + /// * `KeyPairError::Decoding` - if bytes are not valid + fn from_base58(base58: &str) -> Result + where + Self: Sized, + { + let raw = bs58::decode(base58) + .into_vec() + .map_err(|err| KeyPairError::Decoding(err.to_string()))?; + Self::from_bytes(&raw) + } +} + +#[allow(clippy::module_name_repetitions)] +pub trait EphemeraKeypair { + type Signature; + type PublicKey; + + /// Generates a new keypair. Depending on the implementation, the seed may be used to + /// add entropy to the key generation process. + fn generate(seed: Option>) -> Self; + + /// Signs a message with the private key + /// + /// # Arguments + /// * `msg` - message to sign + /// + /// # Returns + /// * `Result` - signature or error + /// + /// # Errors + /// * `KeyPairError::Signing` - if the message cannot be encoded + fn sign>(&self, msg: &M) -> Result; + + /// Verifies the signature of the message using the related public key + /// + /// # Arguments + /// * `msg` - message to verify + /// * `signature` - signature of the message + /// + /// # Returns + /// * `bool` - true if the signature is valid, false otherwise + fn verify>(&self, msg: &M, signature: &Self::Signature) -> bool; + + /// Returns raw bytes of the keypair + /// + /// # Returns + /// * `Vec` - raw bytes of the keypair + fn to_bytes(&self) -> Vec; + + /// Parses keypair from bytes + /// + /// # Arguments + /// * `raw` - raw bytes of the keypair + /// + /// # Returns + /// * `Result` - keypair or error + /// + /// # Errors + /// * `KeyPairError::Decoding` - if bytes are not valid + fn from_bytes(raw: &[u8]) -> Result + where + Self: Sized; + + /// Returns related public key of the keypair + fn public_key(&self) -> Self::PublicKey; + + /// Returns base58 encoded keypair + /// + /// # Returns + /// * `String` - base58 encoded keypair + fn to_base58(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } + + /// Parses keypair from base58 encoded string + /// + /// # Arguments + /// * `base58` - base58 encoded keypair + /// + /// # Returns + /// * `Result` - keypair or error + /// + /// # Errors + /// * `KeyPairError::Decoding` - if bytes are not valid base58 encoded string + fn from_base58(base58: &str) -> Result + where + Self: Sized, + { + let raw = bs58::decode(base58) + .into_vec() + .map_err(|err| KeyPairError::Decoding(err.to_string()))?; + Self::from_bytes(&raw) + } +} diff --git a/ephemera/src/utilities/crypto/mod.rs b/ephemera/src/utilities/crypto/mod.rs new file mode 100644 index 0000000000..f95f26342b --- /dev/null +++ b/ephemera/src/utilities/crypto/mod.rs @@ -0,0 +1,137 @@ +use std::fmt::{Debug, Display}; +use std::hash::Hash; + +use serde::{Deserialize, Serialize}; + +pub use ed25519::{Keypair as Ed25519Keypair, PublicKey as Ed25519PublicKey}; +pub use keypair::{EphemeraKeypair, EphemeraPublicKey, KeyPairError}; + +use crate::codec::Encode; +use crate::utilities::codec::{Codec, EncodingError, EphemeraCodec}; + +pub mod ed25519; +pub mod key_manager; +mod keypair; + +pub type Keypair = Ed25519Keypair; +pub type PublicKey = Ed25519PublicKey; + +#[derive(Clone, PartialEq, Hash, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub struct Signature(Vec); + +impl Signature { + pub fn new(signature: Vec) -> Self { + Self(signature) + } + + pub fn to_bytes(&self) -> Vec { + self.0.clone() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn inner(&self) -> &[u8] { + &self.0 + } + + pub fn to_base58(&self) -> String { + bs58::encode(self.0.clone()).into_string() + } +} + +impl Debug for Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_base58()) + } +} + +impl Display for Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_base58()) + } +} + +#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub(crate) struct Certificate { + pub(crate) signature: Signature, + pub(crate) public_key: PublicKey, +} + +impl Certificate { + pub fn prepare(key_pair: &Keypair, data: &D) -> anyhow::Result { + let data_bytes = data.encode()?; + let signature = key_pair.sign(&data_bytes)?; + let public_key = key_pair.public_key(); + Ok(Self::new(signature, public_key)) + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl From> for Signature { + fn from(signature: Vec) -> Self { + Self::new(signature) + } +} + +impl Certificate { + pub(crate) fn new(signature: Signature, public_key: PublicKey) -> Self { + Self { + signature, + public_key, + } + } + + pub(crate) fn verify(&self, data: &D) -> anyhow::Result { + let data_bytes = data.encode()?; + let valid = self.public_key.verify(&data_bytes, &self.signature); + Ok(valid) + } +} + +impl Encode for Certificate { + fn encode(&self) -> Result, EncodingError> { + let mut result = Codec::encode(&self.signature)?; + result.extend_from_slice(&self.public_key.encode()?); + Ok(result) + } +} + +impl Encode for PublicKey { + fn encode(&self) -> Result, EncodingError> { + Ok(self.to_bytes()) + } +} + +impl Display for Certificate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Signature {}, PublicKey {}", + self.signature.to_base58(), + self.public_key.to_base58() + ) + } +} + +#[cfg(test)] +mod test { + + use crate::crypto::{EphemeraKeypair, EphemeraPublicKey}; + + #[test] + fn test_sign_and_verify() { + let keypair = super::Keypair::generate(None); + let data = "Secret data"; + let signature = keypair.sign(&data.as_bytes()).unwrap(); + let public_key = keypair.public_key(); + let valid = public_key.verify(&data.as_bytes(), &signature); + assert!(valid); + } +} diff --git a/ephemera/src/utilities/hash/mod.rs b/ephemera/src/utilities/hash/mod.rs new file mode 100644 index 0000000000..e95a975da4 --- /dev/null +++ b/ephemera/src/utilities/hash/mod.rs @@ -0,0 +1,109 @@ +use std::fmt::{Debug, Display}; + +use std::str::FromStr; + +use blake2::{Blake2b, Digest}; +use digest::consts::U32; +use serde::{Deserialize, Serialize}; + +pub type Hasher = Blake2bHasher; + +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)] +pub struct Hash([u8; 32]); + +impl Hash { + pub fn new(hash: [u8; 32]) -> Self { + Self(hash) + } + + pub fn inner(&self) -> [u8; 32] { + self.0 + } + + pub(crate) fn base58(&self) -> String { + bs58::encode(self.0).into_string() + } +} + +impl FromStr for Hash { + type Err = bs58::decode::Error; + + fn from_str(s: &str) -> Result { + let bytes = bs58::decode(s).into_vec()?; + let mut hash = [0u8; 32]; + hash.copy_from_slice(&bytes); + Ok(Self(hash)) + } +} + +impl Debug for Hash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.base58()) + } +} + +impl Display for Hash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.base58()) + } +} + +impl From<[u8; 32]> for Hash { + fn from(hash: [u8; 32]) -> Self { + Self(hash) + } +} + +/// A trait for hashing data. +pub(crate) trait EphemeraHasher: Default { + /// Hashes the given data. + fn digest(data: &[u8]) -> [u8; 32]; + + /// Updates the hasher with the given data. + fn update(&mut self, bytes: &[u8]); + + /// Finalizes the hasher and returns the hash. + fn finish(&mut self) -> [u8; 32]; +} + +#[derive(Default)] +pub struct Blake2bHasher { + hasher: Blake2b, +} + +impl EphemeraHasher for Blake2bHasher { + fn digest(data: &[u8]) -> [u8; 32] { + type Blake2b256 = blake2::Blake2b; + let mut dest = [0; 32]; + dest.copy_from_slice(Blake2b256::digest(data).as_slice()); + dest + } + + fn update(&mut self, bytes: &[u8]) { + self.hasher.update(bytes); + } + + fn finish(&mut self) -> [u8; 32] { + self.hasher.finalize_reset().into() + } +} + +pub(crate) trait EphemeraHash { + fn hash(&self, state: &mut H) -> anyhow::Result<()>; +} + +#[cfg(test)] +mod test { + use super::*; + use rand::RngCore; + + #[test] + fn to_base58_parse() { + let mut bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + let hash: Hash = bytes.into(); + let base58 = hash.base58(); + let hash2 = base58.parse::().unwrap(); + assert_eq!(hash, hash2); + } +} diff --git a/ephemera/src/utilities/id/mod.rs b/ephemera/src/utilities/id/mod.rs new file mode 100644 index 0000000000..5548f6070a --- /dev/null +++ b/ephemera/src/utilities/id/mod.rs @@ -0,0 +1,94 @@ +use std::fmt::{Debug, Display}; +use std::str::FromStr; + +use crate::utilities::hash::{EphemeraHash, EphemeraHasher}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub(crate) type EphemeraId = UuidEphemeraIdentifier; + +pub(crate) trait EphemeraIdentifier: ToString { + type Identifier; + + fn generate() -> Self; + + fn inner(&self) -> &Self::Identifier; + + fn into_inner(self) -> Self::Identifier; + + fn as_bytes(&self) -> &[u8]; +} + +#[derive(Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Deserialize, Serialize)] +pub struct UuidEphemeraIdentifier { + identifier: String, +} + +impl FromStr for UuidEphemeraIdentifier { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + Ok(UuidEphemeraIdentifier { + identifier: Uuid::parse_str(s)?.to_string(), + }) + } +} + +impl Display for UuidEphemeraIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.identifier) + } +} + +impl Debug for UuidEphemeraIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.identifier) + } +} + +impl Default for UuidEphemeraIdentifier { + fn default() -> Self { + UuidEphemeraIdentifier { + identifier: Uuid::new_v4().to_string(), + } + } +} + +impl EphemeraHash for UuidEphemeraIdentifier { + fn hash(&self, state: &mut H) -> anyhow::Result<()> { + state.update(self.as_bytes()); + Ok(()) + } +} + +impl EphemeraIdentifier for UuidEphemeraIdentifier { + type Identifier = String; + + fn generate() -> Self { + UuidEphemeraIdentifier::default() + } + + fn inner(&self) -> &Self::Identifier { + &self.identifier + } + + fn into_inner(self) -> Self::Identifier { + self.identifier + } + + fn as_bytes(&self) -> &[u8] { + self.identifier.as_bytes() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_generate_parse() { + let id = UuidEphemeraIdentifier::generate(); + let id2 = UuidEphemeraIdentifier::from_str(&id.to_string()).unwrap(); + assert_eq!(id, id2); + } +} diff --git a/ephemera/src/utilities/merkle/mod.rs b/ephemera/src/utilities/merkle/mod.rs new file mode 100644 index 0000000000..1f0f866539 --- /dev/null +++ b/ephemera/src/utilities/merkle/mod.rs @@ -0,0 +1,162 @@ +use serde::{Deserialize, Serialize}; + +use crate::utilities::hash::{EphemeraHasher, Hash, Hasher}; + +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Serialize, Deserialize)] +pub struct MerkleTree { + leaf_count: usize, + nodes: Vec, +} + +impl MerkleTree { + pub(crate) fn build_tree(leaves: &[Hash]) -> Self { + if leaves.is_empty() { + return Self { + leaf_count: 1, + //So every message which matches this hash will be accepted + //Not sure if it's a problem + nodes: vec![Hash::new([0; 32])], + }; + } + + let leaf_count = leaves.len(); + let mut nodes = Vec::with_capacity(leaf_count * 2); + nodes.extend_from_slice(leaves); + + let mut prev_level_len = leaf_count; + let mut prev_offset = 0; + let mut current_offset = leaf_count; + + while prev_level_len > 1 { + let current_level_len = (prev_level_len + 1) / 2; + + for i in 0..current_level_len { + let prev_index = i * 2; + let left = nodes[prev_offset + prev_index]; + let right = if prev_index + 1 < prev_level_len { + nodes[prev_offset + prev_index + 1] + } else { + nodes[prev_offset + prev_index] + }; + let hash = Hasher::digest(&[left.inner(), right.inner()].concat()).into(); + nodes.push(hash); + } + prev_level_len = current_level_len; + prev_offset = current_offset; + current_offset += current_level_len; + } + Self { leaf_count, nodes } + } + + pub(crate) fn root_hash(&self) -> Hash { + self.nodes[self.nodes.len() - 1] + } + + pub(crate) fn verify_leaf_at_index(&self, hash: Hash, leaf_index: usize) -> bool { + let mut level_offset = 0; + let mut level_len = self.leaf_count; + let mut leaf_index = leaf_index; + let mut current_hash = hash; + while level_offset + level_len < self.nodes.len() { + let level = &self.nodes[level_offset..(level_offset + level_len)]; + if leaf_index % 2 == 0 { + let right_index = if leaf_index + 1 < level_len { + leaf_index + 1 + } else { + leaf_index + }; + current_hash = + Hasher::digest(&[current_hash.inner(), level[right_index].inner()].concat()) + .into(); + } else { + current_hash = + Hasher::digest(&[level[leaf_index - 1].inner(), current_hash.inner()].concat()) + .into(); + } + leaf_index /= 2; + level_offset += level_len; + level_len = (level_len + 1) / 2; + } + current_hash == self.root_hash() + } +} + +#[cfg(test)] +mod tests { + use std::iter; + + use rand::RngCore; + + use crate::utilities::hash::Hash; + + use super::*; + + #[test] + fn test_merkle() { + //Technically testing one implementation against another... + for i in 1..10 { + let mut rnd = rand::thread_rng(); + let leaves = iter::repeat_with(|| { + let mut bytes = [0u8; 32]; + rnd.fill_bytes(&mut bytes); + Hash::new(bytes) + }) + .take(i) + .collect::>(); + + let mut level: Vec = leaves.clone(); + + while level.len() > 1 { + level = level + .chunks(2) + .map(|chunk| { + if chunk.len() == 1 { + Hasher::digest(&[chunk[0].inner(), chunk[0].inner()].concat()).into() + } else { + Hasher::digest(&[chunk[0].inner(), chunk[1].inner()].concat()).into() + } + }) + .collect(); + } + + let root: Hash = level[0]; + + let tree = MerkleTree::build_tree(&leaves); + assert_eq!(tree.root_hash(), root); + } + } + + #[test] + fn test_verify_leaf() { + for i in 0..10 { + let mut rnd = rand::thread_rng(); + let right_leaves = iter::repeat_with(|| { + let mut bytes = [0u8; 32]; + rnd.fill_bytes(&mut bytes); + Hash::new(bytes) + }) + .take(i) + .collect::>(); + + let wrong_leaves = iter::repeat_with(|| { + let mut bytes = [0u8; 32]; + rnd.fill_bytes(&mut bytes); + Hash::new(bytes) + }) + .take(i) + .collect::>(); + + let tree = MerkleTree::build_tree(&right_leaves); + + for (i, (correct, wrong)) in right_leaves + .into_iter() + .zip(wrong_leaves.into_iter()) + .enumerate() + { + assert!(tree.verify_leaf_at_index(correct, i)); + assert!(!tree.verify_leaf_at_index(wrong, i)); + } + } + } +} diff --git a/ephemera/src/utilities/mod.rs b/ephemera/src/utilities/mod.rs new file mode 100644 index 0000000000..724cdba514 --- /dev/null +++ b/ephemera/src/utilities/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod codec; +pub(crate) mod crypto; +pub(crate) mod hash; +pub(crate) mod id; +pub(crate) mod merkle; +pub(crate) mod time; diff --git a/ephemera/src/utilities/time/mod.rs b/ephemera/src/utilities/time/mod.rs new file mode 100644 index 0000000000..0b3f2091c0 --- /dev/null +++ b/ephemera/src/utilities/time/mod.rs @@ -0,0 +1,11 @@ +use chrono::Utc; + +#[allow(clippy::module_name_repetitions)] +pub struct EphemeraTime; + +impl EphemeraTime { + #[allow(clippy::cast_sign_loss)] + pub fn now() -> u64 { + Utc::now().timestamp_millis() as u64 + } +} diff --git a/ephemera/src/websocket/mod.rs b/ephemera/src/websocket/mod.rs new file mode 100644 index 0000000000..73a138f3cb --- /dev/null +++ b/ephemera/src/websocket/mod.rs @@ -0,0 +1 @@ +pub(crate) mod ws_manager; diff --git a/ephemera/src/websocket/ws_manager.rs b/ephemera/src/websocket/ws_manager.rs new file mode 100644 index 0000000000..a7e837a2d4 --- /dev/null +++ b/ephemera/src/websocket/ws_manager.rs @@ -0,0 +1,138 @@ +use std::net::SocketAddr; + +use anyhow::Result; +use futures_util::SinkExt; +use log::{debug, error, info}; +use tokio::sync::broadcast::error::RecvError; +use tokio::{ + net::{TcpListener, TcpStream}, + sync::broadcast, +}; +use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; + +use crate::api::types::ApiBlock; +use crate::block::types::block::Block; + +pub struct WsConnection { + socket: WebSocketStream, + pending_messages_rx: broadcast::Receiver, + address: SocketAddr, +} + +impl WsConnection { + pub fn new( + socket: WebSocketStream, + pending_messages_rx: broadcast::Receiver, + address: SocketAddr, + ) -> WsConnection { + WsConnection { + socket, + pending_messages_rx, + address, + } + } + + pub async fn accept_messages(mut self) { + loop { + match self.pending_messages_rx.recv().await { + Ok(msg) => { + debug!("Sending message to {}", self.address); + if let Err(err) = self.socket.send(msg).await { + error!("Error sending message to websocket client: {:?}", err); + } + } + Err(e) => { + if RecvError::Closed == e { + error!("Error while receiving message from broadcast channel: {:?}, closing connection", e); + break; + } + } + } + } + } +} + +#[derive(Clone)] +pub(crate) struct WsMessageBroadcaster { + pub(crate) pending_messages_tx: broadcast::Sender, +} + +impl WsMessageBroadcaster { + pub(crate) fn new(pending_messages_tx: broadcast::Sender) -> WsMessageBroadcaster { + WsMessageBroadcaster { + pending_messages_tx, + } + } + + pub(crate) fn send_block(&self, block: &Block) -> Result<()> { + debug!("Sending block {} to websocket clients", block.header.hash); + let json = serde_json::to_string::(block.into())?; + let msg = Message::Text(json); + self.pending_messages_tx.send(msg)?; + Ok(()) + } +} + +pub(crate) struct WsManager { + pub(crate) listener: Option, + pub(crate) ws_address: String, + pub(crate) pending_messages_tx: broadcast::Sender, + _pending_messages_rcv: broadcast::Receiver, +} + +impl WsManager { + #[allow(clippy::used_underscore_binding)] + pub(crate) fn new(address: String) -> (WsManager, WsMessageBroadcaster) { + let (pending_messages_tx, _pending_messages_rcv) = broadcast::channel(1000); + let ws_message_broadcast = WsMessageBroadcaster::new(pending_messages_tx.clone()); + let manager = WsManager { + listener: None, + ws_address: address, + pending_messages_tx, + _pending_messages_rcv, + }; + (manager, ws_message_broadcast) + } + + pub(crate) async fn listen(&mut self) -> Result<()> { + let listener = TcpListener::bind(&self.ws_address).await?; + info!("Listening for websocket connections on {}", self.ws_address); + self.listener = Some(listener); + Ok(()) + } + + pub async fn run(mut self) -> Result<()> { + let listener = self.listener.take().expect("Listener not set"); + loop { + tokio::select! { + res = listener.accept() => { + match res { + Ok((stream, addr)) => { + debug!("Accepted websocket connection from: {}", addr); + self.handle_connection(stream, addr); + } + Err(err) => { + return Err(err.into()); + } + } + } + } + } + } + + pub fn handle_connection(&self, stream: TcpStream, addr: SocketAddr) { + let pending_messages_rx = self.pending_messages_tx.subscribe(); + tokio::spawn(async move { + match tokio_tungstenite::accept_async(stream).await { + Ok(ws_stream) => { + let connection = WsConnection::new(ws_stream, pending_messages_rx, addr); + connection.accept_messages().await; + } + Err(err) => { + error!("Error accepting websocket connection: {:?}", err); + } + } + debug!("Websocket connection closed"); + }); + } +} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 5e7a9488d1..cc55f92afc 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -64,11 +64,22 @@ rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger"] } schemars = { version = "0.8", features = ["preserve_order"] } zeroize = { workspace = true } +## ephemera-specific +actix-web = "4" +array-bytes = "6.0.0" +chrono = { version = "0.4.24", default-features = false, features = ["clock"] } +futures-util = "0.3.25" +serde_derive = "1.0.149" +tempfile = "3.3.0" +uuid = { version = "1.3.0", features = ["serde", "v4"] } + ## internal +ephemera = { path = "../ephemera" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } nym-coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } nym-coconut-interface = { path = "../common/coconut-interface" } +nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } nym-config = { path = "../common/config" } cosmwasm-std = { workspace = true } nym-credential-storage = { path = "../common/credential-storage" } diff --git a/nym-api/ephemera_migrations/contract/V1__contract.sql b/nym-api/ephemera_migrations/contract/V1__contract.sql new file mode 100644 index 0000000000..947c316940 --- /dev/null +++ b/nym-api/ephemera_migrations/contract/V1__contract.sql @@ -0,0 +1,19 @@ +CREATE TABLE contract_mixnode_reward +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER NOT NULL, + epoch INTEGER NOT NULL, + nym_api_id INTEGER NOT NULL, + reliability INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + UNIQUE (mix_id, epoch) +); + +CREATE TABLE epoch_info +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + epoch_id INTEGER NOT NULL, + start_time INTEGER NOT NULL, + duration INTEGER NOT NULL, + UNIQUE (epoch_id) +); \ No newline at end of file diff --git a/nym-api/ephemera_migrations/rewardsnew/V1__metrics.sql b/nym-api/ephemera_migrations/rewardsnew/V1__metrics.sql new file mode 100644 index 0000000000..9f5e0e2f04 --- /dev/null +++ b/nym-api/ephemera_migrations/rewardsnew/V1__metrics.sql @@ -0,0 +1,23 @@ +CREATE TABLE mixnode_status +( + mix_id INTEGER NOT NULL, + reliability INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + UNIQUE (mix_id, timestamp) +); + +CREATE TABLE rewarding_report +( + epoch_id INTEGER NOT NULL, + eligible_mixnodes INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + UNIQUE (epoch_id) +); + +CREATE TABLE epoch_blocks +( + epoch_id INTEGER NOT NULL, + block_id INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + UNIQUE (epoch_id, block_id) +); \ No newline at end of file diff --git a/nym-api/ephemera_migrations/rewardsold/V1__metrics.sql b/nym-api/ephemera_migrations/rewardsold/V1__metrics.sql new file mode 100644 index 0000000000..3d5a66c4b8 --- /dev/null +++ b/nym-api/ephemera_migrations/rewardsold/V1__metrics.sql @@ -0,0 +1,6 @@ +CREATE TABLE mixnode_status +( + mix_id INTEGER NOT NULL, + reliability INTEGER NOT NULL, + timestamp INTEGER NOT NULL +); \ No newline at end of file diff --git a/nym-api/src/ephemera/application.rs b/nym-api/src/ephemera/application.rs new file mode 100644 index 0000000000..06c1c016d8 --- /dev/null +++ b/nym-api/src/ephemera/application.rs @@ -0,0 +1,209 @@ +use ephemera::{ + configuration::Configuration, + ephemera_api::{ + ApiBlock, ApiEphemeraMessage, Application, ApplicationResult, CheckBlockResult, + RemoveMessages, + }, +}; +use log::{debug, error, info}; + +use crate::ephemera::client::Client; +use crate::ephemera::epoch::Epoch; +use crate::ephemera::peers::members::MembersProvider; +use crate::ephemera::peers::{NymApiEphemeraPeerInfo, NymPeer}; +use crate::ephemera::reward::aggregator::RewardsAggregator; +use crate::ephemera::reward::{EphemeraAccess, RewardManager}; +use crate::ephemera::Args; +use crate::epoch_operations::MixnodeWithPerformance; +use crate::support::nyxd; +use ephemera::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair}; +use ephemera::ephemera_api::CommandExecutor; +use ephemera::{Ephemera, EphemeraStarterInit}; +use nym_task::TaskManager; + +pub struct NymApi; + +impl NymApi { + pub(crate) async fn run( + args: Args, + ephemera_config: Configuration, + nyxd_client: nyxd::Client, + shutdown: &TaskManager, + ) -> anyhow::Result { + //KEYPAIR - Ephemera keypair or Validator keypair + //Can be a file, keystore etc + let key_pair = Self::read_nym_api_keypair(&ephemera_config)?; + + //EPHEMERA + let ephemera = Self::init_ephemera(ephemera_config, nyxd_client.clone()).await?; + let ephemera_handle = ephemera.handle(); + + //REWARDS + let rewards = + Self::create_rewards_manager(args, nyxd_client, key_pair, ephemera_handle.api.clone()) + .await?; + + //STARTING + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(ephemera.run(shutdown_listener)); + + Ok(rewards) + } + + pub(crate) async fn init_ephemera( + ephemera_config: Configuration, + nyxd_client: nyxd::Client, + ) -> anyhow::Result> { + info!("Initializing ephemera ..."); + + let node_info = ephemera_config.node.clone(); + + let keypair = bs58::decode(&node_info.private_key).into_vec().unwrap(); + let keypair = Keypair::from_bytes(&keypair).unwrap(); + let local_peer_id = keypair.public_key().to_base58(); + + //Members provider for Ephemera + let members_provider = MembersProvider::new(nyxd_client.clone()); + + let cosmos_address = nyxd_client.client_address().await.to_string(); + let ip_address = format!( + "/ip4/{}/tcp/{}", + ephemera_config.node.ip, ephemera_config.libp2p.port + ); + + if !nyxd_client + .get_ephemera_peers() + .await? + .iter() + .any(|peer_info| peer_info.cosmos_address == cosmos_address) + { + let local_peer = NymPeer::new( + cosmos_address, + ip_address, + keypair.public_key(), + local_peer_id.clone(), + ); + members_provider.register_peer(local_peer).await?; + } + + //Application for Ephemera + let rewards_ephemera_application = + RewardsEphemeraApplication::init(local_peer_id, nyxd_client.clone()).await?; + + //EPHEMERA + let ephemera_builder = EphemeraStarterInit::new(ephemera_config)?; + let ephemera_builder = ephemera_builder.with_application(rewards_ephemera_application); + let ephemera_builder = ephemera_builder.with_members_provider(members_provider)?; + let ephemera = ephemera_builder.build(); + Ok(ephemera) + } + + async fn create_rewards_manager( + args: Args, + nyxd_client: nyxd::Client, + key_pair: Keypair, + ephemera_api: CommandExecutor, + ) -> anyhow::Result { + let epoch = Epoch::request_epoch(nyxd_client).await?; + Ok(RewardManager::new( + args.clone(), + EphemeraAccess::new(ephemera_api, key_pair).into(), + Some(RewardsAggregator), + epoch, + )) + } + + fn read_nym_api_keypair(ephemera_config: &Configuration) -> anyhow::Result { + let key_pair = bs58::decode(&ephemera_config.node.private_key).into_vec()?; + let key_pair = Keypair::from_bytes(&key_pair)?; + Ok(key_pair) + } +} + +pub(crate) struct RewardsEphemeraApplicationConfig { + /// Percentage of messages relative to total number of peers + pub(crate) peers_rewards_threshold: u64, +} + +pub(crate) struct RewardsEphemeraApplication { + peer_info: NymApiEphemeraPeerInfo, + app_config: RewardsEphemeraApplicationConfig, +} + +impl RewardsEphemeraApplication { + pub(crate) async fn init( + local_peer_id: String, + nyxd_client: nyxd::Client, + ) -> anyhow::Result { + let peer_info = match NymApiEphemeraPeerInfo::from_ephemera_dev_cluster_conf( + local_peer_id, + nyxd_client, + ) + .await + { + Ok(info) => info, + Err(err) => { + error!("Failed to load peers info: {}", err); + return Err(err); + } + }; + let app_config = RewardsEphemeraApplicationConfig { + peers_rewards_threshold: peer_info.get_peers_count() as u64, + }; + Ok(Self { + peer_info, + app_config, + }) + } +} + +/// - TODO: We should also check that the messages has expected label(like epoch 100) +/// because next block should have only reward info for correct epoch. +impl Application for RewardsEphemeraApplication { + /// Perform validation checks: + /// - Check that the transaction has a valid signature, we don't want to accept garbage messages + /// or messages from unknown peers + fn check_tx(&self, tx: ApiEphemeraMessage) -> ApplicationResult { + if serde_json::from_slice::>(&tx.data).is_err() { + error!("Message is not a valid Reward message"); + return Ok(false); + } + Ok(true) + + //TODO + //PS! message label should also be part of the message hash to prevent replay attacks + } + + /// Agree to accept the block if it contains threshold number of transactions + /// We trust that transactions are valid(checked by check_tx) + fn check_block(&self, block: &ApiBlock) -> ApplicationResult { + info!("Block message count: {}", block.message_count()); + + let block_threshold = ((block.message_count() as f64 + / self.peer_info.get_peers_count() as f64) + * 100.0) as u64; + + if block_threshold > 100 { + error!("Block threshold is greater than 100%!. We expected only single message from each peer"); + return Ok(CheckBlockResult::RejectAndRemoveMessages( + RemoveMessages::All, + )); + } + + if block_threshold >= self.app_config.peers_rewards_threshold { + info!( + "Block accepted {}:{}", + block.header.height, block.header.hash + ); + Ok(CheckBlockResult::Accept) + } else { + debug!("Block rejected: not enough messages"); + Ok(CheckBlockResult::Reject) + } + } + + /// It is possible to use this method as a callback to get notified when block is committed + fn deliver_block(&self, _block: ApiBlock) -> ApplicationResult<()> { + Ok(()) + } +} diff --git a/nym-api/src/ephemera/client.rs b/nym-api/src/ephemera/client.rs new file mode 100644 index 0000000000..146153759a --- /dev/null +++ b/nym-api/src/ephemera/client.rs @@ -0,0 +1,12 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ephemera::error::Result; +use nym_ephemera_common::types::JsonPeerInfo; +use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; + +#[async_trait] +pub trait Client { + async fn get_ephemera_peers(&self) -> Result>; + async fn register_ephemera_peer(&self, peer_info: JsonPeerInfo) -> Result; +} diff --git a/nym-api/src/ephemera/epoch.rs b/nym-api/src/ephemera/epoch.rs new file mode 100644 index 0000000000..646dd451eb --- /dev/null +++ b/nym-api/src/ephemera/epoch.rs @@ -0,0 +1,82 @@ +use crate::support::nyxd; +use chrono::{DateTime, NaiveDateTime, Timelike, Utc}; +use log::info; +use nym_mixnet_contract_common::Interval as RewardsInterval; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use tokio::time::Interval; + +//It synchronizes rewarding across simulated Nym-APIs and the "Smart Contract". +//Maintained by the "Smart Contract" +#[derive(Debug)] +pub struct Epoch { + pub start_time: DateTime, + pub interval: Interval, + pub epoch_start_id: u64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct EpochInfo { + pub epoch_id: u64, + pub start_time: i64, + pub duration: u64, +} + +impl EpochInfo { + pub(crate) fn new(epoch_id: u64, start_time: i64, duration: u64) -> Self { + Self { + epoch_id, + start_time, + duration, + } + } +} + +impl From for Epoch { + fn from(value: RewardsInterval) -> Self { + Epoch::new(EpochInfo::new( + value.current_epoch_id() as u64, + value.current_epoch_start().unix_timestamp(), + value.epoch_length_secs(), + )) + } +} + +impl Epoch { + pub fn new(info: EpochInfo) -> Self { + info!("New epoch duration"); + let start_time = NaiveDateTime::from_timestamp_opt(info.start_time, 0) + .ok_or("Invalid start time") + .unwrap(); + let start_time: DateTime = DateTime::from_utc(start_time, Utc); + let interval = tokio::time::interval(std::time::Duration::from_secs(info.duration)); + Self { + start_time, + interval, + epoch_start_id: start_time.minute().into(), + } + } + + pub fn current_epoch_numer(&self) -> u64 { + let since_start = (Utc::now().timestamp() - self.start_time.timestamp()) as u64; + let nr = since_start / self.interval.period().as_secs(); + nr + self.epoch_start_id + } + + pub(crate) async fn request_epoch(nyxd_client: nyxd::Client) -> anyhow::Result { + let interval = nyxd_client.get_current_interval().await?.interval; + Ok(Epoch::from(interval)) + } +} + +impl Display for Epoch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Epoch {{ start_time: {}, interval: {:?}, epoch_start_id: {} }}", + self.start_time, + self.interval.period(), + self.epoch_start_id + ) + } +} diff --git a/nym-api/src/ephemera/error.rs b/nym-api/src/ephemera/error.rs new file mode 100644 index 0000000000..e8dd0b6ace --- /dev/null +++ b/nym-api/src/ephemera/error.rs @@ -0,0 +1,15 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum EphemeraError { + #[error("Validator client error - {0}")] + ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), + + #[error("Nyxd error - {0}")] + NyxdError(#[from] nym_validator_client::nyxd::error::NyxdError), +} diff --git a/nym-api/src/ephemera/metrics/mod.rs b/nym-api/src/ephemera/metrics/mod.rs new file mode 100644 index 0000000000..cd408564ea --- /dev/null +++ b/nym-api/src/ephemera/metrics/mod.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/nym-api/src/ephemera/metrics/types.rs b/nym-api/src/ephemera/metrics/types.rs new file mode 100644 index 0000000000..eb549a2360 --- /dev/null +++ b/nym-api/src/ephemera/metrics/types.rs @@ -0,0 +1,9 @@ +#[derive(Debug)] +pub struct MixnodeResult { + pub mix_id: u32, + pub reliability: u8, +} + +// value in range 0-100 +#[derive(Clone, Copy, Debug, Default)] +pub struct Uptime(u8); diff --git a/nym-api/src/ephemera/mod.rs b/nym-api/src/ephemera/mod.rs new file mode 100644 index 0000000000..d6960fcba5 --- /dev/null +++ b/nym-api/src/ephemera/mod.rs @@ -0,0 +1,51 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +extern crate core; + +use clap::Parser; +use ephemera::cli::init::Cmd; +use serde_derive::{Deserialize, Serialize}; +use std::path::PathBuf; + +pub(crate) mod application; +pub(crate) mod client; +pub(crate) mod epoch; +pub(crate) mod error; +pub(crate) mod metrics; +pub(crate) mod peers; +pub(crate) mod reward; + +#[derive(Parser, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Args { + #[clap(skip)] + pub ephemera_config: PathBuf, + #[command(flatten)] + #[serde(skip)] + pub cmd: Cmd, + #[clap(skip)] + #[serde(skip, default = "default_block_polling_interval_seconds")] + pub block_polling_interval_seconds: u64, + #[clap(skip)] + #[serde(skip, default = "default_block_polling_max_attempts")] + pub block_polling_max_attempts: u64, +} + +fn default_block_polling_interval_seconds() -> u64 { + 1 +} + +fn default_block_polling_max_attempts() -> u64 { + 60 +} + +impl Default for Args { + fn default() -> Self { + Args { + ephemera_config: Default::default(), + cmd: Default::default(), + block_polling_interval_seconds: default_block_polling_interval_seconds(), + block_polling_max_attempts: default_block_polling_max_attempts(), + } + } +} diff --git a/nym-api/src/ephemera/peers/members.rs b/nym-api/src/ephemera/peers/members.rs new file mode 100644 index 0000000000..c0ff8e286e --- /dev/null +++ b/nym-api/src/ephemera/peers/members.rs @@ -0,0 +1,108 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ephemera::client::Client; +use crate::ephemera::peers::NymPeer; +use crate::support::nyxd; +use cosmwasm_std::Addr; +use ephemera::membership::{PeerInfo, ProviderError}; +use futures_util::future::BoxFuture; +use futures_util::FutureExt; +use nym_ephemera_common::types::JsonPeerInfo; +use std::future::Future; +use std::pin::Pin; +use std::task::Poll::Pending; +use std::task::{Context, Poll}; + +/// Future type which allows user to implement their own peers membership source mechanism. +pub type ProviderFut = BoxFuture<'static, Result, ProviderError>>; + +///[`ProviderFut`] that reads peers from a http endpoint. +/// +/// The endpoint must return a json array of [`JsonPeerInfo`]. +/// # Configuration example +/// ```json +/// [ +/// { +/// "name": "node1", +/// "address": "/ip4/", +/// "public_key": "4XTTMEghav9LZThm6opUaHrdGEEYUkrfkakVg4VAetetBZDWJ" +/// }, +/// { +/// "name": "node2", +/// "address": "/ip4/", +/// "public_key": "4XTTMFQt2tgNRmwRgEAaGQe2NXygsK6Vr3pkuBfYezhDfoVty" +/// } +/// ] +/// ``` +pub struct MembersProvider { + /// The url of the http endpoint. + nyxd_client: nyxd::Client, + fut: Option, +} + +impl MembersProvider { + #[must_use] + pub(crate) fn new(nyxd_client: nyxd::Client) -> Self { + Self { + nyxd_client, + fut: None, + } + } + + pub(crate) async fn register_peer( + &self, + peer_info: NymPeer, + ) -> crate::ephemera::error::Result<()> { + let json_peer_info = JsonPeerInfo::new( + Addr::unchecked(peer_info.cosmos_address), + peer_info.ip_address, + peer_info.public_key.to_string(), + ); + self.nyxd_client + .register_ephemera_peer(json_peer_info) + .await?; + Ok(()) + } + + async fn request_peers(nyxd_client: nyxd::Client) -> Result, ProviderError> { + let peers = nyxd_client + .get_ephemera_peers() + .await + .map_err(|e| ProviderError::GetPeers(e.to_string()))? + .into_iter() + .filter_map(|peer| PeerInfo::try_from(peer).ok()) + .collect(); + Ok(peers) + } +} + +impl Future for MembersProvider { + type Output = Result, ProviderError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.fut.take() { + None => { + self.fut = Some(Box::pin(MembersProvider::request_peers( + self.nyxd_client.clone(), + ))); + } + Some(mut fut) => { + let peers = match fut.poll_unpin(cx) { + Poll::Ready(Ok(peers)) => peers, + Poll::Ready(Err(err)) => { + error!("Failed to get peers: {err}"); + return Poll::Ready(Err(err)); + } + Pending => { + self.fut = Some(fut); + return Pending; + } + }; + + return Poll::Ready(Ok(peers)); + } + } + Pending + } +} diff --git a/nym-api/src/ephemera/peers/mod.rs b/nym-api/src/ephemera/peers/mod.rs new file mode 100644 index 0000000000..0706c51fde --- /dev/null +++ b/nym-api/src/ephemera/peers/mod.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use anyhow::anyhow; + +use crate::ephemera::client::Client; +use crate::support::nyxd; +use ephemera::crypto::PublicKey; + +pub(crate) type PeerId = String; + +pub mod members; + +#[derive(Debug, Clone)] +pub struct NymPeer { + pub cosmos_address: String, + pub ip_address: String, + pub public_key: PublicKey, + pub peer_id: PeerId, +} + +impl NymPeer { + pub(crate) fn new( + cosmos_address: String, + ip_address: String, + public_key: PublicKey, + peer_id: PeerId, + ) -> Self { + Self { + cosmos_address, + ip_address, + public_key, + peer_id, + } + } +} + +// Information about other Nym-Apis. +pub(crate) struct NymApiEphemeraPeerInfo { + pub(crate) _local_peer: NymPeer, + pub(crate) peers: HashMap, +} + +impl NymApiEphemeraPeerInfo { + fn new(_local_peer: NymPeer, peers: HashMap) -> Self { + Self { _local_peer, peers } + } + + pub(crate) fn get_peers_count(&self) -> usize { + self.peers.len() + } + + pub(crate) async fn from_ephemera_dev_cluster_conf( + local_peer_id: String, + nyxd_client: nyxd::Client, + ) -> anyhow::Result { + let mut peers = HashMap::new(); + for peer_info in nyxd_client.get_ephemera_peers().await? { + let public_key = PublicKey::from_str(&peer_info.public_key)?; + + let peer = NymPeer::new( + peer_info.cosmos_address.to_string(), + peer_info.ip_address, + public_key, + peer_info.public_key.clone(), + ); + + peers.insert(peer_info.public_key, peer); + } + + let local_peer = peers + .get(&local_peer_id) + .ok_or(anyhow!("Failed to get local peer"))? + .clone(); + let peer_info = NymApiEphemeraPeerInfo::new(local_peer, peers); + Ok(peer_info) + } +} diff --git a/nym-api/src/ephemera/reward/aggregator.rs b/nym-api/src/ephemera/reward/aggregator.rs new file mode 100644 index 0000000000..e83119c4f3 --- /dev/null +++ b/nym-api/src/ephemera/reward/aggregator.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::Decimal; +use log::{info, trace}; +use nym_mixnet_contract_common::reward_params::Performance; +use std::collections::HashMap; + +use crate::epoch_operations::MixnodeWithPerformance; + +pub(crate) struct RewardsAggregator; + +impl RewardsAggregator { + //Simple mean average + pub(crate) fn aggregate( + &self, + all_rewards: Vec>, + ) -> anyhow::Result> { + let mut mix_rewards = HashMap::new(); + for api_rewards in all_rewards { + for mixnode in api_rewards { + mix_rewards + .entry(mixnode.mix_id) + .or_insert(vec![]) + .push(mixnode.performance); + } + } + trace!("Mix rewards by node: {:?}", mix_rewards); + + trace!("Calculating mean average for each node"); + let mut mean_avg = vec![]; + for (mix_id, rewards) in mix_rewards { + let sum: Decimal = rewards.iter().map(|r| r.value()).sum(); + let avg = sum / Decimal::from_ratio(rewards.len() as u128, 1u128); + let performance = Performance::new(avg)?; + mean_avg.push(MixnodeWithPerformance { + mix_id, + performance, + }); + } + info!("Mean average rewards: {:?}", mean_avg); + + Ok(mean_avg) + } +} diff --git a/nym-api/src/ephemera/reward/mod.rs b/nym-api/src/ephemera/reward/mod.rs new file mode 100644 index 0000000000..e4e815a9e4 --- /dev/null +++ b/nym-api/src/ephemera/reward/mod.rs @@ -0,0 +1,263 @@ +use async_trait::async_trait; +use log::{debug, info, trace}; +use std::time::Duration; + +use crate::epoch_operations::MixnodeWithPerformance; +use ephemera::{ + crypto::Keypair, + ephemera_api::{self, ApiBlock, ApiEphemeraMessage, ApiError, CommandExecutor}, +}; + +use super::epoch::Epoch; +use super::reward::aggregator::RewardsAggregator; +use super::Args; + +pub(crate) mod aggregator; + +pub struct EphemeraAccess { + pub(crate) api: CommandExecutor, + pub(crate) key_pair: Keypair, +} + +#[async_trait] +impl EpochOperations for RewardManager { + async fn perform_epoch_operations( + &mut self, + rewards: Vec, + ) -> anyhow::Result> { + //Submit our own rewards message. + //It will be included in the next block(ours and/or others) + self.send_rewards_to_ephemera(rewards).await?; + + //Assuming that next block includes our rewards message + //This assumptions need to be "configured" by the application. + let prev_block = self.get_last_block().await?; + let next_height = prev_block.header.height + 1; + + //Poll next block which should include all messages from the previous epoch from almost all Nym-Api nodes + let mut counter = 0; + info!( + "Waiting for block with height {next_height} maximum {} seconds", + self.args.block_polling_max_attempts * self.args.block_polling_interval_seconds + ); + loop { + if counter > self.args.block_polling_max_attempts { + error!("Block for height {next_height} is not available after {counter} attempts"); + break; + } + tokio::select! { + Ok(Some(block)) = self.get_block_by_height(next_height) => { + info!("Received local block with height {next_height}, hash:{:?}", block.header.hash); + if let Ok(agg_rewards) = self.try_aggregate_rewards(block.clone()).await{ + info!("Submitted rewards to smart contract"); + let epoch_id = self.epoch.current_epoch_numer(); + self.store_in_dht(epoch_id, &block).await?; + info!("Stored rewards in DHT"); + return Ok(agg_rewards); + } + break; + } + _ = tokio::time::sleep(Duration::from_secs(self.args.block_polling_interval_seconds)) => { + trace!("Block for height {next_height} is not available yet, waiting..."); + } + } + counter += 1; + } + + info!("Querying for block with height {next_height} from the DHT"); + counter = 0; + let epoch_id = self.epoch.current_epoch_numer(); + loop { + if counter > self.args.block_polling_max_attempts { + error!( + "DHT: Block for height {next_height} is not available after {counter} attempts" + ); + break; + } + tokio::select! { + Ok(Some(block)) = self.query_dht(epoch_id) => { + info!("DHT: Received block {block}"); + break; + } + _= tokio::time::sleep(Duration::from_secs(self.args.block_polling_interval_seconds)) => { + trace!("DHT: Block for height {next_height} is not available in yet, waiting..."); + } + } + counter += 1; + } + + // TODO: query smart contract for the nym-api which was able to submit rewards and query its block. + // TODO: Because each Ephemera "sees" all blocks during RB then it might be safe to save them locally + // TODO: already during RB. In case of failure of that node. + + info!("Finished reward calculation for previous epoch"); + Ok(vec![]) + } +} + +impl EphemeraAccess { + pub(crate) fn new(api: CommandExecutor, key_pair: Keypair) -> Self { + Self { api, key_pair } + } +} + +#[async_trait] +pub(crate) trait EpochOperations { + async fn perform_epoch_operations( + &mut self, + rewards: Vec, + ) -> anyhow::Result>; +} + +pub(crate) struct RewardManager { + pub epoch: Epoch, + pub args: Args, + pub ephemera_access: Option, + aggregator: Option, +} + +impl RewardManager +where + Self: EpochOperations, +{ + pub(crate) fn new( + args: Args, + ephemera_access: Option, + aggregator: Option, + epoch: Epoch, + ) -> Self { + info!( + "Starting RewardManager with epoch nr {}", + epoch.current_epoch_numer() + ); + Self { + epoch, + args, + ephemera_access, + aggregator, + } + } + + pub(crate) async fn get_last_block(&self) -> Result { + let access = self + .ephemera_access + .as_ref() + .expect("Ephemera access not set"); + let block = access.api.get_last_block().await?; + Ok(block) + } + + pub(crate) async fn get_block_by_height( + &self, + height: u64, + ) -> Result, ApiError> { + let access = self + .ephemera_access + .as_ref() + .expect("Ephemera access not set"); + let block = access.api.get_block_by_height(height).await?; + Ok(block) + } + + pub(crate) async fn store_in_dht(&self, epoch_id: u64, block: &ApiBlock) -> anyhow::Result<()> { + info!("Storing ourselves as 'winner' in DHT for epoch id: {epoch_id:?}"); + + let access = self + .ephemera_access + .as_ref() + .expect("Ephemera access not set"); + + let key = format!("epoch_id_{epoch_id}").into_bytes(); + let value = serde_json::to_vec(&block).expect("Failed to serialize block"); + + access.api.store_in_dht(key, value).await?; + info!("Sent store request to DHT"); + Ok(()) + } + + pub(crate) async fn query_dht(&self, epoch_id: u64) -> anyhow::Result> { + let access = self + .ephemera_access + .as_ref() + .expect("Ephemera access not set"); + + let key = format!("epoch_id_{epoch_id}").into_bytes(); + + match access.api.query_dht(key).await? { + None => { + info!("No 'winner' found for epoch id from DHT: {epoch_id:?}"); + Ok(None) + } + Some((_, block)) => { + let block = serde_json::from_slice(block.as_slice())?; + info!("'Winner' found for epoch id from DHT: {epoch_id:?} - {block:?}"); + Ok(Some(block)) + } + } + } + + pub(crate) async fn send_rewards_to_ephemera( + &self, + rewards: Vec, + ) -> anyhow::Result<()> { + let ephemera_msg = self.create_ephemera_message(rewards)?; + debug!("Sending rewards to ephemera: {:?}", ephemera_msg); + + let access = self + .ephemera_access + .as_ref() + .expect("Ephemera access not set"); + + access.api.send_ephemera_message(ephemera_msg).await?; + Ok(()) + } + + fn create_ephemera_message( + &self, + rewards: Vec, + ) -> anyhow::Result { + let keypair = &self + .ephemera_access + .as_ref() + .expect("Ephemera access not set") + .key_pair; + + let label = self.epoch.current_epoch_numer().to_string(); + let data = serde_json::to_vec(&rewards)?; + let raw_message = ephemera_api::RawApiEphemeraMessage::new(label, data); + + let certificate = ephemera_api::ApiCertificate::prepare(keypair, &raw_message)?; + let signed_message = ApiEphemeraMessage::new(raw_message, certificate); + + Ok(signed_message) + } + + //By current assumption, all nodes will try submit their aggregated rewards + //and contract will reject all but first one. + async fn try_aggregate_rewards( + &mut self, + block: ApiBlock, + ) -> anyhow::Result> { + info!( + "Calculating aggregated rewards from block with height: {:?}", + block.header.height + ); + let mut mix_node_rewards = vec![]; + + for message in block.messages { + trace!("Message: {}", message); + let mix_node_reward: Vec = + serde_json::from_slice(&message.data)?; + mix_node_rewards.push(mix_node_reward); + } + + let aggregated_rewards = self.aggregator().aggregate(mix_node_rewards)?; + debug!("Aggregated rewards: {:?}", aggregated_rewards); + + Ok(aggregated_rewards) + } + + fn aggregator(&self) -> &RewardsAggregator { + self.aggregator.as_ref().expect("Aggregator not set") + } +} diff --git a/nym-api/src/epoch_operations/error.rs b/nym-api/src/epoch_operations/error.rs index ab354701dc..e92d28700f 100644 --- a/nym-api/src/epoch_operations/error.rs +++ b/nym-api/src/epoch_operations/error.rs @@ -47,6 +47,9 @@ pub enum RewardingError { #[from] source: rand::distributions::WeightedError, }, + + #[error("{0}")] + GenericError(#[from] anyhow::Error), } impl From for RewardingError { diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index 2f82df8248..929cbac770 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -5,8 +5,9 @@ use crate::epoch_operations::RewardedSetUpdater; use cosmwasm_std::{Decimal, Fraction}; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{ExecuteMsg, Interval, MixId}; +use serde_derive::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub(crate) struct MixnodeWithPerformance { pub(crate) mix_id: MixId, diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index d1d58998cc..aa88d0aa66 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -12,6 +12,7 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. +use crate::ephemera::reward::{EpochOperations, RewardManager}; use crate::node_status_api::ONE_DAY; use crate::nym_contract_cache::cache::NymContractCache; use crate::support::nyxd::Client; @@ -32,6 +33,7 @@ mod rewarding; mod transition_beginning; pub struct RewardedSetUpdater { + ephemera_reward_manager: Option, nyxd_client: Client, nym_contract_cache: NymContractCache, storage: NymApiStorage, @@ -45,11 +47,13 @@ impl RewardedSetUpdater { } pub(crate) fn new( + ephemera_reward_manager: Option, nyxd_client: Client, nym_contract_cache: NymContractCache, storage: NymApiStorage, ) -> Self { RewardedSetUpdater { + ephemera_reward_manager, nyxd_client, nym_contract_cache, storage, @@ -58,9 +62,11 @@ impl RewardedSetUpdater { // This is where the epoch gets advanced, and all epoch related transactions originate /// Upon each epoch having finished the following actions are executed by this nym-api: - /// 1. it queries the mixnet contract to check the current `EpochState` in order to figure out whether + /// 1. it computes the rewards for each node using the ephemera channel for the epoch that + /// ended + /// 2. it queries the mixnet contract to check the current `EpochState` in order to figure out whether /// a different nym-api has already started epoch transition (not yet applicable) - /// 2. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: + /// 3. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: /// - if successful, the address of the this validator is going to be saved as being responsible for progressing this epoch. /// What it means in practice is that once we have multiple instances of nym-api running, /// only this one will try to perform the rest of the actions. It will also allow it to @@ -70,21 +76,29 @@ impl RewardedSetUpdater { /// until that is done. /// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over, /// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished. - /// 3. it obtains the current rewarded set and for each node in there (**SORTED BY MIX_ID!!**), + /// 4. it obtains the current rewarded set and for each node in there (**SORTED BY MIX_ID!!**), /// it sends (in a single batch) `RewardMixnode` message with the measured performance. /// Once the final message gets executed, the mixnet contract automatically transitions /// the state to `ReconcilingEvents`. - /// 4. it obtains the number of pending epoch and interval events and repeatedly sends + /// 5. it obtains the number of pending epoch and interval events and repeatedly sends /// `ReconcileEpochEvents` transaction until all of them are resolved. /// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`. - /// 5. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) + /// 6. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) /// determines the new rewarded set. It then assigns layers to the provided nodes taking /// family information into consideration. Finally it sends `AdvanceCurrentEpoch` message /// containing the set and layer information thus rolling over the epoch and changing the state /// to `InProgress`. - /// 6. it purges old (older than 48h) measurement data - /// 7. the whole process repeats once the new epoch finishes - async fn perform_epoch_operations(&self, interval: Interval) -> Result<(), RewardingError> { + /// 7. it purges old (older than 48h) measurement data + /// 8. the whole process repeats once the new epoch finishes + async fn perform_epoch_operations(&mut self, interval: Interval) -> Result<(), RewardingError> { + let mut rewards = self.nodes_to_reward(interval).await; + if let Some(ephemera_reward_manager) = self.ephemera_reward_manager.as_mut() { + rewards = ephemera_reward_manager + .perform_epoch_operations(rewards) + .await?; + } + rewards.sort_by_key(|a| a.mix_id); + log::info!("The current epoch has finished."); log::info!( "Interval id: {}, epoch id: {} (absolute epoch id: {})", @@ -128,7 +142,7 @@ impl RewardedSetUpdater { // Reward all the nodes in the still current, soon to be previous rewarded set log::info!("Rewarding the current rewarded set..."); - self.reward_current_rewarded_set(interval).await?; + self.reward_current_rewarded_set(&rewards, interval).await?; // note: those operations don't really have to be atomic, so it's fine to send them // as separate transactions @@ -260,12 +274,14 @@ impl RewardedSetUpdater { } pub(crate) fn start( + ephemera_reward_manager: Option, nyxd_client: Client, nym_contract_cache: &NymContractCache, storage: &NymApiStorage, shutdown: &TaskManager, ) { let mut rewarded_set_updater = RewardedSetUpdater::new( + ephemera_reward_manager, nyxd_client, nym_contract_cache.to_owned(), storage.to_owned(), diff --git a/nym-api/src/epoch_operations/rewarding.rs b/nym-api/src/epoch_operations/rewarding.rs index d050f7efd6..56084a3dfd 100644 --- a/nym-api/src/epoch_operations/rewarding.rs +++ b/nym-api/src/epoch_operations/rewarding.rs @@ -9,6 +9,7 @@ use nym_mixnet_contract_common::{EpochState, Interval, MixId}; impl RewardedSetUpdater { pub(super) async fn reward_current_rewarded_set( &self, + to_reward: &[MixnodeWithPerformance], current_interval: Interval, ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -34,7 +35,10 @@ impl RewardedSetUpdater { return Err(RewardingError::MidMixRewarding { last_rewarded }); } - if let Err(err) = self._reward_current_rewarded_set(current_interval).await { + if let Err(err) = self + ._reward_current_rewarded_set(to_reward, current_interval) + .await + { log::error!("FAILED to reward rewarded set - {err}"); Err(err) } else { @@ -47,14 +51,12 @@ impl RewardedSetUpdater { async fn _reward_current_rewarded_set( &self, + to_reward: &[MixnodeWithPerformance], current_interval: Interval, ) -> Result<(), RewardingError> { - let mut to_reward = self.nodes_to_reward(current_interval).await; - to_reward.sort_by_key(|a| a.mix_id); - if to_reward.is_empty() { error!("There are no nodes to reward in this epoch - we shouldn't have been in the 'Rewarding' state!"); - } else if let Err(err) = self.nyxd_client.send_rewarding_messages(&to_reward).await { + } else if let Err(err) = self.nyxd_client.send_rewarding_messages(to_reward).await { error!( "failed to perform mixnode rewarding for epoch {}! Error encountered: {err}", current_interval.current_epoch_absolute_id(), @@ -67,7 +69,7 @@ impl RewardedSetUpdater { Ok(()) } - async fn nodes_to_reward(&self, interval: Interval) -> Vec { + pub(crate) async fn nodes_to_reward(&self, interval: Interval) -> Vec { // try to get current up to date view of the network bypassing the cache // in case the epochs were significantly shortened for the purposes of testing let rewarded_set: Vec = match self.nyxd_client.get_rewarded_set_mixnodes().await { diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 5784247e7f..a259879ace 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -12,6 +12,7 @@ use crate::support::cli::CliArgs; use crate::support::config::Config; use crate::support::storage; use crate::support::storage::NymApiStorage; +use ::ephemera::configuration::Configuration as EphemeraConfiguration; use ::nym_config::defaults::setup_env; use anyhow::Result; use circulating_supply_api::cache::CirculatingSupplyCache; @@ -29,6 +30,7 @@ use support::{http, nyxd}; mod circulating_supply_api; mod coconut; +mod ephemera; mod epoch_operations; pub(crate) mod network; mod network_monitor; @@ -126,6 +128,33 @@ async fn start_nym_api_tasks( // and then only start the uptime updater (and the monitor itself, duh) // if the monitoring if it's enabled if config.network_monitor.enabled { + let ephemera_config = + match EphemeraConfiguration::try_load(config.get_ephemera_config_path()) { + Ok(c) => c, + Err(_) => { + config + .get_ephemera_args() + .cmd + .clone() + .execute(Some(&config.get_id())); + EphemeraConfiguration::try_load(config.get_ephemera_config_path()) + .expect("Config file should be created now") + } + }; + let ephemera_reward_manager = if config.ephemera.enabled { + Some( + ephemera::application::NymApi::run( + config.get_ephemera_args().clone(), + ephemera_config, + nyxd_client.clone(), + &shutdown, + ) + .await?, + ) + } else { + None + }; + // if network monitor is enabled, the storage MUST BE available let storage = maybe_storage.unwrap(); @@ -143,7 +172,13 @@ async fn start_nym_api_tasks( // start 'rewarding' if its enabled if config.rewarding.enabled { epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - RewardedSetUpdater::start(nyxd_client, nym_contract_cache_state, storage, &shutdown); + RewardedSetUpdater::start( + ephemera_reward_manager, + nyxd_client, + nym_contract_cache_state, + storage, + &shutdown, + ); } } @@ -164,6 +199,11 @@ async fn run_nym_api(cli_args: CliArgs) -> Result<(), Box, + /// Specifies whether ephemera is used to aggregate monitor data on this API + #[clap(short = 'e', long, requires = "enable_monitor")] + pub(crate) enable_ephemera: Option, + /// Endpoint to nyxd instance from which the monitor will grab nodes to test #[clap(long)] pub(crate) nyxd_validator: Option, @@ -105,6 +109,10 @@ pub(crate) struct CliArgs { hide = true )] pub(crate) enable_coconut: Option, + + /// Ephemera configuration arguments. + #[command(flatten)] + pub(crate) ephemera_args: ephemera::cli::init::Cmd, } pub(crate) fn override_config(config: Config, args: CliArgs) -> Config { @@ -139,12 +147,26 @@ pub(crate) fn override_config(config: Config, args: CliArgs) -> Config { ) .with_optional(Config::with_network_monitor_enabled, args.enable_monitor) .with_optional(Config::with_rewarding_enabled, args.enable_rewarding) + .with_optional(Config::with_ephemera_enabled, args.enable_ephemera) .with_optional( Config::with_disabled_credentials_mode, args.enabled_credentials_mode.map(|b| !b), ) .with_optional(Config::with_announce_address, args.announce_address) .with_optional(Config::with_coconut_signer_enabled, args.enable_coconut) + .with_optional(Config::with_ephemera_ip, args.ephemera_args.ephemera_ip) + .with_optional( + Config::with_ephemera_protocol_port, + args.ephemera_args.ephemera_protocol_port, + ) + .with_optional( + Config::with_ephemera_websocket_port, + args.ephemera_args.ephemera_websocket_port, + ) + .with_optional( + Config::with_ephemera_http_api_port, + args.ephemera_args.ephemera_http_api_port, + ) } pub(crate) fn build_config(args: CliArgs) -> Result { diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 163eaa44db..bc4bdd7b54 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -8,7 +8,7 @@ use crate::support::config::template::CONFIG_TEMPLATE; use nym_config::defaults::{mainnet, NymNetworkDetails}; use nym_config::{ must_get_home, read_config_from_toml_file, save_formatted_config_to_file, NymConfigTemplate, - DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, + DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, DEFAULT_NYM_APIS_DIR, NYM_DIR, }; use nym_validator_client::nyxd; use serde::{Deserialize, Serialize}; @@ -23,8 +23,6 @@ pub(crate) mod old_config_v1_1_21; mod persistence; mod template; -const DEFAULT_NYM_APIS_DIR: &str = "nym-api"; - pub const DEFAULT_LOCAL_VALIDATOR: &str = "http://localhost:26657"; pub const DEFAULT_NYM_API_PORT: u16 = 8080; @@ -96,6 +94,8 @@ pub struct Config { pub rewarding: Rewarding, pub coconut_signer: CoconutSigner, + + pub ephemera: Ephemera, } impl<'a> From<&'a Config> for NymNetworkDetails { @@ -124,6 +124,7 @@ impl Config { circulating_supply_cacher: Default::default(), rewarding: Default::default(), coconut_signer: CoconutSigner::new_default(id.as_ref()), + ephemera: Ephemera::new_default(id.as_ref()), } } @@ -168,6 +169,11 @@ impl Config { self } + pub fn with_ephemera_enabled(mut self, enabled: bool) -> Self { + self.ephemera.enabled = enabled; + self + } + pub fn with_custom_nyxd_validator(mut self, validator: Url) -> Self { self.base.local_validator = validator; self @@ -208,6 +214,30 @@ impl Config { self } + pub fn with_ephemera_ip(mut self, ip: String) -> Self { + self.ephemera.args.cmd.ephemera_ip = Some(ip); + self + } + + pub fn with_ephemera_protocol_port(mut self, port: u16) -> Self { + self.ephemera.args.cmd.ephemera_protocol_port = Some(port); + self + } + + pub fn with_ephemera_websocket_port(mut self, port: u16) -> Self { + self.ephemera.args.cmd.ephemera_websocket_port = Some(port); + self + } + + pub fn with_ephemera_http_api_port(mut self, port: u16) -> Self { + self.ephemera.args.cmd.ephemera_http_api_port = Some(port); + self + } + + pub fn get_id(&self) -> String { + self.base.id.clone() + } + pub fn get_nyxd_url(&self) -> Url { self.base.local_validator.clone() } @@ -223,6 +253,14 @@ impl Config { pub fn get_mnemonic(&self) -> bip39::Mnemonic { self.base.mnemonic.clone() } + + pub fn get_ephemera_args(&self) -> &crate::ephemera::Args { + &self.ephemera.args + } + + pub fn get_ephemera_config_path(&self) -> PathBuf { + self.ephemera.args.ephemera_config.clone() + } } // we only really care about the mnemonic being zeroized @@ -542,3 +580,25 @@ impl Default for CoconutSignerDebug { } } } + +#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default)] +pub struct Ephemera { + pub enabled: bool, + args: crate::ephemera::Args, +} + +impl Ephemera { + fn new_default(id: &str) -> Self { + Ephemera { + enabled: false, + args: crate::ephemera::Args { + ephemera_config: ephemera::configuration::Configuration::ephemera_config_file_home( + Some(id), + ) + .unwrap(), + ..Default::default() + }, + } + } +} diff --git a/nym-api/src/support/config/old_config_v1_1_21.rs b/nym-api/src/support/config/old_config_v1_1_21.rs index 3e5ded9964..2f13baaad0 100644 --- a/nym-api/src/support/config/old_config_v1_1_21.rs +++ b/nym-api/src/support/config/old_config_v1_1_21.rs @@ -156,6 +156,7 @@ impl From for Config { dkg_contract_polling_rate: value.coconut_signer.dkg_contract_polling_rate, }, }, + ephemera: Default::default(), } } } diff --git a/nym-api/src/support/config/template.rs b/nym-api/src/support/config/template.rs index 7ef105d91d..32d4f5e087 100644 --- a/nym-api/src/support/config/template.rs +++ b/nym-api/src/support/config/template.rs @@ -130,4 +130,12 @@ decryption_key_path = '{{ coconut_signer.storage_paths.decryption_key_path }}' # Path to the dkg dealer public key with proof public_key_with_proof_path = '{{ coconut_signer.storage_paths.public_key_with_proof_path }}' +[ephemera] + +enabled = {{ ephemera.enabled }} + +[ephemera.args] + +ephemera_config = '{{ ephemera.args.ephemera_config }}' + "#; diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 501be9e581..790be3ca31 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -18,6 +18,8 @@ use nym_coconut_dkg_common::{ }; use nym_config::defaults::ChainDetails; use nym_contracts_common::dealings::ContractSafeBytes; +use nym_ephemera_common::msg::QueryMsg as EphemeraQueryMsg; +use nym_ephemera_common::types::JsonPeerInfo; use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::RewardingParams; @@ -31,10 +33,11 @@ use nym_validator_client::nyxd::contract_traits::{NameServiceQueryClient, PagedD use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ contract_traits::{ - CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, GroupQueryClient, - MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, - NymContractsProvider, PagedMixnetQueryClient, PagedMultisigQueryClient, - PagedVestingQueryClient, SpDirectoryQueryClient, + CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, EphemeraQueryClient, + EphemeraSigningClient, GroupQueryClient, MixnetQueryClient, MixnetSigningClient, + MultisigQueryClient, MultisigSigningClient, NymContractsProvider, PagedEphemeraQueryClient, + PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, + SpDirectoryQueryClient, }, cosmwasm_client::types::ExecuteResult, CosmWasmClient, Fee, @@ -434,6 +437,25 @@ impl crate::coconut::client::Client for Client { } } +#[async_trait] +impl crate::ephemera::client::Client for Client { + async fn get_ephemera_peers(&self) -> crate::ephemera::error::Result> { + Ok(self.0.read().await.get_all_ephemera_peers().await?) + } + + async fn register_ephemera_peer( + &self, + peer_info: JsonPeerInfo, + ) -> crate::ephemera::error::Result { + Ok(self + .0 + .write() + .await + .register_as_peer(peer_info, None) + .await?) + } +} + #[async_trait] impl DkgQueryClient for Client { async fn query_dkg_contract(&self, query: DkgQueryMsg) -> std::result::Result @@ -444,6 +466,19 @@ impl DkgQueryClient for Client { } } +#[async_trait] +impl EphemeraQueryClient for Client { + async fn query_ephemera_contract( + &self, + query: EphemeraQueryMsg, + ) -> std::result::Result + where + for<'a> T: Deserialize<'a>, + { + self.0.read().await.query_ephemera_contract(query).await + } +} + #[async_trait] impl SpDirectoryQueryClient for Client { async fn query_service_provider_contract( diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 51641a7a86..47977894ba 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -4118,6 +4118,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ephemera-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "nym-contracts-common", +] + [[package]] name = "nym-explorer-api-requests" version = "0.1.0" @@ -4578,6 +4588,7 @@ dependencies = [ "nym-coconut-interface", "nym-config", "nym-contracts-common", + "nym-ephemera-common", "nym-group-contract-common", "nym-mixnet-contract-common", "nym-multisig-contract-common", diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 72ea6b5683..fbe8b1d3f4 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3268,6 +3268,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ephemera-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "nym-contracts-common", +] + [[package]] name = "nym-group-contract-common" version = "0.1.0" @@ -3432,6 +3442,7 @@ dependencies = [ "nym-coconut-interface", "nym-config", "nym-contracts-common", + "nym-ephemera-common", "nym-group-contract-common", "nym-mixnet-contract-common", "nym-multisig-contract-common", diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs index 1c1ca33ae8..be282384f5 100644 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ b/nym-wallet/nym-wallet-types/src/network/qa.rs @@ -23,6 +23,8 @@ pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n1p54qvfde6mpnqvz3dnpa78x2qyyr5k4sgw9qr97mxjgklc5gze9sv6t964"; pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n1xqkp8x4gqwjnhemtemc5dqhwll6w6rrgpywvhka7sh8vz8swul9sp3lv3w"; +pub(crate) const EPHEMERA_CONTRACT_ADDRESS: &str = + "n1xqkp8x4gqwjnhemtemc5dqhwll6w6rrgpywvhka7sh8vz8swul9sp3lv3w"; pub(crate) const SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str = "n1nhdr07kmjns2x8dnp53tdk4qxreze8zdxj6xucyvkdj9tta73rjqa96wps"; pub(crate) const NAME_SERVICE_CONTRACT_ADDRESS: &str = @@ -55,6 +57,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), + ephemera_contract_address: parse_optional_str(EPHEMERA_CONTRACT_ADDRESS), service_provider_directory_contract_address: parse_optional_str( SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS, ), diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index 4c241248c7..0499fa5420 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -23,6 +23,8 @@ pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k"; pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"; +pub(crate) const EPHEMERA_CONTRACT_ADDRESS: &str = + "n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"; // -- Constructor functions -- @@ -51,6 +53,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), + ephemera_contract_address: parse_optional_str(EPHEMERA_CONTRACT_ADDRESS), service_provider_directory_contract_address: None, name_service_contract_address: None, },