diff --git a/Cargo.lock b/Cargo.lock index 88ecd73..119e91c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "approx" @@ -85,7 +85,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -225,9 +225,9 @@ checksum = "5d5dde061bd34119e902bbb2d9b90c5692635cf59fb91d582c2b68043f1b8293" [[package]] name = "arrayref" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" @@ -237,20 +237,20 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" @@ -365,9 +365,9 @@ dependencies = [ [[package]] name = "bounded-collections" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32385ecb91a31bddaf908e8dcf4a15aef1bcd3913cc03ebfad02ff6d568abc1" +checksum = "3d077619e9c237a5d1875166f5e8033e8f6bff0c96f8caf81e1c2d7738c431bf" dependencies = [ "log", "parity-scale-codec", @@ -392,9 +392,9 @@ checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" @@ -404,15 +404,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.1.18" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "shlex", ] @@ -478,9 +478,9 @@ checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -548,7 +548,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -580,18 +580,27 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "derive_more" -version = "0.99.18" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -617,18 +626,18 @@ dependencies = [ [[package]] name = "docify" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2f138ad521dc4a2ced1a4576148a6a610b4c5923933b062a263130a6802ce" +checksum = "a772b62b1837c8f060432ddcc10b17aae1453ef17617a99bc07789252d2a5896" dependencies = [ "docify_macros", ] [[package]] name = "docify_macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a081e51fb188742f5a7a1164ad752121abcb22874b21e2c3b0dd040c515fdad" +checksum = "60e6be249b0a462a14784a99b19bf35a667bb5e09de611738bb7362fa4c95ff7" dependencies = [ "common-path", "derive-syn-parse", @@ -636,7 +645,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.77", + "syn 2.0.90", "termcolor", "toml", "walkdir", @@ -773,7 +782,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -907,7 +916,7 @@ dependencies = [ "proc-macro2", "quote", "sp-crypto-hashing", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -919,7 +928,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -929,7 +938,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-stable2407 dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -969,9 +978,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -984,9 +993,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -994,15 +1003,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1012,38 +1021,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1091,9 +1100,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "group" @@ -1140,6 +1149,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1208,13 +1223,13 @@ dependencies = [ [[package]] name = "impl-trait-for-tuples" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] @@ -1238,12 +1253,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -1275,15 +1290,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -1310,9 +1325,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libsecp256k1" @@ -1364,9 +1379,9 @@ dependencies = [ [[package]] name = "linregress" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de04dcecc58d366391f9920245b85ffa684558a5ef6e7736e754347c3aea9c2" +checksum = "a9eda9dcf4f2a99787827661f312ac3219292549c2ee992bf9a6248ffb066bf7" dependencies = [ "nalgebra", ] @@ -1396,7 +1411,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1410,7 +1425,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1421,7 +1436,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1432,7 +1447,7 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1492,13 +1507,12 @@ dependencies = [ [[package]] name = "nalgebra" -version = "0.32.6" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" +checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" dependencies = [ "approx", "matrixmultiply", - "nalgebra-macros", "num-complex", "num-rational", "num-traits", @@ -1506,17 +1520,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "nalgebra-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "nohash-hasher" version = "0.2.0" @@ -1577,6 +1580,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ + "num-bigint", "num-integer", "num-traits", ] @@ -1602,18 +1606,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" @@ -1835,9 +1839,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1879,7 +1883,7 @@ dependencies = [ "polkavm-common", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1889,7 +1893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ba81f7b5faac81e528eb6158a6f3c9e0bb1008e0ffa19653bc8dea925ecb429" dependencies = [ "polkavm-derive-impl", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1903,12 +1907,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1965,14 +1969,14 @@ checksum = "834da187cfe638ae8abb0203f0b33e5ccdb02a28e7199f2f47b3e2754f50edca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2030,9 +2034,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -2054,19 +2058,19 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -2080,13 +2084,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -2097,9 +2101,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rfc6979" @@ -2134,9 +2138,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -2164,9 +2168,9 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.11.3" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca070c12893629e2cc820a9761bedf6ce1dcddc9852984d1dc734b8bd9bd024" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" dependencies = [ "bitvec", "cfg-if", @@ -2178,14 +2182,14 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.11.3" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d35494501194174bda522a32605929eefc9ecf7e0a326c26db1fdd85881eb62" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] @@ -2274,9 +2278,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -2292,20 +2296,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -2315,9 +2319,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -2393,9 +2397,9 @@ dependencies = [ [[package]] name = "simba" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" dependencies = [ "approx", "num-complex", @@ -2458,7 +2462,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2580,7 +2584,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-stable2407 dependencies = [ "quote", "sp-crypto-hashing", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2590,7 +2594,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-stable2407 dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2740,7 +2744,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2878,7 +2882,7 @@ dependencies = [ "parity-scale-codec", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2917,9 +2921,9 @@ dependencies = [ [[package]] name = "ss58-registry" -version = "1.50.0" +version = "1.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fce22ed1df64d04b262351c8f9d5c6da4f76f79f25ad15529792f893fad25d" +checksum = "19409f13998e55816d1c728395af0b52ec066206341d939e22e7766df9b494b8" dependencies = [ "Inflector", "num-format", @@ -2967,9 +2971,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2993,22 +2997,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -3068,9 +3072,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -3081,9 +3085,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3093,20 +3097,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -3125,9 +3129,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -3200,9 +3204,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" @@ -3215,9 +3219,9 @@ dependencies = [ [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "valuable" @@ -3233,9 +3237,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "w3f-bls" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5da5fa2c6afa2c9158eaa7cd9aee249765eb32b5fb0c63ad8b9e79336a47ec" +checksum = "70a3028804c8bbae2a97a15b71ffc0e308c4b01a520994aafa77d56e94e19024" dependencies = [ "ark-bls12-377", "ark-bls12-381", @@ -3273,9 +3277,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wide" -version = "0.7.28" +version = "0.7.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +checksum = "58e6db2670d2be78525979e9a5f9c69d296fd7d670549fe9ebf70f8708cb5019" dependencies = [ "bytemuck", "safe_arch", @@ -3387,9 +3391,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -3421,7 +3425,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -3441,5 +3445,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] diff --git a/README.md b/README.md index c9956cd..ad5c03b 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,16 @@ and all stake is refunded. Similar to the Collator Selection pallet, this pallet also maintains two kind of block producers: -* `Invulnerables`: accounts that are always selected to become collators. They can only be removed by the pallet's +- `Invulnerables`: accounts that are always selected to become collators. They can only be removed by the pallet's authority. Invulnerables do not receive staking rewards. -* `Candidates`: accounts that compete to be part of the collator set based on delegated stake. +- `Candidates`: accounts that compete to be part of the collator set based on delegated stake. ### Rewards Staking rewards distributed to candidates and their stakers come from the following sources: -* Transaction fees and tips collected for blocks produced. -* An optional per-block flat amount coming from a different pot (for example, Treasury). This is to "top-up" the rewards +- Transaction fees and tips collected for blocks produced. +- An optional per-block flat amount coming from a different pot (for example, Treasury). This is to "top-up" the rewards in case fees and tips are too small. The pallet assumes all rewards are generated from existing funds on the blockchain, and **there is no inflation** @@ -33,14 +33,14 @@ implemented as part of this pallet. Rewards are distributed so that all stakeholders are incentivized to participate: -* Candidates compete to become collators. -* Collators must not misbehave and produce blocks honestly so that they increase the chances to produce more blocks and +- Candidates compete to become collators. +- Collators must not misbehave and produce blocks honestly so that they increase the chances to produce more blocks and this way be more attractive for other users to stake on. -* Stakers must select wisely the candidates they want to deposit the stake on, hence determining the best possible +- Stakers must select wisely the candidates they want to deposit the stake on, hence determining the best possible candidates that are likely to become collators. -* Rewards are proportionally distributed among collators and stakers when the session ends. - * Collators receive an exclusive percentage of them for collating. This is configurable. - * Stakers receive the remaining proportionally to the amount staked in a given collator. +- Rewards are proportionally distributed among collators and stakers when the session ends. + - Collators receive an exclusive percentage of them for collating. This is configurable. + - Stakers receive the remaining proportionally to the amount staked in a given collator. ### Staking @@ -69,7 +69,7 @@ auto-compounded if the user set an auto-compound percentage greater than zero. ### Runtime Configuration | Parameter | Description | -|----------------------------|------------------------------------------------------------------------------------------------------| +| -------------------------- | ---------------------------------------------------------------------------------------------------- | | `RuntimeEvent` | The overarching event type. | | `Currency` | The currency mechanism. | | `RuntimeFreezeReason` | The overarching freeze reason. | @@ -85,7 +85,7 @@ auto-compounded if the user set an auto-compound percentage greater than zero. | `CollatorRegistration` | Validate a collator is registered. | | `MaxStakedCandidates` | Maximum candidates a staker can stake on. | | `MaxStakers` | Maximum stakers per candidate. | -| `MaxSessionRewards` | Maximum number of per-session reward snapshots to keep in storage. | +| `MaxRewardSessions` | Maximum number of per-session reward snapshots to keep in storage. | | `BondUnlockDelay` | Number of blocks to wait before unlocking the bond by a collator. | | `StakeUnlockDelay` | Number of blocks to wait before unlocking the stake by a user. | | `AutoCompoundingThreshold` | Minimum stake needed to enable autocompounding. | diff --git a/src/benchmarking.rs b/src/benchmarking.rs index cdec0f5..0753b87 100644 --- a/src/benchmarking.rs +++ b/src/benchmarking.rs @@ -27,7 +27,6 @@ use frame_support::BoundedBTreeMap; use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, RawOrigin}; use pallet_authorship::EventHandler; use pallet_session::SessionManager; -use sp_runtime::traits::Zero; use sp_runtime::Percent; use sp_std::prelude::*; @@ -129,7 +128,7 @@ fn prepare_staker() -> T::AccountId { let amount = T::Currency::minimum_balance(); MinStake::::set(amount); MinCandidacyBond::::set(amount); - let staker = create_funded_user::("staker", 0, 10000); + let staker = create_funded_user::("staker", 0, 10000000); CollatorStaking::::lock( RawOrigin::Signed(staker.clone()).into(), CollatorStaking::::get_free_balance(&staker), @@ -146,12 +145,14 @@ fn prepare_rewards( let amount = T::Currency::minimum_balance(); MinStake::::set(amount); MinCandidacyBond::::set(amount); - let staker = create_funded_user::("staker", 0, 10000); + + let staker = create_funded_user::("staker", 0, 10000000); CollatorStaking::::lock( RawOrigin::Signed(staker.clone()).into(), CollatorStaking::::get_free_balance(&staker), ) .unwrap(); + CollatorStaking::::set_autocompound_percentage( RawOrigin::Signed(staker.clone()).into(), Percent::from_parts(100), @@ -200,7 +201,7 @@ mod benchmarks { #[benchmark] fn set_invulnerables( - b: Linear<1, { T::MaxInvulnerables::get() }>, + b: Linear<{ T::MinEligibleCollators::get() }, { T::MaxInvulnerables::get() }>, ) -> Result<(), BenchmarkError> { let origin = T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; @@ -537,12 +538,21 @@ mod benchmarks { frame_system::Pallet::::set_block_number(0u32.into()); let caller = whitelisted_caller(); - T::Currency::mint_into(&caller, amount * 2u32.into() * (c + 1).into()).unwrap(); + let bond = MinCandidacyBond::::get(); + T::Currency::mint_into(&caller, amount * 2u32.into() * (c + 1).into() + bond).unwrap(); + + // Here we add the staker as candidate and immediately remove it so that the candidacy bond + // gets released and the corresponding weight accounted for. + CollatorStaking::::do_register_as_candidate(&caller, bond).unwrap(); + CollatorStaking::::try_remove_candidate(&caller, true, CandidacyBondReleaseReason::Idle) + .unwrap(); + CollatorStaking::::lock( RawOrigin::Signed(caller.clone()).into(), CollatorStaking::::get_free_balance(&caller), ) .unwrap(); + for _ in 0..c { CollatorStaking::::unlock( RawOrigin::Signed(caller.clone()).into(), @@ -562,10 +572,12 @@ mod benchmarks { #[benchmark] fn claim_rewards( c: Linear<1, { T::MaxStakedCandidates::get() }>, - r: Linear<1, { T::MaxSessionRewards::get() }>, + r: Linear<1, { T::MaxRewardSessions::get() }>, ) { let (staker, total_rewards, candidates) = prepare_rewards::(c, r); + frame_system::Pallet::::set_block_number(T::BondUnlockDelay::get() + 10u32.into()); + #[extrinsic_call] _(RawOrigin::Signed(staker.clone())); @@ -589,7 +601,7 @@ mod benchmarks { #[benchmark] fn set_autocompound_percentage() { let caller = prepare_staker::(); - let amount = T::Currency::minimum_balance(); + let amount = T::AutoCompoundingThreshold::get(); let candidate = register_single_validator::(0); register_single_candidate::(0); @@ -730,19 +742,6 @@ mod benchmarks { { as SessionManager<_>>::end_session(1); } - - let collator_reward = CollatorRewardPercentage::::get().mul_floor(amount); - if !collator_reward.is_zero() { - for candidate in candidates { - assert_has_event::( - Event::::StakingRewardReceived { - account: candidate.clone(), - amount: collator_reward, - } - .into(), - ); - } - } } #[benchmark] diff --git a/src/lib.rs b/src/lib.rs index ddf1594..26198e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,8 +92,10 @@ pub mod pallet { pub type UserStakeInfoOf = UserStakeInfo< BoundedBTreeSet<::AccountId, ::MaxStakedCandidates>, BalanceOf, + BlockNumberFor, >; pub type CandidateStakeInfoOf = CandidateStakeInfo>; + pub type CandidacyBondReleaseOf = CandidacyBondRelease, BlockNumberFor>; /// A convertor from collators id. Since this pallet does not have stash/controller, this is /// just identity. @@ -187,10 +189,15 @@ pub mod pallet { #[pallet::constant] type StakeUnlockDelay: Get>; + /// Number of blocks to wait before reusing funds previously assigned to a collator. + /// It should be set to at least one session. + #[pallet::constant] + type RestakeUnlockDelay: Get>; + /// Maximum number of rewards to keep in storage. Non-claimed rewards will not be claimable /// after they have been removed. #[pallet::constant] - type MaxSessionRewards: Get; + type MaxRewardSessions: Get; /// Minimum stake needed to enable autocompounding. #[pallet::constant] @@ -261,16 +268,19 @@ pub mod pallet { #[derive( PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, )] - pub struct UserStakeInfo { + pub struct UserStakeInfo { /// The total amount staked in all candidates. pub stake: Balance, + /// Last time an amount was reassigned. + pub maybe_last_unstake: Option<(Balance, BlockNumber)>, /// The candidates where this user staked. pub candidates: AccountIdSet, /// Last session where this user got the rewards. pub maybe_last_reward_session: Option, } - impl Default for UserStakeInfo + impl Default + for UserStakeInfo where AccountIdSet: Default, Balance: Default, @@ -279,6 +289,7 @@ pub mod pallet { Self { stake: Balance::default(), candidates: AccountIdSet::default(), + maybe_last_unstake: None, maybe_last_reward_session: None, } } @@ -295,6 +306,28 @@ pub mod pallet { pub candidates: AccountIdMap, } + /// Reasons a candidady left the candidacy list for. + #[derive( + PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, + )] + pub enum CandidacyBondReleaseReason { + /// The candidacy did not produce at least one block for [`KickThreshold`] blocks. + Idle, + /// The candidate left by itself. + Left, + /// The candidate was replaced by another one with higher bond. + Replaced, + } + + #[derive( + PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, + )] + pub struct CandidacyBondRelease { + pub bond: Balance, + pub block: BlockNumber, + pub reason: CandidacyBondReleaseReason, + } + #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); @@ -313,6 +346,11 @@ pub mod pallet { pub type Candidates = CountedStorageMap<_, Blake2_128Concat, T::AccountId, CandidateInfoOf, OptionQuery>; + /// Map of Candidates that have been removed in the current session. + #[pallet::storage] + pub type SessionRemovedCandidates = + StorageMap<_, Blake2_128Concat, T::AccountId, CandidateInfoOf, OptionQuery>; + /// Last block authored by a collator. #[pallet::storage] pub type LastAuthoredBlock = @@ -321,9 +359,6 @@ pub mod pallet { /// Desired number of candidates. /// /// This should always be less than [`Config::MaxCandidates`] for weights to be correct. - /// - /// IMP: This must be less than the session length, - /// because rewards are distributed for one collator per block. #[pallet::storage] pub type DesiredCandidates = StorageValue<_, u32, ValueQuery>; @@ -404,6 +439,13 @@ pub mod pallet { pub type AutoCompound = StorageMap<_, Blake2_128Concat, T::AccountId, Percent, ValueQuery>; + /// Time (in blocks) to release an ex-candidate's locked candidacy bond. + /// If a candidate leaves the candidacy before its bond is released, the waiting period + /// will restart. + #[pallet::storage] + pub type CandidacyBondReleases = + StorageMap<_, Blake2_128Concat, T::AccountId, CandidacyBondReleaseOf, OptionQuery>; + #[pallet::genesis_config] #[derive(DefaultNoBound)] pub struct GenesisConfig { @@ -418,13 +460,8 @@ pub mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { - let duplicate_invulnerables = self - .invulnerables - .iter() - .collect::>(); - assert_eq!( - duplicate_invulnerables.len(), - self.invulnerables.len(), + assert!( + !Pallet::::has_duplicates(&self.invulnerables), "duplicate invulnerables in genesis." ); @@ -515,6 +552,8 @@ pub mod pallet { NotCandidate, /// There are too many Invulnerables. TooManyInvulnerables, + /// At least one of the invulnerables is duplicated + DuplicatedInvulnerables, /// Account is already an Invulnerable. AlreadyInvulnerable, /// Account is not an Invulnerable. @@ -523,16 +562,12 @@ pub mod pallet { NoAssociatedCollatorId, /// Collator ID is not yet registered. CollatorNotRegistered, - /// Could not insert in the candidate list. - InsertToCandidateListFailed, /// Amount not sufficient to be staked. InsufficientStake, /// DesiredCandidates is out of bounds. TooManyDesiredCandidates, /// Too many unstaking requests. Claim some of them first. TooManyReleaseRequests, - /// Cannot take some candidate's slot while the candidate list is not full. - CanRegister, /// Invalid value for MinStake. It must be lower than or equal to `MinStake`. InvalidMinStake, /// Invalid value for CandidacyBond. It must be higher than or equal to `MinCandidacyBond`. @@ -545,8 +580,6 @@ pub mod pallet { ExtraRewardAlreadyDisabled, /// The amount to fund the extra reward pot must be greater than zero. InvalidFundingAmount, - /// There is nothing to unstake. - NothingToUnstake, /// Cannot add more stakers to a given candidate. TooManyStakers, /// The user does not have enough balance to be locked for staking. @@ -563,6 +596,8 @@ pub mod pallet { NoStakeOnCandidate, /// No rewards to claim as previous claim happened on the same session. NoPendingClaim, + /// Candidate has not been removed in the current session. + NotRemovedCandidate, } #[pallet::hooks] @@ -601,9 +636,10 @@ pub mod pallet { pub fn set_invulnerables(origin: OriginFor, new: Vec) -> DispatchResult { T::UpdateOrigin::ensure_origin(origin)?; - // don't wipe out the collator set + ensure!(!Self::has_duplicates(&new), Error::::DuplicatedInvulnerables); + + // Don't wipe out the collator set if new.is_empty() { - // Casting `u32` to `usize` should be safe on all machines running this. ensure!( Candidates::::count() >= T::MinEligibleCollators::get(), Error::::TooFewEligibleCollators @@ -653,6 +689,14 @@ pub mod pallet { BoundedVec::<_, T::MaxInvulnerables>::try_from(new_with_keys) .map_err(|_| Error::::TooManyInvulnerables)?; + // Make sure that the minimum eligible collator requirement is met. + let total_invulnerables = bounded_invulnerables.len() as u32; + let eligible_collators = total_invulnerables.saturating_add(Candidates::::count()); + ensure!( + eligible_collators >= T::MinEligibleCollators::get(), + Error::::TooFewEligibleCollators + ); + // Invulnerables must be sorted for removal. bounded_invulnerables.sort(); @@ -674,6 +718,12 @@ pub mod pallet { T::UpdateOrigin::ensure_origin(origin)?; ensure!(max <= T::MaxCandidates::get(), Error::::TooManyDesiredCandidates); + let invulnerables = Invulnerables::::get(); + ensure!( + max.saturating_add(invulnerables.len() as u32) >= T::MinEligibleCollators::get(), + Error::::TooFewEligibleCollators + ); + DesiredCandidates::::set(max); Self::deposit_event(Event::NewDesiredCandidates { desired_candidates: max }); Ok(()) @@ -740,7 +790,7 @@ pub mod pallet { Error::::TooFewEligibleCollators ); // Do remove their last authored block. - Self::try_remove_candidate(&who, true)?; + Self::try_remove_candidate(&who, true, CandidacyBondReleaseReason::Left)?; Ok(()) } @@ -844,27 +894,30 @@ pub mod pallet { /// Removes stake from a collator candidate. /// - /// The amount unstaked will remain locked. + /// The amount unstaked will remain locked if the stake was removed from a candidate. #[pallet::call_index(8)] #[pallet::weight(T::WeightInfo::unstake_from())] - pub fn unstake_from(origin: OriginFor, candidate: T::AccountId) -> DispatchResult { + pub fn unstake_from(origin: OriginFor, account: T::AccountId) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(Self::staker_has_claimed(&who), Error::::PreviousRewardsNotClaimed); ensure!( - CandidateStake::::try_get(candidate.clone(), who.clone()).is_ok(), + CandidateStake::::try_get(account.clone(), who.clone()).is_ok(), Error::::NoStakeOnCandidate ); - let _ = Self::do_unstake(&who, &candidate)?; + let (amount, is_candidate) = Self::do_unstake(&who, &account)?; + if is_candidate { + Self::note_last_unstake(&who, amount); + } Ok(()) } /// Removes all stake of a user from all candidates. /// - /// The amount unstaked will remain locked. + /// The amount unstaked from candidates will remain locked. #[pallet::call_index(9)] #[pallet::weight(T::WeightInfo::unstake_all(T::MaxStakedCandidates::get()))] pub fn unstake_all(origin: OriginFor) -> DispatchResultWithPostInfo { @@ -873,20 +926,28 @@ pub mod pallet { ensure!(Self::staker_has_claimed(&who), Error::::PreviousRewardsNotClaimed); let user_stake = UserStake::::get(&who); + let mut amount_in_candidates: BalanceOf = Zero::zero(); for candidate in &user_stake.candidates { - Self::do_unstake(&who, candidate)?; + let (amount, is_candidate) = Self::do_unstake(&who, candidate)?; + if is_candidate { + amount_in_candidates.saturating_accrue(amount); + } + } + if !amount_in_candidates.is_zero() { + Self::note_last_unstake(&who, amount_in_candidates); } Ok(Some(T::WeightInfo::unstake_all(user_stake.candidates.len() as u32)).into()) } - /// Releases all pending [`ReleaseRequest`] for a given account. + /// Releases all pending [`ReleaseRequest`] and candidacy bond for a given account. /// /// This will unlock all funds in [`ReleaseRequest`] that have already expired. #[pallet::call_index(10)] #[pallet::weight(T::WeightInfo::release(T::MaxStakedCandidates::get()))] pub fn release(origin: OriginFor) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; + Self::do_claim_candidacy_bond(&who)?; let operations = Self::do_release(&who)?; Ok(Some(T::WeightInfo::release(operations)).into()) } @@ -1070,8 +1131,9 @@ pub mod pallet { ensure!(amount >= MinCandidacyBond::::get(), Error::::InvalidCandidacyBond); ensure!(Self::get_candidate(&who).is_ok(), Error::::NotCandidate); - let available_balance = - Self::get_free_balance(&who).saturating_add(Self::get_bond(&who)); + let available_balance = T::Currency::balance(&who) + .saturating_sub(Self::get_staked_balance(&who)) + .saturating_sub(Self::get_releasing_balance(&who)); ensure!(available_balance >= amount, Error::::InsufficientFreeBalance); T::Currency::set_freeze(&FreezeReason::CandidacyBond.into(), &who, amount)?; @@ -1088,12 +1150,12 @@ pub mod pallet { #[pallet::call_index(20)] #[pallet::weight(T::WeightInfo::claim_rewards( T::MaxStakedCandidates::get(), - T::MaxSessionRewards::get() + T::MaxRewardSessions::get() ))] pub fn claim_rewards(origin: OriginFor) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - //Staker can't claim in the same session as there are no rewards + // Staker can't claim in the same session as there are no rewards. ensure!(!Self::staker_has_claimed(&who), Error::::NoPendingClaim); let (candidates, rewards) = Self::do_claim_rewards(&who)?; @@ -1159,6 +1221,12 @@ pub mod pallet { (session_total_amount, unclaimable_rewards) } + fn has_duplicates(accounts: &[T::AccountId]) -> bool { + let duplicates = + accounts.iter().collect::>(); + duplicates.len() != accounts.len() + } + /// Claims all rewards from previous sessions. /// /// Returns the number of collators the users added stake to, and the total sessions with rewards. @@ -1218,6 +1286,7 @@ pub mod pallet { } /// Computes pending rewards for a given user. + /// This function is intended to be used in the runtime implementation. pub fn calculate_unclaimed_rewards(who: &T::AccountId) -> BalanceOf { let mut total_rewards: BalanceOf = Zero::zero(); let user_stake_info = UserStake::::get(who); @@ -1280,8 +1349,38 @@ pub mod pallet { stake.saturating_accrue(info.stake); stakers.saturating_inc(); } + // Users are allowed to reuse the old candidacy bond as long as they were + // replaced by another candidate. + CandidacyBondReleases::::try_mutate( + who, + |maybe_bond_release| -> DispatchResult { + if let Some(bond_release) = maybe_bond_release { + if bond_release.reason == CandidacyBondReleaseReason::Replaced + && bond_release.bond >= bond + { + let remaining_lock = + Self::get_releasing_balance(who).saturating_sub(bond); + T::Currency::set_freeze( + &FreezeReason::Releasing.into(), + who, + remaining_lock, + )?; + bond_release.bond.saturating_reduce(bond); + if bond_release.bond.is_zero() { + *maybe_bond_release = None; + } + } + } + Ok(()) + }, + )?; let info = CandidateInfo { stake, stakers }; *maybe_candidate_info = Some(info.clone()); + + // If the candidate left in the current session and is now rejoining + // remove it from the SessionRemovedCandidates + SessionRemovedCandidates::::remove(who); + T::Currency::set_freeze(&FreezeReason::CandidacyBond.into(), who, bond)?; Ok(info) }, @@ -1327,6 +1426,18 @@ pub mod pallet { Ok(pos as u32) } + /// Notes the last unstake operation for a given user + fn note_last_unstake(account: &T::AccountId, amount: BalanceOf) { + UserStake::::mutate(account, |info| { + let (balance, block) = info.maybe_last_unstake.unwrap_or_default(); + let now = Self::current_block_number(); + let final_amount = + if block >= now { amount } else { amount.saturating_add(balance) }; + info.maybe_last_unstake = + Some((final_amount, now.saturating_add(T::RestakeUnlockDelay::get()))); + }); + } + /// Adds stake into a given candidate by providing its address and the amount to stake. /// /// This operation will fail if: @@ -1381,6 +1492,25 @@ pub mod pallet { if user_stake_info.maybe_last_reward_session.is_none() { user_stake_info.maybe_last_reward_session = Some(current_session); } + + // In case the user recently unstaked we cannot allow those funds to be quickly + // reinvested. Otherwise, stakers could potentially move funds right before + // the session ends from one candidate to another, depending on the most + // performant ones during the current session. + if let Some((unavailable_amount, block_limit)) = + user_stake_info.maybe_last_unstake + { + if block_limit < Self::current_block_number() { + let available_amount = + frozen_balance.saturating_sub(unavailable_amount); + ensure!( + available_amount >= amount, + Error::::InsufficientLockedBalance + ); + } else { + user_stake_info.maybe_last_unstake = None; + } + } Ok(()) })?; @@ -1418,23 +1548,25 @@ pub mod pallet { /// Unstakes all funds deposited by `staker` in a given `candidate`. /// - /// Returns the amount unstaked. + /// Returns the amount unstaked, and whether it was unstaked from a candidate or not. fn do_unstake( staker: &T::AccountId, candidate: &T::AccountId, - ) -> Result, DispatchError> { + ) -> Result<(BalanceOf, bool), DispatchError> { let stake = Self::remove_stake(candidate, staker); + let mut is_candidate = true; if !stake.is_zero() { Candidates::::mutate_exists(candidate, |maybe_info| { if let Some(info) = maybe_info { + is_candidate = true; info.stake.saturating_reduce(stake); info.stakers.saturating_dec(); } }); } - Ok(stake) + Ok((stake, is_candidate)) } /// Disable autocompounding if staked balance dropped below the threshold @@ -1470,9 +1602,6 @@ pub mod pallet { user_stake_info.candidates.remove(candidate); }, } - } else { - // This should never occur. - *maybe_user_stake_info = None; } }); } @@ -1486,12 +1615,13 @@ pub mod pallet { stake } - /// Attempts to remove a candidate, identified by its account, if it exists and refunds the stake. + /// Attempts to remove a candidate, identified by its account. /// /// Returns the candidate info prior to its removal. - fn try_remove_candidate( + pub fn try_remove_candidate( who: &T::AccountId, remove_last_authored: bool, + reason: CandidacyBondReleaseReason, ) -> Result, DispatchError> { Candidates::::try_mutate_exists( who, @@ -1500,16 +1630,11 @@ pub mod pallet { if remove_last_authored { LastAuthoredBlock::::remove(who.clone()) } + Self::release_candidacy_bond(who, reason)?; - // We firstly optimistically release the candidacy bond. - let amount = Self::get_bond(who); - T::Currency::set_freeze( - &FreezeReason::CandidacyBond.into(), - who, - Zero::zero(), - )?; - // And now we lock it again to be released. - Self::add_to_release_queue(who, amount, T::BondUnlockDelay::get())?; + // Store removed candidate in SessionRemovedCandidates to properly reward + // the candidate and its stakers at the end of the session. + SessionRemovedCandidates::::insert(who, candidate.clone()); Self::deposit_event(Event::CandidateRemoved { account: who.clone() }); *maybe_candidate = None; @@ -1545,13 +1670,86 @@ pub mod pallet { Ok(()) } + /// Prepares the candidacy bond to be released. + fn release_candidacy_bond( + account: &T::AccountId, + reason: CandidacyBondReleaseReason, + ) -> DispatchResult { + // First attempt to claim a hypothetical older candidacy bond in case the user forgot + // to do so before leaving the candidacy list. + Self::do_claim_candidacy_bond(account)?; + + let bond = Self::get_bond(account); + if !bond.is_zero() { + // We firstly release the current candidacy bond. + T::Currency::set_freeze( + &FreezeReason::CandidacyBond.into(), + account, + Zero::zero(), + )?; + + // Now we freeze it again under a different reason. + let new_releasing_balance = + Self::get_releasing_balance(account).saturating_add(bond); + T::Currency::set_freeze( + &FreezeReason::Releasing.into(), + account, + new_releasing_balance, + )?; + + // And finally update the period. + let release_block = + Self::current_block_number().saturating_add(T::BondUnlockDelay::get()); + CandidacyBondReleases::::mutate(account, |maybe_bond_release| { + let mut final_bond = bond; + if let Some(CandidacyBondRelease { bond: previous_bond, .. }) = + maybe_bond_release + { + // In case there exists a previous bond that could not be claimed at the + // beginning of this function, it gets accumulated with this new bond that + // has just been released. + final_bond.saturating_accrue(*previous_bond); + } + *maybe_bond_release = Some(CandidacyBondRelease { + bond: final_bond, + block: release_block, + reason, + }); + }); + } + Ok(()) + } + + /// Claims the candidacy bond, provided sufficient time has passed. + fn do_claim_candidacy_bond(account: &T::AccountId) -> DispatchResult { + CandidacyBondReleases::::try_mutate(account, |maybe_bond_release| { + if let Some(CandidacyBondRelease { bond, block: bond_release, .. }) = + maybe_bond_release + { + if Self::current_block_number() > *bond_release { + let new_release = + Self::get_releasing_balance(account).saturating_sub(*bond); + T::Currency::set_freeze( + &FreezeReason::Releasing.into(), + account, + new_release, + )?; + *maybe_bond_release = None; + } + } + // We always return a success, as it is not an error if the candidacy bond + // is not ready to be claimed yet. + Ok(()) + }) + } + /// Removes old rewards when a new session starts. /// /// Returns the rewards that have been released. fn remove_old_rewards_if_needed(session: SessionIndex) -> BalanceOf { let mut released_rewards: BalanceOf = Zero::zero(); - if PerSessionRewards::::count() >= T::MaxSessionRewards::get() { - let reward_to_remove = session.saturating_sub(T::MaxSessionRewards::get()); + if PerSessionRewards::::count() >= T::MaxRewardSessions::get() { + let reward_to_remove = session.saturating_sub(T::MaxRewardSessions::get()); PerSessionRewards::::mutate_exists(reward_to_remove, |maybe_reward| { if let Some(reward) = maybe_reward { released_rewards.saturating_accrue(reward.rewards); @@ -1581,32 +1779,47 @@ pub mod pallet { if !rewardable_blocks.is_zero() && !total_rewards.is_zero() { let collator_percentage = CollatorRewardPercentage::::get(); for (collator, blocks) in ProducedBlocks::::drain() { - if let Ok(collator_info) = Self::get_candidate(&collator) { + // Get the collator info of a candidate, in the case that the collator was removed from the + // candidate list during the session, the collator and its stakers must still be rewarded + // for the produced blocks in the session so the info can be obtained from SessionRemovedCandidates. + let info = Self::get_candidate(&collator) + .or_else(|_| { + SessionRemovedCandidates::::take(&collator) + .ok_or(Error::::NotRemovedCandidate) + }) + .ok(); + + if let Some(collator_info) = info { if blocks > rewardable_blocks { // The only case this could happen is if the candidate was an invulnerable during the session. + // Since blocks produced by invulnerables are not currently stored in ProducedBlocks this error + // should not occur. log::warn!("Cannot reward collator {:?} for producing more blocks than rewardable ones", collator); break; } - let rewards_all: BalanceOf = - total_rewards.saturating_mul(blocks.into()) / rewardable_blocks.into(); + let ratio = Perbill::from_rational(blocks, rewardable_blocks); + let rewards_all = ratio * total_rewards; let collator_only_reward = collator_percentage.mul_floor(rewards_all); + let stakers_only_rewards = rewards_all.saturating_sub(collator_only_reward); // Reward collator. Note these rewards are not autocompounded. if let Err(error) = Self::do_reward_single(&collator, collator_only_reward) { log::warn!(target: LOG_TARGET, "Failure rewarding collator {:?}: {:?}", collator, error); } - // No rewards if: + // No rewards for stakers if: // - The collator has no stakers. + // - The actual reward is zero. // - It is the first session. This is because stakers do not receive rewards // for the first session they stake in, so in the worst case they staked // in session zero. - if collator_info.stake.is_zero() || session.is_zero() { + if collator_info.stake.is_zero() + || session.is_zero() || stakers_only_rewards.is_zero() + { break; } // We should be able to insert it, but in case we cannot, simply ignore this reward. - let stakers_only_rewards = rewards_all.saturating_sub(collator_only_reward); if reward_map .try_insert( collator.clone(), @@ -1623,10 +1836,16 @@ pub mod pallet { } let rewardable_collators: u32 = reward_map.len() as u32; - PerSessionRewards::::insert( - session, - SessionInfo { rewards: stakers_rewards, candidates: reward_map }, - ); + + // If there are no rewards for stakers (likely because either no rewards were + // produced at all during the session or because the collator reward percentage is + // set to 100%) then there is no need to insert this. + if !stakers_rewards.is_zero() { + PerSessionRewards::::insert( + session, + SessionInfo { rewards: stakers_rewards, candidates: reward_map }, + ); + } ClaimableRewards::::set(claimable_rewards.saturating_add(stakers_rewards)); (rewardable_collators, total_rewards) } @@ -1717,7 +1936,7 @@ pub mod pallet { let now = Self::current_block_number(); let kick_threshold = T::KickThreshold::get(); let min_collators = T::MinEligibleCollators::get(); - let candidacy_bond = MinCandidacyBond::::get(); + let min_candidacy_bond = MinCandidacyBond::::get(); Candidates::::iter() .filter_map(|(who, info)| { let last_block = LastAuthoredBlock::::get(who.clone()); @@ -1725,14 +1944,19 @@ pub mod pallet { let is_lazy = since_last >= kick_threshold; let bond = Self::get_bond(&who); - if Self::eligible_collators() <= min_collators || (!is_lazy && bond.saturating_add(info.stake) >= candidacy_bond) { + if Self::eligible_collators() <= min_collators || (!is_lazy && bond >= min_candidacy_bond) { // Either this is a good collator (not lazy) or we are at the minimum // that the system needs. They get to stay, as long as they have sufficient deposit plus stake. Some(info) } else { // This collator has not produced a block recently enough. Bye bye. - let _ = Self::try_remove_candidate(&who, true); - None + match Self::try_remove_candidate(&who, true, CandidacyBondReleaseReason::Idle) { + Ok(_) => None, + Err(error) => { + log::warn!("Could not remove candidate {:?}: {:?}", info, error); + Some(info) + }, + } } }) .count() @@ -1756,7 +1980,7 @@ pub mod pallet { ) -> Result { let (candidate, worst_bond) = Self::get_worst_candidate()?; ensure!(bond > worst_bond, Error::::InvalidCandidacyBond); - Self::try_remove_candidate(&candidate, false)?; + Self::try_remove_candidate(&candidate, false, CandidacyBondReleaseReason::Replaced)?; Ok(candidate) } @@ -1772,9 +1996,20 @@ pub mod pallet { /// * The number of selected candidates together with the invulnerables must be greater than /// or equal to the minimum number of eligible collators. /// - /// ## [`MaxCandidates`] + /// ## [`MaxStakedCandidates`] + /// + /// * The amount of staked candidates per account is limited and its maximum value must not be surpassed. + /// + /// ## [`Candidates`] /// - /// * The amount of stakers per account is limited and its maximum value must not be surpassed. + /// * The amount of stakers per Candidate is limited and its maximum value must not be surpassed. + /// * The number of candidates should not exceed the candidate list capacity + /// + /// ## [`PerSessionRewards`] + /// + /// * The amount of stored sessions must not exceed the capacity established by the maximum + /// amount of sessions kept in storage + #[cfg(any(test, feature = "try-runtime"))] pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { let desired_candidates = DesiredCandidates::::get(); @@ -1809,8 +2044,8 @@ pub mod pallet { ); ensure!( - PerSessionRewards::::count() <= T::MaxSessionRewards::get(), - "Per-session reward count must not exceed MaxSessionRewards" + PerSessionRewards::::count() <= T::MaxRewardSessions::get(), + "Per-session reward count must not exceed MaxRewardSessions" ); Ok(()) @@ -1855,6 +2090,12 @@ pub mod pallet { let removed = candidates_len_before.saturating_sub(active_candidates_count); let result = Self::assemble_collators(); + // Although the removed candidates are passively deleted from SessionRemovedCandidates + // during the distribution of session rewards, it is possible that a removed candidate + // is not removed if the candidate didn't produce and blocks during the session. For that + // reason the leftover keys in the SessionRemovedCandidates StorageMap must be cleared. + let _ = SessionRemovedCandidates::::clear(T::MaxCandidates::get(), None); + frame_system::Pallet::::register_extra_weight_unchecked( T::WeightInfo::new_session(removed, candidates_len_before), DispatchClass::Mandatory, @@ -1916,7 +2157,29 @@ where } sp_api::decl_runtime_apis! { - /// This runtime api allows to query the two pot accounts. + /// This runtime api allows to query: + /// - The main pallet's pot account. + /// - The extra rewards pot account. + /// - Accumulated rewards for an account. + /// - Whether a given account has rewards pending to be claimed or not. + /// + /// Sample implementation: + /// ```ignore + /// impl pallet_collator_staking::CollatorStakingApi for Runtime { + /// fn main_pot_account() -> AccountId { + /// CollatorStaking::account_id() + /// } + /// fn extra_reward_pot_account() -> AccountId { + /// CollatorStaking::extra_reward_account_id() + /// } + /// fn total_rewards(account: AccountId) -> Balance { + /// CollatorStaking::calculate_unclaimed_rewards(&account) + /// } + /// fn should_claim(account: AccountId) -> bool { + /// !CollatorStaking::staker_has_claimed(&account) + /// } + /// } + /// ``` pub trait CollatorStakingApi where AccountId: Codec, diff --git a/src/mock.rs b/src/mock.rs index c3b4832..8279fcf 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -200,7 +200,7 @@ pub struct IdentityCollatorMock(PhantomData); impl sp_runtime::traits::Convert> for IdentityCollatorMock { fn convert(acc: AccountId) -> Option { match acc { - 1000 => None, + 1000..2000 => None, _ => Some(acc), } } @@ -232,7 +232,8 @@ impl Config for Test { type MaxStakers = ConstU32<25>; type BondUnlockDelay = ConstU64<5>; type StakeUnlockDelay = ConstU64<2>; - type MaxSessionRewards = ConstU32<10>; + type RestakeUnlockDelay = ConstU64<10>; + type MaxRewardSessions = ConstU32<10>; type AutoCompoundingThreshold = ConstU64<60>; type WeightInfo = (); } diff --git a/src/tests.rs b/src/tests.rs index 078ab17..4363388 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -15,13 +15,13 @@ use crate as collator_staking; use crate::{ - mock::*, AutoCompound, BalanceOf, CandidateInfo, CandidateStakeInfo, Candidates, + mock::*, AutoCompound, BalanceOf, CandidacyBondRelease, CandidacyBondReleaseReason, + CandidacyBondReleases, CandidateInfo, CandidateStake, CandidateStakeInfo, Candidates, ClaimableRewards, CollatorRewardPercentage, Config, CurrentSession, DesiredCandidates, Error, Event, ExtraReward, FreezeReason, Invulnerables, LastAuthoredBlock, MinCandidacyBond, MinStake, - PerSessionRewards, ProducedBlocks, ReleaseQueues, StakeTarget, TotalBlocks, UserStake, - UserStakeInfo, + PerSessionRewards, ProducedBlocks, ReleaseQueues, ReleaseRequest, SessionRemovedCandidates, + StakeTarget, TotalBlocks, UserStake, UserStakeInfo, }; -use crate::{CandidateStake, ReleaseRequest}; use frame_support::pallet_prelude::TypedGet; use frame_support::traits::fungible::InspectFreeze; use frame_support::traits::tokens::Preservation::Preserve; @@ -153,6 +153,40 @@ mod set_invulnerables { }); } + #[test] + fn it_should_not_allow_duplicated_invulnerables() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + assert_eq!(Invulnerables::::get(), vec![1, 2]); + let new_with_duplicated = vec![1, 1, 2, 4, 3, 2]; + + assert_noop!( + CollatorStaking::set_invulnerables( + RuntimeOrigin::signed(RootAccount::get()), + new_with_duplicated + ), + Error::::DuplicatedInvulnerables + ); + }); + } + + #[test] + fn it_should_not_allow_too_many_invalid_invulnerables() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + assert_eq!(Invulnerables::::get(), vec![1, 2]); + let new_with_many_invalid = vec![1000, 1001, 1002, 1003, 1004, 1005, 1006]; + + assert_noop!( + CollatorStaking::set_invulnerables( + RuntimeOrigin::signed(RootAccount::get()), + new_with_many_invalid + ), + Error::::TooFewEligibleCollators + ); + }); + } + #[test] fn should_not_allow_to_set_invulnerables_if_already_candidates() { new_test_ext().execute_with(|| { @@ -198,7 +232,7 @@ mod set_desired_candidates { CollatorStaking::set_desired_candidates(RuntimeOrigin::signed(1), 2), BadOrigin ); - // rejects bad origin + // rejects too many assert_noop!( CollatorStaking::set_desired_candidates( RuntimeOrigin::signed(RootAccount::get()), @@ -208,6 +242,29 @@ mod set_desired_candidates { ); }); } + + #[test] + fn cannot_set_desired_candidates_if_under_min_collator_limit() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + // given + assert_eq!(DesiredCandidates::::get(), 2); + assert_eq!(::MinEligibleCollators::get(), 1); + register_candidates(3..=3); + + assert_ok!(CollatorStaking::set_invulnerables( + RuntimeOrigin::signed(RootAccount::get()), + vec![] + )); + assert_noop!( + CollatorStaking::set_desired_candidates( + RuntimeOrigin::signed(RootAccount::get()), + 0 + ), + Error::::TooFewEligibleCollators + ); + }); + } } mod add_invulnerable { @@ -514,7 +571,6 @@ mod set_min_candidacy_bond { let candidate_5 = CandidateInfo { stake: 0, stakers: 0 }; register_candidates(3..=5); - lock_for_staking(3..=5); assert_eq!( candidate_list(), vec![(5, candidate_5.clone()), (3, candidate_3.clone()), (4, candidate_4.clone())] @@ -559,21 +615,9 @@ mod set_min_candidacy_bond { bond_amount: 20, })); assert_eq!(MinCandidacyBond::::get(), 20); - assert_ok!(CollatorStaking::stake( - RuntimeOrigin::signed(5), - vec![StakeTarget { candidate: 5, stake: 20 }].try_into().unwrap() - )); - let new_candidate_5 = CandidateInfo { stake: 20, stakers: 1 }; - assert_eq!( - candidate_list(), - vec![ - (3, candidate_3.clone()), - (4, candidate_4.clone()), - (5, new_candidate_5.clone()) - ] - ); + assert_ok!(CollatorStaking::update_candidacy_bond(RuntimeOrigin::signed(5), 20)); CollatorStaking::kick_stale_candidates(); - assert_eq!(candidate_list(), vec![(5, new_candidate_5)]); + assert_eq!(candidate_list(), vec![(5, candidate_5)]); }); } } @@ -779,6 +823,10 @@ mod register_as_candidate { // the candidate leaves assert_ok!(CollatorStaking::leave_intent(RuntimeOrigin::signed(3))); + assert_eq!( + SessionRemovedCandidates::::get(3), + Some(CandidateInfo { stake: 60, stakers: 1 }) + ); // the stake remains the same assert_eq!( CandidateStake::::get(3, 4), @@ -799,6 +847,95 @@ mod register_as_candidate { assert_eq!(Candidates::::get(3), Some(CandidateInfo { stake: 60, stakers: 1 })); }); } + + #[test] + fn register_as_candidate_reuses_old_bond_if_replaced() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + + // given + assert_eq!(DesiredCandidates::::get(), 2); + assert_eq!(MinCandidacyBond::::get(), 10); + assert_eq!(Candidates::::count(), 0); + assert_eq!(Invulnerables::::get(), vec![1, 2]); + + // register the first time + assert_eq!(Balances::balance(&3), 100); + assert_eq!( + CandidateStake::::get(3, 3), + CandidateStakeInfo { stake: 0, session: 0 } + ); + register_candidates(3..=3); + assert_eq!(Balances::balance_frozen(&FreezeReason::CandidacyBond.into(), &3), 10); + assert_eq!( + CandidateStake::::get(3, 3), + CandidateStakeInfo { stake: 0, session: 0 } + ); + assert_eq!(Candidates::::count(), 1); + assert_eq!(Candidates::::get(3), Some(CandidateInfo { stake: 0, stakers: 0 })); + assert_eq!(CollatorStaking::get_bond(&3), 10); + + // the candidate is replaced (artificially) + assert_ok!(CollatorStaking::leave_intent(RuntimeOrigin::signed(3))); + CandidacyBondReleases::::mutate(3, |maybe_bond_release| { + let bond_release = maybe_bond_release.as_mut().unwrap(); + bond_release.reason = CandidacyBondReleaseReason::Replaced; + }); + assert_eq!(CollatorStaking::get_releasing_balance(&3), 10); + assert_eq!(CollatorStaking::get_bond(&3), 0); + + // and finally rejoins using the old candidacy bond + assert_ok!(CollatorStaking::register_as_candidate( + RuntimeOrigin::signed(3), + MinCandidacyBond::::get() + )); + assert_eq!(CollatorStaking::get_releasing_balance(&3), 0); + assert_eq!(CollatorStaking::get_bond(&3), 10); + }); + } + + #[test] + fn register_as_candidate_does_not_reuse_old_bond_if_wrong_reason() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + + // given + assert_eq!(DesiredCandidates::::get(), 2); + assert_eq!(MinCandidacyBond::::get(), 10); + assert_eq!(Candidates::::count(), 0); + assert_eq!(Invulnerables::::get(), vec![1, 2]); + + // register the first time + assert_eq!(Balances::balance(&3), 100); + assert_eq!( + CandidateStake::::get(3, 3), + CandidateStakeInfo { stake: 0, session: 0 } + ); + register_candidates(3..=3); + assert_eq!(Balances::balance_frozen(&FreezeReason::CandidacyBond.into(), &3), 10); + assert_eq!( + CandidateStake::::get(3, 3), + CandidateStakeInfo { stake: 0, session: 0 } + ); + assert_eq!(Candidates::::count(), 1); + assert_eq!(Candidates::::get(3), Some(CandidateInfo { stake: 0, stakers: 0 })); + assert_eq!(CollatorStaking::get_bond(&3), 10); + + // the candidate removes itself + assert_ok!(CollatorStaking::leave_intent(RuntimeOrigin::signed(3))); + assert_eq!(CollatorStaking::get_releasing_balance(&3), 10); + assert_eq!(CollatorStaking::get_bond(&3), 0); + + // and finally rejoins using the old candidacy bond + assert_ok!(CollatorStaking::register_as_candidate( + RuntimeOrigin::signed(3), + MinCandidacyBond::::get() + )); + // the old locked candidacy bond should remain + assert_eq!(CollatorStaking::get_releasing_balance(&3), 10); + assert_eq!(CollatorStaking::get_bond(&3), 10); + }); + } } mod leave_intent { @@ -873,20 +1010,50 @@ mod leave_intent { // Unstake request is created assert_eq!(ReleaseQueues::::get(3), vec![]); assert_eq!(Balances::balance_frozen(&FreezeReason::CandidacyBond.into(), &3), 10); + + assert_eq!(CandidacyBondReleases::::get(3), None); assert_ok!(CollatorStaking::leave_intent(RuntimeOrigin::signed(3))); + assert_eq!( + CandidacyBondReleases::::get(3), + Some(CandidacyBondRelease { + bond: 10, + block: 6, + reason: CandidacyBondReleaseReason::Left + }) + ); - let release_request = ReleaseRequest { - block: 6, // higher delay - amount: 10, - }; assert_eq!(Balances::balance_frozen(&FreezeReason::CandidacyBond.into(), &3), 0); assert_eq!(Balances::balance_frozen(&FreezeReason::Releasing.into(), &3), 10); assert_eq!( CandidateStake::::get(3, 3), CandidateStakeInfo { stake: 0, session: 0 } ); - assert_eq!(ReleaseQueues::::get(3), vec![release_request]); assert_eq!(LastAuthoredBlock::::get(3), 0); + assert_eq!( + SessionRemovedCandidates::::get(3), + Some(CandidateInfo { stake: 0, stakers: 0 }) + ); + }); + } + + #[test] + fn leave_with_release_queue_full_should_work() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + + register_candidates(3..=3); + + assert_eq!(ReleaseQueues::::get(3), vec![]); + let release_queue_max_len = ::MaxStakedCandidates::get(); + assert_ok!(CollatorStaking::lock( + RuntimeOrigin::signed(3), + (release_queue_max_len * 2) as u64 + )); + for _ in 0..release_queue_max_len { + assert_ok!(CollatorStaking::unlock(RuntimeOrigin::signed(3), Some(2))); + } + assert_eq!(ReleaseQueues::::get(3).len() as u32, release_queue_max_len); + assert_ok!(CollatorStaking::leave_intent(RuntimeOrigin::signed(3))); }); } } @@ -917,6 +1084,46 @@ mod stake { }); } + #[test] + fn cannot_stake_if_recently_unstaked() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + + register_candidates(3..=3); + lock_for_staking(3..=3); + assert_ok!(CollatorStaking::stake( + RuntimeOrigin::signed(3), + vec![StakeTarget { candidate: 3, stake: 20 }].try_into().unwrap() + )); + assert_eq!( + UserStake::::get(3), + UserStakeInfo { + stake: 20, + candidates: bbtreeset![3], + maybe_last_unstake: None, + maybe_last_reward_session: Some(0), + } + ); + assert_ok!(CollatorStaking::unstake_from(RuntimeOrigin::signed(3), 3)); + assert_eq!( + UserStake::::get(3), + UserStakeInfo { + stake: 0, + candidates: BoundedBTreeSet::new(), + maybe_last_unstake: Some((20, 11)), + maybe_last_reward_session: None, + } + ); + assert_noop!( + CollatorStaking::stake( + RuntimeOrigin::signed(4), + vec![StakeTarget { candidate: 3, stake: 20 }].try_into().unwrap() + ), + Error::::InsufficientLockedBalance + ); + }); + } + #[test] fn cannot_stake_if_under_minstake() { new_test_ext().execute_with(|| { @@ -975,6 +1182,7 @@ mod stake { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -986,6 +1194,7 @@ mod stake { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1014,6 +1223,7 @@ mod stake { UserStakeInfo { stake: 20, candidates: bbtreeset![3], + maybe_last_unstake: None, maybe_last_reward_session: Some(0) } ); @@ -1042,6 +1252,7 @@ mod stake { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1078,6 +1289,7 @@ mod stake { UserStakeInfo { stake: 40, candidates: bbtreeset![3, 4], + maybe_last_unstake: None, maybe_last_reward_session: Some(0) } ); @@ -1106,6 +1318,7 @@ mod stake { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1225,6 +1438,7 @@ mod stake { UserStakeInfo { stake: 32, candidates: bbtreeset![3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], + maybe_last_unstake: None, maybe_last_reward_session: Some(0) } ); @@ -1293,6 +1507,7 @@ mod stake { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1337,6 +1552,7 @@ mod claim_rewards { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1377,6 +1593,7 @@ mod unstake_from { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1403,6 +1620,7 @@ mod unstake_from { UserStakeInfo { stake: 30, candidates: bbtreeset![3, 4], + maybe_last_unstake: None, maybe_last_reward_session: Some(0) } ); @@ -1425,6 +1643,7 @@ mod unstake_from { UserStakeInfo { stake: 10, candidates: bbtreeset![4], + maybe_last_unstake: Some((20, 11)), maybe_last_reward_session: Some(0) } ); @@ -1451,6 +1670,7 @@ mod unstake_from { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1465,6 +1685,7 @@ mod unstake_from { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1479,6 +1700,7 @@ mod unstake_from { UserStakeInfo { stake: 20, candidates: bbtreeset![3], + maybe_last_unstake: None, maybe_last_reward_session: Some(0), } ); @@ -1493,6 +1715,7 @@ mod unstake_from { UserStakeInfo { stake: 30, candidates: bbtreeset![3, 4], + maybe_last_unstake: None, maybe_last_reward_session: Some(0), } ); @@ -1524,6 +1747,7 @@ mod unstake_from { UserStakeInfo { stake: 10, candidates: bbtreeset![4], + maybe_last_unstake: Some((20, 11)), maybe_last_reward_session: Some(0), } ); @@ -1556,6 +1780,7 @@ mod unstake_from { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1590,6 +1815,7 @@ mod unstake_from { UserStakeInfo { stake: 30, candidates: bbtreeset![3, 4], + maybe_last_unstake: None, maybe_last_reward_session: Some(0), } ); @@ -1602,6 +1828,7 @@ mod unstake_from { UserStakeInfo { stake: 30, candidates: bbtreeset![3, 4], + maybe_last_unstake: None, maybe_last_reward_session: Some(0), } ); @@ -1622,6 +1849,7 @@ mod unstake_from { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1661,6 +1889,7 @@ mod unstake_all { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1685,6 +1914,7 @@ mod unstake_all { UserStakeInfo { stake: 30, candidates: bbtreeset![3, 4], + maybe_last_unstake: None, maybe_last_reward_session: Some(0), } ); @@ -1697,6 +1927,7 @@ mod unstake_all { UserStakeInfo { stake: 30, candidates: bbtreeset![3, 4], + maybe_last_unstake: None, maybe_last_reward_session: Some(0), } ); @@ -1726,6 +1957,7 @@ mod unstake_all { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: Some((30, 11)), maybe_last_reward_session: None, } ); @@ -1746,6 +1978,7 @@ mod unstake_all { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -1822,6 +2055,7 @@ mod set_autocompound_percentage { UserStakeInfo { stake: 0, candidates: BoundedBTreeSet::new(), + maybe_last_unstake: None, maybe_last_reward_session: None, } ); @@ -2376,6 +2610,7 @@ mod collator_rewards { })); }); } + #[test] fn should_reward_collator() { new_test_ext().execute_with(|| { @@ -2442,6 +2677,10 @@ mod collator_rewards { Balances::free_balance(CollatorStaking::account_id()) - Balances::minimum_balance(), 18 ); + // we can safely remove the collator, as rewards will be delivered anyway to both + // the collator itself and its stakers. + assert_ok!(CollatorStaking::leave_intent(RuntimeOrigin::signed(4))); + initialize_to_block(20); System::assert_has_event(RuntimeEvent::CollatorStaking(Event::SessionEnded { index: 1, @@ -2491,22 +2730,53 @@ mod collator_rewards { new_test_ext().execute_with(|| { initialize_to_block(1); - let max_rewards = ::MaxSessionRewards::get(); + let max_rewards = ::MaxRewardSessions::get(); assert_eq!(max_rewards, 10); register_candidates(3..=3); - for session in 0..max_rewards { - initialize_to_block(((session + 1) * 10) as u64); - assert_eq!(PerSessionRewards::::count(), session + 1); + lock_for_staking(3..=3); + assert_ok!(CollatorStaking::stake( + RuntimeOrigin::signed(3), + vec![StakeTarget { candidate: 3, stake: 60 }].try_into().unwrap() + )); + + // There are no rewards for first session, so we move one forward. + initialize_to_block(10); + + for session in 1..11 { + Balances::mint_into(&CollatorStaking::account_id(), 100).unwrap(); + ProducedBlocks::::insert(3, 10); + let current_block = ((session + 1) * 10) as u64; + LastAuthoredBlock::::insert(3, current_block); + initialize_to_block(current_block); + assert_eq!(PerSessionRewards::::count(), session); } - assert_eq!(CollatorStaking::current_block_number(), 100); + assert_eq!(CollatorStaking::current_block_number(), 110); assert_eq!(PerSessionRewards::::count(), 10); - assert!(PerSessionRewards::::get(0).is_some()); - // now rewards for session zero should be removed - initialize_to_block(110); - assert_eq!(CollatorStaking::current_block_number(), 110); + // now rewards for session one should be removed + assert!(PerSessionRewards::::get(1).is_some()); + Balances::mint_into(&CollatorStaking::account_id(), 100).unwrap(); + ProducedBlocks::::insert(3, 10); + LastAuthoredBlock::::insert(3, 120); + initialize_to_block(120); + assert_eq!(CollatorStaking::current_block_number(), 120); assert_eq!(PerSessionRewards::::count(), 10); - assert!(PerSessionRewards::::get(0).is_none()); + assert!(PerSessionRewards::::get(1).is_none()); + }); + } + + #[test] + fn should_not_insert_rewards_of_zero_value() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + + let max_rewards = ::MaxRewardSessions::get(); + assert_eq!(max_rewards, 10); + register_candidates(3..=3); + for session in 0..max_rewards { + initialize_to_block(((session + 1) * 10) as u64); + assert_eq!(PerSessionRewards::::count(), 0); + } }); } @@ -3220,8 +3490,12 @@ mod session_management { assert_eq!(Balances::balance_frozen(&FreezeReason::CandidacyBond.into(), &3), 0); assert_eq!(Balances::balance_frozen(&FreezeReason::Releasing.into(), &3), 10); assert_eq!( - ReleaseQueues::::get(3), - vec![ReleaseRequest { block: 25, amount: 10 }] + CandidacyBondReleases::::get(3), + Some(CandidacyBondRelease { + bond: 10, + block: 25, + reason: CandidacyBondReleaseReason::Idle + }) ); }); } @@ -3270,8 +3544,12 @@ mod session_management { // kicked collator gets funds back after a delay assert_eq!(Balances::balance_frozen(&FreezeReason::Releasing.into(), &5), 10); assert_eq!( - ReleaseQueues::::get(5), - vec![ReleaseRequest { block: 25, amount: 10 }] + CandidacyBondReleases::::get(5), + Some(CandidacyBondRelease { + bond: 10, + block: 25, + reason: CandidacyBondReleaseReason::Idle + }) ); }); }