From 8d7403e99ce29bcbeac12f6c993233ccfddfce6d Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 7 Oct 2024 17:55:49 -0400 Subject: [PATCH] Merge sim-rs-setup Squashed commit of the following: commit 116785227eacb12e02e8a7067e08b97275e23cfe Author: Simon Gellis Date: Mon Oct 7 17:40:53 2024 -0400 Pass the seed in through configuration commit 2ca6e9593f53448ce261bd148088289891819a10 Author: Simon Gellis Date: Mon Oct 7 17:24:37 2024 -0400 Add README and fix some IO quirks commit 3c382363bb05f15137843fd47353abc4123f66dc Author: Simon Gellis Date: Mon Oct 7 16:53:36 2024 -0400 Update tx modeling parameters commit e4e752560d1cbaa0abd093294610dce2a7cc024f Author: Simon Gellis Date: Mon Oct 7 16:09:35 2024 -0400 Block propagation commit 6cc2c5d40881cbc32cb6e0b06885a294d2f38455 Author: Simon Gellis Date: Fri Oct 4 16:40:10 2024 -0400 Simulate basic transactions commit 0ed84c667000e78e14894e015804b02380fd6ec7 Author: Simon Gellis Date: Thu Oct 3 18:02:50 2024 -0400 Emit block generation events to a file commit b957f13ddd024bf96f598672fcd95a85aa1de41a Author: Simon Gellis Date: Thu Oct 3 17:08:20 2024 -0400 Fix VRF computation to match actual probability commit 17ef4e4b9b53d8847abe09bf4ccb4a3afc476759 Author: Simon Gellis Date: Thu Oct 3 10:38:40 2024 -0400 Simulate simple slot lottery --- sim-rs/.gitignore | 2 + sim-rs/Cargo.lock | 1063 ++++++++++++++++++++++++++++++++++ sim-rs/Cargo.toml | 20 + sim-rs/README.md | 16 + sim-rs/src/config.rs | 132 +++++ sim-rs/src/events.rs | 228 ++++++++ sim-rs/src/main.rs | 61 ++ sim-rs/src/network.rs | 89 +++ sim-rs/src/probability.rs | 29 + sim-rs/src/sim.rs | 331 +++++++++++ sim-rs/test_data/simple.toml | 21 + 11 files changed, 1992 insertions(+) create mode 100644 sim-rs/.gitignore create mode 100644 sim-rs/Cargo.lock create mode 100644 sim-rs/Cargo.toml create mode 100644 sim-rs/README.md create mode 100644 sim-rs/src/config.rs create mode 100644 sim-rs/src/events.rs create mode 100644 sim-rs/src/main.rs create mode 100644 sim-rs/src/network.rs create mode 100644 sim-rs/src/probability.rs create mode 100644 sim-rs/src/sim.rs create mode 100644 sim-rs/test_data/simple.toml diff --git a/sim-rs/.gitignore b/sim-rs/.gitignore new file mode 100644 index 0000000..cfac028 --- /dev/null +++ b/sim-rs/.gitignore @@ -0,0 +1,2 @@ +/target/ +/output \ No newline at end of file diff --git a/sim-rs/Cargo.lock b/sim-rs/Cargo.lock new file mode 100644 index 0000000..8036bec --- /dev/null +++ b/sim-rs/Cargo.lock @@ -0,0 +1,1063 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "logos" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6b6e02facda28ca5fb8dbe4b152496ba3b1bd5a4b40bb2b1b2d8ad74e0f39b" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32eb6b5f26efacd015b000bfc562186472cd9b34bdba3f6b264e2a052676d10" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d0c5463c911ef55624739fc353238b4e310f0144be1f875dc42fec6bfd5ec" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "netsim-async" +version = "0.1.0" +source = "git+https://github.com/input-output-hk/ce-netsim.git#0bae993258474e622fcea20b8c114794e0665471" +dependencies = [ + "anyhow", + "netsim-core", + "tokio", +] + +[[package]] +name = "netsim-core" +version = "0.1.0" +source = "git+https://github.com/input-output-hk/ce-netsim.git#0bae993258474e622fcea20b8c114794e0665471" +dependencies = [ + "anyhow", + "logos", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "sim-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "ctrlc", + "futures", + "netsim-async", + "rand", + "rand_chacha", + "rand_distr", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/sim-rs/Cargo.toml b/sim-rs/Cargo.toml new file mode 100644 index 0000000..17039e9 --- /dev/null +++ b/sim-rs/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "sim-rs" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +ctrlc = "3" +futures = "0.3" +netsim-async = { git = "https://github.com/input-output-hk/ce-netsim.git" } +rand = "0.8" +rand_chacha = "0.3" +rand_distr = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +toml = "0.8" +tracing = "0.1" +tracing-subscriber = "0.3" diff --git a/sim-rs/README.md b/sim-rs/README.md new file mode 100644 index 0000000..46d19a3 --- /dev/null +++ b/sim-rs/README.md @@ -0,0 +1,16 @@ +# Leios Simulation + +This directory contains a (very heavily WIP) simulation of the Leios protocol. It produces a stream of events which can be used to visualize or analyze the behavior of Simplified Leios. + +## Running the project + +```sh +cargo run --release input_path [output_path] + +# for example... +cargo run --release ./test_data/simple.toml output/simple.json +``` + +The `input_path` is a TOML file which describes protocol parameters, the network topology, and other necessary configuration. Input files for predefined scenarios are in the `test_data` directory. + +While the simulation is running, it will log what's going on to the console. You can stop it at any time with ctrl+c, and when you do it will save the stream of events to `output_path`. \ No newline at end of file diff --git a/sim-rs/src/config.rs b/sim-rs/src/config.rs new file mode 100644 index 0000000..4e678aa --- /dev/null +++ b/sim-rs/src/config.rs @@ -0,0 +1,132 @@ +use std::{fmt::Display, fs, path::Path, time::Duration}; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +use crate::probability::FloatDistribution; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct PoolId(usize); +impl Display for PoolId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} +impl PoolId { + pub fn to_inner(self) -> usize { + self.0 + } + pub fn from_usize(value: usize) -> Self { + Self(value) + } +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "distribution", rename_all = "snake_case")] +enum DistributionConfig { + Normal { mean: f64, std_dev: f64 }, + Exp { lambda: f64, scale: Option }, + LogNormal { mu: f64, sigma: f64 }, +} +impl From for FloatDistribution { + fn from(value: DistributionConfig) -> Self { + match value { + DistributionConfig::Normal { mean, std_dev } => { + FloatDistribution::normal(mean, std_dev) + } + DistributionConfig::Exp { lambda, scale } => { + FloatDistribution::scaled_exp(lambda, scale.unwrap_or(1.)) + } + DistributionConfig::LogNormal { mu, sigma } => FloatDistribution::log_normal(mu, sigma), + } + } +} + +#[derive(Debug, Deserialize)] +struct RawConfig { + seed: Option, + pools: Vec, + links: Vec, + block_generation_probability: f64, + max_block_size: u64, + max_tx_size: u64, + transaction_frequency_ms: DistributionConfig, + transaction_size_bytes: DistributionConfig, +} + +#[derive(Debug, Deserialize)] +struct RawPoolConfig { + stake: u64, +} + +#[derive(Debug, Deserialize)] +struct RawLinkConfig { + pools: [usize; 2], + latency_ms: u64, +} + +#[derive(Debug, Clone)] +pub struct SimConfiguration { + pub seed: u64, + pub pools: Vec, + pub links: Vec, + pub block_generation_probability: f64, + pub max_block_size: u64, + pub max_tx_size: u64, + pub transaction_frequency_ms: FloatDistribution, + pub transaction_size_bytes: FloatDistribution, +} + +#[derive(Debug, Clone)] +pub struct PoolConfiguration { + pub id: PoolId, + pub stake: u64, + pub peers: Vec, +} + +#[derive(Debug, Clone)] +pub struct LinkConfiguration { + pub pools: [PoolId; 2], + pub latency: Duration, +} + +impl From for SimConfiguration { + fn from(value: RawConfig) -> Self { + let mut pools: Vec = value + .pools + .into_iter() + .enumerate() + .map(|(index, raw)| PoolConfiguration { + id: PoolId(index), + stake: raw.stake, + peers: vec![], + }) + .collect(); + let mut links = vec![]; + for link in value.links { + let [id1, id2] = link.pools; + pools[id1].peers.push(PoolId(id2)); + pools[id2].peers.push(PoolId(id1)); + links.push(LinkConfiguration { + pools: [PoolId(id1), PoolId(id2)], + latency: Duration::from_millis(link.latency_ms), + }); + } + Self { + seed: value.seed.unwrap_or_default(), + pools, + links, + block_generation_probability: value.block_generation_probability, + max_block_size: value.max_block_size, + max_tx_size: value.max_tx_size, + transaction_frequency_ms: value.transaction_frequency_ms.into(), + transaction_size_bytes: value.transaction_size_bytes.into(), + } + } +} + +pub fn read_config(filename: &Path) -> Result { + let file = fs::read_to_string(filename)?; + let raw_config: RawConfig = toml::from_str(&file)?; + Ok(raw_config.into()) +} diff --git a/sim-rs/src/events.rs b/sim-rs/src/events.rs new file mode 100644 index 0000000..524b90e --- /dev/null +++ b/sim-rs/src/events.rs @@ -0,0 +1,228 @@ +use std::{collections::BTreeMap, fs, path::PathBuf, time::Instant}; + +use anyhow::Result; +use serde::Serialize; +use tokio::sync::mpsc; +use tracing::{info, warn}; + +use crate::config::{PoolId, SimConfiguration}; + +pub enum Event { + Slot { + number: u64, + block: Option, + }, + BlockReceived { + slot: u64, + sender: PoolId, + recipient: PoolId, + }, + Transaction { + id: u64, + bytes: u64, + }, +} + +#[derive(Clone, PartialEq, Eq)] +pub struct Block { + pub slot: u64, + pub publisher: PoolId, + pub conflicts: Vec, + pub transactions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Transaction { + pub id: u64, + pub bytes: u64, +} + +#[derive(Clone, Serialize)] +enum OutputEvent { + BlockGenerated { + time: u128, + slot: u64, + publisher: PoolId, + transactions: Vec, + }, + BlockReceived { + time: u128, + slot: u64, + sender: PoolId, + recipient: PoolId, + }, + TransactionCreated { + time: u128, + id: u64, + bytes: u64, + }, +} + +#[derive(Clone)] +pub struct EventTracker(mpsc::UnboundedSender<(Event, Instant)>); + +impl EventTracker { + pub fn new(inner: mpsc::UnboundedSender<(Event, Instant)>) -> Self { + Self(inner) + } + + pub fn track_slot(&self, number: u64, block: Option) { + self.send(Event::Slot { number, block }); + } + + pub fn track_block_received(&self, slot: u64, sender: PoolId, recipient: PoolId) { + self.send(Event::BlockReceived { + slot, + sender, + recipient, + }); + } + + pub fn track_transaction(&self, transaction: &Transaction) { + self.send(Event::Transaction { + id: transaction.id, + bytes: transaction.bytes, + }); + } + + fn send(&self, event: Event) { + if self.0.send((event, Instant::now())).is_err() { + warn!("tried sending event after aggregator finished"); + } + } +} + +pub struct EventMonitor { + pool_ids: Vec, + start: Instant, + events_source: mpsc::UnboundedReceiver<(Event, Instant)>, + output_path: Option, +} + +impl EventMonitor { + pub fn new( + config: &SimConfiguration, + events_source: mpsc::UnboundedReceiver<(Event, Instant)>, + output_path: Option, + ) -> Self { + let pool_ids = config.pools.iter().map(|p| p.id).collect(); + let start = Instant::now(); + Self { + pool_ids, + start, + events_source, + output_path, + } + } + + // Monitor and report any events emitted by the simulation, + // including any aggregated stats at the end. + pub async fn run(mut self) -> Result<()> { + let mut blocks_published: BTreeMap = BTreeMap::new(); + let mut blocks_rejected: BTreeMap = BTreeMap::new(); + let mut pending_tx_sizes: BTreeMap = BTreeMap::new(); + + let mut filled_slots = 0u64; + let mut empty_slots = 0u64; + let mut published_txs = 0u64; + let mut published_bytes = 0u64; + + let mut output = vec![]; + while let Some((event, timestamp)) = self.events_source.recv().await { + self.compute_output_events(&mut output, &event, timestamp); + match event { + Event::Slot { number, block } => { + if let Some(block) = block { + info!( + "Pool {} published a block in slot {number}.", + block.publisher + ); + filled_slots += 1; + for published_tx in block.transactions { + published_txs += 1; + published_bytes += published_tx.bytes; + pending_tx_sizes.remove(&published_tx.id); + } + *blocks_published.entry(block.publisher).or_default() += 1; + + for conflict in block.conflicts { + *blocks_rejected.entry(conflict).or_default() += 1; + } + } else { + info!("No pools published a block in slot {number}."); + empty_slots += 1; + } + } + Event::BlockReceived { .. } => {} + Event::Transaction { id, bytes } => { + pending_tx_sizes.insert(id, bytes); + } + } + } + + info!("{filled_slots} block(s) were published."); + info!("{empty_slots} slot(s) had no blocks."); + info!("{published_txs} transaction(s) ({published_bytes} byte(s)) made it on-chain."); + + info!( + "{} transaction(s) ({} byte(s)) did not reach a block.", + pending_tx_sizes.len(), + pending_tx_sizes.into_values().sum::() + ); + + for id in self.pool_ids { + let published = blocks_published.get(&id).copied().unwrap_or_default(); + info!("Pool {id} published {published} block(s)"); + let rejected = blocks_rejected.get(&id).copied().unwrap_or_default(); + info!("Pool {id} failed to publish {rejected} block(s) due to conflicts."); + } + + if let Some(path) = self.output_path { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, serde_json::to_vec(&output)?)?; + } + Ok(()) + } + + fn compute_output_events( + &self, + output: &mut Vec, + event: &Event, + timestamp: Instant, + ) { + let time = timestamp.duration_since(self.start).as_nanos(); + match event { + Event::Slot { number, block } => { + if let Some(block) = block { + output.push(OutputEvent::BlockGenerated { + time, + slot: *number, + publisher: block.publisher, + transactions: block.transactions.iter().map(|t| t.id).collect(), + }); + } + } + Event::Transaction { id, bytes } => { + output.push(OutputEvent::TransactionCreated { + time, + id: *id, + bytes: *bytes, + }); + } + Event::BlockReceived { + slot, + sender, + recipient, + } => { + output.push(OutputEvent::BlockReceived { + time, + slot: *slot, + sender: *sender, + recipient: *recipient, + }); + } + } + } +} diff --git a/sim-rs/src/main.rs b/sim-rs/src/main.rs new file mode 100644 index 0000000..66ff57e --- /dev/null +++ b/sim-rs/src/main.rs @@ -0,0 +1,61 @@ +use std::{path::PathBuf, process}; + +use anyhow::Result; +use clap::Parser; +use config::read_config; +use events::{EventMonitor, EventTracker}; +use sim::Simulation; +use tokio::{ + pin, select, + sync::{mpsc, oneshot}, +}; +use tracing::warn; + +mod config; +mod events; +mod network; +mod probability; +mod sim; + +#[derive(Parser)] +struct Args { + filename: PathBuf, + output: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt().compact().without_time().init(); + + // Handle ctrl+c (SIGINT) at an application level, so we can report on necessary stats before shutting down. + let (ctrlc_sink, ctrlc_source) = oneshot::channel(); + let mut ctrlc_sink = Some(ctrlc_sink); + ctrlc::set_handler(move || { + if let Some(sink) = ctrlc_sink.take() { + let _ = sink.send(()); + } else { + warn!("force quitting"); + process::exit(0); + } + })?; + + let args = Args::parse(); + let config = read_config(&args.filename)?; + + let (events_sink, events_source) = mpsc::unbounded_channel(); + let monitor = EventMonitor::new(&config, events_source, args.output).run(); + pin!(monitor); + + let tracker = EventTracker::new(events_sink); + let mut simulation = Simulation::new(config)?; + + select! { + _ = simulation.run(tracker) => {} + result = &mut monitor => { result? } + _ = ctrlc_source => {} + }; + + simulation.shutdown()?; + monitor.await?; + Ok(()) +} diff --git a/sim-rs/src/network.rs b/sim-rs/src/network.rs new file mode 100644 index 0000000..e790ec1 --- /dev/null +++ b/sim-rs/src/network.rs @@ -0,0 +1,89 @@ +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use netsim_async::{ + Edge, EdgePolicy, HasBytesSize, SimContext, SimId, SimSocketReadHalf, SimSocketWriteHalf, +}; + +use crate::config::PoolId; + +pub struct Network { + context: SimContext, + id_lookup: IdLookup, +} + +impl Network { + pub fn new() -> Self { + Self { + context: SimContext::new(), + id_lookup: IdLookup::default(), + } + } + + pub fn shutdown(self) -> Result<()> { + self.context.shutdown() + } + + pub fn open(&mut self, pool_id: PoolId) -> Result<(NetworkSource, NetworkSink)> { + let socket = self.context.open()?; + self.id_lookup.add_id_mapping(pool_id, socket.id()); + + let (inner_source, inner_sink) = socket.into_split(); + let source = NetworkSource(inner_source, self.id_lookup.clone()); + let sink = NetworkSink(inner_sink, self.id_lookup.clone()); + Ok((source, sink)) + } + + pub fn set_edge_policy(&mut self, from: PoolId, to: PoolId, policy: EdgePolicy) -> Result<()> { + let from = self.id_lookup.find_sim_id(from); + let to = self.id_lookup.find_sim_id(to); + let edge = Edge::new((from, to)); + self.context.set_edge_policy(edge, policy) + } +} + +pub struct NetworkSource(SimSocketReadHalf, IdLookup); +impl NetworkSource { + pub async fn recv(&mut self) -> Option<(PoolId, T)> { + let (sim_id, msg) = self.0.recv().await?; + let pool_id = self.1.find_pool_id(sim_id); + Some((pool_id, msg)) + } +} + +pub struct NetworkSink(SimSocketWriteHalf, IdLookup); +impl NetworkSink { + pub fn send_to(&self, to: PoolId, msg: T) -> Result<()> { + let sim_id = self.1.find_sim_id(to); + self.0.send_to(sim_id, msg) + } +} + +// We must map between PoolId (which this code has control over) +// and SimId (an opaque type from the netsim library). +// PoolId is sequentially assigned, so we can look it up by index. +#[derive(Default, Clone)] +struct IdLookup(Arc>>); +impl IdLookup { + fn add_id_mapping(&self, pool_id: PoolId, sim_id: SimId) { + let mut id_list = self.0.write().expect("id list rwlock poisoned"); + assert_eq!(pool_id.to_inner(), id_list.len()); + id_list.push(sim_id); + } + + fn find_sim_id(&self, pool_id: PoolId) -> SimId { + let id_list = self.0.read().expect("id list rwlock poisoned!"); + *id_list + .get(pool_id.to_inner()) + .expect("unrecognized pool id") + } + + fn find_pool_id(&self, sim_id: SimId) -> PoolId { + let id_list = self.0.read().expect("id list rwlock poisoned!"); + let index = id_list + .iter() + .position(|&id| id == sim_id) + .expect("unrecognized sim id"); + PoolId::from_usize(index) + } +} diff --git a/sim-rs/src/probability.rs b/sim-rs/src/probability.rs new file mode 100644 index 0000000..9e1c4de --- /dev/null +++ b/sim-rs/src/probability.rs @@ -0,0 +1,29 @@ +use rand_distr::{Distribution, Exp, LogNormal, Normal}; + +#[derive(Debug, Clone, Copy)] +pub enum FloatDistribution { + Normal(Normal), + ScaledExp(Exp, f64), + LogNormal(LogNormal), +} +impl FloatDistribution { + pub fn normal(mean: f64, std_dev: f64) -> Self { + Self::Normal(Normal::new(mean, std_dev).unwrap()) + } + pub fn scaled_exp(lambda: f64, scale: f64) -> Self { + Self::ScaledExp(Exp::new(lambda).unwrap(), scale) + } + pub fn log_normal(mu: f64, sigma: f64) -> Self { + Self::LogNormal(LogNormal::new(mu, sigma).unwrap()) + } +} + +impl Distribution for FloatDistribution { + fn sample(&self, rng: &mut R) -> f64 { + match self { + Self::Normal(d) => d.sample(rng), + Self::ScaledExp(d, scale) => d.sample(rng) * scale, + Self::LogNormal(d) => d.sample(rng), + } + } +} diff --git a/sim-rs/src/sim.rs b/sim-rs/src/sim.rs new file mode 100644 index 0000000..a122efb --- /dev/null +++ b/sim-rs/src/sim.rs @@ -0,0 +1,331 @@ +use std::{ + cmp::Reverse, + collections::{BTreeMap, BTreeSet, BinaryHeap, VecDeque}, + time::{Duration, Instant}, +}; + +use anyhow::{bail, Context, Result}; +use futures::{stream::FuturesUnordered, StreamExt}; +use netsim_async::{EdgePolicy, HasBytesSize, Latency}; +use rand::Rng as _; +use rand_chacha::{rand_core::SeedableRng, ChaChaRng}; +use rand_distr::Distribution as _; +use tokio::{select, time}; + +use crate::{ + config::{PoolConfiguration, PoolId, SimConfiguration}, + events::{Block, EventTracker, Transaction}, + network::{Network, NetworkSink, NetworkSource}, + probability::FloatDistribution, +}; + +pub struct Simulation { + rng: ChaChaRng, + network: Network, + pools: BTreeMap, + msg_sources: BTreeMap>, + max_block_size: u64, + max_tx_size: u64, + next_slot: u64, + next_tx_id: u64, + transaction_frequency_ms: FloatDistribution, + transaction_size_bytes: FloatDistribution, + event_queue: BinaryHeap, + unpublished_txs: VecDeque, +} + +impl Simulation { + pub fn new(config: SimConfiguration) -> Result { + let total_stake = config.pools.iter().map(|p| p.stake).sum(); + + let mut network = Network::new(); + + let rng = ChaChaRng::seed_from_u64(config.seed); + let mut pools = BTreeMap::new(); + let mut msg_sources = BTreeMap::new(); + for pool_config in &config.pools { + let id = pool_config.id; + let (msg_source, msg_sink) = network.open(id).context("could not open socket")?; + let pool = Pool::new(pool_config, &config, total_stake, msg_sink); + msg_sources.insert(pool.id, msg_source); + pools.insert(pool.id, pool); + } + for link_config in config.links { + network.set_edge_policy( + link_config.pools[0], + link_config.pools[1], + EdgePolicy { + latency: Latency::new(link_config.latency), + ..EdgePolicy::default() + }, + )?; + } + + let mut sim = Self { + rng, + network, + pools, + msg_sources, + max_block_size: config.max_block_size, + max_tx_size: config.max_tx_size, + transaction_frequency_ms: config.transaction_frequency_ms, + transaction_size_bytes: config.transaction_size_bytes, + next_slot: 0, + next_tx_id: 0, + event_queue: BinaryHeap::new(), + unpublished_txs: VecDeque::new(), + }; + sim.queue_event(SimulationEvent::NewSlot, Duration::ZERO); + sim.queue_event(SimulationEvent::NewTransaction, Duration::ZERO); + + Ok(sim) + } + + // Run the simulation indefinitely. + pub async fn run(&mut self, tracker: EventTracker) -> Result<()> { + while let Some(event) = self.next_event().await { + match event { + SimulationEvent::NewSlot => self.run_slot_lottery(&tracker)?, + SimulationEvent::NewTransaction => self.generate_tx(&tracker), + SimulationEvent::NetworkMessage { from, to, msg } => { + let Some(target) = self.pools.get_mut(&to) else { + bail!("unrecognized message target {to}"); + }; + match msg { + SimulationMessage::Block(block) => { + tracker.track_block_received(block.slot, from, to); + target.receive_block(from, block)?; + } + } + } + } + } + Ok(()) + } + + pub fn shutdown(self) -> Result<()> { + self.network.shutdown() + } + + fn queue_event(&mut self, event: SimulationEvent, after: Duration) { + self.event_queue + .push(FutureEvent(Instant::now() + after, event)); + } + + async fn next_event(&mut self) -> Option { + let queued_event = self.event_queue.peek().cloned(); + + let next_queued_event = async move { + let FutureEvent(instant, event) = queued_event?; + time::sleep_until(instant.into()).await; + Some(event) + }; + + let mut next_incoming_message = FuturesUnordered::new(); + for (id, source) in self.msg_sources.iter_mut() { + next_incoming_message.push(async move { + let (from, msg) = source.recv().await?; + Some(SimulationEvent::NetworkMessage { from, to: *id, msg }) + }); + } + + select! { + Some(event) = next_queued_event => { + self.event_queue.pop(); + Some(event) + } + Some(Some(event)) = next_incoming_message.next() => Some(event), + else => None + } + } + + fn run_slot_lottery(&mut self, tracker: &EventTracker) -> Result<()> { + let vrf_winners: Vec<(PoolId, u64)> = self + .pools + .values() + .filter_map(|pool| { + let result = pool.run_vrf(&mut self.rng)?; + Some((pool.id, result)) + }) + .collect(); + + let winner = vrf_winners + .iter() + .max_by_key(|(_, result)| *result) + .map(|(id, _)| *id); + + if let Some(publisher) = winner { + let conflicts = vrf_winners + .into_iter() + .filter_map(|(id, _)| if publisher != id { Some(id) } else { None }) + .collect(); + + // Fill a block with as many pending transactions as can fit + let mut size = 0; + let mut transactions = vec![]; + while let Some(tx) = self.unpublished_txs.front() { + if size + tx.bytes > self.max_block_size { + break; + } + size += tx.bytes; + transactions.push(self.unpublished_txs.pop_front().unwrap()); + } + + let block = Block { + slot: self.next_slot, + publisher, + conflicts, + transactions, + }; + self.pools + .get_mut(&publisher) + .unwrap() + .publish_block(&block)?; + tracker.track_slot(self.next_slot, Some(block)); + } else { + tracker.track_slot(self.next_slot, None); + } + + self.next_slot += 1; + self.queue_event(SimulationEvent::NewSlot, Duration::from_secs(1)); + Ok(()) + } + + fn generate_tx(&mut self, tracker: &EventTracker) { + let id = self.next_tx_id; + let bytes = self + .max_tx_size + .min(self.transaction_size_bytes.sample(&mut self.rng) as u64); + let tx = Transaction { id, bytes }; + + tracker.track_transaction(&tx); + self.unpublished_txs.push_back(tx); + + self.next_tx_id += 1; + let ms_until_tx = self.transaction_frequency_ms.sample(&mut self.rng) as u64; + self.queue_event( + SimulationEvent::NewTransaction, + Duration::from_millis(ms_until_tx), + ); + } +} + +struct Pool { + id: PoolId, + msg_sink: NetworkSink, + target_vrf_stake: u64, + total_stake: u64, + peers: Vec, + blocks_sent_to_peers: BTreeSet<(PoolId, u64)>, +} + +impl Pool { + fn new( + config: &PoolConfiguration, + sim_config: &SimConfiguration, + total_stake: u64, + msg_sink: NetworkSink, + ) -> Self { + let id = config.id; + let target_vrf_stake = compute_target_vrf_stake( + config.stake, + total_stake, + sim_config.block_generation_probability, + ); + let peers = config.peers.clone(); + Self { + id, + msg_sink, + target_vrf_stake, + total_stake, + peers, + blocks_sent_to_peers: BTreeSet::new(), + } + } + + fn publish_block(&mut self, block: &Block) -> Result<()> { + for peer in &self.peers { + if self.blocks_sent_to_peers.insert((*peer, block.slot)) { + self.msg_sink + .send_to(*peer, SimulationMessage::Block(block.clone()))? + } + } + Ok(()) + } + + fn receive_block(&mut self, from: PoolId, block: Block) -> Result<()> { + self.blocks_sent_to_peers.insert((from, block.slot)); + self.publish_block(&block) + } + + // Simulates the output of a VRF using this pool's stake. + fn run_vrf(&self, rng: &mut ChaChaRng) -> Option { + let result = rng.gen_range(0..self.total_stake); + if result < self.target_vrf_stake { + Some(result) + } else { + None + } + } +} + +fn compute_target_vrf_stake( + stake: u64, + total_stake: u64, + block_generation_probability: f64, +) -> u64 { + let ratio = stake as f64 / total_stake as f64; + let p_success = 1. - (1. - block_generation_probability).powf(ratio); + (total_stake as f64 * p_success) as u64 +} + +// wrapper struct which holds a SimulationEvent, +// but is ordered by a timestamp (in reverse) +#[derive(Clone)] +struct FutureEvent(Instant, SimulationEvent); +impl FutureEvent { + fn key(&self) -> Reverse { + Reverse(self.0) + } +} + +impl PartialEq for FutureEvent { + fn eq(&self, other: &Self) -> bool { + self.key() == other.key() + } +} +impl Eq for FutureEvent {} +impl PartialOrd for FutureEvent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for FutureEvent { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.key().cmp(&other.key()) + } +} + +#[derive(Clone)] +enum SimulationEvent { + NewSlot, + NewTransaction, + NetworkMessage { + from: PoolId, + to: PoolId, + msg: SimulationMessage, + }, +} + +#[derive(Clone)] +enum SimulationMessage { + Block(Block), +} + +impl HasBytesSize for SimulationMessage { + fn bytes_size(&self) -> u64 { + match self { + Self::Block(block) => block.transactions.iter().map(|t| t.bytes).sum(), + } + } +} diff --git a/sim-rs/test_data/simple.toml b/sim-rs/test_data/simple.toml new file mode 100644 index 0000000..0fb0298 --- /dev/null +++ b/sim-rs/test_data/simple.toml @@ -0,0 +1,21 @@ +seed = 0x3c3373756e646165 +pools = [ + { stake = 1000000 }, + { stake = 1000000 }, + { stake = 1000000 }, + { stake = 1000000 }, + { stake = 1000000 }, + { stake = 10000000 }, +] +links = [ + { pools = [0, 1], latency_ms = 30 }, + { pools = [0, 2], latency_ms = 50 }, + { pools = [1, 3], latency_ms = 30 }, + { pools = [1, 4], latency_ms = 50 }, + { pools = [2, 5], latency_ms = 30 }, +] +block_generation_probability = 0.05 +max_block_size = 90112 +max_tx_size = 16384 +transaction_frequency_ms = { distribution = "exp", lambda = 0.85, scale = 1000 } +transaction_size_bytes = { distribution = "log_normal", mu = 6.85, sigma = 1.13 }