diff --git a/.github/workflows/neurons-rust-checks.yml b/.github/workflows/neurons-rust-checks.yml new file mode 100644 index 0000000..e73aef9 --- /dev/null +++ b/.github/workflows/neurons-rust-checks.yml @@ -0,0 +1,31 @@ +name: Neurons Rust Checks +on: + pull_request: + push: + branches: + - main +jobs: + voting-check: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./neurons + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 + with: + workspaces: ./neurons + cache-all-crates: true + + - name: Run tests + run: | + cargo test + + - name: Formatting check + run: | + cargo fmt --check + + - name: Lint with clippy + run: + cargo lint diff --git a/.github/workflows/voting-checks.yml b/.github/workflows/voting-checks.yml new file mode 100644 index 0000000..403c0c8 --- /dev/null +++ b/.github/workflows/voting-checks.yml @@ -0,0 +1,41 @@ +name: Voting Contract Checks +on: + pull_request: + push: + branches: + - main +jobs: + voting-check: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./contracts + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 + with: + workspaces: ./contracts + cache-all-crates: true + + - name: Install soroban cli + run: | + cargo install --locked --version 20.2.0 soroban-cli + + - name: Build contracts + run: | + soroban contract build + + - name: Run tests + run: | + cargo test + + - name: Formatting check + run: | + cargo fmt --check + + - name: Lint with clippy + run: + cargo lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc4d5ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +target + +**/test_snapshots/** diff --git a/contracts/.cargo/config.toml b/contracts/.cargo/config.toml new file mode 100644 index 0000000..4178fa3 --- /dev/null +++ b/contracts/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +lint = "clippy --all-targets --all-features -- -W clippy::pedantic -A clippy::missing_errors_doc -A clippy::missing_panics_doc -A clippy::module_name_repetitions -A clippy::must_use_candidate" diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock new file mode 100644 index 0000000..e956bb3 --- /dev/null +++ b/contracts/Cargo.lock @@ -0,0 +1,1466 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cc" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mocks" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "offchain" +version = "0.1.0" +dependencies = [ + "mocks", + "soroban-fixed-point-math", + "soroban-sdk", + "soroban_decimal_numbers", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "platforms" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +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 = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "soroban-builtin-sdk-macros" +version = "20.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc32c6e817f3ca269764ec0d7d14da6210b74a5bf14d4e745aa3ee860558900" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "soroban-env-common" +version = "20.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c14e18d879c520ff82612eaae0590acaf6a7f3b977407e1abb1c9e31f94c7814" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", +] + +[[package]] +name = "soroban-env-guest" +version = "20.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5122ca2abd5ebcc1e876a96b9b44f87ce0a0e06df8f7c09772ddb58b159b7454" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "20.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114a0fa0d0cc39d0be16b1ee35b6e5f4ee0592ddcf459bde69391c02b03cf520" +dependencies = [ + "backtrace", + "curve25519-dalek", + "ed25519-dalek", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "rand", + "rand_chacha", + "sha2", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey", +] + +[[package]] +name = "soroban-env-macros" +version = "20.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13e3f8c86f812e0669e78fcb3eae40c385c6a9dd1a4886a1de733230b4fcf27" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-fixed-point-math" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230e5902daf9de6e7591aa7864dcf763ff96914a4460a0294a5dfd62b3181e0f" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "20.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a54708f44890e0546180db6b4f530e2a88d83b05a9b38a131caa21d005e25a" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "20.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fc8be9068dd4e0212d8b13ad61089ea87e69ac212c262914503a961c8dc3a3" +dependencies = [ + "arbitrary", + "bytes-lit", + "ctor", + "ed25519-dalek", + "rand", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey", +] + +[[package]] +name = "soroban-sdk-macros" +version = "20.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db20def4ead836663633f58d817d0ed8e1af052c9650a04adf730525af85b964" +dependencies = [ + "crate-git-revision", + "darling", + "itertools", + "proc-macro2", + "quote", + "rustc_version", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-spec" +version = "20.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eefeb5d373b43f6828145d00f0c5cc35e96db56a6671ae9614f84beb2711cab" +dependencies = [ + "base64 0.13.1", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "20.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3152bca4737ef734ac37fe47b225ee58765c9095970c481a18516a2b287c7a33" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2", + "soroban-spec", + "stellar-xdr", + "syn", + "thiserror", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "soroban_decimal_numbers" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f3200c7c9328eb132524f05529b5466ec5044fd193b3913f61cdaf22e0e031a" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + +[[package]] +name = "stellar-xdr" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e59cdf3eb4467fb5a4b00b52e7de6dca72f67fac6f9b700f55c95a5d86f09c9d" +dependencies = [ + "arbitrary", + "base64 0.13.1", + "crate-git-revision", + "escape-bytes", + "hex", + "serde", + "serde_with", + "stellar-strkey", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wasmi_arena" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser" +version = "0.88.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8cf7dd82407fe68161bedcd57fde15596f32ebf6e9b3bdbf3ae1da20e38e5e" +dependencies = [ + "indexmap 1.9.3", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +dependencies = [ + "indexmap-nostd", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml new file mode 100644 index 0000000..263decf --- /dev/null +++ b/contracts/Cargo.toml @@ -0,0 +1,21 @@ +[workspace] +resolver = "2" +members = ["offchain"] + +[workspace.dependencies] +soroban-sdk = "20.4.0" +soroban-fixed-point-math = "~1.0.0" + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true + +[profile.release-with-logs] +inherits = "release" +debug-assertions = true diff --git a/contracts/offchain/Cargo.toml b/contracts/offchain/Cargo.toml new file mode 100644 index 0000000..5b12700 --- /dev/null +++ b/contracts/offchain/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "offchain" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +soroban-sdk = { workspace = true, features = ["alloc"] } +soroban_decimal_numbers = "0.1.2" +soroban-fixed-point-math.workspace = true + +[dev_dependencies] +soroban-sdk = { workspace = true, features = ["testutils", "alloc"] } +mocks = { path = "mocks" } + +[features] +testutils = ["soroban-sdk/testutils", "soroban-sdk/alloc"] +estimations = [] diff --git a/contracts/offchain/README.md b/contracts/offchain/README.md new file mode 100644 index 0000000..9b2a665 --- /dev/null +++ b/contracts/offchain/README.md @@ -0,0 +1,17 @@ +# NQG + +This contract is a part of implementation of +the [Neural Quorum Governance](https://stellarcommunityfund.gitbook.io/module-library) mechanism. + +![architecture](../voting_system/image.png) + +Currently, because +of [resource constraints](https://developers.stellar.org/docs/reference/resource-limits-fees#resource-limits) and to +preserve voter privacy, neurons are computed off-chain and uploaded to the contract. + +The contract adds up results of each layer and computers the final voting power for each voter. This voting power is +stored on-chain for future reference. + +This voting power is used to compute the final score for each submission: Each `Yes` and `No` vote is multiplied by +respective users voting powers and tallied. + diff --git a/contracts/offchain/mocks/Cargo.toml b/contracts/offchain/mocks/Cargo.toml new file mode 100644 index 0000000..4c958f8 --- /dev/null +++ b/contracts/offchain/mocks/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mocks" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +soroban-sdk = { workspace = true } diff --git a/contracts/offchain/mocks/src/lib.rs b/contracts/offchain/mocks/src/lib.rs new file mode 100644 index 0000000..d24a613 --- /dev/null +++ b/contracts/offchain/mocks/src/lib.rs @@ -0,0 +1,36 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, contracttype, Env, Map, String, I256}; + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct NeuronResultKeyData { + layer_id: String, + neuron_id: String, + round: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum DataKey { + NeuronResultKey(NeuronResultKeyData), +} + +#[contract] +pub struct MockContract; + +#[contractimpl] +impl MockContract { + pub fn is_mock(_env: &Env) -> bool { + true + } + + pub fn get_stored_neuron_result(env: &Env) -> Map { + let key = DataKey::NeuronResultKey(NeuronResultKeyData { + layer_id: String::from_str(env, "0"), + neuron_id: String::from_str(env, "0"), + round: 25, + }); + env.storage().temporary().get(&key).unwrap() + } +} diff --git a/contracts/offchain/src/admin.rs b/contracts/offchain/src/admin.rs new file mode 100644 index 0000000..2a80597 --- /dev/null +++ b/contracts/offchain/src/admin.rs @@ -0,0 +1,23 @@ +use soroban_sdk::{Address, Env}; + +use crate::DataKey; + +pub mod traits; + +pub(crate) fn require_admin(env: &Env) { + let admin = get_admin(env); + admin.require_auth(); +} + +pub(crate) fn get_admin(env: &Env) -> Address { + assert!(is_set_admin(env), "Admin not set"); + env.storage().instance().get(&DataKey::Admin).unwrap() +} + +pub(crate) fn set_admin(env: &Env, admin: &Address) { + env.storage().instance().set(&DataKey::Admin, admin); +} + +pub(crate) fn is_set_admin(env: &Env) -> bool { + env.storage().instance().has(&DataKey::Admin) +} diff --git a/contracts/offchain/src/admin/traits.rs b/contracts/offchain/src/admin/traits.rs new file mode 100644 index 0000000..00f4296 --- /dev/null +++ b/contracts/offchain/src/admin/traits.rs @@ -0,0 +1,8 @@ +use soroban_sdk::{Address, BytesN, Env}; + +pub trait Admin { + /// Transfer ownership of the contract to `new_admin` address. + fn transfer_admin(env: Env, new_admin: Address); + /// Upgrade the implementation of the contract with one identified by `wasm_hash`. + fn upgrade(env: Env, wasm_hash: BytesN<32>); +} diff --git a/contracts/offchain/src/lib.rs b/contracts/offchain/src/lib.rs new file mode 100644 index 0000000..36d82d9 --- /dev/null +++ b/contracts/offchain/src/lib.rs @@ -0,0 +1,407 @@ +#![no_std] +#![allow(clippy::needless_pass_by_value)] + +extern crate alloc; + +use alloc::string::ToString; + +use soroban_fixed_point_math::SorobanFixedPoint; +use soroban_sdk::{ + contract, contractimpl, contracttype, Address, BytesN, Env, Map, String, Vec, I256, +}; + +use admin::{get_admin, is_set_admin, require_admin}; + +use crate::admin::set_admin; +use crate::admin::traits::Admin; +use crate::neural_governance::traits::Governance; +pub use crate::neural_governance::LayerAggregator; +use crate::neural_governance::{aggregate_result, Layer, Neuron, NGQ}; +use crate::storage::{ + read_layer, read_neural_governance, read_neuron, read_neuron_result, read_submission_votes, + read_submissions, read_voting_powers, remove_layer, remove_neuron, write_layer, + write_neural_governance, write_neuron, write_neuron_result, write_submission_votes, + write_submissions, write_voting_powers, LayerKeyData, NeuronKeyData, NeuronResultKeyData, + SubmissionVotesKeyData, SubmissionsKeyData, VotingPowersKeyData, +}; +use crate::types::{Vote, VotingSystemError, ABSTAIN_VOTING_POWER}; + +mod admin; +mod neural_governance; +mod storage; +pub mod types; + +pub const DECIMALS: i128 = 1_000_000_000_000_000_000; + +#[contract] +pub struct VotingSystemOffchain; + +type ContractResult = Result; + +#[derive(Clone, Debug, PartialEq)] +#[contracttype] +pub enum DataKey { + /// storage type: instance + NeuralGovernance, + /// storage type: instance + /// Map + Submissions(SubmissionsKeyData), + /// storage type: instance + /// Map> - users to the vector of users they delegated their votes to + Delegatees, + // storage type: instance + // Map - users to their delegation rank + DelegationRanks, + /// storage type: instance + /// u32 + CurrentLayerId, + Admin, + /// u32 + CurrentRound, + NeuronKey(NeuronKeyData), + NeuronResultKey(NeuronResultKeyData), + LayerKey(LayerKeyData), + SubmissionVotes(SubmissionVotesKeyData), + VotingPowers(VotingPowersKeyData), +} + +#[contractimpl] +impl VotingSystemOffchain { + /// Initialize the governance contract. + pub fn initialize(env: Env, admin: Address, current_round: u32) { + assert!(!is_set_admin(&env), "Admin already set"); + set_admin(&env, &admin); + + let neural_governance = NGQ::new(&env); + env.storage() + .instance() + .set(&DataKey::CurrentRound, ¤t_round); + write_neural_governance(&env, neural_governance); + } + + /// Get the current active round. + pub fn get_current_round(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::CurrentRound) + .unwrap() + } + + /// Change the active round. + pub fn set_current_round(env: Env, round: u32) { + let admin = get_admin(&env); + admin.require_auth(); + + env.storage().instance().set(&DataKey::CurrentRound, &round); + } + + /// Set multiple submissions. + pub fn set_submissions(env: Env, new_submissions: Vec) { + let admin = get_admin(&env); + admin.require_auth(); + + let mut submissions = Vec::new(&env); + + for submission in new_submissions { + if submissions.contains(submission.clone()) { + continue; + } + submissions.push_back(submission); + } + + write_submissions(&env, Self::get_current_round(&env), &submissions); + } + + /// Get submissions for the active round. + pub fn get_submissions(env: &Env) -> Vec { + read_submissions(env, Self::get_current_round(env)) + } + + /// Set votes for a submission. + pub fn set_votes_for_submission( + env: &Env, + submission_id: String, + votes: Map, + ) -> Result<(), VotingSystemError> { + require_admin(env); + if !read_submissions(env, Self::get_current_round(env)).contains(submission_id.clone()) { + return Err(VotingSystemError::SubmissionDoesNotExist); + } + + write_submission_votes(env, &submission_id, Self::get_current_round(env), &votes); + Ok(()) + } + + /// Get votes for the submission for a specific round. + pub fn get_votes_for_submission_round( + env: &Env, + submission_id: String, + round: u32, + ) -> Result, VotingSystemError> { + read_submission_votes(env, &submission_id, round) + } + + /// Get votes for the submission for the active round + pub fn get_votes_for_submission( + env: &Env, + submission_id: String, + ) -> Result, VotingSystemError> { + Self::get_votes_for_submission_round(env, submission_id, Self::get_current_round(env)) + } + + /// Compute the final voting power of a submission. + /// + /// Requires calling `calculate_voting_powers` first to compute and store voting powers for the round. + /// + /// # Panics: + /// + /// The function will panic if no voting powers are set for the active round. + pub fn tally_submission(env: &Env, submission_id: String) -> Result { + let submission_votes = Self::get_votes_for_submission(env, submission_id.clone())?; + let mut submission_voting_power_plus = I256::from_i32(env, 0); + let mut submission_voting_power_minus = I256::from_i32(env, 0); + let voting_powers = Self::get_voting_powers(env.clone())?; + + for (voter_id, vote) in submission_votes { + let voting_power = match vote { + Vote::Abstain => I256::from_i32(env, ABSTAIN_VOTING_POWER), + _ => voting_powers + .get(voter_id) + .ok_or(VotingSystemError::NGQResultForVoterMissing)?, + }; + match vote { + Vote::Yes => { + submission_voting_power_plus = submission_voting_power_plus.add(&voting_power); + } + Vote::No => { + submission_voting_power_minus = + submission_voting_power_minus.add(&voting_power); + } + Vote::Abstain => (), + }; + } + + Ok(submission_voting_power_plus.sub(&submission_voting_power_minus)) + } +} + +#[contractimpl] +impl Admin for VotingSystemOffchain { + fn transfer_admin(env: Env, new_admin: Address) { + require_admin(&env); + set_admin(&env, &new_admin); + } + + fn upgrade(env: Env, wasm_hash: BytesN<32>) { + require_admin(&env); + + env.deployer().update_current_contract_wasm(wasm_hash); + } +} + +#[contractimpl] +impl Governance for VotingSystemOffchain { + fn add_layer( + env: Env, + raw_neurons: Vec<(String, I256)>, + layer_aggregator: LayerAggregator, + ) -> Result<(), VotingSystemError> { + require_admin(&env); + + let layer_id = next_layer_id(&env); + let layer_id = String::from_str(&env, layer_id.to_string().as_str()); + + create_or_update_layer(env, layer_id, raw_neurons, layer_aggregator); + + Ok(()) + } + + fn remove_layer(env: Env, layer_id: String) -> Result<(), VotingSystemError> { + let mut neural_governance = read_neural_governance(&env).unwrap(); + let index = neural_governance + .layers + .iter() + .position(|id| id == layer_id) + .ok_or(VotingSystemError::LayerMissing)?; + let layer = read_layer(&env, &layer_id)?; + + for neuron_id in layer.neurons { + remove_neuron(&env, &layer_id, &neuron_id); + } + remove_layer(&env, &layer_id); + + neural_governance + .layers + .remove(u32::try_from(index).unwrap()); + + write_neural_governance(&env, neural_governance); + + Ok(()) + } + + fn update_layer( + env: Env, + layer_id: String, + raw_neurons: Vec<(String, I256)>, + layer_aggregator: LayerAggregator, + ) -> Result<(), VotingSystemError> { + let layer = read_layer(&env, &layer_id)?; + + for neuron_id in layer.neurons { + remove_neuron(&env, &layer_id, &neuron_id); + } + + create_or_update_layer(env, layer_id, raw_neurons, layer_aggregator); + + Ok(()) + } + + fn get_layer(env: Env, layer_id: String) -> Result { + read_layer(&env, &layer_id) + } + + fn get_neuron( + env: Env, + layer_id: String, + neuron_id: String, + ) -> Result { + read_neuron(&env, &layer_id, &neuron_id) + } + + fn get_neuron_result_round( + env: &Env, + layer_id: String, + neuron_id: String, + round: u32, + ) -> Result, VotingSystemError> { + read_neuron_result(env, &layer_id, &neuron_id, round) + } + + fn get_neuron_result( + env: &Env, + layer_id: String, + neuron_id: String, + ) -> Result, VotingSystemError> { + Self::get_neuron_result_round(env, layer_id, neuron_id, Self::get_current_round(env)) + } + + fn set_neuron_result(env: Env, layer_id: String, neuron_id: String, result: Map) { + require_admin(&env); + + write_neuron_result( + &env, + &layer_id, + &neuron_id, + Self::get_current_round(&env), + &result, + ); + } + + /// Get a result of a whole layer + /// + /// Gets a result of each neuron and aggregates them using a configured aggregator function + fn get_layer_result( + env: Env, + layer_id: String, + ) -> Result, VotingSystemError> { + let layer = read_layer(&env, &layer_id)?; + let mut result: Map> = Map::new(&env); + + for neuron_id in layer.neurons { + let neuron_result = Self::get_neuron_result(&env, layer_id.clone(), neuron_id.clone())?; + let neuron = read_neuron(&env, &layer_id, &neuron_id)?; + let neuron_result = weigh_neuron_result(&env, &neuron.weight, neuron_result); + for (user, new) in neuron_result { + let mut previous = result.get(user.clone()).unwrap_or_else(|| Vec::new(&env)); + previous.push_back(new); + result.set(user, previous); + } + } + + Ok(aggregate_result( + &env, + result, + layer.aggregator, + I256::from_i128(&env, DECIMALS), + )) + } + + fn calculate_voting_powers(env: Env) -> Result<(), VotingSystemError> { + require_admin(&env); + + let neural_governance = read_neural_governance(&env).unwrap(); + let mut result: Map = Map::new(&env); + for layer_id in neural_governance.layers { + let layer_result = VotingSystemOffchain::get_layer_result(env.clone(), layer_id)?; + for (key, value) in layer_result { + result.set( + key.clone(), + value.add(&result.get(key).unwrap_or_else(|| I256::from_i32(&env, 0))), + ); + } + } + + write_voting_powers(&env, Self::get_current_round(&env), &result); + Ok(()) + } + + fn get_voting_powers(env: Env) -> Result, VotingSystemError> { + read_voting_powers(&env, Self::get_current_round(&env)) + } + + /// Get a current neural governance setup + fn get_neural_governance(env: &Env) -> Result { + read_neural_governance(env) + } +} + +fn create_or_update_layer( + env: Env, + layer_id: String, + raw_neurons: Vec<(String, I256)>, + layer_aggregator: LayerAggregator, +) { + let mut neural_governance = read_neural_governance(&env).unwrap(); + + let mut neurons = Vec::new(&env); + + for (neuron_id_raw, (name, weight)) in raw_neurons.into_iter().enumerate() { + let neuron_id = String::from_str(&env, neuron_id_raw.to_string().as_str()); + + let neuron_details = Neuron::create(name, weight); + write_neuron(&env, &layer_id, &neuron_id, &neuron_details); + + neurons.push_back(neuron_id); + } + + let layer = Layer::create(neurons, layer_aggregator); + write_layer(&env, &layer_id, &layer); + + neural_governance.layers.push_back(layer_id); + write_neural_governance(&env, neural_governance); +} + +fn weigh_neuron_result(env: &Env, weight: &I256, result: Map) -> Map { + let mut scaled = Map::new(env); + + for (key, value) in result { + scaled.set( + key, + value.fixed_mul_floor(env, weight.clone(), I256::from_i128(env, DECIMALS)), + ); + } + + scaled +} + +fn next_layer_id(env: &Env) -> u32 { + let id: u32 = env + .storage() + .instance() + .get(&DataKey::CurrentLayerId) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::CurrentLayerId, &(id + 1)); + id +} diff --git a/contracts/offchain/src/neural_governance.rs b/contracts/offchain/src/neural_governance.rs new file mode 100644 index 0000000..3f13eca --- /dev/null +++ b/contracts/offchain/src/neural_governance.rs @@ -0,0 +1,203 @@ +#![allow(non_upper_case_globals)] + +use soroban_fixed_point_math::SorobanFixedPoint; +use soroban_sdk::{contracttype, Env, Map, String, Vec, I256}; + +pub mod traits; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum LayerAggregator { + Sum, + Product, +} + +#[contracttype] +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Neuron { + pub name: String, + pub weight: I256, +} + +impl Neuron { + pub fn create(name: String, weight: I256) -> Self { + Self { name, weight } + } +} + +#[contracttype] +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Layer { + /// Vec of `neuron_id`s + pub neurons: Vec, + pub aggregator: LayerAggregator, +} + +impl Layer { + pub fn create(neurons: Vec, aggregator: LayerAggregator) -> Self { + Self { + neurons, + aggregator, + } + } +} + +#[allow(clippy::upper_case_acronyms)] +#[contracttype] +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NGQ { + /// Vec of `layer_id`s + pub layers: Vec, +} + +impl NGQ { + pub fn new(env: &Env) -> Self { + Self { + layers: Vec::new(env), + } + } +} + +pub(crate) fn aggregate_result( + env: &Env, + result: Map>, + layer_aggregator: LayerAggregator, + decimals: I256, +) -> Map { + let mut aggregated_result = Map::new(env); + for (user, res) in result { + let res = match layer_aggregator { + LayerAggregator::Sum => res.iter().reduce(|acc, e| acc.add(&e)), + LayerAggregator::Product => res + .iter() + .reduce(|acc, e| acc.fixed_mul_floor(env, e, decimals.clone())), + } + .unwrap_or_else(|| I256::from_i128(env, 0)); + aggregated_result.set(user, res); + } + aggregated_result +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::vec; + + #[test] + fn creating_neuron() { + let env = Env::default(); + + let name = String::from_str(&env, "abc"); + let weight = I256::from_i32(&env, 100); + let neuron = Neuron::create(name.clone(), weight.clone()); + assert_eq!(neuron, Neuron { name, weight }); + } + + #[test] + fn creating_layer() { + let env = Env::default(); + + let layer = Layer::create( + vec![&env, String::from_str(&env, "neuron1")], + LayerAggregator::Sum, + ); + assert_eq!( + layer, + Layer { + neurons: vec![&env, String::from_str(&env, "neuron1")], + aggregator: LayerAggregator::Sum + } + ); + } + + #[test] + fn creating_ngq() { + let env = Env::default(); + + let ngq = NGQ::new(&env); + assert_eq!(ngq, NGQ { layers: vec![&env] }); + } + + #[test] + fn aggregate_empty() { + let env = Env::default(); + + let user1 = String::from_str(&env, "user1"); + + let mut result: Map> = Map::new(&env); + result.set(user1.clone(), vec![&env]); + + let aggregated = aggregate_result( + &env, + result, + LayerAggregator::Product, + I256::from_i128(&env, 1), + ); + assert_eq!(aggregated.get(user1).unwrap(), I256::from_i32(&env, 0)); + } + + #[test] + fn aggregate_sum() { + let env = Env::default(); + + let user1 = String::from_str(&env, "user1"); + let user2 = String::from_str(&env, "user2"); + + let mut result: Map> = Map::new(&env); + result.set( + user1.clone(), + vec![&env, I256::from_i128(&env, 1), I256::from_i128(&env, 2)], + ); + result.set( + user2.clone(), + vec![&env, I256::from_i128(&env, 3), I256::from_i128(&env, 4)], + ); + + let aggregated = + aggregate_result(&env, result, LayerAggregator::Sum, I256::from_i128(&env, 1)); + assert_eq!( + aggregated.get(user1.clone()).unwrap(), + I256::from_i128(&env, 3) + ); + assert_eq!( + aggregated.get(user2.clone()).unwrap(), + I256::from_i128(&env, 7) + ); + } + + #[test] + fn aggregate_product() { + let env = Env::default(); + + let user1 = String::from_str(&env, "user1"); + let user2 = String::from_str(&env, "user2"); + + let mut result: Map> = Map::new(&env); + result.set( + user1.clone(), + vec![&env, I256::from_i128(&env, 1), I256::from_i128(&env, 2)], + ); + result.set( + user2.clone(), + vec![&env, I256::from_i128(&env, 3), I256::from_i128(&env, 4)], + ); + + let aggregated = aggregate_result( + &env, + result, + LayerAggregator::Product, + I256::from_i128(&env, 1), + ); + assert_eq!( + aggregated.get(user1.clone()).unwrap(), + I256::from_i128(&env, 2) + ); + assert_eq!( + aggregated.get(user2.clone()).unwrap(), + I256::from_i128(&env, 12) + ); + } +} diff --git a/contracts/offchain/src/neural_governance/traits.rs b/contracts/offchain/src/neural_governance/traits.rs new file mode 100644 index 0000000..7c0c7ef --- /dev/null +++ b/contracts/offchain/src/neural_governance/traits.rs @@ -0,0 +1,79 @@ +use crate::neural_governance::{Layer, LayerAggregator, Neuron, NGQ}; +use crate::types::VotingSystemError; +use soroban_sdk::{Env, Map, String, Vec, I256}; + +pub trait Governance { + /// Add a new layer to the contract. + /// + /// # Arguments + /// + /// * `raw_neurons`: tuples of neuron names and their respective weights. + /// * `layer_aggregator`: a function used to aggregate the neuron results within the layer. + fn add_layer( + env: Env, + raw_neurons: Vec<(String, I256)>, + layer_aggregator: LayerAggregator, + ) -> Result<(), VotingSystemError>; + + /// Remove a layer from the contract + /// + /// # Arguments + /// + /// * `layer_id`: ID of the layer to remove + fn remove_layer(env: Env, layer_id: String) -> Result<(), VotingSystemError>; + + /// Update an existing layer + /// + /// # Arguments + /// + /// * `layer_id`: ID of the layer to update + /// * `raw_neurons`: tuples of neuron names and their respective weights. + /// * `layer_aggregator`: a function used to aggregate the neuron results within the layer. + fn update_layer( + env: Env, + layer_id: String, + raw_neurons: Vec<(String, I256)>, + layer_aggregator: LayerAggregator, + ) -> Result<(), VotingSystemError>; + + // TODO docs + fn get_layer(env: Env, layer_id: String) -> Result; + + // TODO docs + fn get_neuron( + env: Env, + layer_id: String, + neuron_id: String, + ) -> Result; + + /// Get a map of user public keys and their voting powers for a neuron for a specific round. + fn get_neuron_result_round( + env: &Env, + layer_id: String, + neuron_id: String, + round: u32, + ) -> Result, VotingSystemError>; + + /// Get a map of user public keys and their voting powers for a neuron for the active round. + fn get_neuron_result( + env: &Env, + layer_id: String, + neuron_id: String, + ) -> Result, VotingSystemError>; + + /// Set neuron result for the active round. + fn set_neuron_result(env: Env, layer_id: String, neuron_id: String, result: Map); + + /// Get a map of user public keys and their voting powers for a layer for the active round. + fn get_layer_result(env: Env, layer_id: String) + -> Result, VotingSystemError>; + + /// Calculate final voting powers for the active round and write them to contract storage. + fn calculate_voting_powers(env: Env) -> Result<(), VotingSystemError>; + + /// Get a map of user public keys and their voting powers for whole governance for the active round. + fn get_voting_powers(env: Env) -> Result, VotingSystemError>; + + /// Get a representation of the current NGQ setup. + fn get_neural_governance(env: &Env) -> Result; +} diff --git a/contracts/offchain/src/storage.rs b/contracts/offchain/src/storage.rs new file mode 100644 index 0000000..9d00537 --- /dev/null +++ b/contracts/offchain/src/storage.rs @@ -0,0 +1,141 @@ +use soroban_sdk::{Env, Map, String, Vec, I256}; + +use crate::neural_governance::{Layer, Neuron, NGQ}; +use crate::storage::key_data::{ + get_layer_key, get_neuron_key, get_neuron_result_key, get_submission_votes_key, + get_submissions_key, get_voting_powers_key, +}; +use crate::types::{Vote, VotingSystemError}; +use crate::{ContractResult, DataKey}; + +pub use crate::storage::key_data::{ + LayerKeyData, NeuronKeyData, NeuronResultKeyData, SubmissionVotesKeyData, SubmissionsKeyData, + VotingPowersKeyData, +}; + +mod key_data; + +pub(crate) fn read_layer(env: &Env, layer_id: &String) -> ContractResult { + let key = get_layer_key(layer_id); + env.storage() + .temporary() + .get(&key) + .ok_or(VotingSystemError::LayerMissing) +} + +pub(crate) fn write_layer(env: &Env, layer_id: &String, layer: &Layer) { + let key = get_layer_key(layer_id); + env.storage().temporary().set(&key, layer); +} + +pub(crate) fn remove_layer(env: &Env, layer_id: &String) { + let key = get_layer_key(layer_id); + env.storage().temporary().remove(&key); +} + +pub(crate) fn read_neuron( + env: &Env, + layer_id: &String, + neuron_id: &String, +) -> ContractResult { + let key = get_neuron_key(layer_id, neuron_id); + env.storage() + .temporary() + .get(&key) + .ok_or(VotingSystemError::NeuronMissing) +} + +pub(crate) fn write_neuron(env: &Env, layer_id: &String, neuron_id: &String, neuron: &Neuron) { + let key = get_neuron_key(layer_id, neuron_id); + env.storage().temporary().set(&key, neuron); +} + +pub(crate) fn remove_neuron(env: &Env, layer_id: &String, neuron_id: &String) { + let key = get_neuron_key(layer_id, neuron_id); + env.storage().temporary().remove(&key); +} + +pub(crate) fn read_neuron_result( + env: &Env, + layer_id: &String, + neuron_id: &String, + round: u32, +) -> ContractResult> { + let key = get_neuron_result_key(layer_id, neuron_id, round); + env.storage() + .temporary() + .get(&key) + .ok_or(VotingSystemError::NeuronResultNotSet) +} + +pub(crate) fn write_neuron_result( + env: &Env, + layer_id: &String, + neuron_id: &String, + round: u32, + result: &Map, +) { + let key = get_neuron_result_key(layer_id, neuron_id, round); + env.storage().temporary().set(&key, result); +} + +pub(crate) fn read_submission_votes( + env: &Env, + submission_id: &String, + round: u32, +) -> ContractResult> { + let key = get_submission_votes_key(submission_id, round); + env.storage() + .persistent() + .get(&key) + .ok_or(VotingSystemError::VotesForSubmissionNotSet) +} + +pub(crate) fn write_submission_votes( + env: &Env, + submission_id: &String, + round: u32, + votes: &Map, +) { + let key = get_submission_votes_key(submission_id, round); + env.storage().persistent().set(&key, votes); +} + +pub(crate) fn read_submissions(env: &Env, round: u32) -> Vec { + let key = get_submissions_key(round); + env.storage() + .persistent() + .get(&key) + .unwrap_or_else(|| Vec::new(env)) +} + +pub(crate) fn write_submissions(env: &Env, round: u32, submissions: &Vec) { + let key = get_submissions_key(round); + env.storage().persistent().set(&key, submissions); +} + +pub(crate) fn read_neural_governance(env: &Env) -> ContractResult { + env.storage() + .instance() + .get(&DataKey::NeuralGovernance) + .ok_or(VotingSystemError::NeuralGovernanceNotSet) +} + +pub(crate) fn write_neural_governance(env: &Env, neural_governance: NGQ) { + env.storage() + .instance() + .set(&DataKey::NeuralGovernance, &neural_governance); +} + +pub(crate) fn read_voting_powers(env: &Env, round: u32) -> ContractResult> { + let key = get_voting_powers_key(round); + env.storage() + .persistent() + .get(&key) + .ok_or(VotingSystemError::VotingPowersNotSet) +} + +pub(crate) fn write_voting_powers(env: &Env, round: u32, voting_powers: &Map) { + let key = get_voting_powers_key(round); + env.storage().persistent().set(&key, voting_powers); +} diff --git a/contracts/offchain/src/storage/key_data.rs b/contracts/offchain/src/storage/key_data.rs new file mode 100644 index 0000000..39d0732 --- /dev/null +++ b/contracts/offchain/src/storage/key_data.rs @@ -0,0 +1,136 @@ +use crate::DataKey; +use soroban_sdk::{contracttype, String}; + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct LayerKeyData { + layer_id: String, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct NeuronResultKeyData { + layer_id: String, + neuron_id: String, + round: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct NeuronKeyData { + layer_id: String, + neuron_id: String, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct SubmissionVotesKeyData { + submission_id: String, + round: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct SubmissionsKeyData { + round: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct VotingPowersKeyData { + round: u32, +} + +pub fn get_layer_key(layer_id: &String) -> DataKey { + let data = LayerKeyData { + layer_id: layer_id.clone(), + }; + DataKey::LayerKey(data) +} + +pub fn get_neuron_key(layer_id: &String, neuron_id: &String) -> DataKey { + let data = NeuronKeyData { + layer_id: layer_id.clone(), + neuron_id: neuron_id.clone(), + }; + DataKey::NeuronKey(data) +} + +pub fn get_neuron_result_key(layer_id: &String, neuron_id: &String, round: u32) -> DataKey { + let data = NeuronResultKeyData { + layer_id: layer_id.clone(), + neuron_id: neuron_id.clone(), + round, + }; + DataKey::NeuronResultKey(data) +} + +pub fn get_submission_votes_key(submission_id: &String, round: u32) -> DataKey { + let data = SubmissionVotesKeyData { + submission_id: submission_id.clone(), + round, + }; + DataKey::SubmissionVotes(data) +} + +pub fn get_submissions_key(round: u32) -> DataKey { + let data = SubmissionsKeyData { round }; + DataKey::Submissions(data) +} + +pub fn get_voting_powers_key(round: u32) -> DataKey { + let data = VotingPowersKeyData { round }; + DataKey::VotingPowers(data) +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::Env; + + #[test] + fn constructing_leyer_key() { + let env = Env::default(); + + let layer_id = String::from_str(&env, "1"); + + let key = get_layer_key(&layer_id); + assert_eq!(key, DataKey::LayerKey(LayerKeyData { layer_id })); + } + + #[test] + fn constructing_neuron_key() { + let env = Env::default(); + + let layer_id = String::from_str(&env, "1"); + let neuron_id = String::from_str(&env, "2"); + + let key = get_neuron_key(&layer_id, &neuron_id); + assert_eq!( + key, + DataKey::NeuronKey(NeuronKeyData { + layer_id, + neuron_id + }) + ); + } + + #[test] + fn constructing_neuron_result_key() { + let env = Env::default(); + + let layer_id = String::from_str(&env, "1"); + let neuron_id = String::from_str(&env, "2"); + let round = 25; + + let key = get_neuron_result_key(&layer_id, &neuron_id, round); + assert_eq!( + key, + DataKey::NeuronResultKey(NeuronResultKeyData { + layer_id, + neuron_id, + round + }) + ); + } +} diff --git a/contracts/offchain/src/types.rs b/contracts/offchain/src/types.rs new file mode 100644 index 0000000..5bb66d5 --- /dev/null +++ b/contracts/offchain/src/types.rs @@ -0,0 +1,32 @@ +use soroban_sdk::{contracterror, contracttype}; + +pub const ABSTAIN_VOTING_POWER: i32 = 0; + +#[contracttype] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Vote { + Yes, + No, + Abstain, +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub enum VotingSystemError { + UnknownError = 0, + NeuralGovernanceNotSet = 1, + DelegateesNotFound = 2, + UnexpectedValue = 3, + TooManyDelegatees = 4, + NotEnoughDelegatees = 5, + UnknownVote = 6, + NeuronResultNotSet = 7, + InvalidLayerAggregator = 8, + LayerMissing = 9, + NeuronMissing = 10, + NGQResultForVoterMissing = 11, + DelegationCalculationFailed = 12, + VotesForSubmissionNotSet = 13, + SubmissionDoesNotExist = 14, + VotingPowersNotSet = 15, +} diff --git a/contracts/offchain/tests/e2e/auth.rs b/contracts/offchain/tests/e2e/auth.rs new file mode 100644 index 0000000..694e5cf --- /dev/null +++ b/contracts/offchain/tests/e2e/auth.rs @@ -0,0 +1,112 @@ +use soroban_sdk::testutils::{ + Address as AddressTrait, AuthorizedFunction, AuthorizedInvocation, MockAuth, MockAuthInvoke, +}; +use soroban_sdk::xdr::{ScErrorCode, ScErrorType}; +use soroban_sdk::{vec, Address, Env, Error, IntoVal, String, Symbol}; + +use crate::e2e::common::contract_utils::deploy_contract_without_initialization; + +#[test] +fn uninitialized_contract_is_not_callable() { + let env = Env::default(); + env.mock_all_auths(); + let contract_client = deploy_contract_without_initialization(&env); + + let result = contract_client.try_set_current_round(&25); + assert_eq!( + result, + Err(Ok(Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InvalidAction + ))) + ); +} + +#[test] +fn auth() { + let env = Env::default(); + env.mock_all_auths(); + let contract_client = deploy_contract_without_initialization(&env); + + let admin = Address::generate(&env); + contract_client.initialize(&admin, &25); + + let submission_name = String::from_str(&env, "abc"); + contract_client.set_submissions(&vec![&env, submission_name.clone()]); + + dbg!(&env.auths()); + + assert_eq!( + env.auths(), + std::vec![( + admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + contract_client.address.clone(), + Symbol::new(&env, "set_submissions"), + (vec![&env, submission_name.clone()],).into_val(&env) + )), + sub_invocations: std::vec![], + } + ),] + ); +} + +#[test] +fn transfer_admin() { + let env = Env::default(); + let contract_client = deploy_contract_without_initialization(&env); + + let admin = Address::generate(&env); + contract_client.initialize(&admin, &25); + + // Transfer admin + let new_admin = Address::generate(&env); + env.mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_client.address, + fn_name: "transfer_admin", + args: (&new_admin,).into_val(&env), + sub_invokes: &[], + }, + }]); + contract_client.transfer_admin(&new_admin); + + // Verify old admin can no longer modify state + let submission_name = String::from_str(&env, "abc"); + env.mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_client.address, + fn_name: "add_submission", + args: (submission_name.clone(),).into_val(&env), + sub_invokes: &[], + }, + }]); + let result = contract_client.try_set_submissions(&vec![&env, submission_name.clone()]); + assert!(result.is_err()); + + // Verify new admin can modify state + env.mock_auths(&[MockAuth { + address: &new_admin, + invoke: &MockAuthInvoke { + contract: &contract_client.address, + fn_name: "set_submissions", + args: (vec![&env, submission_name.clone()],).into_val(&env), + sub_invokes: &[], + }, + }]); + contract_client.set_submissions(&vec![&env, submission_name]); +} + +#[test] +#[should_panic(expected = "Error(WasmVm, InvalidAction)")] +fn set_admin_again_panics() { + let env = Env::default(); + let contract_client = deploy_contract_without_initialization(&env); + + let admin = Address::generate(&env); + contract_client.initialize(&admin, &25); + contract_client.initialize(&admin, &25); +} diff --git a/contracts/offchain/tests/e2e/common/contract_utils.rs b/contracts/offchain/tests/e2e/common/contract_utils.rs new file mode 100644 index 0000000..c26a333 --- /dev/null +++ b/contracts/offchain/tests/e2e/common/contract_utils.rs @@ -0,0 +1,20 @@ +use offchain::{VotingSystemOffchain, VotingSystemOffchainClient}; +use soroban_sdk::testutils::Address as AddressTrait; +use soroban_sdk::{Address, Env}; + +pub fn deploy_contract_without_initialization(env: &Env) -> VotingSystemOffchainClient { + let contract_id = env.register_contract(None, VotingSystemOffchain); + let contract_client = VotingSystemOffchainClient::new(env, &contract_id); + + contract_client +} + +pub fn deploy_contract(env: &Env) -> VotingSystemOffchainClient { + let contract_client = deploy_contract_without_initialization(env); + + env.mock_all_auths(); + let admin = Address::generate(env); + contract_client.initialize(&admin, &25); + + contract_client +} diff --git a/contracts/offchain/tests/e2e/common/mod.rs b/contracts/offchain/tests/e2e/common/mod.rs new file mode 100644 index 0000000..f082306 --- /dev/null +++ b/contracts/offchain/tests/e2e/common/mod.rs @@ -0,0 +1 @@ +pub mod contract_utils; diff --git a/contracts/offchain/tests/e2e/governance.rs b/contracts/offchain/tests/e2e/governance.rs new file mode 100644 index 0000000..d670125 --- /dev/null +++ b/contracts/offchain/tests/e2e/governance.rs @@ -0,0 +1,156 @@ +use soroban_sdk::{vec, Env, String, Vec, I256}; + +use offchain::types::VotingSystemError; +use offchain::LayerAggregator; + +use crate::e2e::common::contract_utils::deploy_contract; + +#[test] +fn add_layer() { + let env = Env::default(); + let contract_client = deploy_contract(&env); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, Vec::new(&env)); + + let neurons = vec![ + &env, + (String::from_str(&env, "aaa"), I256::from_i32(&env, 100)), + (String::from_str(&env, "b"), I256::from_i32(&env, 2000)), + ]; + contract_client.add_layer(&neurons, &LayerAggregator::Sum); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, vec![&env, String::from_str(&env, "0")]); + + let layer = contract_client.get_layer(&String::from_str(&env, "0")); + assert_eq!(layer.aggregator, LayerAggregator::Sum); + assert_eq!( + layer.neurons, + vec![ + &env, + String::from_str(&env, "0"), + String::from_str(&env, "1") + ] + ); + + let neuron_0 = + contract_client.get_neuron(&String::from_str(&env, "0"), &String::from_str(&env, "0")); + assert_eq!(neuron_0.name, String::from_str(&env, "aaa")); + assert_eq!(neuron_0.weight, I256::from_i32(&env, 100)); + + let neuron_1 = + contract_client.get_neuron(&String::from_str(&env, "0"), &String::from_str(&env, "1")); + assert_eq!(neuron_1.name, String::from_str(&env, "b")); + assert_eq!(neuron_1.weight, I256::from_i32(&env, 2000)); +} + +#[test] +fn remove_layer() { + let env = Env::default(); + let contract_client = deploy_contract(&env); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, Vec::new(&env)); + + let neurons = vec![ + &env, + (String::from_str(&env, "aaa"), I256::from_i32(&env, 100)), + (String::from_str(&env, "b"), I256::from_i32(&env, 2000)), + ]; + contract_client.add_layer(&neurons, &LayerAggregator::Sum); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, vec![&env, String::from_str(&env, "0")]); + + contract_client.remove_layer(&String::from_str(&env, "0")); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, vec![&env]); + + assert_eq!( + contract_client.try_get_layer(&String::from_str(&env, "0")), + Err(Ok(VotingSystemError::LayerMissing)) + ); + assert_eq!( + contract_client.try_get_neuron(&String::from_str(&env, "0"), &String::from_str(&env, "0")), + Err(Ok(VotingSystemError::NeuronMissing)) + ); + assert_eq!( + contract_client.try_get_neuron(&String::from_str(&env, "0"), &String::from_str(&env, "1")), + Err(Ok(VotingSystemError::NeuronMissing)) + ); +} + +#[test] +fn update_layer() { + let env = Env::default(); + let contract_client = deploy_contract(&env); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, Vec::new(&env)); + + let neurons = vec![ + &env, + (String::from_str(&env, "aaa"), I256::from_i32(&env, 100)), + (String::from_str(&env, "b"), I256::from_i32(&env, 2000)), + ]; + contract_client.add_layer(&neurons, &LayerAggregator::Sum); + + let neurons = vec![ + &env, + (String::from_str(&env, "cc"), I256::from_i32(&env, 3)), + ]; + contract_client.update_layer( + &String::from_str(&env, "0"), + &neurons, + &LayerAggregator::Product, + ); + + let layer = contract_client.get_layer(&String::from_str(&env, "0")); + assert_eq!(layer.neurons, vec![&env, String::from_str(&env, "0")]); + assert_eq!(layer.aggregator, LayerAggregator::Product); + + let neuron = + contract_client.get_neuron(&String::from_str(&env, "0"), &String::from_str(&env, "0")); + assert_eq!(neuron.name, String::from_str(&env, "cc")); + assert_eq!(neuron.weight, I256::from_i32(&env, 3)); + + assert_eq!( + contract_client.try_get_neuron(&String::from_str(&env, "0"), &String::from_str(&env, "1")), + Err(Ok(VotingSystemError::NeuronMissing)) + ); +} + +#[test] +fn add_layer_after_removing() { + let env = Env::default(); + let contract_client = deploy_contract(&env); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, Vec::new(&env)); + + let neurons = vec![ + &env, + (String::from_str(&env, "aaa"), I256::from_i32(&env, 100)), + (String::from_str(&env, "b"), I256::from_i32(&env, 2000)), + ]; + contract_client.add_layer(&neurons, &LayerAggregator::Sum); + + contract_client.remove_layer(&String::from_str(&env, "0")); + + let neurons = vec![&env, (String::from_str(&env, "c"), I256::from_i32(&env, 1))]; + contract_client.add_layer(&neurons, &LayerAggregator::Product); + + let governance = contract_client.get_neural_governance(); + assert_eq!(governance.layers, vec![&env, String::from_str(&env, "1")]); + + let layer = contract_client.get_layer(&String::from_str(&env, "1")); + assert_eq!(layer.aggregator, LayerAggregator::Product); + assert_eq!(layer.neurons, vec![&env, String::from_str(&env, "0"),]); + + let neuron_0 = + contract_client.get_neuron(&String::from_str(&env, "1"), &String::from_str(&env, "0")); + assert_eq!(neuron_0.name, String::from_str(&env, "c")); + assert_eq!(neuron_0.weight, I256::from_i32(&env, 1)); +} diff --git a/contracts/offchain/tests/e2e/mod.rs b/contracts/offchain/tests/e2e/mod.rs new file mode 100644 index 0000000..5d4ce65 --- /dev/null +++ b/contracts/offchain/tests/e2e/mod.rs @@ -0,0 +1,10 @@ +#[cfg(feature = "testutils")] +mod auth; +#[cfg(feature = "testutils")] +pub(crate) mod common; +#[cfg(feature = "testutils")] +mod governance; +#[cfg(feature = "testutils")] +mod upgrade; +#[cfg(feature = "testutils")] +mod voting; diff --git a/contracts/offchain/tests/e2e/upgrade.rs b/contracts/offchain/tests/e2e/upgrade.rs new file mode 100644 index 0000000..22bff53 --- /dev/null +++ b/contracts/offchain/tests/e2e/upgrade.rs @@ -0,0 +1,48 @@ +use crate::e2e::common::contract_utils::deploy_contract; +use soroban_sdk::{Env, Map, String, I256}; + +mod mock_contract { + soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/mocks.wasm"); +} + +#[test] +fn upgrade_contract() { + let env = Env::default(); + let hash = env.deployer().upload_contract_wasm(mock_contract::WASM); + + let contract_client = deploy_contract(&env); + let address = contract_client.address.clone(); + + contract_client.upgrade(&hash); + + let new_contract_client = mock_contract::Client::new(&env, &address); + assert!(new_contract_client.is_mock()); +} + +#[test] +fn storage_is_retained_after_upgrade() { + let env = Env::default(); + let hash = env.deployer().upload_contract_wasm(mock_contract::WASM); + + let contract_client = deploy_contract(&env); + let address = contract_client.address.clone(); + + // Store data using old impl + let mut result = Map::new(&env); + result.set(String::from_str(&env, "user1"), I256::from_i32(&env, 100)); + result.set(String::from_str(&env, "user2"), I256::from_i32(&env, 200)); + + contract_client.set_neuron_result( + &String::from_str(&env, "0"), + &String::from_str(&env, "0"), + &result, + ); + + // Upgrade the contract + contract_client.upgrade(&hash); + let new_contract_client = mock_contract::Client::new(&env, &address); + + // Check if data is persisted + let new_result = new_contract_client.get_stored_neuron_result(); + assert_eq!(result, new_result); +} diff --git a/contracts/offchain/tests/e2e/voting.rs b/contracts/offchain/tests/e2e/voting.rs new file mode 100644 index 0000000..11c828f --- /dev/null +++ b/contracts/offchain/tests/e2e/voting.rs @@ -0,0 +1,259 @@ +use soroban_sdk::{vec, Env, Map, String, Vec, I256}; + +use offchain::types::{Vote, VotingSystemError}; +use offchain::{LayerAggregator, DECIMALS}; + +use crate::e2e::common::contract_utils::deploy_contract; + +#[allow(clippy::identity_op)] +#[test] +fn voting_data_upload() { + let env = Env::default(); + let contract_client = deploy_contract(&env); + env.budget().reset_unlimited(); + + let mut raw_neurons: Vec<(String, I256)> = Vec::new(&env); + raw_neurons.push_back(( + String::from_str(&env, "Dummy"), + I256::from_i128(&env, 2 * DECIMALS), + )); + raw_neurons.push_back(( + String::from_str(&env, "TrustGraph"), + I256::from_i128(&env, 1 * DECIMALS), + )); + contract_client.add_layer(&raw_neurons, &LayerAggregator::Sum); + + let user1 = String::from_str(&env, "user1"); + let user2 = String::from_str(&env, "user2"); + let user3 = String::from_str(&env, "user3"); + let submission1 = String::from_str(&env, "submission1"); + let submission2 = String::from_str(&env, "submission2"); + + contract_client.set_submissions(&vec![&env, submission1.clone(), submission2.clone()]); + + let mut votes_submission1 = Map::new(&env); + votes_submission1.set(user1.clone(), Vote::Yes); + votes_submission1.set(user2.clone(), Vote::Yes); + votes_submission1.set(user3.clone(), Vote::Yes); + + // TODO use different votes here + let mut votes_submission2 = Map::new(&env); + votes_submission2.set(user1.clone(), Vote::Yes); + votes_submission2.set(user2.clone(), Vote::No); + votes_submission2.set(user3.clone(), Vote::Abstain); + + contract_client.set_votes_for_submission(&submission1, &votes_submission1); + contract_client.set_votes_for_submission(&submission2, &votes_submission2); + + contract_client.set_submissions(&vec![&env, submission1.clone(), submission2.clone()]); + + let mut neuron_result = Map::new(&env); + neuron_result.set(user1.clone(), I256::from_i128(&env, 100 * DECIMALS)); + neuron_result.set(user2.clone(), I256::from_i128(&env, 200 * DECIMALS)); + neuron_result.set(user3.clone(), I256::from_i128(&env, 300 * DECIMALS)); + + let mut neuron_result2 = Map::new(&env); + neuron_result2.set(user1.clone(), I256::from_i128(&env, 1000 * DECIMALS)); + neuron_result2.set(user2.clone(), I256::from_i128(&env, 2000 * DECIMALS)); + neuron_result2.set(user3.clone(), I256::from_i128(&env, 3000 * DECIMALS)); + + contract_client.set_neuron_result( + &String::from_str(&env, "0"), + &String::from_str(&env, "0"), + &neuron_result, + ); + contract_client.set_neuron_result( + &String::from_str(&env, "0"), + &String::from_str(&env, "1"), + &neuron_result2, + ); + + env.budget().reset_default(); + contract_client.calculate_voting_powers(); + let result = contract_client.tally_submission(&submission1); + println!("{}", env.budget()); + + assert_eq!( + result, + I256::from_i128( + &env, + (100 * 2 + 200 * 2 + 300 * 2 + 1000 + 2000 + 3000) * DECIMALS + ) + ); + + env.budget().reset_default(); + let result2 = contract_client.tally_submission(&submission2); + println!("{}", env.budget()); + + assert_eq!( + result2, + I256::from_i128(&env, (100 * 2 - 200 * 2 + 1000 - 2000 + 0) * DECIMALS) + ); +} + +#[test] +fn setting_votes_for_unknown_submission() { + let env = Env::default(); + env.budget().reset_unlimited(); + + let contract_client = deploy_contract(&env); + assert_eq!( + contract_client + .try_set_votes_for_submission(&String::from_str(&env, "sub1"), &Map::new(&env)) + .unwrap_err() + .unwrap(), + VotingSystemError::SubmissionDoesNotExist + ); +} + +#[test] +fn adding_duplicate_submissions() { + let env = Env::default(); + env.budget().reset_unlimited(); + + let contract_client = deploy_contract(&env); + + contract_client.set_submissions(&vec![ + &env, + String::from_str(&env, "a"), + String::from_str(&env, "a"), + ]); + + let submissions = contract_client.get_submissions(); + let mut expected = Vec::new(&env); + expected.push_back(String::from_str(&env, "a")); + + assert_eq!(submissions, expected); +} + +#[test] +fn setting_round() { + let env = Env::default(); + env.budget().reset_unlimited(); + + let contract_client = deploy_contract(&env); + + contract_client.set_current_round(&20); + assert_eq!(contract_client.get_current_round(), 20); + + contract_client.set_current_round(&30); + assert_eq!(contract_client.get_current_round(), 30); +} + +#[test] +fn set_bump_round_flow() { + let env = Env::default(); + env.budget().reset_unlimited(); + + let contract_client = deploy_contract(&env); + contract_client.set_current_round(&25); + + let submission = String::from_str(&env, "sub1"); + let user1 = String::from_str(&env, "user1"); + let user2 = String::from_str(&env, "user2"); + let neuron0 = String::from_str(&env, "0"); + let layer0 = String::from_str(&env, "0"); + + // Setup contract + contract_client.add_layer( + &soroban_sdk::vec![ + &env, + ( + neuron0.clone(), + I256::from_i128(&env, 1_000_000_000_000_000_000) + ) + ], + &LayerAggregator::Sum, + ); + + // Set votes and results for round 25 + contract_client.set_submissions(&vec![&env, submission.clone()]); + + let mut votes25 = Map::new(&env); + votes25.set(user1.clone(), Vote::Yes); + votes25.set(user2.clone(), Vote::No); + contract_client.set_votes_for_submission(&submission, &votes25); + let expected25 = votes25.clone(); + + let mut result25 = Map::new(&env); + result25.set(user1.clone(), I256::from_i128(&env, 100)); + result25.set(user2.clone(), I256::from_i128(&env, 200)); + contract_client.set_neuron_result(&layer0, &neuron0, &result25); + + // Verify results are set + assert_eq!( + contract_client.get_votes_for_submission(&submission), + expected25 + ); + assert_eq!( + contract_client.get_neuron_result(&layer0, &neuron0), + result25 + ); + + // Verify submission is active + assert!(contract_client + .get_submissions() + .contains(submission.clone())); + + // Bump the round + contract_client.set_current_round(&26); + + // Verify results are unset for previous round submission + assert_eq!( + contract_client + .try_get_votes_for_submission(&submission) + .unwrap_err() + .unwrap(), + VotingSystemError::VotesForSubmissionNotSet + ); + assert_eq!( + contract_client + .try_get_neuron_result(&layer0, &neuron0) + .unwrap_err() + .unwrap(), + VotingSystemError::NeuronResultNotSet + ); + + // Set votes and results for round 26 + let new_submission = String::from_str(&env, "sub2"); + contract_client.set_submissions(&vec![&env, new_submission.clone()]); + + let mut votes26 = Map::new(&env); + votes26.set(user1.clone(), Vote::No); + votes26.set(user2.clone(), Vote::Yes); + contract_client.set_votes_for_submission(&new_submission, &votes26); + let expected26 = votes26.clone(); + + let mut result26 = Map::new(&env); + result26.set(user1.clone(), I256::from_i128(&env, 5000)); + result26.set(user2.clone(), I256::from_i128(&env, 6000)); + contract_client.set_neuron_result(&layer0, &neuron0, &result26); + + // Verify results are set + assert_eq!( + contract_client.get_votes_for_submission(&new_submission), + expected26 + ); + assert_eq!( + contract_client.get_neuron_result(&layer0, &neuron0), + result26 + ); + + // Verify new submission is active and old is not + assert!(contract_client + .get_submissions() + .contains(new_submission.clone())); + assert!(!contract_client + .get_submissions() + .contains(submission.clone())); + + // Verify historical results are still accessible + assert_eq!( + contract_client.get_votes_for_submission_round(&submission, &25), + expected25 + ); + assert_eq!( + contract_client.get_neuron_result_round(&layer0, &neuron0, &25), + result25 + ); +} diff --git a/contracts/offchain/tests/main.rs b/contracts/offchain/tests/main.rs new file mode 100644 index 0000000..b6ae44f --- /dev/null +++ b/contracts/offchain/tests/main.rs @@ -0,0 +1 @@ +mod e2e; diff --git a/neurons/.cargo/config.toml b/neurons/.cargo/config.toml new file mode 100644 index 0000000..c0a269b --- /dev/null +++ b/neurons/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +lint = "clippy --all-targets --all-features -- -W clippy::pedantic -A clippy::missing_errors_doc -A clippy::module_name_repetitions" diff --git a/neurons/.gitignore b/neurons/.gitignore new file mode 100644 index 0000000..e190553 --- /dev/null +++ b/neurons/.gitignore @@ -0,0 +1,3 @@ +data +target +result diff --git a/neurons/Cargo.lock b/neurons/Cargo.lock new file mode 100644 index 0000000..aec43f5 --- /dev/null +++ b/neurons/Cargo.lock @@ -0,0 +1,115 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "neurons" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/neurons/Cargo.toml b/neurons/Cargo.toml new file mode 100644 index 0000000..b572515 --- /dev/null +++ b/neurons/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "neurons" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.80" +camino = "1.1.6" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" +serde_repr = "0.1.18" diff --git a/neurons/README.md b/neurons/README.md new file mode 100644 index 0000000..7fedf8b --- /dev/null +++ b/neurons/README.md @@ -0,0 +1,37 @@ +# Neurons + +This code is an implementation of `neurons` +from the [Neural Quorum Governance](https://stellarcommunityfund.gitbook.io/module-library). +The data computed by this package is uploaded to the voting contract. + +This is the source code used to calculate voting powers for each neuron used in NQG mechanism. +It also normalized the votes of voters (converts delegations to final votes) for them to be uploaded to the contract. + +## Inputs + +Neurons expect the inputs to be provided in `json` format. The inputs are loaded from `data/` directory. + +## Outputs + +Computed voting powers and normalized votes are written to `result/` directory. + +## Running + +```shell +cargo run +``` + +## Neurons + +### Assigned Reputation Neuron + +Assigns voting power based on voter discord rank. + +### Prior Voting History Neuron + +Assigns voting power based on rounds voter previously participated in. + +### Trust Graph Neuron + +Assigns voting power based on trust assigned to voter by other voters. +It uses min-max normalized PageRank algorithm to compute the score. diff --git a/neurons/src/lib.rs b/neurons/src/lib.rs new file mode 100644 index 0000000..738003b --- /dev/null +++ b/neurons/src/lib.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +pub mod neurons; +pub mod quorum; + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum Vote { + Yes, + No, + Delegate, + Abstain, +} diff --git a/neurons/src/main.rs b/neurons/src/main.rs new file mode 100644 index 0000000..7e35d04 --- /dev/null +++ b/neurons/src/main.rs @@ -0,0 +1,79 @@ +use camino::Utf8Path; +use neurons::neurons::assigned_reputation::AssignedReputationNeuron; +use neurons::neurons::prior_voting_history::PriorVotingHistoryNeuron; +use neurons::neurons::trust_graph::TrustGraphNeuron; +use neurons::neurons::Neuron; +use neurons::quorum::normalize_votes; +use neurons::Vote; +use serde::Serialize; +use std::collections::{BTreeMap, HashMap}; +use std::fs; + +pub const DECIMALS: i64 = 1_000_000_000_000_000_000; + +fn write_result(file_name: &str, data: &T) +where + T: Serialize, +{ + let serialized = serde_json::to_string(&data).unwrap(); + fs::write(file_name, serialized).unwrap(); +} + +fn to_sorted_map(data: HashMap) -> BTreeMap +where + K: Ord, +{ + data.into_iter().collect() +} + +#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] +fn to_fixed_point_decimal(val: f64) -> i128 { + (val * DECIMALS as f64) as i128 +} + +fn calculate_neuron_results(users: &[String], neurons: Vec>) { + for neuron in neurons { + let result = neuron.calculate_result(users); + let result: HashMap = result + .into_iter() + .map(|(key, value)| (key, to_fixed_point_decimal(value).to_string())) + .collect(); + let result = to_sorted_map(result); + write_result(&format!("result/{}.json", neuron.name()), &result); + } +} + +fn main() { + let path = Utf8Path::new("data/previous_rounds_for_users.json"); + let prior_voting_history_neuron = PriorVotingHistoryNeuron::try_from_file(path).unwrap(); + + let path = Utf8Path::new("data/users_reputation.json"); + let assigned_reputation_neuron = AssignedReputationNeuron::try_from_file(path).unwrap(); + + let path = Utf8Path::new("data/trusted_for_user.json"); + let trust_graph_neuron = TrustGraphNeuron::try_from_file(path).unwrap(); + + let users_raw = fs::read_to_string("data/voters.json").unwrap(); + let users: Vec = serde_json::from_str(users_raw.as_str()).unwrap(); + + let votes_raw = fs::read_to_string("data/votes.json").unwrap(); + let votes: HashMap> = + serde_json::from_str(votes_raw.as_str()).unwrap(); + let delegatees_for_user_raw = fs::read_to_string("data/delegatees_for_user.json").unwrap(); + let delegatees_for_user: HashMap> = + serde_json::from_str(delegatees_for_user_raw.as_str()).unwrap(); + let normalized_votes = normalize_votes(votes, &delegatees_for_user).unwrap(); + write_result( + "result/normalized_votes.json", + &to_sorted_map(normalized_votes), + ); + + calculate_neuron_results( + &users, + vec![ + Box::new(prior_voting_history_neuron), + Box::new(assigned_reputation_neuron), + Box::new(trust_graph_neuron), + ], + ); +} diff --git a/neurons/src/neurons.rs b/neurons/src/neurons.rs new file mode 100644 index 0000000..8b1c9e8 --- /dev/null +++ b/neurons/src/neurons.rs @@ -0,0 +1,11 @@ +use std::collections::HashMap; + +pub mod assigned_reputation; +pub mod prior_voting_history; +pub mod trust_graph; + +pub trait Neuron { + fn name(&self) -> String; + + fn calculate_result(&self, users: &[String]) -> HashMap; +} diff --git a/neurons/src/neurons/assigned_reputation.rs b/neurons/src/neurons/assigned_reputation.rs new file mode 100644 index 0000000..d8b78e2 --- /dev/null +++ b/neurons/src/neurons/assigned_reputation.rs @@ -0,0 +1,58 @@ +use crate::neurons::Neuron; +use anyhow::Result; +use camino::Utf8Path; +use serde_repr::Deserialize_repr; +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; + +#[derive(Deserialize_repr, Clone, Debug)] +#[repr(i32)] +pub enum ReputationTier { + Unknown = -1, + Verified = 0, + Pathfinder = 1, + Navigator = 2, + Pilot = 3, +} + +#[derive(Clone, Debug)] +pub struct AssignedReputationNeuron { + users_reputation: HashMap, +} + +impl AssignedReputationNeuron { + pub fn try_from_file(path: &Utf8Path) -> Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + + let users_reputation = serde_json::from_reader(reader)?; + Ok(Self { users_reputation }) + } +} + +fn reputation_bonus(reputation_tier: &ReputationTier) -> f64 { + match reputation_tier { + ReputationTier::Unknown | ReputationTier::Verified => 0.0, + ReputationTier::Pathfinder => 0.1, + ReputationTier::Navigator => 0.2, + ReputationTier::Pilot => 0.3, + } +} + +impl Neuron for AssignedReputationNeuron { + fn name(&self) -> String { + "assigned_reputation_neuron".to_string() + } + + fn calculate_result(&self, users: &[String]) -> HashMap { + let mut result = HashMap::new(); + + for user in users { + let bonus = reputation_bonus(self.users_reputation.get(user).unwrap()); + result.insert(user.into(), bonus); + } + + result + } +} diff --git a/neurons/src/neurons/prior_voting_history.rs b/neurons/src/neurons/prior_voting_history.rs new file mode 100644 index 0000000..d494020 --- /dev/null +++ b/neurons/src/neurons/prior_voting_history.rs @@ -0,0 +1,64 @@ +use crate::neurons::Neuron; +use anyhow::Result; +use camino::Utf8Path; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[derive(Clone, Debug)] +pub struct PriorVotingHistoryNeuron { + users_round_history: HashMap>, +} + +impl PriorVotingHistoryNeuron { + // FIXME this design is not scalable, what if there are multiple files required + pub fn try_from_file(path: &Utf8Path) -> Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + + let users_round_history = serde_json::from_reader(reader)?; + Ok(Self { + users_round_history, + }) + } +} + +fn round_bonus(round: u32) -> f64 { + match round { + 21.. => 0.1, + _ => 0.0, + } +} + +fn calculate_bonus(rounds_participated: &[u32]) -> f64 { + rounds_participated + .iter() + .map(|round| round_bonus(*round)) + .sum() +} + +impl Neuron for PriorVotingHistoryNeuron { + fn name(&self) -> String { + "prior_voting_history_neuron".to_string() + } + + fn calculate_result(&self, users: &[String]) -> HashMap { + let mut result = HashMap::new(); + + for user in users { + let bonus = calculate_bonus( + &self + .users_round_history + .get(user) + .cloned() + .unwrap_or_else(Vec::new), + ); + result.insert(user.into(), bonus); + } + + result + } +} diff --git a/neurons/src/neurons/trust_graph.rs b/neurons/src/neurons/trust_graph.rs new file mode 100644 index 0000000..54b3a41 --- /dev/null +++ b/neurons/src/neurons/trust_graph.rs @@ -0,0 +1,135 @@ +use crate::neurons::Neuron; +use camino::Utf8Path; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::BufReader; + +#[derive(Clone, Debug)] +pub struct TrustGraphNeuron { + trusted_for_user: HashMap>, +} + +impl TrustGraphNeuron { + pub fn try_from_file(path: &Utf8Path) -> anyhow::Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + + let trusted_for_user = serde_json::from_reader(reader)?; + Ok(Self { trusted_for_user }) + } +} + +#[allow(clippy::cast_precision_loss)] +fn calculate_page_rank( + nodes: &Vec, + edges: &Vec<(String, Vec)>, + iterations: u32, + damping_factor: f64, +) -> HashMap { + let mut page_ranks: HashMap = HashMap::new(); + for node in nodes { + page_ranks.insert(node.clone(), 1.0 / nodes.len() as f64); + } + for _ in 0..iterations { + let mut new_ranks: HashMap = HashMap::new(); + for node in nodes { + let mut rank = (1.0 - damping_factor) / nodes.len() as f64; + for (other_node, other_node_edges) in edges { + if other_node_edges.contains(node) { + let pr = page_ranks.get(other_node).unwrap_or(&0.0); + rank += (damping_factor * pr) / other_node_edges.len() as f64; + } + } + new_ranks.insert(node.clone(), rank); + } + page_ranks = new_ranks; + } + + page_ranks +} + +fn min_max_normalize_result(result: HashMap) -> HashMap { + let min = result.values().copied().reduce(f64::min).unwrap(); + let max = result.values().copied().reduce(f64::max).unwrap(); + + result + .into_iter() + .map(|(key, value)| { + let new_value = (value - min) / (max - min); + (key, new_value) + }) + .collect() +} + +impl Neuron for TrustGraphNeuron { + fn name(&self) -> String { + "trust_graph_neuron".to_string() + } + + fn calculate_result(&self, users: &[String]) -> HashMap { + let mut result = HashMap::new(); + + let mut nodes = HashSet::new(); + let mut edges = Vec::new(); + + for (user, edge) in &self.trusted_for_user { + nodes.insert(user.clone()); + for other_user in edge { + nodes.insert(other_user.clone()); + } + edges.push((user.clone(), edge.clone())); + } + let nodes: Vec = nodes.into_iter().collect(); + + let page_rank_result = calculate_page_rank(&nodes, &edges, 1000, 0.85); + let page_rank_result = min_max_normalize_result(page_rank_result); + + for user in users { + let page_rank = *page_rank_result.get(user).unwrap_or(&0.0); + result.insert(user.into(), page_rank); + } + + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_f64_near { + ( $a:expr, $b:expr ) => { + let eps = 0.001f64; + assert!( + ($a - $b).abs() < eps, + "Values a = {}, b = {} are not near", + $a, + $b + ); + }; + } + + #[test] + fn simple() { + let mut trusted_for_user = HashMap::new(); + trusted_for_user.insert("A".to_string(), vec!["B".to_string(), "C".to_string()]); + trusted_for_user.insert("B".to_string(), vec!["A".to_string()]); + trusted_for_user.insert("C".to_string(), vec!["A".to_string(), "B".to_string()]); + trusted_for_user.insert("D".to_string(), vec!["A".to_string()]); + trusted_for_user.insert("E".to_string(), vec![]); + + let trust_graph_neuron = TrustGraphNeuron { trusted_for_user }; + let result = trust_graph_neuron.calculate_result( + &["A", "B", "C", "D", "E"] + .into_iter() + .map(std::string::ToString::to_string) + .collect::>(), + ); + + assert_f64_near!(result.get("A").unwrap(), &1.0); + assert_f64_near!(result.get("B").unwrap(), &0.704); + assert_f64_near!(result.get("C").unwrap(), &0.465); + assert_f64_near!(result.get("D").unwrap(), &0.0); + assert_f64_near!(result.get("E").unwrap(), &0.0); + } +} diff --git a/neurons/src/quorum.rs b/neurons/src/quorum.rs new file mode 100644 index 0000000..0b87a46 --- /dev/null +++ b/neurons/src/quorum.rs @@ -0,0 +1,105 @@ +use crate::Vote; +use anyhow::{anyhow, bail, Result}; +use std::collections::HashMap; + +const QUORUM_SIZE: u32 = 5; +const QUORUM_ABSOLUTE_PARTICIPATION_THRESHOLD: f64 = 1.0 / 2.0; +const QUORUM_RELATIVE_PARTICIPATION_THRESHOLD: f64 = 2.0 / 3.0; + +#[allow(clippy::implicit_hasher)] +pub fn normalize_votes( + votes: HashMap>, + delegatees_for_user: &HashMap>, +) -> Result>> { + votes + .into_iter() + .map(|(submission, submission_votes)| { + let submission_votes = + normalize_votes_for_submission(&submission_votes, delegatees_for_user)?; + Ok((submission, submission_votes)) + }) + .collect::>() +} + +fn normalize_votes_for_submission( + submission_votes: &HashMap, + delegatees_for_user: &HashMap>, +) -> Result> { + submission_votes + .clone() + .into_iter() + .map(|(user, vote)| { + if vote == Vote::Delegate { + let delegatees = delegatees_for_user + .get(&user) + .ok_or_else(|| anyhow!("Delegatees missing for user {user}"))?; + let normalized_vote = calculate_quorum_consensus(delegatees, submission_votes)?; + Ok((user, normalized_vote)) + } else { + Ok((user, vote)) + } + }) + .collect::>() +} + +fn calculate_quorum_consensus( + delegatees: &[String], + submission_votes: &HashMap, +) -> Result { + let valid_delegates: Vec<&String> = delegatees + .iter() + .filter(|delegatee| { + let delegatee_vote = submission_votes.get(*delegatee).unwrap_or(&Vote::Abstain); + matches!(delegatee_vote, Vote::Yes | Vote::No) + }) + .collect(); + + let selected_delegatees = if valid_delegates.len() < QUORUM_SIZE as usize { + valid_delegates.as_slice() + } else { + &valid_delegates[..QUORUM_SIZE as usize] + }; + + let mut quorum_size = 0; + let mut agreement: i32 = 0; + for &delegatee in selected_delegatees { + let delegatee_vote = submission_votes.get(delegatee).unwrap_or(&Vote::Abstain); + + if delegatee_vote == &Vote::Delegate { + continue; + } + + quorum_size += 1; + match delegatee_vote { + Vote::Yes => agreement += 1, + Vote::No => agreement -= 1, + Vote::Abstain => {} + Vote::Delegate => { + bail!("Invalid delegatee operation"); + } + }; + } + + let absolute_agreement: f64 = f64::from(agreement) / f64::from(QUORUM_SIZE); + let relative_agreement: f64 = if quorum_size > 0 { + f64::from(agreement) / f64::from(quorum_size) + } else { + 0.0 + }; + + Ok( + if absolute_agreement.abs() > QUORUM_ABSOLUTE_PARTICIPATION_THRESHOLD { + if relative_agreement.abs() > QUORUM_RELATIVE_PARTICIPATION_THRESHOLD { + if relative_agreement > 0.0 { + Vote::Yes + } else { + Vote::No + } + } else { + Vote::Abstain + } + } else { + Vote::Abstain + }, + ) +}