From cb4f684221191de4a9f0fc6fa4f5e7087765ca26 Mon Sep 17 00:00:00 2001 From: Peter White Date: Fri, 18 Oct 2024 16:59:50 -0600 Subject: [PATCH 1/8] feat: add pallet revive and use revive for pop api (#345) --- Cargo.lock | 296 +- Cargo.toml | 32 +- extension/Cargo.toml | 8 +- extension/src/decoding.rs | 8 +- extension/src/environment.rs | 53 +- extension/src/functions.rs | 24 +- extension/src/lib.rs | 23 +- extension/src/mock.rs | 21 +- pallets/revive/Cargo.toml | 122 + pallets/revive/README.md | 104 + pallets/revive/fixtures/Cargo.toml | 36 + pallets/revive/fixtures/build.rs | 217 + pallets/revive/fixtures/build/Cargo.toml | 19 + pallets/revive/fixtures/contracts/balance.rs | 33 + .../revive/fixtures/contracts/balance_of.rs | 36 + pallets/revive/fixtures/contracts/call.rs | 49 + .../contracts/call_diverging_out_len.rs | 110 + .../fixtures/contracts/call_return_code.rs | 57 + .../revive/fixtures/contracts/call_runtime.rs | 42 + .../contracts/call_runtime_and_call.rs | 53 + .../contracts/call_with_flags_and_value.rs | 51 + .../fixtures/contracts/call_with_limit.rs | 52 + .../fixtures/contracts/caller_contract.rs | 153 + .../fixtures/contracts/caller_is_origin_n.rs | 38 + .../fixtures/contracts/chain_extension.rs | 42 + .../contracts/chain_extension_temp_storage.rs | 66 + pallets/revive/fixtures/contracts/chain_id.rs | 37 + .../fixtures/contracts/common/Cargo.toml | 11 + .../fixtures/contracts/common/src/lib.rs | 196 + .../fixtures/contracts/create1_with_value.rs | 39 + .../contracts/create_storage_and_call.rs | 58 + .../create_storage_and_instantiate.rs | 57 + .../create_transient_storage_and_call.rs | 60 + .../fixtures/contracts/crypto_hashes.rs | 84 + .../contracts/debug_message_invalid_utf8.rs | 33 + .../debug_message_logging_disabled.rs | 33 + .../fixtures/contracts/debug_message_works.rs | 33 + .../fixtures/contracts/delegate_call.rs | 49 + .../fixtures/contracts/delegate_call_lib.rs | 49 + .../contracts/delegate_call_simple.rs | 36 + .../contracts/destroy_and_transfer.rs | 86 + pallets/revive/fixtures/contracts/drain.rs | 41 + pallets/revive/fixtures/contracts/dummy.rs | 42 + .../fixtures/contracts/ecdsa_recover.rs | 44 + .../contracts/event_and_return_on_deploy.rs | 37 + .../revive/fixtures/contracts/event_size.rs | 39 + .../fixtures/contracts/float_instruction.rs | 34 + .../contracts/instantiate_return_code.rs | 52 + .../fixtures/contracts/instr_benchmark.rs | 41 + .../contracts/locking_delegate_dependency.rs | 68 + .../revive/fixtures/contracts/multi_store.rs | 43 + .../contracts/new_set_code_hash_contract.rs | 32 + pallets/revive/fixtures/contracts/noop.rs | 44 + .../fixtures/contracts/ok_trap_revert.rs | 44 + pallets/revive/fixtures/contracts/oom_ro.rs | 44 + .../fixtures/contracts/oom_rw_included.rs | 44 + .../fixtures/contracts/oom_rw_trailing.rs | 44 + .../fixtures/contracts/read_only_call.rs | 50 + pallets/revive/fixtures/contracts/recurse.rs | 54 + .../fixtures/contracts/return_data_api.rs | 166 + .../fixtures/contracts/return_with_data.rs | 47 + .../fixtures/contracts/run_out_of_gas.rs | 32 + .../fixtures/contracts/self_destruct.rs | 57 + .../contracts/self_destructing_constructor.rs | 32 + .../fixtures/contracts/set_code_hash.rs | 37 + .../fixtures/contracts/set_empty_storage.rs | 32 + .../contracts/set_transient_storage.rs | 41 + .../fixtures/contracts/sr25519_verify.rs | 48 + pallets/revive/fixtures/contracts/storage.rs | 63 + .../revive/fixtures/contracts/storage_size.rs | 50 + .../revive/fixtures/contracts/store_call.rs | 42 + .../revive/fixtures/contracts/store_deploy.rs | 42 + .../contracts/transfer_return_code.rs | 38 + .../fixtures/contracts/transient_storage.rs | 55 + .../revive/fixtures/contracts/xcm_execute.rs | 40 + pallets/revive/fixtures/contracts/xcm_send.rs | 42 + pallets/revive/fixtures/src/lib.rs | 74 + pallets/revive/mock-network/Cargo.toml | 89 + pallets/revive/mock-network/src/lib.rs | 152 + pallets/revive/mock-network/src/mocks.rs | 18 + .../mock-network/src/mocks/msg_queue.rs | 186 + .../src/mocks/relay_message_queue.rs | 52 + pallets/revive/mock-network/src/parachain.rs | 346 ++ .../src/parachain/contracts_config.rs | 27 + pallets/revive/mock-network/src/primitives.rs | 23 + .../revive/mock-network/src/relay_chain.rs | 239 + pallets/revive/mock-network/src/tests.rs | 204 + pallets/revive/proc-macro/Cargo.toml | 23 + pallets/revive/proc-macro/src/lib.rs | 617 +++ pallets/revive/src/address.rs | 130 + .../revive/src/benchmarking/call_builder.rs | 224 + pallets/revive/src/benchmarking/code.rs | 70 + pallets/revive/src/benchmarking/mod.rs | 1748 +++++++ pallets/revive/src/benchmarking_dummy.rs | 38 + pallets/revive/src/chain_extension.rs | 359 ++ pallets/revive/src/debug.rs | 110 + pallets/revive/src/exec.rs | 4290 ++++++++++++++++ pallets/revive/src/gas.rs | 417 ++ pallets/revive/src/lib.rs | 1220 +++++ pallets/revive/src/limits.rs | 148 + pallets/revive/src/primitives.rs | 285 ++ pallets/revive/src/storage.rs | 486 ++ pallets/revive/src/storage/meter.rs | 886 ++++ pallets/revive/src/test_utils.rs | 67 + pallets/revive/src/test_utils/builder.rs | 227 + pallets/revive/src/tests.rs | 4362 +++++++++++++++++ pallets/revive/src/tests/pallet_dummy.rs | 53 + pallets/revive/src/tests/test_debug.rs | 246 + pallets/revive/src/transient_storage.rs | 692 +++ pallets/revive/src/wasm/mod.rs | 364 ++ pallets/revive/src/wasm/runtime.rs | 2044 ++++++++ pallets/revive/src/weights.rs | 1770 +++++++ pallets/revive/uapi/Cargo.toml | 31 + pallets/revive/uapi/src/flags.rs | 90 + pallets/revive/uapi/src/host.rs | 643 +++ pallets/revive/uapi/src/host/riscv32.rs | 568 +++ pallets/revive/uapi/src/lib.rs | 132 + pop-api/Cargo.toml | 15 +- pop-api/integration-tests/Cargo.toml | 12 +- pop-api/integration-tests/build.rs | 58 - .../create_token_in_constructor/Cargo.toml | 4 +- .../create_token_in_constructor/lib.rs | 38 +- .../contracts/fungibles/Cargo.toml | 6 +- .../contracts/fungibles/lib.rs | 20 +- .../integration-tests/src/fungibles/mod.rs | 153 +- .../integration-tests/src/fungibles/utils.rs | 56 +- pop-api/integration-tests/src/lib.rs | 29 +- runtime/devnet/Cargo.toml | 8 + runtime/devnet/src/config/api/mod.rs | 9 +- runtime/devnet/src/config/api/versioning.rs | 14 +- runtime/devnet/src/config/contracts.rs | 90 - runtime/devnet/src/config/mod.rs | 3 +- runtime/devnet/src/config/revive.rs | 51 + runtime/devnet/src/lib.rs | 71 +- 134 files changed, 28237 insertions(+), 417 deletions(-) create mode 100644 pallets/revive/Cargo.toml create mode 100644 pallets/revive/README.md create mode 100644 pallets/revive/fixtures/Cargo.toml create mode 100644 pallets/revive/fixtures/build.rs create mode 100644 pallets/revive/fixtures/build/Cargo.toml create mode 100644 pallets/revive/fixtures/contracts/balance.rs create mode 100644 pallets/revive/fixtures/contracts/balance_of.rs create mode 100644 pallets/revive/fixtures/contracts/call.rs create mode 100644 pallets/revive/fixtures/contracts/call_diverging_out_len.rs create mode 100644 pallets/revive/fixtures/contracts/call_return_code.rs create mode 100644 pallets/revive/fixtures/contracts/call_runtime.rs create mode 100644 pallets/revive/fixtures/contracts/call_runtime_and_call.rs create mode 100644 pallets/revive/fixtures/contracts/call_with_flags_and_value.rs create mode 100644 pallets/revive/fixtures/contracts/call_with_limit.rs create mode 100644 pallets/revive/fixtures/contracts/caller_contract.rs create mode 100644 pallets/revive/fixtures/contracts/caller_is_origin_n.rs create mode 100644 pallets/revive/fixtures/contracts/chain_extension.rs create mode 100644 pallets/revive/fixtures/contracts/chain_extension_temp_storage.rs create mode 100644 pallets/revive/fixtures/contracts/chain_id.rs create mode 100644 pallets/revive/fixtures/contracts/common/Cargo.toml create mode 100644 pallets/revive/fixtures/contracts/common/src/lib.rs create mode 100644 pallets/revive/fixtures/contracts/create1_with_value.rs create mode 100644 pallets/revive/fixtures/contracts/create_storage_and_call.rs create mode 100644 pallets/revive/fixtures/contracts/create_storage_and_instantiate.rs create mode 100644 pallets/revive/fixtures/contracts/create_transient_storage_and_call.rs create mode 100644 pallets/revive/fixtures/contracts/crypto_hashes.rs create mode 100644 pallets/revive/fixtures/contracts/debug_message_invalid_utf8.rs create mode 100644 pallets/revive/fixtures/contracts/debug_message_logging_disabled.rs create mode 100644 pallets/revive/fixtures/contracts/debug_message_works.rs create mode 100644 pallets/revive/fixtures/contracts/delegate_call.rs create mode 100644 pallets/revive/fixtures/contracts/delegate_call_lib.rs create mode 100644 pallets/revive/fixtures/contracts/delegate_call_simple.rs create mode 100644 pallets/revive/fixtures/contracts/destroy_and_transfer.rs create mode 100644 pallets/revive/fixtures/contracts/drain.rs create mode 100644 pallets/revive/fixtures/contracts/dummy.rs create mode 100644 pallets/revive/fixtures/contracts/ecdsa_recover.rs create mode 100644 pallets/revive/fixtures/contracts/event_and_return_on_deploy.rs create mode 100644 pallets/revive/fixtures/contracts/event_size.rs create mode 100644 pallets/revive/fixtures/contracts/float_instruction.rs create mode 100644 pallets/revive/fixtures/contracts/instantiate_return_code.rs create mode 100644 pallets/revive/fixtures/contracts/instr_benchmark.rs create mode 100644 pallets/revive/fixtures/contracts/locking_delegate_dependency.rs create mode 100644 pallets/revive/fixtures/contracts/multi_store.rs create mode 100644 pallets/revive/fixtures/contracts/new_set_code_hash_contract.rs create mode 100644 pallets/revive/fixtures/contracts/noop.rs create mode 100644 pallets/revive/fixtures/contracts/ok_trap_revert.rs create mode 100644 pallets/revive/fixtures/contracts/oom_ro.rs create mode 100644 pallets/revive/fixtures/contracts/oom_rw_included.rs create mode 100644 pallets/revive/fixtures/contracts/oom_rw_trailing.rs create mode 100644 pallets/revive/fixtures/contracts/read_only_call.rs create mode 100644 pallets/revive/fixtures/contracts/recurse.rs create mode 100644 pallets/revive/fixtures/contracts/return_data_api.rs create mode 100644 pallets/revive/fixtures/contracts/return_with_data.rs create mode 100644 pallets/revive/fixtures/contracts/run_out_of_gas.rs create mode 100644 pallets/revive/fixtures/contracts/self_destruct.rs create mode 100644 pallets/revive/fixtures/contracts/self_destructing_constructor.rs create mode 100644 pallets/revive/fixtures/contracts/set_code_hash.rs create mode 100644 pallets/revive/fixtures/contracts/set_empty_storage.rs create mode 100644 pallets/revive/fixtures/contracts/set_transient_storage.rs create mode 100644 pallets/revive/fixtures/contracts/sr25519_verify.rs create mode 100644 pallets/revive/fixtures/contracts/storage.rs create mode 100644 pallets/revive/fixtures/contracts/storage_size.rs create mode 100644 pallets/revive/fixtures/contracts/store_call.rs create mode 100644 pallets/revive/fixtures/contracts/store_deploy.rs create mode 100644 pallets/revive/fixtures/contracts/transfer_return_code.rs create mode 100644 pallets/revive/fixtures/contracts/transient_storage.rs create mode 100644 pallets/revive/fixtures/contracts/xcm_execute.rs create mode 100644 pallets/revive/fixtures/contracts/xcm_send.rs create mode 100644 pallets/revive/fixtures/src/lib.rs create mode 100644 pallets/revive/mock-network/Cargo.toml create mode 100644 pallets/revive/mock-network/src/lib.rs create mode 100644 pallets/revive/mock-network/src/mocks.rs create mode 100644 pallets/revive/mock-network/src/mocks/msg_queue.rs create mode 100644 pallets/revive/mock-network/src/mocks/relay_message_queue.rs create mode 100644 pallets/revive/mock-network/src/parachain.rs create mode 100644 pallets/revive/mock-network/src/parachain/contracts_config.rs create mode 100644 pallets/revive/mock-network/src/primitives.rs create mode 100644 pallets/revive/mock-network/src/relay_chain.rs create mode 100644 pallets/revive/mock-network/src/tests.rs create mode 100644 pallets/revive/proc-macro/Cargo.toml create mode 100644 pallets/revive/proc-macro/src/lib.rs create mode 100644 pallets/revive/src/address.rs create mode 100644 pallets/revive/src/benchmarking/call_builder.rs create mode 100644 pallets/revive/src/benchmarking/code.rs create mode 100644 pallets/revive/src/benchmarking/mod.rs create mode 100644 pallets/revive/src/benchmarking_dummy.rs create mode 100644 pallets/revive/src/chain_extension.rs create mode 100644 pallets/revive/src/debug.rs create mode 100644 pallets/revive/src/exec.rs create mode 100644 pallets/revive/src/gas.rs create mode 100644 pallets/revive/src/lib.rs create mode 100644 pallets/revive/src/limits.rs create mode 100644 pallets/revive/src/primitives.rs create mode 100644 pallets/revive/src/storage.rs create mode 100644 pallets/revive/src/storage/meter.rs create mode 100644 pallets/revive/src/test_utils.rs create mode 100644 pallets/revive/src/test_utils/builder.rs create mode 100644 pallets/revive/src/tests.rs create mode 100644 pallets/revive/src/tests/pallet_dummy.rs create mode 100644 pallets/revive/src/tests/test_debug.rs create mode 100644 pallets/revive/src/transient_storage.rs create mode 100644 pallets/revive/src/wasm/mod.rs create mode 100644 pallets/revive/src/wasm/runtime.rs create mode 100644 pallets/revive/src/weights.rs create mode 100644 pallets/revive/uapi/Cargo.toml create mode 100644 pallets/revive/uapi/src/flags.rs create mode 100644 pallets/revive/uapi/src/host.rs create mode 100644 pallets/revive/uapi/src/host/riscv32.rs create mode 100644 pallets/revive/uapi/src/lib.rs delete mode 100644 pop-api/integration-tests/build.rs delete mode 100644 runtime/devnet/src/config/contracts.rs create mode 100644 runtime/devnet/src/config/revive.rs diff --git a/Cargo.lock b/Cargo.lock index 3f1349eb..ce9196c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -822,7 +822,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.36.0", + "object 0.36.4", "rustc-demangle", ] @@ -3094,6 +3094,12 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difflib" version = "0.4.0" @@ -3966,7 +3972,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", ] [[package]] @@ -4033,7 +4039,7 @@ dependencies = [ "sp-staking", "sp-state-machine", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", "sp-weights", "static_assertions", "tt-call", @@ -5664,6 +5670,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "libc" version = "0.2.155" @@ -7195,9 +7207,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -7559,7 +7571,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", ] [[package]] @@ -7862,7 +7874,7 @@ dependencies = [ "bitflags 1.3.2", "parity-scale-codec", "paste", - "polkavm-derive", + "polkavm-derive 0.9.1", "scale-info", ] @@ -8302,7 +8314,7 @@ dependencies = [ "sp-runtime", "sp-staking", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", ] [[package]] @@ -8491,6 +8503,83 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-revive" +version = "0.1.0" +dependencies = [ + "array-bytes", + "assert_matches", + "bitflags 1.3.2", + "environmental", + "frame-benchmarking", + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "log", + "pallet-assets", + "pallet-balances", + "pallet-message-queue", + "pallet-proxy", + "pallet-revive-fixtures", + "pallet-revive-proc-macro", + "pallet-revive-uapi", + "pallet-timestamp", + "pallet-utility", + "parity-scale-codec", + "paste", + "polkavm 0.11.0", + "pretty_assertions", + "rlp", + "scale-info", + "serde", + "sp-api", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", + "sp-std", + "sp-tracing 16.0.0", + "staging-xcm", + "staging-xcm-builder", + "wat", +] + +[[package]] +name = "pallet-revive-fixtures" +version = "0.1.0" +dependencies = [ + "anyhow", + "frame-system", + "log", + "parity-wasm", + "polkavm-linker 0.11.0", + "sp-core", + "sp-io", + "sp-runtime", + "tempfile", + "toml 0.8.14", +] + +[[package]] +name = "pallet-revive-proc-macro" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "pallet-revive-uapi" +version = "0.1.0" +dependencies = [ + "bitflags 1.3.2", + "parity-scale-codec", + "paste", + "polkavm-derive 0.11.0", + "scale-info", +] + [[package]] name = "pallet-root-testing" version = "12.0.0" @@ -8973,7 +9062,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", "staging-parachain-info", "staging-xcm", "staging-xcm-executor", @@ -9866,7 +9955,7 @@ dependencies = [ "sp-crypto-hashing", "sp-externalities", "sp-io", - "sp-tracing", + "sp-tracing 17.0.0", "thiserror", "tracing-gum", ] @@ -10338,7 +10427,7 @@ dependencies = [ "parity-scale-codec", "polkadot-primitives", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", ] [[package]] @@ -10553,9 +10642,22 @@ checksum = "8a3693e5efdb2bf74e449cd25fd777a28bd7ed87e41f5d5da75eb31b4de48b94" dependencies = [ "libc", "log", - "polkavm-assembler", - "polkavm-common", - "polkavm-linux-raw", + "polkavm-assembler 0.9.0", + "polkavm-common 0.9.0", + "polkavm-linux-raw 0.9.0", +] + +[[package]] +name = "polkavm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1195fbc12f11645143a4f3974bf909d25c7f7efddcc6f4e57688d9a518c90bae" +dependencies = [ + "libc", + "log", + "polkavm-assembler 0.11.0", + "polkavm-common 0.11.0", + "polkavm-linux-raw 0.11.0", ] [[package]] @@ -10567,6 +10669,15 @@ dependencies = [ "log", ] +[[package]] +name = "polkavm-assembler" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b0399659fe7a5370c3e3464188888d29069cfa46d99631d19834a379c15826" +dependencies = [ + "log", +] + [[package]] name = "polkavm-common" version = "0.9.0" @@ -10576,13 +10687,32 @@ dependencies = [ "log", ] +[[package]] +name = "polkavm-common" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254b19b64ff9b57c06b32c0affed961cb9a32429b8d3e5cf2633cad7fbb3e270" +dependencies = [ + "log", + "polkavm-assembler 0.11.0", +] + [[package]] name = "polkavm-derive" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae8c4bea6f3e11cd89bb18bcdddac10bd9a24015399bd1c485ad68a985a19606" dependencies = [ - "polkavm-derive-impl-macro", + "polkavm-derive-impl-macro 0.9.0", +] + +[[package]] +name = "polkavm-derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f565f0106fbb3598d64b8528d5cb966b6a87a8dd93fbcfe09fb6388ff2865711" +dependencies = [ + "polkavm-derive-impl-macro 0.11.0", ] [[package]] @@ -10591,7 +10721,19 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c4fdfc49717fb9a196e74a5d28e0bc764eb394a2c803eb11133a31ac996c60c" dependencies = [ - "polkavm-common", + "polkavm-common 0.9.0", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "314445fb5688b4769354087d92be2ac94c487e63ffe74a6fb7bb312e57f20827" +dependencies = [ + "polkavm-common 0.11.0", "proc-macro2", "quote", "syn 2.0.66", @@ -10603,7 +10745,17 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ba81f7b5faac81e528eb6158a6f3c9e0bb1008e0ffa19653bc8dea925ecb429" dependencies = [ - "polkavm-derive-impl", + "polkavm-derive-impl 0.9.0", + "syn 2.0.66", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf952e05bc5ce7d81293bae18cb44c271c78615b201d75e983cdcc40d5c6ef1" +dependencies = [ + "polkavm-derive-impl 0.11.0", "syn 2.0.66", ] @@ -10617,7 +10769,22 @@ dependencies = [ "hashbrown 0.14.5", "log", "object 0.32.2", - "polkavm-common", + "polkavm-common 0.9.0", + "regalloc2 0.9.3", + "rustc-demangle", +] + +[[package]] +name = "polkavm-linker" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535a2095a186ccde2cd2fa721d6370c495586d47714817565b2f6621d31164b3" +dependencies = [ + "gimli 0.28.1", + "hashbrown 0.14.5", + "log", + "object 0.36.4", + "polkavm-common 0.11.0", "regalloc2 0.9.3", "rustc-demangle", ] @@ -10628,6 +10795,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26e85d3456948e650dff0cfc85603915847faf893ed1e66b020bb82ef4557120" +[[package]] +name = "polkavm-linux-raw" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011697430dfcfe800d1d7c540ef69e3bdd66e9037cc38f01fee1c2e0908011e" + [[package]] name = "polling" version = "2.8.0" @@ -10693,7 +10866,7 @@ dependencies = [ "impl-trait-for-tuples", "log", "pallet-balances", - "pallet-contracts", + "pallet-revive", "pallet-timestamp", "parity-scale-codec", "rand", @@ -10842,6 +11015,7 @@ dependencies = [ "pallet-nfts-runtime-api", "pallet-preimage", "pallet-proxy", + "pallet-revive", "pallet-scheduler", "pallet-session", "pallet-sudo", @@ -11080,6 +11254,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.1.25" @@ -12351,7 +12535,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-state-machine", - "sp-tracing", + "sp-tracing 17.0.0", ] [[package]] @@ -12770,7 +12954,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3b703a33dcb7cddf19176fdf12294b9a6408125836b0f4afee3e6969e7f190" dependencies = [ - "polkavm", + "polkavm 0.9.3", "sc-allocator", "sp-maybe-compressed-blob", "sp-wasm-interface", @@ -12785,7 +12969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26fe58d9cacfab73e5595fa84b80f7bd03efebe54a0574daaeb221a1d1f7ab80" dependencies = [ "log", - "polkavm", + "polkavm 0.9.3", "sc-executor-common", "sp-wasm-interface", ] @@ -13394,7 +13578,7 @@ dependencies = [ "sp-core", "sp-rpc", "sp-runtime", - "sp-tracing", + "sp-tracing 17.0.0", "thiserror", "tracing", "tracing-log 0.2.0", @@ -13435,7 +13619,7 @@ dependencies = [ "sp-core", "sp-crypto-hashing", "sp-runtime", - "sp-tracing", + "sp-tracing 17.0.0", "sp-transaction-pool", "substrate-prometheus-endpoint", "thiserror", @@ -14715,7 +14899,7 @@ dependencies = [ "libsecp256k1", "log", "parity-scale-codec", - "polkavm-derive", + "polkavm-derive 0.9.1", "rustversion", "secp256k1", "sp-core", @@ -14725,7 +14909,7 @@ dependencies = [ "sp-runtime-interface", "sp-state-machine", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", "sp-trie", "tracing", "tracing-core", @@ -14887,13 +15071,13 @@ dependencies = [ "bytes", "impl-trait-for-tuples", "parity-scale-codec", - "polkavm-derive", + "polkavm-derive 0.9.1", "primitive-types", "sp-externalities", "sp-runtime-interface-proc-macro", "sp-std", "sp-storage", - "sp-tracing", + "sp-tracing 17.0.0", "sp-wasm-interface", "static_assertions", ] @@ -15019,6 +15203,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sp-tracing" +version = "16.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0351810b9d074df71c4514c5228ed05c250607cba131c1c9d1526760ab69c05c" +dependencies = [ + "parity-scale-codec", + "sp-std", + "tracing", + "tracing-core", + "tracing-subscriber 0.2.25", +] + [[package]] name = "sp-tracing" version = "17.0.0" @@ -15508,12 +15705,12 @@ dependencies = [ "merkleized-metadata", "parity-scale-codec", "parity-wasm", - "polkavm-linker", + "polkavm-linker 0.9.2", "sc-executor", "sp-core", "sp-io", "sp-maybe-compressed-blob", - "sp-tracing", + "sp-tracing 17.0.0", "sp-version", "strum 0.26.2", "tempfile", @@ -16622,6 +16819,15 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-encoder" +version = "0.218.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b896fa8ceb71091ace9bcb81e853f54043183a1c9667cf93422c40252ffa0a" +dependencies = [ + "leb128", +] + [[package]] name = "wasm-instrument" version = "0.4.0" @@ -16971,6 +17177,28 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wast" +version = "218.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a53cd1f0fa505df97557e36a58bddb8296e2fcdcd089529545ebfdb18a1b9d7" +dependencies = [ + "bumpalo", + "leb128", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.218.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f87f8e14e776762e07927c27c2054d2cf678aab9aae2d431a79b3e31e4dd391" +dependencies = [ + "wast", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -17581,7 +17809,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "sp-tracing", + "sp-tracing 17.0.0", "staging-xcm", "staging-xcm-executor", ] @@ -17644,6 +17872,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index c6e15c69..ab623009 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,11 @@ license = "Unlicense" repository = "https://github.com/r0gue-io/pop-node/" [workspace] -exclude = [ "extension/contract", "pop-api", "tests/contracts" ] +exclude = [ + "extension/contract", + "pop-api", + "tests/contracts", +] members = [ "integration-tests", "node", @@ -194,3 +198,29 @@ ismp-parachain-runtime-api = { git = "https://github.com/r0gue-io/ismp", branch pallet-ismp = { git = "https://github.com/r0gue-io/ismp", branch = "polkadot-v1.14.0", default-features = false } pallet-ismp-rpc = { git = "https://github.com/r0gue-io/ismp", branch = "polkadot-v1.14.0", default-features = false } pallet-ismp-runtime-api = { git = "https://github.com/r0gue-io/ismp", branch = "polkadot-v1.14.0", default-features = false } + + +# revive dependencies +anyhow = { version = "1.0.81" } +array-bytes = { version = "6.2.2", default-features = false } +assert_matches = { version = "1.5.0" } +bitflags = { version = "1.3.2" } +environmental = { version = "1.1.4", default-features = false } +pallet-revive = { path = "pallets/revive", default-features = false } +pallet-revive-fixtures = { path = "pallets/revive/fixtures", default-features = false } +pallet-revive-mock-network = { default-features = false, path = "pallets/revive/mock-network" } +pallet-revive-proc-macro = { path = "pallets/revive/proc-macro", default-features = false } +pallet-revive-uapi = { path = "pallets/revive/uapi", default-features = false } +parity-wasm = { version = "0.45.0" } +paste = { version = "1.0.14", default-features = false } +pretty_assertions = { version = "1.3.0" } +proc-macro2 = { version = "1.0.64" } +quote = { version = "1.0.33" } +rlp = { version = "0.5.2", default-features = false } +sp-tracing = { version = "16.0.0", default-features = false } +syn = { version = "2.0.53" } +tempfile = { version = "3.8.1" } +toml = { version = "0.8.8" } +wat = { version = "1.0.0" } + +[workspace.lints.rust] diff --git a/extension/Cargo.toml b/extension/Cargo.toml index 6ca5e071..86c3f4b7 100644 --- a/extension/Cargo.toml +++ b/extension/Cargo.toml @@ -20,7 +20,7 @@ log.workspace = true # Substrate frame-support.workspace = true frame-system.workspace = true -pallet-contracts.workspace = true +pallet-revive.workspace = true sp-core.workspace = true sp-runtime.workspace = true sp-std.workspace = true @@ -29,7 +29,7 @@ sp-std.workspace = true contract-build.workspace = true env_logger.workspace = true pallet-balances.workspace = true -pallet-contracts.workspace = true +pallet-revive.workspace = true pallet-timestamp.workspace = true rand.workspace = true scale-info.workspace = true @@ -41,7 +41,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-balances/runtime-benchmarks", - "pallet-contracts/runtime-benchmarks", + "pallet-revive/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -51,7 +51,7 @@ std = [ "frame-system/std", "log/std", "pallet-balances/std", - "pallet-contracts/std", + "pallet-revive/std", "pallet-timestamp/std", "sp-core/std", "sp-io/std", diff --git a/extension/src/decoding.rs b/extension/src/decoding.rs index 7269a49a..10e2b5d2 100644 --- a/extension/src/decoding.rs +++ b/extension/src/decoding.rs @@ -94,9 +94,9 @@ impl< /// Error to be returned when decoding fails. pub struct DecodingFailed(PhantomData); -impl Get for DecodingFailed { +impl Get for DecodingFailed { fn get() -> DispatchError { - pallet_contracts::Error::::DecodingFailed.into() + pallet_revive::Error::::DecodingFailed.into() } } @@ -155,7 +155,7 @@ mod tests { fn decoding_failed_error_type_works() { assert_eq!( DecodingFailed::::get(), - pallet_contracts::Error::::DecodingFailed.into() + pallet_revive::Error::::DecodingFailed.into() ) } @@ -164,7 +164,7 @@ mod tests { let input = vec![100]; let mut env = MockEnvironment::new(0, input.clone()); let result = EnumDecodes::decode(&mut env); - assert_eq!(result, Err(pallet_contracts::Error::::DecodingFailed.into())); + assert_eq!(result, Err(pallet_revive::Error::::DecodingFailed.into())); } #[test] diff --git a/extension/src/environment.rs b/extension/src/environment.rs index dc3818e8..015e78c0 100644 --- a/extension/src/environment.rs +++ b/extension/src/environment.rs @@ -1,14 +1,19 @@ use core::fmt::Debug; +use codec::{Decode, Encode}; use frame_support::pallet_prelude::Weight; -use pallet_contracts::chain_extension::{BufInBufOutState, ChargedAmount, Result, State}; +use pallet_revive::{ + chain_extension::{ChargedAmount, Result}, + wasm::Memory, + AddressMapper, DefaultAddressMapper, +}; use sp_std::vec::Vec; use crate::AccountIdOf; /// Provides access to the parameters passed to a chain extension and its execution environment. /// -/// A wrapper trait for `pallet_contracts::chain_extension::Environment`. All comments have been +/// A wrapper trait for `pallet_revive::chain_extension::Environment`. All comments have been /// copied solely for consistent developer experience in line with the wrapped type. pub trait Environment { /// The account identifier type for the runtime. @@ -55,12 +60,12 @@ pub trait Environment { fn ext(&mut self) -> impl Ext; } -/// A wrapper type for `pallet_contracts::chain_extension::Environment`. -pub(crate) struct Env<'a, 'b, E: pallet_contracts::chain_extension::Ext, S: State>( - pub(crate) pallet_contracts::chain_extension::Environment<'a, 'b, E, S>, +/// A wrapper type for `pallet_revive::chain_extension::Environment`. +pub(crate) struct Env<'a, 'b, E: pallet_revive::chain_extension::Ext, S: ?Sized + Memory>( + pub(crate) pallet_revive::chain_extension::Environment<'a, 'b, E, S>, ); -impl<'a, 'b, E: pallet_contracts::chain_extension::Ext, S: State> Environment +impl<'a, 'b, E: pallet_revive::chain_extension::Ext, S: ?Sized + Memory> Environment for Env<'a, 'b, E, S> { type AccountId = AccountIdOf; @@ -89,8 +94,8 @@ impl<'a, 'b, E: pallet_contracts::chain_extension::Ext, S: State> Environment /// A state that uses a buffer as input. /// -/// A wrapper trait for `pallet_contracts::chain_extension::BufIn` related function available on -/// `pallet_contracts::chain_extension::Environment`. All comments have been copied solely for +/// A wrapper trait for `pallet_revive::chain_extension::BufIn` related function available on +/// `pallet_revive::chain_extension::Environment`. All comments have been copied solely for /// consistent developer experience in line with the wrapped type. pub trait BufIn { /// The length of the input as passed in as `input_len`. @@ -111,7 +116,9 @@ pub trait BufIn { fn read(&self, max_len: u32) -> Result>; } -impl<'a, 'b, E: pallet_contracts::chain_extension::Ext> BufIn for Env<'a, 'b, E, BufInBufOutState> { +impl<'a, 'b, E: pallet_revive::chain_extension::Ext, M: ?Sized + Memory> BufIn + for Env<'a, 'b, E, M> +{ fn in_len(&self) -> u32 { self.0.in_len() } @@ -123,8 +130,8 @@ impl<'a, 'b, E: pallet_contracts::chain_extension::Ext> BufIn for Env<'a, 'b, E, /// A state that uses a buffer as output. /// -/// A wrapper trait for `pallet_contracts::chain_extension::BufOut` related function available on -/// `pallet_contracts::chain_extension::Environment`. All comments have been copied solely for +/// A wrapper trait for `pallet_revive::chain_extension::BufOut` related function available on +/// `pallet_revive::chain_extension::Environment`. All comments have been copied solely for /// consistent developer experience in line with the wrapped type. pub trait BufOut { /// Write the supplied buffer to contract memory. @@ -142,8 +149,8 @@ pub trait BufOut { ) -> Result<()>; } -impl<'a, 'b, E: pallet_contracts::chain_extension::Ext> BufOut - for Env<'a, 'b, E, BufInBufOutState> +impl<'a, 'b, E: pallet_revive::chain_extension::Ext, M: ?Sized + Memory> BufOut + for Env<'a, 'b, E, M> { fn write( &mut self, @@ -161,32 +168,34 @@ impl<'a, 'b, E: pallet_contracts::chain_extension::Ext> BufOut /// This interface is specialized to an account of the executing code, so all operations are /// implicitly performed on that account. /// -/// A wrapper trait for `pallet_contracts::chain_extension::Ext`. All comments have been copied +/// A wrapper trait for `pallet_revive::chain_extension::Ext`. All comments have been copied /// solely for consistent developer experience in line with the wrapped type. pub trait Ext { /// The account identifier type for the runtime. type AccountId; /// Returns a reference to the account id of the current contract. - fn address(&self) -> &Self::AccountId; + fn address(&self) -> Self::AccountId; } impl Ext for () { type AccountId = (); - fn address(&self) -> &Self::AccountId { - &() + fn address(&self) -> Self::AccountId { + () } } -/// A wrapper type for a type implementing `pallet_contracts::chain_extension::Ext`. -pub(crate) struct ExternalEnvironment<'a, T: pallet_contracts::chain_extension::Ext>(&'a mut T); +/// A wrapper type for a type implementing `pallet_revive::chain_extension::Ext`. +pub(crate) struct ExternalEnvironment<'a, T: pallet_revive::chain_extension::Ext>(&'a mut T); -impl<'a, E: pallet_contracts::chain_extension::Ext> Ext for ExternalEnvironment<'a, E> { +impl<'a, E: pallet_revive::chain_extension::Ext> Ext for ExternalEnvironment<'a, E> { type AccountId = AccountIdOf; - fn address(&self) -> &Self::AccountId { - self.0.address() + fn address(&self) -> Self::AccountId { + // TODO: hacky way to get AccountId32 to match the Self::AccountId type + let encoded_addr = DefaultAddressMapper::to_account_id(&self.0.address()).encode(); + Self::AccountId::decode(&mut &encoded_addr[..]).unwrap() } } diff --git a/extension/src/functions.rs b/extension/src/functions.rs index 73a5fdf9..f5875091 100644 --- a/extension/src/functions.rs +++ b/extension/src/functions.rs @@ -5,7 +5,7 @@ use super::*; /// A chain extension function. pub trait Function { /// The configuration of the contracts module. - type Config: pallet_contracts::Config; + type Config: pallet_revive::Config; /// Optional error conversion. type Error: ErrorConverter; @@ -22,7 +22,7 @@ pub trait Function { pub struct DispatchCall(PhantomData<(M, C, D, F, E, L)>); impl< Matcher: Matches, - Config: pallet_contracts::Config + Config: pallet_revive::Config + frame_system::Config< RuntimeCall: GetDispatchInfo + Dispatchable, >, @@ -84,7 +84,7 @@ pub struct ReadState::Resul ); impl< Matcher: Matches, - Config: pallet_contracts::Config, + Config: pallet_revive::Config, Read: Readable + Debug, Decoder: Decode>, Filter: Contains, @@ -214,7 +214,7 @@ impl ErrorConverter for () { // Support tuples of at least one function (required for type resolution) and a maximum of ten. #[impl_trait_for_tuples::impl_for_tuples(1, 10)] #[tuple_types_custom_trait_bound(Function + Matches)] -impl Function for Tuple { +impl Function for Tuple { type Config = Runtime; type Error = (); @@ -233,7 +233,7 @@ impl Function for Tuple { // Otherwise returns error indicating an unmatched request. log::error!("no function could be matched"); - Err(pallet_contracts::Error::::DecodingFailed.into()) + Err(pallet_revive::Error::::DecodingFailed.into()) } } @@ -370,18 +370,18 @@ mod tests { #[test] fn dispatch_call_adjusts_weight() { - let migrate_weight = ::WeightInfo::migrate(); + let migrate_weight = ::WeightInfo::migrate(); let migration_noop_weight = - ::WeightInfo::migration_noop(); + ::WeightInfo::migration_noop(); new_test_ext().execute_with(|| { // Attempt to perform non-existent migration with additional weight limit specified. let extra_weight = Weight::from_parts(123456789, 12345); let weight_limit = migration_noop_weight.saturating_add(extra_weight); - let call = RuntimeCall::Contracts(pallet_contracts::Call::migrate { weight_limit }); + let call = RuntimeCall::Contracts(pallet_revive::Call::migrate { weight_limit }); let encoded_call = call.encode(); let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); let expected: DispatchError = - pallet_contracts::Error::::NoMigrationPerformed.into(); + pallet_revive::Error::::NoMigrationPerformed.into(); assert_eq!(DispatchCall::execute(&mut env).err().unwrap(), expected); // Ensure pre-dispatch weight is weight function + weight limit assert_eq!(call.get_dispatch_info().weight, migrate_weight + weight_limit); @@ -399,7 +399,7 @@ mod tests { // Invalid encoded runtime call. let input = vec![0, 99]; let mut env = MockEnvironment::new(FuncId::get(), input.clone()); - let error = pallet_contracts::Error::::DecodingFailed.into(); + let error = pallet_revive::Error::::DecodingFailed.into(); let expected = <() as ErrorConverter>::convert(error, &mut env).err(); assert_eq!(DispatchCall::execute(&mut env).err(), expected); } @@ -498,7 +498,7 @@ mod tests { // Invalid encoded runtime state read. let input = vec![0]; let mut env = MockEnvironment::new(FuncId::get(), input.clone()); - let error = pallet_contracts::Error::::DecodingFailed.into(); + let error = pallet_revive::Error::::DecodingFailed.into(); let expected = <() as ErrorConverter>::convert(error, &mut env).err(); assert_eq!(ReadState::execute(&mut env).err(), expected); } @@ -524,7 +524,7 @@ mod tests { fn execute_tuple_with_invalid_function_fails() { let input = vec![]; let mut env = MockEnvironment::new(INVALID_FUNC_ID, input.clone()); - let error = pallet_contracts::Error::::DecodingFailed.into(); + let error = pallet_revive::Error::::DecodingFailed.into(); let expected = <() as ErrorConverter>::convert(error, &mut env).err(); assert_eq!(Functions::execute(&mut env).err(), expected); } diff --git a/extension/src/lib.rs b/extension/src/lib.rs index c2480c06..0da73fc8 100644 --- a/extension/src/lib.rs +++ b/extension/src/lib.rs @@ -14,11 +14,14 @@ pub use functions::{ Converter, DefaultConverter, DispatchCall, ErrorConverter, Function, ReadState, Readable, }; pub use matching::{Equals, FunctionId, Matches}; -pub use pallet_contracts::chain_extension::{Result, RetVal, State}; -use pallet_contracts::{ - chain_extension::{ChainExtension, InitState, RetVal::Converging}, +use pallet_revive::{ + chain_extension::{ChainExtension, RetVal::Converging}, WeightInfo, }; +pub use pallet_revive::{ + chain_extension::{Result, RetVal}, + wasm::Memory, +}; use sp_core::Get; use sp_runtime::{traits::Dispatchable, DispatchError}; use sp_std::vec::Vec; @@ -35,7 +38,7 @@ mod mock; mod tests; type AccountIdOf = ::AccountId; -pub type ContractWeightsOf = ::WeightInfo; +pub type ContractWeightsOf = ::WeightInfo; type RuntimeCallOf = ::RuntimeCall; /// A configurable chain extension. @@ -43,7 +46,7 @@ type RuntimeCallOf = ::RuntimeCall; pub struct Extension(PhantomData); impl ChainExtension for Extension where - Runtime: pallet_contracts::Config + Runtime: pallet_revive::Config + frame_system::Config< RuntimeCall: GetDispatchInfo + Dispatchable, >, @@ -53,17 +56,17 @@ where /// /// # Parameters /// - `env`: Access to the remaining arguments and the execution environment. - fn call>( + fn call, M: ?Sized + Memory>( &mut self, - env: pallet_contracts::chain_extension::Environment, + env: pallet_revive::chain_extension::Environment, ) -> Result { - let mut env = environment::Env(env.buf_in_buf_out()); + let mut env = environment::Env(env); self.call(&mut env) } } impl< - Runtime: pallet_contracts::Config, + Runtime: pallet_revive::Config, Config: self::Config>, > Extension { @@ -137,7 +140,7 @@ mod extension { let mut extension = Extension::::default(); assert!(matches!( extension.call(&mut env), - Err(error) if error == pallet_contracts::Error::::DecodingFailed.into() + Err(error) if error == pallet_revive::Error::::DecodingFailed.into() )); // Charges weight. assert_eq!(env.charged(), overhead_weight(input.len() as u32)) diff --git a/extension/src/mock.rs b/extension/src/mock.rs index c085aebe..5a55a529 100644 --- a/extension/src/mock.rs +++ b/extension/src/mock.rs @@ -8,7 +8,7 @@ use frame_support::{ traits::{fungible::Inspect, ConstU32, Everything, Nothing}, }; use frame_system::{pallet_prelude::BlockNumberFor, EnsureSigned}; -use pallet_contracts::{chain_extension::RetVal, DefaultAddressGenerator, Frame, Schedule}; +use pallet_revive::{chain_extension::RetVal, DefaultAddressGenerator, Frame, Schedule}; use sp_runtime::{BuildStorage, DispatchError, Perbill}; use crate::{ @@ -18,15 +18,14 @@ use crate::{ }; pub(crate) const ALICE: u64 = 1; -pub(crate) const DEBUG_OUTPUT: pallet_contracts::DebugInfo = - pallet_contracts::DebugInfo::UnsafeDebug; +pub(crate) const DEBUG_OUTPUT: pallet_revive::DebugInfo = pallet_revive::DebugInfo::UnsafeDebug; pub(crate) const GAS_LIMIT: Weight = Weight::from_parts(500_000_000_000, 3 * 1024 * 1024); pub(crate) const INIT_AMOUNT: ::Balance = 100_000_000; pub(crate) const INVALID_FUNC_ID: u32 = 0; pub(crate) type AccountId = AccountIdOf; pub(crate) type Balance = - <::Currency as Inspect>>::Balance; + <::Currency as Inspect>>::Balance; type DispatchCallWith>> = DispatchCall< // Registered with func id 1 WithFuncId, @@ -61,7 +60,7 @@ frame_support::construct_runtime!( System: frame_system, Balances: pallet_balances, Timestamp: pallet_timestamp, - Contracts: pallet_contracts, + Contracts: pallet_revive, } ); @@ -81,7 +80,7 @@ impl pallet_balances::Config for Test { #[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig as pallet_timestamp::DefaultConfig)] impl pallet_timestamp::Config for Test {} -impl pallet_contracts::Config for Test { +impl pallet_revive::Config for Test { type AddressGenerator = DefaultAddressGenerator; type ApiVersion = (); type CallFilter = (); @@ -233,13 +232,13 @@ impl Processor for RemoveFirstByte { // A function that does nothing. pub struct Noop(PhantomData<(M, C)>); -impl Function for Noop { +impl Function for Noop { type Config = Config; type Error = (); fn execute( _env: &mut (impl environment::Environment + crate::BufIn), - ) -> pallet_contracts::chain_extension::Result { + ) -> pallet_revive::chain_extension::Result { Ok(RetVal::Converging(0)) } } @@ -297,7 +296,7 @@ impl> + Clone> environment::En fn charge_weight( &mut self, amount: Weight, - ) -> pallet_contracts::chain_extension::Result { + ) -> pallet_revive::chain_extension::Result { self.charged.push(amount); Ok(amount) } @@ -324,7 +323,7 @@ impl environment::BufIn for Environment { self.buffer.len() as u32 } - fn read(&self, _max_len: u32) -> pallet_contracts::chain_extension::Result> { + fn read(&self, _max_len: u32) -> pallet_revive::chain_extension::Result> { // TODO: handle max_len Ok(self.buffer.clone()) } @@ -336,7 +335,7 @@ impl environment::BufOut for Environment { buffer: &[u8], _allow_skip: bool, _weight_per_byte: Option, - ) -> pallet_contracts::chain_extension::Result<()> { + ) -> pallet_revive::chain_extension::Result<()> { self.buffer = buffer.to_vec(); Ok(()) } diff --git a/pallets/revive/Cargo.toml b/pallets/revive/Cargo.toml new file mode 100644 index 00000000..070d5f83 --- /dev/null +++ b/pallets/revive/Cargo.toml @@ -0,0 +1,122 @@ +[package] +authors.workspace = true +description = "FRAME pallet for PolkaVM contracts." +edition.workspace = true +homepage.workspace = true +include = [ "CHANGELOG.md", "README.md", "build.rs", "src/**/*" ] +license = "Apache-2.0" +name = "pallet-revive" +readme = "README.md" +repository.workspace = true +version = "0.1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +bitflags = { workspace = true } +codec = { features = [ + "derive", + "max-encoded-len", +], workspace = true } +environmental = { workspace = true } +impl-trait-for-tuples = { workspace = true } +log = { workspace = true } +paste = { workspace = true } +polkavm = { version = "0.11.0", default-features = false } +rlp = { workspace = true } +scale-info = { features = [ "derive" ], workspace = true } +serde = { optional = true, features = [ "derive" ], workspace = true, default-features = true } + +# Polkadot SDK Dependencies +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { optional = true, workspace = true } +pallet-revive-fixtures = { workspace = true, default-features = false } +pallet-revive-proc-macro = { workspace = true, default-features = true } +pallet-revive-uapi = { workspace = true, default-features = true } +sp-api = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +xcm = { workspace = true } +xcm-builder = { workspace = true } + +[dev-dependencies] +array-bytes = { workspace = true, default-features = true } +assert_matches = { workspace = true } +pallet-revive-fixtures = { workspace = true, default-features = true } +pretty_assertions = { workspace = true } +wat = { workspace = true } + +# Polkadot SDK Dependencies +pallet-assets = { workspace = true, default-features = true } +pallet-balances = { workspace = true, default-features = true } +pallet-message-queue = { workspace = true, default-features = true } +pallet-proxy = { workspace = true, default-features = true } +pallet-timestamp = { workspace = true, default-features = true } +pallet-utility = { workspace = true, default-features = true } +sp-keystore = { workspace = true, default-features = true } +sp-tracing = { workspace = true, default-features = true } +xcm-builder = { workspace = true, default-features = true } + +[features] +default = [ "std" ] +# enabling this feature will require having a riscv toolchain installed +# if no tests are ran and runtime benchmarks will not work +# apart from this the pallet will stay functional +riscv = [ "pallet-revive-fixtures/riscv" ] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-message-queue/runtime-benchmarks", + "pallet-proxy/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "pallet-utility/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", +] +std = [ + "codec/std", + "environmental/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances?/std", + "pallet-proxy/std", + "pallet-revive-fixtures/std", + "pallet-timestamp/std", + "pallet-utility/std", + "polkavm/std", + "rlp/std", + "scale-info/std", + "serde", + "sp-api/std", + "sp-core/std", + "sp-io/std", + "sp-keystore/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-assets/try-runtime", + "pallet-balances/try-runtime", + "pallet-message-queue/try-runtime", + "pallet-proxy/try-runtime", + "pallet-timestamp/try-runtime", + "pallet-utility/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/revive/README.md b/pallets/revive/README.md new file mode 100644 index 00000000..5352e636 --- /dev/null +++ b/pallets/revive/README.md @@ -0,0 +1,104 @@ +# Revive Pallet + +This is an **experimental** module that provides functionality for the runtime to deploy and execute PolkaVM +smart-contracts. It is a heavily modified `pallet_contracts` fork. + +## Overview + +This module extends accounts based on the [`frame_support::traits::fungible`] traits to have smart-contract +functionality. It can be used with other modules that implement accounts based on [`frame_support::traits::fungible`]. +These "smart-contract accounts" have the ability to instantiate smart-contracts and make calls to other contract and +non-contract accounts. + +The smart-contract code is stored once, and later retrievable via its `code_hash`. This means that multiple +smart-contracts can be instantiated from the same `code`, without replicating the code each time. + +When a smart-contract is called, its associated code is retrieved via the code hash and gets executed. This call can +alter the storage entries of the smart-contract account, instantiate new smart-contracts, or call other smart-contracts. + +Finally, when an account is reaped, its associated code and storage of the smart-contract account will also be deleted. + +### Weight + +Senders must specify a [`Weight`](https://paritytech.github.io/substrate/master/sp_weights/struct.Weight.html) limit +with every call, as all instructions invoked by the smart-contract require weight. Unused weight is refunded after the +call, regardless of the execution outcome. + +If the weight limit is reached, then all calls and state changes (including balance transfers) are only reverted at the +current call's contract level. For example, if contract A calls B and B runs out of weight mid-call, then all of B's +calls are reverted. Assuming correct error handling by contract A, A's other calls and state changes still persist. + +One `ref_time` `Weight` is defined as one picosecond of execution time on the runtime's reference machine. + +### Revert Behaviour + +Contract call failures are not cascading. When failures occur in a sub-call, they do not "bubble up", and the call will +only revert at the specific contract level. For example, if contract A calls contract B, and B fails, A can decide how +to handle that failure, either proceeding or reverting A's changes. + +## Interface + +### Dispatchable functions + +Those are documented in the [reference +documentation](https://paritytech.github.io/substrate/master/pallet_revive/index.html#dispatchable-functions). + +## Usage + +This module executes PolkaVM smart contracts. These can potentially be written in any language that compiles to +RISC-V. For now, the only officially supported languages are Solidity (via [`revive`](https://github.com/xermicus/revive)) +and Rust (check the `fixtures` directory for Rust examples). + +## Debugging + +Contracts can emit messages to the client when called as RPC through the +[`debug_message`](https://paritytech.github.io/substrate/master/pallet_revive/trait.SyscallDocs.html#tymethod.debug_message) +API. + +Those messages are gathered into an internal buffer and sent to the RPC client. It is up to the individual client if +and how those messages are presented to the user. + +This buffer is also printed as a debug message. In order to see these messages on the node console the log level for the +`runtime::revive` target needs to be raised to at least the `debug` level. However, those messages are easy to +overlook because of the noise generated by block production. A good starting point for observing them on the console is +using this command line in the root directory of the Substrate repository: + +```bash +cargo run --release -- --dev -lerror,runtime::revive=debug +``` + +This raises the log level of `runtime::revive` to `debug` and all other targets to `error` in order to prevent them +from spamming the console. + +`--dev`: Use a dev chain spec `--tmp`: Use temporary storage for chain data (the chain state is deleted on exit) + +## Host function tracing + +For contract authors, it can be a helpful debugging tool to see which host functions are called, with which arguments, +and what the result was. + +In order to see these messages on the node console, the log level for the `runtime::revive::strace` target needs to +be raised to the `trace` level. + +Example: + +```bash +cargo run --release -- --dev -lerror,runtime::revive::strace=trace,runtime::revive=debug +``` + +## Unstable Interfaces + +Driven by the desire to have an iterative approach in developing new contract interfaces this pallet contains the +concept of an unstable interface. Akin to the rust nightly compiler it allows us to add new interfaces but mark them as +unstable so that contract languages can experiment with them and give feedback before we stabilize those. + +In order to access interfaces which don't have a stable `#[api_version(x)]` in [`runtime.rs`](src/wasm/runtime.rs) +one need to set `pallet_revive::Config::UnsafeUnstableInterface` to `ConstU32`. +**It should be obvious that any production runtime should never be compiled with this feature: In addition to be +subject to change or removal those interfaces might not have proper weights associated with them and are therefore +considered unsafe**. + +New interfaces are generally added as unstable and might go through several iterations before they are promoted to a +stable interface. + +License: Apache-2.0 diff --git a/pallets/revive/fixtures/Cargo.toml b/pallets/revive/fixtures/Cargo.toml new file mode 100644 index 00000000..9fd10ca7 --- /dev/null +++ b/pallets/revive/fixtures/Cargo.toml @@ -0,0 +1,36 @@ +[package] +authors.workspace = true +description = "Fixtures for testing and benchmarking" +edition.workspace = true +license.workspace = true +name = "pallet-revive-fixtures" +publish = true +version = "0.1.0" + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true, default-features = true, optional = true } +frame-system = { workspace = true, default-features = true, optional = true } +log = { workspace = true } +sp-core = { workspace = true, default-features = true, optional = true } +sp-io = { workspace = true, default-features = true, optional = true } +sp-runtime = { workspace = true, default-features = true, optional = true } + +[build-dependencies] +anyhow = { workspace = true, default-features = true } +parity-wasm = { workspace = true } +polkavm-linker = { version = "0.11.0" } +tempfile = { workspace = true } +toml = { workspace = true } + +[features] +default = [ "std" ] +# only if the feature is set we are building the test fixtures +# this is because it requires a custom toolchain supporting polkavm +# we will remove this once there is an upstream toolchain +# note: frame-system is included to make formatting happy +riscv = [ "frame-system/default" ] +# only when std is enabled all fixtures are available +std = [ "anyhow", "frame-system", "log/std", "sp-core", "sp-io", "sp-runtime" ] diff --git a/pallets/revive/fixtures/build.rs b/pallets/revive/fixtures/build.rs new file mode 100644 index 00000000..0f7edfc3 --- /dev/null +++ b/pallets/revive/fixtures/build.rs @@ -0,0 +1,217 @@ +// 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. + +//! Compile text fixtures to PolkaVM binaries. +use anyhow::Result; + +fn main() -> Result<()> { + build::run() +} + +#[cfg(feature = "riscv")] +mod build { + use std::{ + cfg, env, fs, + path::{Path, PathBuf}, + process::Command, + }; + + use anyhow::{bail, Context}; + + use super::Result; + + /// A contract entry. + struct Entry { + /// The path to the contract source file. + path: PathBuf, + } + + impl Entry { + /// Create a new contract entry from the given path. + fn new(path: PathBuf) -> Self { + Self { path } + } + + /// Return the path to the contract source file. + fn path(&self) -> &str { + self.path.to_str().expect("path is valid unicode; qed") + } + + /// Return the name of the contract. + fn name(&self) -> &str { + self.path + .file_stem() + .expect("file exits; qed") + .to_str() + .expect("name is valid unicode; qed") + } + + /// Return the name of the polkavm file. + fn out_filename(&self) -> String { + format!("{}.polkavm", self.name()) + } + } + + /// Collect all contract entries from the given source directory. + /// Contracts that have already been compiled are filtered out. + fn collect_entries(contracts_dir: &Path) -> Vec { + fs::read_dir(contracts_dir) + .expect("src dir exists; qed") + .filter_map(|file| { + let path = file.expect("file exists; qed").path(); + if path.extension().map_or(true, |ext| ext != "rs") { + return None + } + + Some(Entry::new(path)) + }) + .collect::>() + } + + /// Create a `Cargo.toml` to compile the given contract entries. + fn create_cargo_toml<'a>( + fixtures_dir: &Path, + entries: impl Iterator, + output_dir: &Path, + ) -> Result<()> { + let mut cargo_toml: toml::Value = toml::from_str(include_str!("./build/Cargo.toml"))?; + let mut set_dep = |name, path| -> Result<()> { + cargo_toml["dependencies"][name]["path"] = toml::Value::String( + fixtures_dir.join(path).canonicalize()?.to_str().unwrap().to_string(), + ); + Ok(()) + }; + set_dep("uapi", "../uapi")?; + set_dep("common", "./contracts/common")?; + + cargo_toml["bin"] = toml::Value::Array( + entries + .map(|entry| { + let name = entry.name(); + let path = entry.path(); + toml::Value::Table(toml::toml! { + name = name + path = path + }) + }) + .collect::>(), + ); + + let cargo_toml = toml::to_string_pretty(&cargo_toml)?; + fs::write(output_dir.join("Cargo.toml"), cargo_toml).map_err(Into::into) + } + + fn invoke_build(current_dir: &Path) -> Result<()> { + let encoded_rustflags = [ + "-Crelocation-model=pie", + "-Clink-arg=--emit-relocs", + "-Clink-arg=--export-dynamic-symbol=__polkavm_symbol_export_hack__*", + ] + .join("\x1f"); + + let build_res = Command::new(env::var("CARGO")?) + .current_dir(current_dir) + .env_clear() + .env("PATH", env::var("PATH").unwrap_or_default()) + .env("CARGO_ENCODED_RUSTFLAGS", encoded_rustflags) + .env("RUSTUP_TOOLCHAIN", "rve-nightly") + .env("RUSTC_BOOTSTRAP", "1") + .env("RUSTUP_HOME", env::var("RUSTUP_HOME").unwrap_or_default()) + .args([ + "build", + "--release", + "--target=riscv32ema-unknown-none-elf", + "-Zbuild-std=core", + "-Zbuild-std-features=panic_immediate_abort", + ]) + .output() + .expect("failed to execute process"); + + if build_res.status.success() { + return Ok(()) + } + + let stderr = String::from_utf8_lossy(&build_res.stderr); + + if stderr.contains("'rve-nightly' is not installed") { + eprintln!("RISC-V toolchain is not installed.\nDownload and install toolchain from https://github.com/paritytech/rustc-rv32e-toolchain."); + eprintln!("{}", stderr); + } else { + eprintln!("{}", stderr); + } + + bail!("Failed to build contracts"); + } + + /// Post-process the compiled code. + fn post_process(input_path: &Path, output_path: &Path) -> Result<()> { + let mut config = polkavm_linker::Config::default(); + config.set_strip(true); + config.set_optimize(false); + let orig = + fs::read(input_path).with_context(|| format!("Failed to read {:?}", input_path))?; + let linked = polkavm_linker::program_from_elf(config, orig.as_ref()) + .map_err(|err| anyhow::format_err!("Failed to link polkavm program: {}", err))?; + fs::write(output_path, linked).map_err(Into::into) + } + + /// Write the compiled contracts to the given output directory. + fn write_output(build_dir: &Path, out_dir: &Path, entries: Vec) -> Result<()> { + for entry in entries { + post_process( + &build_dir.join("target/riscv32ema-unknown-none-elf/release").join(entry.name()), + &out_dir.join(entry.out_filename()), + )?; + } + + Ok(()) + } + + pub fn run() -> Result<()> { + let fixtures_dir: PathBuf = env::var("CARGO_MANIFEST_DIR")?.into(); + let contracts_dir = fixtures_dir.join("contracts"); + let uapi_dir = fixtures_dir.parent().expect("uapi dir exits; qed").join("uapi"); + let out_dir: PathBuf = env::var("OUT_DIR")?.into(); + + // the fixtures have a dependency on the uapi crate + println!("cargo::rerun-if-changed={}", fixtures_dir.display()); + println!("cargo::rerun-if-changed={}", uapi_dir.display()); + + let entries = collect_entries(&contracts_dir); + if entries.is_empty() { + return Ok(()) + } + + let tmp_dir = tempfile::tempdir()?; + let tmp_dir_path = tmp_dir.path(); + + create_cargo_toml(&fixtures_dir, entries.iter(), tmp_dir.path())?; + invoke_build(tmp_dir_path)?; + + write_output(tmp_dir_path, &out_dir, entries)?; + Ok(()) + } +} + +#[cfg(not(feature = "riscv"))] +mod build { + use super::Result; + + pub fn run() -> Result<()> { + Ok(()) + } +} diff --git a/pallets/revive/fixtures/build/Cargo.toml b/pallets/revive/fixtures/build/Cargo.toml new file mode 100644 index 00000000..503c12d4 --- /dev/null +++ b/pallets/revive/fixtures/build/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2021" +name = "contracts" +publish = false +version = "1.0.0" + +# Binary targets are injected dynamically by the build script. +[[bin]] + +# All paths are injected dynamically by the build script. +[dependencies] +common = { package = 'pallet-revive-fixtures-common', path = "" } +polkavm-derive = { version = "0.11.0" } +uapi = { package = 'pallet-revive-uapi', path = "", default-features = false } + +[profile.release] +codegen-units = 1 +lto = true +opt-level = 3 diff --git a/pallets/revive/fixtures/contracts/balance.rs b/pallets/revive/fixtures/contracts/balance.rs new file mode 100644 index 00000000..4606135d --- /dev/null +++ b/pallets/revive/fixtures/contracts/balance.rs @@ -0,0 +1,33 @@ +// 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. + +#![no_std] +#![no_main] + +use common::u64_output; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let balance = u64_output!(api::balance,); + assert_eq!(balance, 0); +} diff --git a/pallets/revive/fixtures/contracts/balance_of.rs b/pallets/revive/fixtures/contracts/balance_of.rs new file mode 100644 index 00000000..1f94d3e5 --- /dev/null +++ b/pallets/revive/fixtures/contracts/balance_of.rs @@ -0,0 +1,36 @@ +// 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. + +#![no_std] +#![no_main] + +use common::{input, u64_output}; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(address: &[u8; 20],); + + let reported_free_balance = u64_output!(api::balance_of, address); + + assert_ne!(reported_free_balance, 0); +} diff --git a/pallets/revive/fixtures/contracts/call.rs b/pallets/revive/fixtures/contracts/call.rs new file mode 100644 index 00000000..ee515488 --- /dev/null +++ b/pallets/revive/fixtures/contracts/call.rs @@ -0,0 +1,49 @@ +// 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. + +//! This calls another contract as passed as its account id. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + callee_input: [u8; 4], + callee_addr: &[u8; 20], + ); + + // Call the callee + api::call( + uapi::CallFlags::empty(), + callee_addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much proof_size to devote for the execution. 0 = all. + None, // No deposit limit. + &[0u8; 32], // Value transferred to the contract. + callee_input, + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/call_diverging_out_len.rs b/pallets/revive/fixtures/contracts/call_diverging_out_len.rs new file mode 100644 index 00000000..e13162f8 --- /dev/null +++ b/pallets/revive/fixtures/contracts/call_diverging_out_len.rs @@ -0,0 +1,110 @@ +// 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. + +//! This tests that the correct output data is written when the provided +//! output buffer length is smaller than what was actually returned during +//! calls and instantiations. +//! +//! To not need an additional callee fixture, we call ourself recursively +//! and also instantiate our own code hash (constructor and recursive calls +//! always return `BUF_SIZE` bytes of data). + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +const BUF_SIZE: usize = 8; +static DATA: [u8; BUF_SIZE] = [1, 2, 3, 4, 5, 6, 7, 8]; + +/// Call `callee_address` with an output buf of size `N` +/// and expect the call output to match `expected_output`. +fn assert_call(callee_address: &[u8; 20], expected_output: [u8; BUF_SIZE]) { + let mut output_buf = [0u8; BUF_SIZE]; + let mut output_buf_capped = &mut &mut output_buf[..N]; + + api::call( + uapi::CallFlags::ALLOW_REENTRY, + callee_address, + 0u64, + 0u64, + None, + &[0u8; 32], + &[], + Some(output_buf_capped), + ) + .unwrap(); + + // The (capped) output buf should get properly resized + assert_eq!(output_buf_capped.len(), N); + assert_eq!(output_buf, expected_output); +} + +/// Instantiate this contract with an output buf of size `N` +/// and expect the instantiate output to match `expected_output`. +fn assert_instantiate(expected_output: [u8; BUF_SIZE]) { + let mut code_hash = [0; 32]; + api::own_code_hash(&mut code_hash); + + let mut output_buf = [0u8; BUF_SIZE]; + let mut output_buf_capped = &mut &mut output_buf[..N]; + + api::instantiate( + &code_hash, + 0u64, + 0u64, + None, + &[0; 32], + &[0; 32], + None, + Some(output_buf_capped), + None, + ) + .unwrap(); + + // The (capped) output buf should get properly resized + assert_eq!(output_buf_capped.len(), N); + assert_eq!(output_buf, expected_output); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + api::return_value(uapi::ReturnFlags::empty(), &DATA); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let mut caller_address = [0u8; 20]; + api::caller(&mut caller_address); + + let mut callee_address = [0u8; 20]; + api::address(&mut callee_address); + + // we already recurse; return data + if caller_address == callee_address { + api::return_value(uapi::ReturnFlags::empty(), &DATA); + } + + assert_call::<0>(&callee_address, [0; 8]); + assert_call::<4>(&callee_address, [1, 2, 3, 4, 0, 0, 0, 0]); + + assert_instantiate::<0>([0; 8]); + assert_instantiate::<4>([1, 2, 3, 4, 0, 0, 0, 0]); +} diff --git a/pallets/revive/fixtures/contracts/call_return_code.rs b/pallets/revive/fixtures/contracts/call_return_code.rs new file mode 100644 index 00000000..d0d7c1be --- /dev/null +++ b/pallets/revive/fixtures/contracts/call_return_code.rs @@ -0,0 +1,57 @@ +// 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. + +//! This calls the supplied dest and transfers 100 balance during this call and copies +//! the return code of this call to the output buffer. +//! It also forwards its input to the callee. +#![no_std] +#![no_main] + +use common::{input, u256_bytes}; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 100, + callee_addr: &[u8; 20], + value: &[u8; 32], + input: [u8], + ); + + // Call the callee + let err_code = match api::call( + uapi::CallFlags::empty(), + callee_addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much proof_size to devote for the execution. 0 = all. + None, // No deposit limit. + value, // Value transferred to the contract. + input, + None, + ) { + Ok(_) => 0u32, + Err(code) => code as u32, + }; + + api::return_value(uapi::ReturnFlags::empty(), &err_code.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/call_runtime.rs b/pallets/revive/fixtures/contracts/call_runtime.rs new file mode 100644 index 00000000..2b132398 --- /dev/null +++ b/pallets/revive/fixtures/contracts/call_runtime.rs @@ -0,0 +1,42 @@ +// 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. + +//! This passes its input to `call_runtime` and returns the return value to its caller. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + // Fixture calls should fit into 100 bytes. + input!(100, call: [u8], ); + + // Use the call passed as input to call the runtime. + let err_code = match api::call_runtime(call) { + Ok(_) => 0u32, + Err(code) => code as u32, + }; + + api::return_value(uapi::ReturnFlags::empty(), &err_code.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/call_runtime_and_call.rs b/pallets/revive/fixtures/contracts/call_runtime_and_call.rs new file mode 100644 index 00000000..8c8aee96 --- /dev/null +++ b/pallets/revive/fixtures/contracts/call_runtime_and_call.rs @@ -0,0 +1,53 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 512, + callee_input: [u8; 4], + callee_addr: &[u8; 20], + call: [u8], + ); + + // Use the call passed as input to call the runtime. + api::call_runtime(call).unwrap(); + + // Call the callee + api::call( + uapi::CallFlags::empty(), + callee_addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much proof_size to devote for the execution. 0 = all. + None, // No deposit limit. + &[0u8; 32], // Value transferred to the contract. + callee_input, + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/call_with_flags_and_value.rs b/pallets/revive/fixtures/contracts/call_with_flags_and_value.rs new file mode 100644 index 00000000..330393e7 --- /dev/null +++ b/pallets/revive/fixtures/contracts/call_with_flags_and_value.rs @@ -0,0 +1,51 @@ +// 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. + +//! This fixture calls the account_id with the flags and value. +#![no_std] +#![no_main] + +use common::{input, u256_bytes}; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 256, + callee_addr: &[u8; 20], + flags: u32, + value: u64, + forwarded_input: [u8], + ); + + api::call( + uapi::CallFlags::from_bits(flags).unwrap(), + callee_addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much proof_size to devote for the execution. 0 = all. + None, // No deposit limit. + &u256_bytes(value), // Value transferred to the contract. + forwarded_input, + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/call_with_limit.rs b/pallets/revive/fixtures/contracts/call_with_limit.rs new file mode 100644 index 00000000..6ab892a6 --- /dev/null +++ b/pallets/revive/fixtures/contracts/call_with_limit.rs @@ -0,0 +1,52 @@ +// 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. + +//! This fixture calls the account_id with the 2D Weight limit. +//! It returns the result of the call as output data. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 256, + callee_addr: &[u8; 20], + ref_time: u64, + proof_size: u64, + forwarded_input: [u8], + ); + + api::call( + uapi::CallFlags::empty(), + callee_addr, + ref_time, + proof_size, + None, // No deposit limit. + &[0u8; 32], // value transferred to the contract. + forwarded_input, + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/caller_contract.rs b/pallets/revive/fixtures/contracts/caller_contract.rs new file mode 100644 index 00000000..f9a30b87 --- /dev/null +++ b/pallets/revive/fixtures/contracts/caller_contract.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. + +#![no_std] +#![no_main] + +use common::{input, u256_bytes}; +use uapi::{HostFn, HostFnImpl as api, ReturnErrorCode}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(code_hash: &[u8; 32],); + + // The value to transfer on instantiation and calls. Chosen to be greater than existential + // deposit. + let value = u256_bytes(32768u64); + let salt = [0u8; 32]; + + // Callee will use the first 4 bytes of the input to return an exit status. + let input = [0u8, 1, 34, 51, 68, 85, 102, 119]; + let reverted_input = [1u8, 34, 51, 68, 85, 102, 119]; + + // Fail to deploy the contract since it returns a non-zero exit status. + let res = api::instantiate( + code_hash, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &value, + &reverted_input, + None, + None, + Some(&salt), + ); + assert!(matches!(res, Err(ReturnErrorCode::CalleeReverted))); + + // Fail to deploy the contract due to insufficient ref_time weight. + let res = api::instantiate( + code_hash, + 1u64, // too little ref_time weight + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &value, + &input, + None, + None, + Some(&salt), + ); + assert!(matches!(res, Err(ReturnErrorCode::CalleeTrapped))); + + // Fail to deploy the contract due to insufficient proof_size weight. + let res = api::instantiate( + code_hash, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 1u64, // Too little proof_size weight + None, // No deposit limit. + &value, + &input, + None, + None, + Some(&salt), + ); + assert!(matches!(res, Err(ReturnErrorCode::CalleeTrapped))); + + // Deploy the contract successfully. + let mut callee = [0u8; 20]; + + api::instantiate( + code_hash, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &value, + &input, + Some(&mut callee), + None, + Some(&salt), + ) + .unwrap(); + + // Call the new contract and expect it to return failing exit code. + let res = api::call( + uapi::CallFlags::empty(), + &callee, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &value, + &reverted_input, + None, + ); + assert!(matches!(res, Err(ReturnErrorCode::CalleeReverted))); + + // Fail to call the contract due to insufficient ref_time weight. + let res = api::call( + uapi::CallFlags::empty(), + &callee, + 1u64, // Too little ref_time weight. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &value, + &input, + None, + ); + assert!(matches!(res, Err(ReturnErrorCode::CalleeTrapped))); + + // Fail to call the contract due to insufficient proof_size weight. + let res = api::call( + uapi::CallFlags::empty(), + &callee, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 1u64, // too little proof_size weight + None, // No deposit limit. + &value, + &input, + None, + ); + assert!(matches!(res, Err(ReturnErrorCode::CalleeTrapped))); + + // Call the contract successfully. + let mut output = [0u8; 4]; + api::call( + uapi::CallFlags::empty(), + &callee, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &value, + &input, + Some(&mut &mut output[..]), + ) + .unwrap(); + assert_eq!(&output, &input[4..]) +} diff --git a/pallets/revive/fixtures/contracts/caller_is_origin_n.rs b/pallets/revive/fixtures/contracts/caller_is_origin_n.rs new file mode 100644 index 00000000..fd6f5980 --- /dev/null +++ b/pallets/revive/fixtures/contracts/caller_is_origin_n.rs @@ -0,0 +1,38 @@ +// 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. + +//! This fixture calls caller_is_origin `n` times. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(n: u32, ); + + for _ in 0..n { + let _ = api::caller_is_origin(); + } +} diff --git a/pallets/revive/fixtures/contracts/chain_extension.rs b/pallets/revive/fixtures/contracts/chain_extension.rs new file mode 100644 index 00000000..474df00d --- /dev/null +++ b/pallets/revive/fixtures/contracts/chain_extension.rs @@ -0,0 +1,42 @@ +// 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. + +//! Call chain extension by passing through input and output of this contract. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(input, 8, func_id: u32,); + + // the chain extension passes through the input and returns it as output + let mut output_buffer = [0u8; 32]; + let output = &mut &mut output_buffer[0..input.len()]; + + let ret_id = api::call_chain_extension(func_id, input, Some(output)); + assert_eq!(ret_id, func_id); + + api::return_value(uapi::ReturnFlags::empty(), output); +} diff --git a/pallets/revive/fixtures/contracts/chain_extension_temp_storage.rs b/pallets/revive/fixtures/contracts/chain_extension_temp_storage.rs new file mode 100644 index 00000000..22d6c5b5 --- /dev/null +++ b/pallets/revive/fixtures/contracts/chain_extension_temp_storage.rs @@ -0,0 +1,66 @@ +// 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. + +//! Call chain extension two times with the specified func_ids +//! It then calls itself once +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + input, + func_id1: u32, + func_id2: u32, + stop_recurse: u8, + ); + + api::call_chain_extension(func_id1, input, None); + api::call_chain_extension(func_id2, input, None); + + if stop_recurse == 0 { + // Setup next call + input[0..4].copy_from_slice(&((3 << 16) | 2u32).to_le_bytes()); + input[4..8].copy_from_slice(&((3 << 16) | 3u32).to_le_bytes()); + input[8] = 1u8; + + // Read the contract address. + let mut addr = [0u8; 20]; + api::address(&mut addr); + + // call self + api::call( + uapi::CallFlags::ALLOW_REENTRY, + &addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much proof_size to devote for the execution. 0 = all. + None, // No deposit limit. + &[0u8; 32], // Value transferred to the contract. + input, + None, + ) + .unwrap(); + } +} diff --git a/pallets/revive/fixtures/contracts/chain_id.rs b/pallets/revive/fixtures/contracts/chain_id.rs new file mode 100644 index 00000000..ce7a0cc6 --- /dev/null +++ b/pallets/revive/fixtures/contracts/chain_id.rs @@ -0,0 +1,37 @@ +// 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. + +#![no_std] +#![no_main] + +extern crate common; + +use uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + call() +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let mut buf = [0; 32]; + api::chain_id(&mut buf); + api::return_value(ReturnFlags::empty(), &buf); +} diff --git a/pallets/revive/fixtures/contracts/common/Cargo.toml b/pallets/revive/fixtures/contracts/common/Cargo.toml new file mode 100644 index 00000000..17e03db9 --- /dev/null +++ b/pallets/revive/fixtures/contracts/common/Cargo.toml @@ -0,0 +1,11 @@ +[package] +authors.workspace = true +description = "Common utilities for pallet-revive-fixtures." +edition.workspace = true +license.workspace = true +name = "pallet-revive-fixtures-common" +publish = false +version = "1.0.0" + +[dependencies] +uapi = { package = 'pallet-revive-uapi', path = "../../../uapi", default-features = false } diff --git a/pallets/revive/fixtures/contracts/common/src/lib.rs b/pallets/revive/fixtures/contracts/common/src/lib.rs new file mode 100644 index 00000000..abfba282 --- /dev/null +++ b/pallets/revive/fixtures/contracts/common/src/lib.rs @@ -0,0 +1,196 @@ +// 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. +#![no_std] + +pub use uapi::{HostFn, HostFnImpl as api}; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Safety: The unimp instruction is guaranteed to trap + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked(); + } +} + +/// Utility macro to read input passed to a contract. +/// +/// Example: +/// +/// ``` +/// input$!( +/// var1: u32, // [0, 4) var1 decoded as u32 +/// var2: [u8; 32], // [4, 36) var2 decoded as a [u8] slice +/// var3: u8, // [36, 37) var3 decoded as a u8 +/// ); +/// +/// // Input and size can be specified as well: +/// input$!( +/// input, // input buffer (optional) +/// 512, // input size (optional) +/// var4: u32, // [0, 4) var4 decoded as u32 +/// var5: [u8], // [4, ..) var5 decoded as a [u8] slice +/// ); +/// ``` +#[macro_export] +macro_rules! input { + (@inner $input:expr, $cursor:expr,) => {}; + (@size $size:expr, ) => { $size }; + + // Match a u8 variable. + // e.g input!(var1: u8, ); + (@inner $input:expr, $cursor:expr, $var:ident: u8, $($rest:tt)*) => { + let $var = $input[$cursor]; + input!(@inner $input, $cursor + 1, $($rest)*); + }; + + // Size of u8 variable. + (@size $size:expr, $var:ident: u8, $($rest:tt)*) => { + input!(@size $size + 1, $($rest)*) + }; + + // Match a u64 variable. + // e.g input!(var1: u64, ); + (@inner $input:expr, $cursor:expr, $var:ident: u64, $($rest:tt)*) => { + let $var = u64::from_le_bytes($input[$cursor..$cursor + 8].try_into().unwrap()); + input!(@inner $input, $cursor + 8, $($rest)*); + }; + + // Size of u64 variable. + (@size $size:expr, $var:ident: u64, $($rest:tt)*) => { + input!(@size $size + 8, $($rest)*) + }; + + // Match a u32 variable. + // e.g input!(var1: u32, ); + (@inner $input:expr, $cursor:expr, $var:ident: u32, $($rest:tt)*) => { + let $var = u32::from_le_bytes($input[$cursor..$cursor + 4].try_into().unwrap()); + input!(@inner $input, $cursor + 4, $($rest)*); + }; + + // Size of u32 variable. + (@size $size:expr, $var:ident: u32, $($rest:tt)*) => { + input!(@size $size + 4, $($rest)*) + }; + + // Match a u8 slice with the remaining bytes. + // e.g input!(512, var1: [u8; 32], var2: [u8], ); + (@inner $input:expr, $cursor:expr, $var:ident: [u8],) => { + let $var = &$input[$cursor..]; + }; + + // Match a u8 slice of the given size. + // e.g input!(var1: [u8; 32], ); + (@inner $input:expr, $cursor:expr, $var:ident: [u8; $n:expr], $($rest:tt)*) => { + let $var = &$input[$cursor..$cursor+$n]; + input!(@inner $input, $cursor + $n, $($rest)*); + }; + + // Match an array reference of the given size. + // e.g input!(var1: &[u8; 32], ); + (@inner $input:expr, $cursor:expr, $var:ident: &[u8; $n:expr], $($rest:tt)*) => { + let $var: &[u8; $n] = &$input[$cursor..$cursor+$n].try_into().unwrap(); + input!(@inner $input, $cursor + $n, $($rest)*); + }; + + // Size of a u8 slice. + (@size $size:expr, $var:ident: [u8; $n:expr], $($rest:tt)*) => { + input!(@size $size + $n, $($rest)*) + }; + + // Size of an array reference. + (@size $size:expr, $var:ident: &[u8; $n:expr], $($rest:tt)*) => { + input!(@size $size + $n, $($rest)*) + }; + + // Entry point, with the buffer and it's size specified first. + // e.g input!(buffer, 512, var1: u32, var2: [u8], ); + ($buffer:ident, $size:expr, $($rest:tt)*) => { + let mut $buffer = [0u8; $size]; + let $buffer = &mut &mut $buffer[..]; + $crate::api::input($buffer); + input!(@inner $buffer, 0, $($rest)*); + }; + + // Entry point, with the name of the buffer specified and size of the input buffer computed. + // e.g input!(buffer, var1: u32, var2: u64, ); + ($buffer: ident, $($rest:tt)*) => { + input!($buffer, input!(@size 0, $($rest)*), $($rest)*); + }; + + // Entry point, with the size of the input buffer computed. + // e.g input!(var1: u32, var2: u64, ); + ($($rest:tt)*) => { + input!(buffer, $($rest)*); + }; +} + +/// Utility macro to invoke a host function that expect a `output: &mut &mut [u8]` as last argument. +/// +/// Example: +/// ``` +/// // call `api::caller` and store the output in `caller` +/// output!(caller, [0u8; 32], api::caller,); +/// +/// // call `api::get_storage` and store the output in `address` +/// output!(address, [0u8; 32], api::get_storage, &[1u8; 32]); +/// ``` +#[macro_export] +macro_rules! output { + ($output: ident, $buffer: expr, $host_fn:path, $($arg:expr),*) => { + let mut $output = $buffer; + let $output = &mut &mut $output[..]; + $host_fn($($arg,)* $output); + }; +} + +/// Similar to `output!` but unwraps the result. +#[macro_export] +macro_rules! unwrap_output { + ($output: ident, $buffer: expr, $host_fn:path, $($arg:expr),*) => { + let mut $output = $buffer; + let $output = &mut &mut $output[..]; + $host_fn($($arg,)* $output).unwrap(); + }; +} + +/// Call the host function and convert the [u8; 32] output to u64. +#[macro_export] +macro_rules! u64_output { + ($host_fn:path, $($arg:expr),*) => {{ + let mut buffer = [1u8; 32]; + $host_fn($($arg,)* &mut buffer); + assert!(buffer[8..].iter().all(|&x| x == 0)); + u64::from_le_bytes(buffer[..8].try_into().unwrap()) + }}; +} + +/// Convert a u64 into a [u8; 32]. +pub const fn u256_bytes(value: u64) -> [u8; 32] { + let mut buffer = [0u8; 32]; + let bytes = value.to_le_bytes(); + + buffer[0] = bytes[0]; + buffer[1] = bytes[1]; + buffer[2] = bytes[2]; + buffer[3] = bytes[3]; + buffer[4] = bytes[4]; + buffer[5] = bytes[5]; + buffer[6] = bytes[6]; + buffer[7] = bytes[7]; + buffer +} diff --git a/pallets/revive/fixtures/contracts/create1_with_value.rs b/pallets/revive/fixtures/contracts/create1_with_value.rs new file mode 100644 index 00000000..c6adab82 --- /dev/null +++ b/pallets/revive/fixtures/contracts/create1_with_value.rs @@ -0,0 +1,39 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(code_hash: &[u8; 32],); + + let mut value = [0; 32]; + api::value_transferred(&mut value); + + // Deploy the contract with no salt (equivalent to create1). + let ret = api::instantiate(code_hash, 0u64, 0u64, None, &value, &[], None, None, None); + assert!(ret.is_ok()); +} diff --git a/pallets/revive/fixtures/contracts/create_storage_and_call.rs b/pallets/revive/fixtures/contracts/create_storage_and_call.rs new file mode 100644 index 00000000..4fa2db0c --- /dev/null +++ b/pallets/revive/fixtures/contracts/create_storage_and_call.rs @@ -0,0 +1,58 @@ +// 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. + +//! This calls another contract as passed as its account id. It also creates some storage. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + buffer, + input: [u8; 4], + callee: &[u8; 20], + deposit_limit: &[u8; 32], + ); + + // create 4 byte of storage before calling + api::set_storage(StorageFlags::empty(), buffer, &[1u8; 4]); + + // Call the callee + api::call( + uapi::CallFlags::empty(), + callee, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + Some(deposit_limit), + &[0u8; 32], // Value transferred to the contract. + input, + None, + ) + .unwrap(); + + // create 8 byte of storage after calling + // item of 12 bytes because we override 4 bytes + api::set_storage(StorageFlags::empty(), buffer, &[1u8; 12]); +} diff --git a/pallets/revive/fixtures/contracts/create_storage_and_instantiate.rs b/pallets/revive/fixtures/contracts/create_storage_and_instantiate.rs new file mode 100644 index 00000000..46370645 --- /dev/null +++ b/pallets/revive/fixtures/contracts/create_storage_and_instantiate.rs @@ -0,0 +1,57 @@ +// 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. + +//! This instantiates another contract and passes some input to its constructor. +#![no_std] +#![no_main] + +use common::{input, u256_bytes}; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + input: [u8; 4], + code_hash: &[u8; 32], + deposit_limit: &[u8; 32], + ); + + let value = u256_bytes(10_000u64); + let salt = [0u8; 32]; + let mut address = [0u8; 20]; + + api::instantiate( + code_hash, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + Some(deposit_limit), + &value, + input, + Some(&mut address), + None, + Some(&salt), + ) + .unwrap(); + + // Return the deployed contract address. + api::return_value(uapi::ReturnFlags::empty(), &address); +} diff --git a/pallets/revive/fixtures/contracts/create_transient_storage_and_call.rs b/pallets/revive/fixtures/contracts/create_transient_storage_and_call.rs new file mode 100644 index 00000000..d2efb26e --- /dev/null +++ b/pallets/revive/fixtures/contracts/create_transient_storage_and_call.rs @@ -0,0 +1,60 @@ +// 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. + +//! This calls another contract as passed as its account id. It also creates some transient storage. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +static BUFFER: [u8; 512] = [0u8; 512]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + buffer, + len: u32, + input: [u8; 4], + callee: &[u8; 20], + ); + + let rounds = len as usize / BUFFER.len(); + let rest = len as usize / BUFFER.len(); + for i in 0..rounds { + api::set_storage(StorageFlags::TRANSIENT, &i.to_le_bytes(), &BUFFER); + } + api::set_storage(StorageFlags::TRANSIENT, &u32::MAX.to_le_bytes(), &BUFFER[..rest]); + + // Call the callee + api::call( + uapi::CallFlags::empty(), + callee, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, + &[0u8; 32], // Value transferred to the contract. + input, + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/crypto_hashes.rs b/pallets/revive/fixtures/contracts/crypto_hashes.rs new file mode 100644 index 00000000..35cc03f1 --- /dev/null +++ b/pallets/revive/fixtures/contracts/crypto_hashes.rs @@ -0,0 +1,84 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +/// Called by the tests. +/// +/// The `call` function expects data in a certain format in the input buffer. +/// +/// 1. The first byte encodes an identifier for the crypto hash function under test. (*) +/// 2. The rest encodes the input data that is directly fed into the crypto hash function chosen in +/// 1. +/// +/// The `deploy` function then computes the chosen crypto hash function +/// given the input and puts the result into the output buffer. +/// After contract execution the test driver then asserts that the returned +/// values are equal to the expected bytes for the input and chosen hash +/// function. +/// +/// (*) The possible value for the crypto hash identifiers can be found below: +/// +/// | value | Algorithm | Bit Width | +/// |-------|-----------|-----------| +/// | 0 | SHA2 | 256 | +/// | 1 | KECCAK | 256 | +/// | 2 | BLAKE2 | 256 | +/// | 3 | BLAKE2 | 128 | +/// --------------------------------- + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 256, + chosen_hash_fn: u8, + input: [u8], + ); + + match chosen_hash_fn { + 1 => { + let mut output = [0u8; 32]; + api::hash_sha2_256(input, &mut output); + api::return_value(uapi::ReturnFlags::empty(), &output); + }, + 2 => { + let mut output = [0u8; 32]; + api::hash_keccak_256(input, &mut output); + api::return_value(uapi::ReturnFlags::empty(), &output); + }, + 3 => { + let mut output = [0u8; 32]; + api::hash_blake2_256(input, &mut output); + api::return_value(uapi::ReturnFlags::empty(), &output); + }, + 4 => { + let mut output = [0u8; 16]; + api::hash_blake2_128(input, &mut output); + api::return_value(uapi::ReturnFlags::empty(), &output); + }, + _ => panic!("unknown crypto hash function identifier"), + } +} diff --git a/pallets/revive/fixtures/contracts/debug_message_invalid_utf8.rs b/pallets/revive/fixtures/contracts/debug_message_invalid_utf8.rs new file mode 100644 index 00000000..6c850a9e --- /dev/null +++ b/pallets/revive/fixtures/contracts/debug_message_invalid_utf8.rs @@ -0,0 +1,33 @@ +// 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. + +//! Emit a debug message with an invalid utf-8 code. +#![no_std] +#![no_main] + +extern crate common; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + api::debug_message(b"\xFC").unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/debug_message_logging_disabled.rs b/pallets/revive/fixtures/contracts/debug_message_logging_disabled.rs new file mode 100644 index 00000000..0ce2b6b5 --- /dev/null +++ b/pallets/revive/fixtures/contracts/debug_message_logging_disabled.rs @@ -0,0 +1,33 @@ +// 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. + +//! Emit a "Hello World!" debug message but assume that logging is disabled. +#![no_std] +#![no_main] + +extern crate common; +use uapi::{HostFn, HostFnImpl as api, ReturnErrorCode}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + assert_eq!(api::debug_message(b"Hello World!"), Err(ReturnErrorCode::LoggingDisabled)); +} diff --git a/pallets/revive/fixtures/contracts/debug_message_works.rs b/pallets/revive/fixtures/contracts/debug_message_works.rs new file mode 100644 index 00000000..3a250950 --- /dev/null +++ b/pallets/revive/fixtures/contracts/debug_message_works.rs @@ -0,0 +1,33 @@ +// 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. + +//! Emit a "Hello World!" debug message. +#![no_std] +#![no_main] + +extern crate common; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + api::debug_message(b"Hello World!").unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/delegate_call.rs b/pallets/revive/fixtures/contracts/delegate_call.rs new file mode 100644 index 00000000..9fd15540 --- /dev/null +++ b/pallets/revive/fixtures/contracts/delegate_call.rs @@ -0,0 +1,49 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(code_hash: &[u8; 32],); + + let mut key = [0u8; 32]; + key[0] = 1u8; + + let mut value = [0u8; 32]; + let value = &mut &mut value[..]; + value[0] = 2u8; + + api::set_storage(StorageFlags::empty(), &key, value); + api::get_storage(StorageFlags::empty(), &key, value).unwrap(); + assert!(value[0] == 2u8); + + let input = [0u8; 0]; + api::delegate_call(uapi::CallFlags::empty(), code_hash, &input, None).unwrap(); + + api::get_storage(StorageFlags::empty(), &key, value).unwrap(); + assert!(value[0] == 1u8); +} diff --git a/pallets/revive/fixtures/contracts/delegate_call_lib.rs b/pallets/revive/fixtures/contracts/delegate_call_lib.rs new file mode 100644 index 00000000..95c1bd2a --- /dev/null +++ b/pallets/revive/fixtures/contracts/delegate_call_lib.rs @@ -0,0 +1,49 @@ +// 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. + +#![no_std] +#![no_main] + +use common::u64_output; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let mut key = [0u8; 32]; + key[0] = 1u8; + + // Place a value in storage. + let mut value = [0u8; 32]; + let value = &mut &mut value[..]; + value[0] = 1u8; + api::set_storage(StorageFlags::empty(), &key, value); + + // Assert that `value_transferred` is equal to the value + // passed to the `caller` contract: 1337. + let value = u64_output!(api::value_transferred,); + assert_eq!(value, 1337); + + // Assert that ALICE is the caller of the contract. + let mut caller = [0u8; 20]; + api::caller(&mut caller); + assert_eq!(caller, [1u8; 20]); +} diff --git a/pallets/revive/fixtures/contracts/delegate_call_simple.rs b/pallets/revive/fixtures/contracts/delegate_call_simple.rs new file mode 100644 index 00000000..20f8ec33 --- /dev/null +++ b/pallets/revive/fixtures/contracts/delegate_call_simple.rs @@ -0,0 +1,36 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(code_hash: &[u8; 32],); + + // Delegate call into passed code hash. + let input = [0u8; 0]; + api::delegate_call(uapi::CallFlags::empty(), code_hash, &input, None).unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/destroy_and_transfer.rs b/pallets/revive/fixtures/contracts/destroy_and_transfer.rs new file mode 100644 index 00000000..8342f4ac --- /dev/null +++ b/pallets/revive/fixtures/contracts/destroy_and_transfer.rs @@ -0,0 +1,86 @@ +// 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. + +#![no_std] +#![no_main] + +use common::{input, u256_bytes}; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +const ADDRESS_KEY: [u8; 32] = [0u8; 32]; +const VALUE: [u8; 32] = u256_bytes(65536); + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + input!(code_hash: &[u8; 32],); + + let input = [0u8; 0]; + let mut address = [0u8; 20]; + let salt = [47u8; 32]; + + api::instantiate( + code_hash, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &VALUE, + &input, + Some(&mut address), + None, + Some(&salt), + ) + .unwrap(); + + // Return the deployed contract address. + api::set_storage(StorageFlags::empty(), &ADDRESS_KEY, &address); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let mut callee_addr = [0u8; 20]; + let callee = &mut &mut callee_addr[..]; + api::get_storage(StorageFlags::empty(), &ADDRESS_KEY, callee).unwrap(); + assert!(callee.len() == 20); + + // Calling the destination contract with non-empty input data should fail. + let res = api::call( + uapi::CallFlags::empty(), + &callee_addr, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &VALUE, + &[0u8; 1], + None, + ); + assert!(matches!(res, Err(uapi::ReturnErrorCode::CalleeTrapped))); + + // Call the destination contract regularly, forcing it to self-destruct. + api::call( + uapi::CallFlags::empty(), + &callee_addr, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, // How much proof_size weight to devote for the execution. 0 = all. + None, // No deposit limit. + &VALUE, + &[0u8; 0], + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/drain.rs b/pallets/revive/fixtures/contracts/drain.rs new file mode 100644 index 00000000..0d644a42 --- /dev/null +++ b/pallets/revive/fixtures/contracts/drain.rs @@ -0,0 +1,41 @@ +// 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. + +#![no_std] +#![no_main] + +use common::{u256_bytes, u64_output}; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let balance = u64_output!(api::balance,); + let minimum_balance = u64_output!(api::minimum_balance,); + + // Make the transferred value exceed the balance by adding the minimum balance. + let balance = balance + minimum_balance; + + // Try to self-destruct by sending more balance to the 0 address. + // The call will fail because a contract transfer has a keep alive requirement. + let res = api::transfer(&[0u8; 20], &u256_bytes(balance)); + assert!(matches!(res, Err(uapi::ReturnErrorCode::TransferFailed))); +} diff --git a/pallets/revive/fixtures/contracts/dummy.rs b/pallets/revive/fixtures/contracts/dummy.rs new file mode 100644 index 00000000..c7294e99 --- /dev/null +++ b/pallets/revive/fixtures/contracts/dummy.rs @@ -0,0 +1,42 @@ +// 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. +#![no_std] +#![no_main] + +extern crate common; + +use uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +// Export that is never called. We can put code here that should be in the binary +// but is never supposed to be run. +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call_never() { + // Make sure the 0xDEADBEEF pattern appears in the binary by + // making it opaque to the optimizer. The benchmarking code will + // just find and replace this pattern to make the code unique when + // necessary. + api::return_value(ReturnFlags::empty(), &[0xDE, 0xAD, 0xBE, 0xEF]); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() {} diff --git a/pallets/revive/fixtures/contracts/ecdsa_recover.rs b/pallets/revive/fixtures/contracts/ecdsa_recover.rs new file mode 100644 index 00000000..0f28ca2c --- /dev/null +++ b/pallets/revive/fixtures/contracts/ecdsa_recover.rs @@ -0,0 +1,44 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + signature: [u8; 65], + hash: [u8; 32], + ); + + let mut output = [0u8; 33]; + api::ecdsa_recover( + &signature[..].try_into().unwrap(), + &hash[..].try_into().unwrap(), + &mut output, + ) + .unwrap(); + api::return_value(uapi::ReturnFlags::empty(), &output); +} diff --git a/pallets/revive/fixtures/contracts/event_and_return_on_deploy.rs b/pallets/revive/fixtures/contracts/event_and_return_on_deploy.rs new file mode 100644 index 00000000..5c438c1a --- /dev/null +++ b/pallets/revive/fixtures/contracts/event_and_return_on_deploy.rs @@ -0,0 +1,37 @@ +// 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. + +#![no_std] +#![no_main] + +extern crate common; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + let buffer = [1u8, 2, 3, 4]; + let topics = [[42u8; 32]; 1]; + api::deposit_event(&topics, &buffer); + api::return_value(uapi::ReturnFlags::empty(), &buffer); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + unreachable!() +} diff --git a/pallets/revive/fixtures/contracts/event_size.rs b/pallets/revive/fixtures/contracts/event_size.rs new file mode 100644 index 00000000..7f04ae42 --- /dev/null +++ b/pallets/revive/fixtures/contracts/event_size.rs @@ -0,0 +1,39 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +static BUFFER: [u8; 16 * 1024 + 1] = [0u8; 16 * 1024 + 1]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(len: u32,); + + let data = &BUFFER[..len as usize]; + let topics = [[0u8; 32]; 0]; + + api::deposit_event(&topics, data); +} diff --git a/pallets/revive/fixtures/contracts/float_instruction.rs b/pallets/revive/fixtures/contracts/float_instruction.rs new file mode 100644 index 00000000..b1eaaf85 --- /dev/null +++ b/pallets/revive/fixtures/contracts/float_instruction.rs @@ -0,0 +1,34 @@ +// 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. + +#![no_std] +#![no_main] + +extern crate common; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() {} + +#[no_mangle] +pub extern "C" fn add(a: f32, b: f32) -> f32 { + a + b +} diff --git a/pallets/revive/fixtures/contracts/instantiate_return_code.rs b/pallets/revive/fixtures/contracts/instantiate_return_code.rs new file mode 100644 index 00000000..9764859c --- /dev/null +++ b/pallets/revive/fixtures/contracts/instantiate_return_code.rs @@ -0,0 +1,52 @@ +// 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. + +#![no_std] +#![no_main] + +use common::{input, u256_bytes}; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(buffer, 36, code_hash: &[u8; 32],); + let input = &buffer[32..]; + + let err_code = match api::instantiate( + code_hash, + 0u64, // How much ref_time weight to devote for the execution. 0 = all. + 0u64, /* How much proof_size weight to devote for the execution. 0 = + * all. */ + None, // No deposit limit. + &u256_bytes(10_000u64), // Value to transfer. + input, + None, + None, + Some(&[0u8; 32]), // Salt. + ) { + Ok(_) => 0u32, + Err(code) => code as u32, + }; + + // Exit with success and take transfer return code to the output buffer. + api::return_value(uapi::ReturnFlags::empty(), &err_code.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/instr_benchmark.rs b/pallets/revive/fixtures/contracts/instr_benchmark.rs new file mode 100644 index 00000000..c5fb382c --- /dev/null +++ b/pallets/revive/fixtures/contracts/instr_benchmark.rs @@ -0,0 +1,41 @@ +// 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. +#![no_std] +#![no_main] + +extern crate common; + +use common::input; +use uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(rounds: u32, start: u32, div: u32, mult: u32, add: u32, ); + + let mut acc = start; + + for _ in 0..rounds { + acc = acc / div * mult + add; + } + + api::return_value(ReturnFlags::empty(), start.to_le_bytes().as_ref()); +} diff --git a/pallets/revive/fixtures/contracts/locking_delegate_dependency.rs b/pallets/revive/fixtures/contracts/locking_delegate_dependency.rs new file mode 100644 index 00000000..2efacb4e --- /dev/null +++ b/pallets/revive/fixtures/contracts/locking_delegate_dependency.rs @@ -0,0 +1,68 @@ +// 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. + +//! This contract tests the behavior of locking / unlocking delegate_dependencies when delegate +//! calling into a contract. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +const ETH_ALICE: [u8; 20] = [1u8; 20]; + +/// Load input data and perform the action specified by the input. +/// If `delegate_call` is true, then delegate call into the contract. +fn load_input(delegate_call: bool) { + input!( + action: u32, + code_hash: &[u8; 32], + ); + + match action { + // 1 = Lock delegate dependency + 1 => { + api::lock_delegate_dependency(code_hash); + }, + // 2 = Unlock delegate dependency + 2 => { + api::unlock_delegate_dependency(code_hash); + }, + // 3 = Terminate + 3 => { + api::terminate(Ð_ALICE); + }, + // Everything else is a noop + _ => {}, + } + + if delegate_call { + api::delegate_call(uapi::CallFlags::empty(), code_hash, &[], None).unwrap(); + } +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + load_input(false); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + load_input(true); +} diff --git a/pallets/revive/fixtures/contracts/multi_store.rs b/pallets/revive/fixtures/contracts/multi_store.rs new file mode 100644 index 00000000..079a4548 --- /dev/null +++ b/pallets/revive/fixtures/contracts/multi_store.rs @@ -0,0 +1,43 @@ +// 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. + +//! Does two stores to two separate storage items +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +static BUFFER: [u8; 512] = [0u8; 512]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + size1: u32, + size2: u32, + ); + + // Place a values in storage sizes are specified in the input buffer. + // We don't care about the contents of the storage item. + api::set_storage(StorageFlags::empty(), &[1u8; 32], &BUFFER[0..size1 as _]); + api::set_storage(StorageFlags::empty(), &[2u8; 32], &BUFFER[0..size2 as _]); +} diff --git a/pallets/revive/fixtures/contracts/new_set_code_hash_contract.rs b/pallets/revive/fixtures/contracts/new_set_code_hash_contract.rs new file mode 100644 index 00000000..2a59b6e3 --- /dev/null +++ b/pallets/revive/fixtures/contracts/new_set_code_hash_contract.rs @@ -0,0 +1,32 @@ +// 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. + +#![no_std] +#![no_main] + +extern crate common; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + api::return_value(uapi::ReturnFlags::empty(), &2u32.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/noop.rs b/pallets/revive/fixtures/contracts/noop.rs new file mode 100644 index 00000000..48d8a689 --- /dev/null +++ b/pallets/revive/fixtures/contracts/noop.rs @@ -0,0 +1,44 @@ +// 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. +#![no_std] +#![no_main] + +extern crate common; + +use common::input; +use uapi::HostFn; + +#[polkavm_derive::polkavm_import] +extern "C" { + pub fn noop(); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(rounds: u32, ); + + for _ in 0..rounds { + unsafe { + noop(); + } + } +} diff --git a/pallets/revive/fixtures/contracts/ok_trap_revert.rs b/pallets/revive/fixtures/contracts/ok_trap_revert.rs new file mode 100644 index 00000000..55115f86 --- /dev/null +++ b/pallets/revive/fixtures/contracts/ok_trap_revert.rs @@ -0,0 +1,44 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + ok_trap_revert(); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + ok_trap_revert(); +} + +#[no_mangle] +fn ok_trap_revert() { + input!(buffer, 4,); + match buffer.first().unwrap_or(&0) { + 1 => api::return_value(uapi::ReturnFlags::REVERT, &[0u8; 0]), + 2 => panic!(), + _ => {}, + }; +} diff --git a/pallets/revive/fixtures/contracts/oom_ro.rs b/pallets/revive/fixtures/contracts/oom_ro.rs new file mode 100644 index 00000000..41c080d5 --- /dev/null +++ b/pallets/revive/fixtures/contracts/oom_ro.rs @@ -0,0 +1,44 @@ +// 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. + +//! This creates a large ro section. Even though it is zero +//! initialized we expect them to be included into the blob. +//! This means it will fail at the blob size check. + +#![no_std] +#![no_main] + +extern crate common; + +use uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +static BUFFER: [u8; 1025 * 1024] = [0; 1025 * 1024]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call_never() { + // make sure the buffer is not optimized away + api::return_value(ReturnFlags::empty(), &BUFFER); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() {} diff --git a/pallets/revive/fixtures/contracts/oom_rw_included.rs b/pallets/revive/fixtures/contracts/oom_rw_included.rs new file mode 100644 index 00000000..2cdcf7ba --- /dev/null +++ b/pallets/revive/fixtures/contracts/oom_rw_included.rs @@ -0,0 +1,44 @@ +// 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. + +//! This creates a large rw section but with its contents +//! included into the blob. It should be rejected for its +//! blob size. + +#![no_std] +#![no_main] + +extern crate common; + +use uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +static mut BUFFER: [u8; 513 * 1024] = [42; 513 * 1024]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub unsafe extern "C" fn call_never() { + // make sure the buffer is not optimized away + api::return_value(ReturnFlags::empty(), &BUFFER); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() {} diff --git a/pallets/revive/fixtures/contracts/oom_rw_trailing.rs b/pallets/revive/fixtures/contracts/oom_rw_trailing.rs new file mode 100644 index 00000000..993be8e9 --- /dev/null +++ b/pallets/revive/fixtures/contracts/oom_rw_trailing.rs @@ -0,0 +1,44 @@ +// 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. + +//! This creates a large rw section but the trailing zeroes +//! are removed by the linker. It should be rejected even +//! though the blob is small enough. + +#![no_std] +#![no_main] + +extern crate common; + +use uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +static mut BUFFER: [u8; 1025 * 1024] = [0; 1025 * 1024]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub unsafe extern "C" fn call_never() { + // make sure the buffer is not optimized away + api::return_value(ReturnFlags::empty(), &BUFFER); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() {} diff --git a/pallets/revive/fixtures/contracts/read_only_call.rs b/pallets/revive/fixtures/contracts/read_only_call.rs new file mode 100644 index 00000000..ea74d568 --- /dev/null +++ b/pallets/revive/fixtures/contracts/read_only_call.rs @@ -0,0 +1,50 @@ +// 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. + +// This fixture tests if read-only call works as expected. +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 256, + callee_addr: &[u8; 20], + callee_input: [u8], + ); + + // Call the callee + api::call( + uapi::CallFlags::READ_ONLY, + callee_addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much proof_size to devote for the execution. 0 = all. + None, // No deposit limit. + &[0u8; 32], // Value transferred to the contract. + callee_input, + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/recurse.rs b/pallets/revive/fixtures/contracts/recurse.rs new file mode 100644 index 00000000..2e70d67d --- /dev/null +++ b/pallets/revive/fixtures/contracts/recurse.rs @@ -0,0 +1,54 @@ +// 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. + +//! This fixture calls itself as many times as passed as argument. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(calls_left: u32, ); + + // own address + let mut addr = [0u8; 20]; + api::address(&mut addr); + + if calls_left == 0 { + return + } + + api::call( + uapi::CallFlags::ALLOW_REENTRY, + &addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much deposit_limit to devote for the execution. 0 = all. + None, // No deposit limit. + &[0u8; 32], // Value transferred to the contract. + &(calls_left - 1).to_le_bytes(), + None, + ) + .unwrap(); +} diff --git a/pallets/revive/fixtures/contracts/return_data_api.rs b/pallets/revive/fixtures/contracts/return_data_api.rs new file mode 100644 index 00000000..846396b0 --- /dev/null +++ b/pallets/revive/fixtures/contracts/return_data_api.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. + +//! This tests that the `return_data_size` and `return_data_copy` APIs work. +//! +//! It does so by calling and instantiating the "return_with_data" fixture, +//! which always echoes back the input[4..] regardless of the call outcome. +//! +//! We also check that the saved return data is properly reset after a trap +//! and unaffected by plain transfers. + +#![no_std] +#![no_main] + +use common::{input, u256_bytes}; +use uapi::{HostFn, HostFnImpl as api}; + +const INPUT_BUF_SIZE: usize = 128; +static INPUT_DATA: [u8; INPUT_BUF_SIZE] = [0xFF; INPUT_BUF_SIZE]; +/// The "return_with_data" fixture echoes back 4 bytes less than the input +const OUTPUT_BUF_SIZE: usize = INPUT_BUF_SIZE - 4; +static OUTPUT_DATA: [u8; OUTPUT_BUF_SIZE] = [0xEE; OUTPUT_BUF_SIZE]; + +fn assert_return_data_after_call(input: &[u8]) { + assert_return_data_size_of(OUTPUT_BUF_SIZE as u64); + assert_plain_transfer_does_not_reset(OUTPUT_BUF_SIZE as u64); + assert_return_data_copy(&input[4..]); + reset_return_data(); +} + +/// Assert that what we get from [api::return_data_copy] matches `whole_return_data`, +/// either fully or partially with an offset and limited size. +fn assert_return_data_copy(whole_return_data: &[u8]) { + // The full return data should match + let mut buf = OUTPUT_DATA; + let mut full = &mut buf[..whole_return_data.len()]; + api::return_data_copy(&mut full, 0); + assert_eq!(whole_return_data, full); + + // Partial return data should match + let mut buf = OUTPUT_DATA; + let offset = 5; // we just pick some offset + let size = 32; // we just pick some size + let mut partial = &mut buf[offset..offset + size]; + api::return_data_copy(&mut partial, offset as u32); + assert_eq!(*partial, whole_return_data[offset..offset + size]); +} + +/// This function panics in a recursive contract call context. +fn recursion_guard() -> [u8; 20] { + let mut caller_address = [0u8; 20]; + api::caller(&mut caller_address); + + let mut own_address = [0u8; 20]; + api::address(&mut own_address); + + assert_ne!(caller_address, own_address); + + own_address +} + +/// Call ourselves recursively, which panics the callee and thus resets the return data. +fn reset_return_data() { + api::call( + uapi::CallFlags::ALLOW_REENTRY, + &recursion_guard(), + 0u64, + 0u64, + None, + &[0u8; 32], + &[0u8; 32], + None, + ) + .unwrap_err(); + assert_return_data_size_of(0); +} + +/// Assert [api::return_data_size] to match the `expected` value. +fn assert_return_data_size_of(expected: u64) { + let mut return_data_size = [0xff; 32]; + api::return_data_size(&mut return_data_size); + assert_eq!(return_data_size, u256_bytes(expected)); +} + +/// Assert [api::return_data_size] to match the `expected` value after a plain transfer +/// (plain transfers don't issue a call and so should not reset the return data) +fn assert_plain_transfer_does_not_reset(expected: u64) { + api::transfer(&[0; 20], &u256_bytes(128)).unwrap(); + assert_return_data_size_of(expected); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(code_hash: &[u8; 32],); + + // We didn't do anything yet; return data size should be 0 + assert_return_data_size_of(0); + + recursion_guard(); + + let mut address_buf = [0; 20]; + let construct_input = |exit_flag| { + let mut input = INPUT_DATA; + input[0] = exit_flag; + input[9] = 7; + input[17 / 2] = 127; + input[89 / 2] = 127; + input + }; + let mut instantiate = |exit_flag| { + api::instantiate( + code_hash, + 0u64, + 0u64, + None, + &[0; 32], + &construct_input(exit_flag), + Some(&mut address_buf), + None, + None, + ) + }; + let call = |exit_flag, address_buf| { + api::call( + uapi::CallFlags::empty(), + address_buf, + 0u64, + 0u64, + None, + &[0; 32], + &construct_input(exit_flag), + None, + ) + }; + + instantiate(0).unwrap(); + assert_return_data_after_call(&construct_input(0)[..]); + + instantiate(1).unwrap_err(); + assert_return_data_after_call(&construct_input(1)[..]); + + call(0, &address_buf).unwrap(); + assert_return_data_after_call(&construct_input(0)[..]); + + call(1, &address_buf).unwrap_err(); + assert_return_data_after_call(&construct_input(1)[..]); +} diff --git a/pallets/revive/fixtures/contracts/return_with_data.rs b/pallets/revive/fixtures/contracts/return_with_data.rs new file mode 100644 index 00000000..47a1cc91 --- /dev/null +++ b/pallets/revive/fixtures/contracts/return_with_data.rs @@ -0,0 +1,47 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + call(); +} + +/// Reads the first byte as the exit status and copy all but the first 4 bytes of the input as +/// output data. +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + input, 128, + exit_status: [u8; 4], + output: [u8], + ); + + // Burn some PoV, clear_storage consumes some PoV as in order to clear the storage we need to we + // need to read its size first. + api::clear_storage(StorageFlags::empty(), b""); + + let exit_status = uapi::ReturnFlags::from_bits(exit_status[0] as u32).unwrap(); + api::return_value(exit_status, output); +} diff --git a/pallets/revive/fixtures/contracts/run_out_of_gas.rs b/pallets/revive/fixtures/contracts/run_out_of_gas.rs new file mode 100644 index 00000000..11eaaa7c --- /dev/null +++ b/pallets/revive/fixtures/contracts/run_out_of_gas.rs @@ -0,0 +1,32 @@ +// 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. + +#![no_std] +#![no_main] + +extern crate common; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + #[allow(clippy::empty_loop)] + loop {} +} diff --git a/pallets/revive/fixtures/contracts/self_destruct.rs b/pallets/revive/fixtures/contracts/self_destruct.rs new file mode 100644 index 00000000..52497999 --- /dev/null +++ b/pallets/revive/fixtures/contracts/self_destruct.rs @@ -0,0 +1,57 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +const ETH_DJANGO: [u8; 20] = [4u8; 20]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + // If the input data is not empty, then recursively call self with empty input data. + // This should trap instead of self-destructing since a contract cannot be removed, while it's + // in the execution stack. If the recursive call traps, then trap here as well. + input!(input, 4,); + + if !input.is_empty() { + let mut addr = [0u8; 20]; + api::address(&mut addr); + + api::call( + uapi::CallFlags::ALLOW_REENTRY, + &addr, + 0u64, // How much ref_time to devote for the execution. 0 = all. + 0u64, // How much proof_size to devote for the execution. 0 = all. + None, // No deposit limit. + &[0u8; 32], // Value to transfer. + &[0u8; 0], + None, + ) + .unwrap(); + } else { + // Try to terminate and give balance to django. + api::terminate(Ð_DJANGO); + } +} diff --git a/pallets/revive/fixtures/contracts/self_destructing_constructor.rs b/pallets/revive/fixtures/contracts/self_destructing_constructor.rs new file mode 100644 index 00000000..3285aecb --- /dev/null +++ b/pallets/revive/fixtures/contracts/self_destructing_constructor.rs @@ -0,0 +1,32 @@ +// 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. + +#![no_std] +#![no_main] + +extern crate common; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + api::terminate(&[0u8; 20]); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() {} diff --git a/pallets/revive/fixtures/contracts/set_code_hash.rs b/pallets/revive/fixtures/contracts/set_code_hash.rs new file mode 100644 index 00000000..75995d7b --- /dev/null +++ b/pallets/revive/fixtures/contracts/set_code_hash.rs @@ -0,0 +1,37 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(addr: &[u8; 32],); + api::set_code_hash(addr).unwrap(); + + // we return 1 after setting new code_hash + // next `call` will NOT return this value, because contract code has been changed + api::return_value(uapi::ReturnFlags::empty(), &1u32.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/set_empty_storage.rs b/pallets/revive/fixtures/contracts/set_empty_storage.rs new file mode 100644 index 00000000..f8bbfe3f --- /dev/null +++ b/pallets/revive/fixtures/contracts/set_empty_storage.rs @@ -0,0 +1,32 @@ +// 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. + +#![no_std] +#![no_main] + +extern crate common; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + api::set_storage(StorageFlags::empty(), &[0u8; 32], &[0u8; 4]); +} diff --git a/pallets/revive/fixtures/contracts/set_transient_storage.rs b/pallets/revive/fixtures/contracts/set_transient_storage.rs new file mode 100644 index 00000000..a8a1fbd6 --- /dev/null +++ b/pallets/revive/fixtures/contracts/set_transient_storage.rs @@ -0,0 +1,41 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +static BUFFER: [u8; 512] = [0u8; 512]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(len: u32, ); + + let rounds = len as usize / BUFFER.len(); + let rest = len as usize / BUFFER.len(); + for i in 0..rounds { + api::set_storage(StorageFlags::TRANSIENT, &i.to_le_bytes(), &BUFFER); + } + api::set_storage(StorageFlags::TRANSIENT, &u32::MAX.to_le_bytes(), &BUFFER[..rest]); +} diff --git a/pallets/revive/fixtures/contracts/sr25519_verify.rs b/pallets/revive/fixtures/contracts/sr25519_verify.rs new file mode 100644 index 00000000..8920ce0d --- /dev/null +++ b/pallets/revive/fixtures/contracts/sr25519_verify.rs @@ -0,0 +1,48 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + signature: [u8; 64], + pub_key: [u8; 32], + msg: [u8; 11], + ); + + let exit_status = match api::sr25519_verify( + &signature.try_into().unwrap(), + msg, + &pub_key.try_into().unwrap(), + ) { + Ok(_) => 0u32, + Err(code) => code as u32, + }; + + // Exit with success and take transfer return code to the output buffer. + api::return_value(uapi::ReturnFlags::empty(), &exit_status.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/storage.rs b/pallets/revive/fixtures/contracts/storage.rs new file mode 100644 index 00000000..dc21e322 --- /dev/null +++ b/pallets/revive/fixtures/contracts/storage.rs @@ -0,0 +1,63 @@ +// 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. + +//! This contract tests the storage APIs. It sets and clears storage values using the different +//! versions of the storage APIs. +#![no_std] +#![no_main] + +use common::unwrap_output; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + const KEY: [u8; 32] = [1u8; 32]; + const VALUE_1: [u8; 4] = [1u8; 4]; + const VALUE_2: [u8; 4] = [2u8; 4]; + const VALUE_3: [u8; 4] = [3u8; 4]; + + api::set_storage(StorageFlags::empty(), &KEY, &VALUE_1); + assert_eq!(api::contains_storage(StorageFlags::empty(), &KEY), Some(VALUE_1.len() as _)); + unwrap_output!(val, [0u8; 4], api::get_storage, StorageFlags::empty(), &KEY); + assert_eq!(**val, VALUE_1); + + let existing = api::set_storage(StorageFlags::empty(), &KEY, &VALUE_2); + assert_eq!(existing, Some(VALUE_1.len() as _)); + unwrap_output!(val, [0u8; 4], api::get_storage, StorageFlags::empty(), &KEY); + assert_eq!(**val, VALUE_2); + + api::clear_storage(StorageFlags::empty(), &KEY); + assert_eq!(api::contains_storage(StorageFlags::empty(), &KEY), None); + + let existing = api::set_storage(StorageFlags::empty(), &KEY, &VALUE_3); + assert_eq!(existing, None); + assert_eq!(api::contains_storage(StorageFlags::empty(), &KEY), Some(VALUE_1.len() as _)); + unwrap_output!(val, [0u8; 32], api::get_storage, StorageFlags::empty(), &KEY); + assert_eq!(**val, VALUE_3); + + api::clear_storage(StorageFlags::empty(), &KEY); + assert_eq!(api::contains_storage(StorageFlags::empty(), &KEY), None); + let existing = api::set_storage(StorageFlags::empty(), &KEY, &VALUE_3); + assert_eq!(existing, None); + unwrap_output!(val, [0u8; 32], api::take_storage, StorageFlags::empty(), &KEY); + assert_eq!(**val, VALUE_3); +} diff --git a/pallets/revive/fixtures/contracts/storage_size.rs b/pallets/revive/fixtures/contracts/storage_size.rs new file mode 100644 index 00000000..617e8d2e --- /dev/null +++ b/pallets/revive/fixtures/contracts/storage_size.rs @@ -0,0 +1,50 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +static mut BUFFER: [u8; 16 * 1024 + 1] = [0u8; 16 * 1024 + 1]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(len: u32, ); + + let data = unsafe { + &BUFFER[..len as usize] + }; + + // Place a garbage value in storage, the size of which is specified by the call input. + let mut key = [0u8; 32]; + key[0] = 1; + + api::set_storage(StorageFlags::empty(), &key, data); + + let data = unsafe { + &mut &mut BUFFER[..] + }; + api::get_storage(StorageFlags::empty(), &key, data).unwrap(); + assert_eq!(data.len(), len as usize); +} diff --git a/pallets/revive/fixtures/contracts/store_call.rs b/pallets/revive/fixtures/contracts/store_call.rs new file mode 100644 index 00000000..b08d4451 --- /dev/null +++ b/pallets/revive/fixtures/contracts/store_call.rs @@ -0,0 +1,42 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +static BUFFER: [u8; 512] = [0u8; 512]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(len: u32, ); + + let data = &BUFFER[..len as usize]; + + // Place a garbage value in storage, the size of which is specified by the call input. + let mut key = [0u8; 32]; + key[0] = 1; + + api::set_storage(StorageFlags::empty(), &key, data); +} diff --git a/pallets/revive/fixtures/contracts/store_deploy.rs b/pallets/revive/fixtures/contracts/store_deploy.rs new file mode 100644 index 00000000..e08c79d7 --- /dev/null +++ b/pallets/revive/fixtures/contracts/store_deploy.rs @@ -0,0 +1,42 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +static BUFFER: [u8; 16 * 1024 + 1] = [0u8; 16 * 1024 + 1]; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + input!(len: u32, ); + + let data = &BUFFER[..len as usize]; + + // place a garbage value in storage, the size of which is specified by the call input. + let mut key = [0u8; 32]; + key[0] = 1; + + api::set_storage(StorageFlags::empty(), &key, data); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() {} diff --git a/pallets/revive/fixtures/contracts/transfer_return_code.rs b/pallets/revive/fixtures/contracts/transfer_return_code.rs new file mode 100644 index 00000000..bfeca9b8 --- /dev/null +++ b/pallets/revive/fixtures/contracts/transfer_return_code.rs @@ -0,0 +1,38 @@ +// 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. + +#![no_std] +#![no_main] + +use common::u256_bytes; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let ret_code = match api::transfer(&[0u8; 20], &u256_bytes(100u64)) { + Ok(_) => 0u32, + Err(code) => code as u32, + }; + + // Exit with success and take transfer return code to the output buffer. + api::return_value(uapi::ReturnFlags::empty(), &ret_code.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/transient_storage.rs b/pallets/revive/fixtures/contracts/transient_storage.rs new file mode 100644 index 00000000..aa0a6943 --- /dev/null +++ b/pallets/revive/fixtures/contracts/transient_storage.rs @@ -0,0 +1,55 @@ +// 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. + +//! This contract tests the transient storage APIs. +#![no_std] +#![no_main] + +use common::unwrap_output; +use uapi::{HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + const KEY: [u8; 32] = [1u8; 32]; + const VALUE_1: [u8; 4] = [1u8; 4]; + const VALUE_2: [u8; 5] = [2u8; 5]; + const VALUE_3: [u8; 6] = [3u8; 6]; + + let existing = api::set_storage(StorageFlags::TRANSIENT, &KEY, &VALUE_1); + assert_eq!(existing, None); + assert_eq!(api::contains_storage(StorageFlags::TRANSIENT, &KEY), Some(VALUE_1.len() as _)); + unwrap_output!(val, [0u8; 32], api::get_storage, StorageFlags::TRANSIENT, &KEY); + assert_eq!(**val, VALUE_1); + + let existing = api::set_storage(StorageFlags::TRANSIENT, &KEY, &VALUE_2); + assert_eq!(existing, Some(VALUE_1.len() as _)); + unwrap_output!(val, [0u8; 32], api::get_storage, StorageFlags::TRANSIENT, &KEY); + assert_eq!(**val, VALUE_2); + + assert_eq!(api::clear_storage(StorageFlags::TRANSIENT, &KEY), Some(VALUE_2.len() as _)); + assert_eq!(api::contains_storage(StorageFlags::TRANSIENT, &KEY), None); + + let existing = api::set_storage(StorageFlags::TRANSIENT, &KEY, &VALUE_3); + assert_eq!(existing, None); + unwrap_output!(val, [0u8; 128], api::take_storage, StorageFlags::TRANSIENT, &KEY); + assert_eq!(**val, VALUE_3); +} diff --git a/pallets/revive/fixtures/contracts/xcm_execute.rs b/pallets/revive/fixtures/contracts/xcm_execute.rs new file mode 100644 index 00000000..1d570ffe --- /dev/null +++ b/pallets/revive/fixtures/contracts/xcm_execute.rs @@ -0,0 +1,40 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(512, msg: [u8],); + + #[allow(deprecated)] + let err_code = match api::xcm_execute(msg) { + Ok(_) => 0u32, + Err(code) => code as u32, + }; + + api::return_value(uapi::ReturnFlags::empty(), &err_code.to_le_bytes()); +} diff --git a/pallets/revive/fixtures/contracts/xcm_send.rs b/pallets/revive/fixtures/contracts/xcm_send.rs new file mode 100644 index 00000000..6d4629e7 --- /dev/null +++ b/pallets/revive/fixtures/contracts/xcm_send.rs @@ -0,0 +1,42 @@ +// 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. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 512, + dest: [u8; 3], + msg: [u8], + ); + + let mut message_id = [0u8; 32]; + + #[allow(deprecated)] + api::xcm_send(dest, msg, &mut message_id).unwrap(); + api::return_value(uapi::ReturnFlags::empty(), &message_id); +} diff --git a/pallets/revive/fixtures/src/lib.rs b/pallets/revive/fixtures/src/lib.rs new file mode 100644 index 00000000..5548dca6 --- /dev/null +++ b/pallets/revive/fixtures/src/lib.rs @@ -0,0 +1,74 @@ +// 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. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +/// Load a given wasm module and returns a wasm binary contents along with it's hash. +#[cfg(feature = "std")] +pub fn compile_module(fixture_name: &str) -> anyhow::Result<(Vec, sp_core::H256)> { + let out_dir: std::path::PathBuf = env!("OUT_DIR").into(); + let fixture_path = out_dir.join(format!("{fixture_name}.polkavm")); + log::debug!("Loading fixture from {fixture_path:?}"); + let binary = std::fs::read(fixture_path)?; + let code_hash = sp_io::hashing::keccak_256(&binary); + Ok((binary, sp_core::H256(code_hash))) +} + +/// Fixtures used in runtime benchmarks. +/// +/// We explicitly include those fixtures into the binary to make them +/// available in no-std environments (runtime benchmarks). +pub mod bench { + use alloc::vec::Vec; + + #[cfg(feature = "riscv")] + macro_rules! fixture { + ($name: literal) => { + include_bytes!(concat!(env!("OUT_DIR"), "/", $name, ".polkavm")) + }; + } + #[cfg(not(feature = "riscv"))] + macro_rules! fixture { + ($name: literal) => { + &[] + }; + } + pub const DUMMY: &[u8] = fixture!("dummy"); + pub const NOOP: &[u8] = fixture!("noop"); + pub const INSTR: &[u8] = fixture!("instr_benchmark"); + + pub fn dummy_unique(replace_with: u32) -> Vec { + let mut dummy = DUMMY.to_vec(); + let idx = dummy + .windows(4) + .position(|w| w == &[0xDE, 0xAD, 0xBE, 0xEF]) + .expect("Benchmark fixture contains this pattern; qed"); + dummy[idx..idx + 4].copy_from_slice(&replace_with.to_le_bytes()); + dummy + } +} + +#[cfg(test)] +mod test { + #[test] + fn out_dir_should_have_compiled_mocks() { + let out_dir: std::path::PathBuf = env!("OUT_DIR").into(); + assert!(out_dir.join("dummy.polkavm").exists()); + } +} diff --git a/pallets/revive/mock-network/Cargo.toml b/pallets/revive/mock-network/Cargo.toml new file mode 100644 index 00000000..b4a68196 --- /dev/null +++ b/pallets/revive/mock-network/Cargo.toml @@ -0,0 +1,89 @@ +[package] +authors.workspace = true +description = "A mock network for testing pallet-revive." +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "pallet-revive-mock-network" +repository.workspace = true +version = "0.1.0" + +[lints] +workspace = true + +[dependencies] +codec = { features = [ "derive", "max-encoded-len" ], workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-assets = { workspace = true, default-features = true } +pallet-balances = { workspace = true, default-features = true } +pallet-message-queue = { workspace = true, default-features = true } +pallet-proxy = { workspace = true, default-features = true } +pallet-revive = { workspace = true, default-features = true } +pallet-revive-proc-macro = { workspace = true, default-features = true } +pallet-revive-uapi = { workspace = true } +pallet-timestamp = { workspace = true, default-features = true } +pallet-utility = { workspace = true, default-features = true } +pallet-xcm = { workspace = true } +polkadot-parachain-primitives = { workspace = true, default-features = true } +polkadot-primitives = { workspace = true, default-features = true } +polkadot-runtime-parachains = { workspace = true, default-features = true } +scale-info = { features = [ "derive" ], workspace = true } +sp-api = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-keystore = { workspace = true, default-features = true } +sp-runtime = { workspace = true } +sp-tracing = { workspace = true, default-features = true } +xcm = { workspace = true } +xcm-builder = { workspace = true, default-features = true } +xcm-executor = { workspace = true } +xcm-simulator = { workspace = true, default-features = true } + +[dev-dependencies] +assert_matches = { workspace = true } +pallet-revive-fixtures = { workspace = true } +pretty_assertions = { workspace = true } + +[features] +default = [ "std" ] +riscv = [ "pallet-revive-fixtures/riscv" ] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-message-queue/runtime-benchmarks", + "pallet-proxy/runtime-benchmarks", + "pallet-revive/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "pallet-utility/runtime-benchmarks", + "pallet-xcm/runtime-benchmarks", + "polkadot-parachain-primitives/runtime-benchmarks", + "polkadot-primitives/runtime-benchmarks", + "polkadot-runtime-parachains/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-proxy/std", + "pallet-revive-fixtures/std", + "pallet-revive/std", + "pallet-timestamp/std", + "pallet-utility/std", + "pallet-xcm/std", + "scale-info/std", + "sp-api/std", + "sp-core/std", + "sp-io/std", + "sp-keystore/std", + "sp-runtime/std", + "xcm-executor/std", + "xcm/std", +] diff --git a/pallets/revive/mock-network/src/lib.rs b/pallets/revive/mock-network/src/lib.rs new file mode 100644 index 00000000..84899465 --- /dev/null +++ b/pallets/revive/mock-network/src/lib.rs @@ -0,0 +1,152 @@ +// Copyright Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +pub mod mocks; +pub mod parachain; +pub mod primitives; +pub mod relay_chain; + +#[cfg(all(test, feature = "riscv"))] +mod tests; + +use crate::primitives::{AccountId, UNITS}; +pub use pallet_revive::test_utils::{ALICE, BOB}; +use sp_runtime::BuildStorage; +use xcm::latest::prelude::*; +use xcm_executor::traits::ConvertLocation; +pub use xcm_simulator::TestExt; +use xcm_simulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain}; + +// Accounts +pub const ADMIN: sp_runtime::AccountId32 = sp_runtime::AccountId32::new([0u8; 32]); + +// Balances +pub const INITIAL_BALANCE: u128 = 1_000_000_000 * UNITS; + +decl_test_parachain! { + pub struct ParaA { + Runtime = parachain::Runtime, + XcmpMessageHandler = parachain::MsgQueue, + DmpMessageHandler = parachain::MsgQueue, + new_ext = para_ext(1), + } +} + +decl_test_relay_chain! { + pub struct Relay { + Runtime = relay_chain::Runtime, + RuntimeCall = relay_chain::RuntimeCall, + RuntimeEvent = relay_chain::RuntimeEvent, + XcmConfig = relay_chain::XcmConfig, + MessageQueue = relay_chain::MessageQueue, + System = relay_chain::System, + new_ext = relay_ext(), + } +} + +decl_test_network! { + pub struct MockNet { + relay_chain = Relay, + parachains = vec![ + (1, ParaA), + ], + } +} + +pub fn relay_sovereign_account_id() -> AccountId { + let location: Location = (Parent,).into(); + parachain::SovereignAccountOf::convert_location(&location).unwrap() +} + +pub fn parachain_sovereign_account_id(para: u32) -> AccountId { + let location: Location = (Parachain(para),).into(); + relay_chain::SovereignAccountOf::convert_location(&location).unwrap() +} + +pub fn parachain_account_sovereign_account_id( + para: u32, + who: sp_runtime::AccountId32, +) -> AccountId { + let location: Location = ( + Parachain(para), + AccountId32 { network: Some(relay_chain::RelayNetwork::get()), id: who.into() }, + ) + .into(); + relay_chain::SovereignAccountOf::convert_location(&location).unwrap() +} + +pub fn para_ext(para_id: u32) -> sp_io::TestExternalities { + use parachain::{MsgQueue, Runtime, System}; + + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![ + (ALICE, INITIAL_BALANCE), + (relay_sovereign_account_id(), INITIAL_BALANCE), + (BOB, INITIAL_BALANCE), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_assets::GenesisConfig:: { + assets: vec![ + (0u128, ADMIN, false, 1u128), // Create derivative asset for relay's native token + ], + metadata: Default::default(), + accounts: vec![ + (0u128, ALICE, INITIAL_BALANCE), + (0u128, relay_sovereign_account_id(), INITIAL_BALANCE), + ], + next_asset_id: None, + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + sp_tracing::try_init_simple(); + System::set_block_number(1); + MsgQueue::set_para_id(para_id.into()); + }); + ext +} + +pub fn relay_ext() -> sp_io::TestExternalities { + use relay_chain::{Runtime, System}; + + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![ + (ALICE, INITIAL_BALANCE), + (parachain_sovereign_account_id(1), INITIAL_BALANCE), + (parachain_account_sovereign_account_id(1, ALICE), INITIAL_BALANCE), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext +} + +pub type ParachainPalletXcm = pallet_xcm::Pallet; +pub type ParachainBalances = pallet_balances::Pallet; diff --git a/pallets/revive/mock-network/src/mocks.rs b/pallets/revive/mock-network/src/mocks.rs new file mode 100644 index 00000000..6903f01e --- /dev/null +++ b/pallets/revive/mock-network/src/mocks.rs @@ -0,0 +1,18 @@ +// Copyright Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +pub mod msg_queue; +pub mod relay_message_queue; diff --git a/pallets/revive/mock-network/src/mocks/msg_queue.rs b/pallets/revive/mock-network/src/mocks/msg_queue.rs new file mode 100644 index 00000000..88ab8135 --- /dev/null +++ b/pallets/revive/mock-network/src/mocks/msg_queue.rs @@ -0,0 +1,186 @@ +// Copyright Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! Parachain runtime mock. + +use codec::{Decode, Encode}; + +use frame_support::weights::Weight; +use polkadot_parachain_primitives::primitives::{ + DmpMessageHandler, Id as ParaId, XcmpMessageFormat, XcmpMessageHandler, +}; +use polkadot_primitives::BlockNumber as RelayBlockNumber; +use sp_runtime::traits::{Get, Hash}; + +use xcm::{latest::prelude::*, VersionedXcm}; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type XcmExecutor: ExecuteXcm; + } + + #[pallet::call] + impl Pallet {} + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type ParachainId = StorageValue<_, ParaId, ValueQuery>; + + #[pallet::storage] + /// A queue of received DMP messages + pub(super) type ReceivedDmp = StorageValue<_, Vec>, ValueQuery>; + + impl Get for Pallet { + fn get() -> ParaId { + ParachainId::::get() + } + } + + pub type MessageId = [u8; 32]; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Some XCM was executed OK. + Success(Option), + /// Some XCM failed. + Fail(Option, XcmError), + /// Bad XCM version used. + BadVersion(Option), + /// Bad XCM format used. + BadFormat(Option), + + // DMP + /// Downward message is invalid XCM. + InvalidFormat(MessageId), + /// Downward message is unsupported version of XCM. + UnsupportedVersion(MessageId), + /// Downward message executed with the given outcome. + ExecutedDownward(MessageId, Outcome), + } + + impl Pallet { + pub fn set_para_id(para_id: ParaId) { + ParachainId::::put(para_id); + } + + pub fn parachain_id() -> ParaId { + ParachainId::::get() + } + + pub fn received_dmp() -> Vec> { + ReceivedDmp::::get() + } + + fn handle_xcmp_message( + sender: ParaId, + _sent_at: RelayBlockNumber, + xcm: VersionedXcm, + max_weight: Weight, + ) -> Result { + let hash = Encode::using_encoded(&xcm, T::Hashing::hash); + let mut message_hash = Encode::using_encoded(&xcm, sp_io::hashing::blake2_256); + let (result, event) = match Xcm::::try_from(xcm) { + Ok(xcm) => { + let location = (Parent, Parachain(sender.into())); + match T::XcmExecutor::prepare_and_execute( + location, + xcm, + &mut message_hash, + max_weight, + Weight::zero(), + ) { + Outcome::Error { error } => (Err(error), Event::Fail(Some(hash), error)), + Outcome::Complete { used } => (Ok(used), Event::Success(Some(hash))), + // As far as the caller is concerned, this was dispatched without error, so + // we just report the weight used. + Outcome::Incomplete { used, error } => + (Ok(used), Event::Fail(Some(hash), error)), + } + }, + Err(()) => (Err(XcmError::UnhandledXcmVersion), Event::BadVersion(Some(hash))), + }; + Self::deposit_event(event); + result + } + } + + impl XcmpMessageHandler for Pallet { + fn handle_xcmp_messages<'a, I: Iterator>( + iter: I, + max_weight: Weight, + ) -> Weight { + for (sender, sent_at, data) in iter { + let mut data_ref = data; + let _ = XcmpMessageFormat::decode(&mut data_ref) + .expect("Simulator encodes with versioned xcm format; qed"); + + let mut remaining_fragments = data_ref; + while !remaining_fragments.is_empty() { + if let Ok(xcm) = + VersionedXcm::::decode(&mut remaining_fragments) + { + let _ = Self::handle_xcmp_message(sender, sent_at, xcm, max_weight); + } else { + debug_assert!(false, "Invalid incoming XCMP message data"); + } + } + } + max_weight + } + } + + impl DmpMessageHandler for Pallet { + fn handle_dmp_messages( + iter: impl Iterator)>, + limit: Weight, + ) -> Weight { + for (_i, (_sent_at, data)) in iter.enumerate() { + let mut id = sp_io::hashing::blake2_256(&data[..]); + let maybe_versioned = VersionedXcm::::decode(&mut &data[..]); + match maybe_versioned { + Err(_) => { + Self::deposit_event(Event::InvalidFormat(id)); + }, + Ok(versioned) => match Xcm::try_from(versioned) { + Err(()) => Self::deposit_event(Event::UnsupportedVersion(id)), + Ok(x) => { + let outcome = T::XcmExecutor::prepare_and_execute( + Parent, + x.clone(), + &mut id, + limit, + Weight::zero(), + ); + ReceivedDmp::::append(x); + Self::deposit_event(Event::ExecutedDownward(id, outcome)); + }, + }, + } + } + limit + } + } +} diff --git a/pallets/revive/mock-network/src/mocks/relay_message_queue.rs b/pallets/revive/mock-network/src/mocks/relay_message_queue.rs new file mode 100644 index 00000000..50f0febd --- /dev/null +++ b/pallets/revive/mock-network/src/mocks/relay_message_queue.rs @@ -0,0 +1,52 @@ +// Copyright Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +use frame_support::{parameter_types, weights::Weight}; +use xcm::latest::prelude::*; +use xcm_simulator::{ + AggregateMessageOrigin, ProcessMessage, ProcessMessageError, UmpQueueId, WeightMeter, +}; + +use crate::relay_chain::{RuntimeCall, XcmConfig}; + +parameter_types! { + /// Amount of weight that can be spent per block to service messages. + pub MessageQueueServiceWeight: Weight = Weight::from_parts(1_000_000_000, 1_000_000); + pub const MessageQueueHeapSize: u32 = 65_536; + pub const MessageQueueMaxStale: u32 = 16; +} + +/// Message processor to handle any messages that were enqueued into the `MessageQueue` pallet. +pub struct MessageProcessor; +impl ProcessMessage for MessageProcessor { + type Origin = AggregateMessageOrigin; + + fn process_message( + message: &[u8], + origin: Self::Origin, + meter: &mut WeightMeter, + id: &mut [u8; 32], + ) -> Result { + let para = match origin { + AggregateMessageOrigin::Ump(UmpQueueId::Para(para)) => para, + }; + xcm_builder::ProcessXcmMessage::< + Junction, + xcm_executor::XcmExecutor, + RuntimeCall, + >::process_message(message, Junction::Parachain(para.into()), meter, id) + } +} diff --git a/pallets/revive/mock-network/src/parachain.rs b/pallets/revive/mock-network/src/parachain.rs new file mode 100644 index 00000000..0fd2248d --- /dev/null +++ b/pallets/revive/mock-network/src/parachain.rs @@ -0,0 +1,346 @@ +// Copyright Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! Parachain runtime mock. + +mod contracts_config; +use crate::{ + mocks::msg_queue::pallet as mock_msg_queue, + primitives::{AccountId, AssetIdForAssets, Balance}, +}; +use core::marker::PhantomData; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{AsEnsureOriginWithArg, Contains, ContainsPair, Everything, EverythingBut, Nothing}, + weights::{ + constants::{WEIGHT_PROOF_SIZE_PER_MB, WEIGHT_REF_TIME_PER_SECOND}, + Weight, + }, +}; +use frame_system::{EnsureRoot, EnsureSigned}; +use pallet_xcm::XcmPassthrough; +use sp_core::{ConstU32, ConstU64, H256}; +use sp_runtime::traits::{Get, IdentityLookup, MaybeEquivalence}; + +use xcm::latest::prelude::*; +use xcm_builder::{ + AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowTopLevelPaidExecutionFrom, + ConvertedConcreteId, EnsureXcmOrigin, FixedRateOfFungible, FixedWeightBounds, + FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, IsConcrete, NativeAsset, + NoChecking, ParentAsSuperuser, ParentIsPreset, SignedAccountId32AsNative, SignedToAccountId32, + SovereignSignedViaLocation, WithComputedOrigin, +}; +use xcm_executor::{traits::JustTry, Config, XcmExecutor}; + +pub type SovereignAccountOf = + (AccountId32Aliases, ParentIsPreset); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Block = Block; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub ExistentialDeposit: Balance = 1; + pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Runtime { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<0>; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type WeightInfo = (); +} + +parameter_types! { + pub const AssetDeposit: u128 = 1_000_000; + pub const MetadataDepositBase: u128 = 1_000_000; + pub const MetadataDepositPerByte: u128 = 100_000; + pub const AssetAccountDeposit: u128 = 1_000_000; + pub const ApprovalDeposit: u128 = 1_000_000; + pub const AssetsStringLimit: u32 = 50; + pub const RemoveItemsLimit: u32 = 50; +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetIdForAssets; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type AssetAccountDeposit = AssetAccountDeposit; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = AssetsStringLimit; + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type RemoveItemsLimit = RemoveItemsLimit; + type AssetIdParameter = AssetIdForAssets; + type CallbackHandle = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +parameter_types! { + pub const ReservedXcmpWeight: Weight = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND.saturating_div(4), 0); + pub const ReservedDmpWeight: Weight = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND.saturating_div(4), 0); +} + +parameter_types! { + pub const KsmLocation: Location = Location::parent(); + pub const TokenLocation: Location = Here.into_location(); + pub const RelayNetwork: NetworkId = ByGenesis([0; 32]); + pub UniversalLocation: InteriorLocation = [GlobalConsensus(RelayNetwork::get()), Parachain(MsgQueue::parachain_id().into())].into(); +} + +pub type XcmOriginToCallOrigin = ( + SovereignSignedViaLocation, + ParentAsSuperuser, + SignedAccountId32AsNative, + XcmPassthrough, +); + +parameter_types! { + pub const XcmInstructionWeight: Weight = Weight::from_parts(1_000, 1_000); + pub TokensPerSecondPerMegabyte: (AssetId, u128, u128) = (AssetId(Parent.into()), 1_000_000_000_000, 1024 * 1024); + pub const MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; + pub ForeignPrefix: Location = (Parent,).into(); + pub CheckingAccount: AccountId = PolkadotXcm::check_account(); + pub TrustedLockPairs: (Location, AssetFilter) = + (Parent.into(), Wild(AllOf { id: AssetId(Parent.into()), fun: WildFungible })); +} + +pub fn estimate_message_fee(number_of_instructions: u64) -> u128 { + let weight = estimate_weight(number_of_instructions); + + estimate_fee_for_weight(weight) +} + +pub fn estimate_weight(number_of_instructions: u64) -> Weight { + XcmInstructionWeight::get().saturating_mul(number_of_instructions) +} + +pub fn estimate_fee_for_weight(weight: Weight) -> u128 { + let (_, units_per_second, units_per_mb) = TokensPerSecondPerMegabyte::get(); + + units_per_second * (weight.ref_time() as u128) / (WEIGHT_REF_TIME_PER_SECOND as u128) + + units_per_mb * (weight.proof_size() as u128) / (WEIGHT_PROOF_SIZE_PER_MB as u128) +} + +pub type LocalBalancesTransactor = + FungibleAdapter, SovereignAccountOf, AccountId, ()>; + +pub struct FromLocationToAsset(PhantomData<(Location, AssetId)>); +impl MaybeEquivalence + for FromLocationToAsset +{ + fn convert(value: &Location) -> Option { + match value.unpack() { + (1, []) => Some(0 as AssetIdForAssets), + (1, [Parachain(para_id)]) => Some(*para_id as AssetIdForAssets), + _ => None, + } + } + + fn convert_back(_id: &AssetIdForAssets) -> Option { + None + } +} + +pub type ForeignAssetsTransactor = FungiblesAdapter< + Assets, + ConvertedConcreteId< + AssetIdForAssets, + Balance, + FromLocationToAsset, + JustTry, + >, + SovereignAccountOf, + AccountId, + NoChecking, + CheckingAccount, +>; + +/// Means for transacting assets on this chain +pub type AssetTransactors = (LocalBalancesTransactor, ForeignAssetsTransactor); + +pub struct ParentRelay; +impl Contains for ParentRelay { + fn contains(location: &Location) -> bool { + location.contains_parents_only(1) + } +} +pub struct ThisParachain; +impl Contains for ThisParachain { + fn contains(location: &Location) -> bool { + matches!(location.unpack(), (0, [Junction::AccountId32 { .. }])) + } +} + +pub type XcmRouter = crate::ParachainXcmRouter; + +pub type Barrier = ( + xcm_builder::AllowUnpaidExecutionFrom, + WithComputedOrigin< + (AllowExplicitUnpaidExecutionFrom, AllowTopLevelPaidExecutionFrom), + UniversalLocation, + ConstU32<1>, + >, +); + +parameter_types! { + pub NftCollectionOne: AssetFilter + = Wild(AllOf { fun: WildNonFungible, id: AssetId((Parent, GeneralIndex(1)).into()) }); + pub NftCollectionOneForRelay: (AssetFilter, Location) + = (NftCollectionOne::get(), Parent.into()); + pub RelayNativeAsset: AssetFilter = Wild(AllOf { fun: WildFungible, id: AssetId((Parent, Here).into()) }); + pub RelayNativeAssetForRelay: (AssetFilter, Location) = (RelayNativeAsset::get(), Parent.into()); +} +pub type TrustedTeleporters = + (xcm_builder::Case, xcm_builder::Case); +pub type TrustedReserves = EverythingBut>; + +pub struct XcmConfig; +impl Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = XcmRouter; + type AssetTransactor = AssetTransactors; + type OriginConverter = XcmOriginToCallOrigin; + type IsReserve = (NativeAsset, TrustedReserves); + type IsTeleporter = TrustedTeleporters; + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = FixedRateOfFungible; + type ResponseHandler = PolkadotXcm; + type AssetTrap = PolkadotXcm; + type AssetLocker = PolkadotXcm; + type AssetExchanger = (); + type AssetClaims = PolkadotXcm; + type SubscriptionService = PolkadotXcm; + type PalletInstancesInfo = AllPalletsWithSystem; + type FeeManager = (); + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type MessageExporter = (); + type UniversalAliases = Nothing; + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Everything; + type Aliasers = Nothing; + type TransactionalProcessor = FrameTransactionalProcessor; + type HrmpNewChannelOpenRequestHandler = (); + type HrmpChannelAcceptedHandler = (); + type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; +} + +impl mock_msg_queue::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type XcmExecutor = XcmExecutor; +} + +pub type LocalOriginToLocation = SignedToAccountId32; + +pub struct TrustedLockerCase(PhantomData); +impl> ContainsPair for TrustedLockerCase { + fn contains(origin: &Location, asset: &Asset) -> bool { + let (o, a) = T::get(); + a.matches(asset) && &o == origin + } +} + +impl pallet_xcm::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = EnsureXcmOrigin; + type XcmRouter = XcmRouter; + type ExecuteXcmOrigin = EnsureXcmOrigin; + type XcmExecuteFilter = Everything; + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = Nothing; + type XcmReserveTransferFilter = Everything; + type Weigher = FixedWeightBounds; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; + type Currency = Balances; + type CurrencyMatcher = IsConcrete; + type TrustedLockers = TrustedLockerCase; + type SovereignAccountOf = SovereignAccountOf; + type MaxLockers = ConstU32<8>; + type MaxRemoteLockConsumers = ConstU32<0>; + type RemoteLockConsumerIdentifier = (); + type WeightInfo = pallet_xcm::TestWeightInfo; + type AdminOrigin = EnsureRoot; +} + +type Block = frame_system::mocking::MockBlock; + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<1>; + type WeightInfo = (); +} + +construct_runtime!( + pub enum Runtime + { + System: frame_system, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + MsgQueue: mock_msg_queue, + PolkadotXcm: pallet_xcm, + Contracts: pallet_revive, + Assets: pallet_assets, + } +); diff --git a/pallets/revive/mock-network/src/parachain/contracts_config.rs b/pallets/revive/mock-network/src/parachain/contracts_config.rs new file mode 100644 index 00000000..c13c337d --- /dev/null +++ b/pallets/revive/mock-network/src/parachain/contracts_config.rs @@ -0,0 +1,27 @@ +// Copyright Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +use super::{Balances, Runtime, RuntimeCall, RuntimeEvent}; +use crate::parachain::RuntimeHoldReason; +use frame_support::derive_impl; + +#[derive_impl(pallet_revive::config_preludes::TestDefaultConfig)] +impl pallet_revive::Config for Runtime { + type AddressMapper = pallet_revive::DefaultAddressMapper; + type Currency = Balances; + type Time = super::Timestamp; + type Xcm = pallet_xcm::Pallet; +} diff --git a/pallets/revive/mock-network/src/primitives.rs b/pallets/revive/mock-network/src/primitives.rs new file mode 100644 index 00000000..cb9b2f00 --- /dev/null +++ b/pallets/revive/mock-network/src/primitives.rs @@ -0,0 +1,23 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +pub type Balance = u128; + +pub const UNITS: Balance = 10_000_000_000; +pub const CENTS: Balance = UNITS / 100; // 100_000_000 + +pub type AccountId = sp_runtime::AccountId32; +pub type AssetIdForAssets = u128; diff --git a/pallets/revive/mock-network/src/relay_chain.rs b/pallets/revive/mock-network/src/relay_chain.rs new file mode 100644 index 00000000..705578cd --- /dev/null +++ b/pallets/revive/mock-network/src/relay_chain.rs @@ -0,0 +1,239 @@ +// Copyright Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! Relay chain runtime mock. + +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{Contains, Everything, Nothing}, + weights::Weight, +}; + +use frame_system::EnsureRoot; +use sp_core::{ConstU32, H256}; +use sp_runtime::traits::IdentityLookup; + +use polkadot_parachain_primitives::primitives::Id as ParaId; +use polkadot_runtime_parachains::{configuration, origin, shared}; +use xcm::latest::prelude::*; +use xcm_builder::{ + AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowSubscriptionsFrom, + AllowTopLevelPaidExecutionFrom, ChildParachainAsNative, ChildParachainConvertsVia, + ChildSystemParachainAsSuperuser, DescribeAllTerminal, DescribeFamily, FixedRateOfFungible, + FixedWeightBounds, FrameTransactionalProcessor, FungibleAdapter, HashedDescription, IsConcrete, + SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, WithComputedOrigin, +}; +use xcm_executor::{Config, XcmExecutor}; + +use super::{ + mocks::relay_message_queue::*, + primitives::{AccountId, Balance}, +}; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Block = Block; + type Nonce = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub ExistentialDeposit: Balance = 1; + pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = MaxLocks; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<0>; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; +} + +impl shared::Config for Runtime { + type DisabledValidators = (); +} + +impl configuration::Config for Runtime { + type WeightInfo = configuration::TestWeightInfo; +} + +parameter_types! { + pub RelayNetwork: NetworkId = ByGenesis([0; 32]); + pub const TokenLocation: Location = Here.into_location(); + pub UniversalLocation: InteriorLocation = RelayNetwork::get().into(); + pub UnitWeightCost: u64 = 1_000; +} + +pub type SovereignAccountOf = ( + HashedDescription>, + AccountId32Aliases, + ChildParachainConvertsVia, +); + +pub type LocalBalancesTransactor = + FungibleAdapter, SovereignAccountOf, AccountId, ()>; + +pub type AssetTransactors = LocalBalancesTransactor; + +type LocalOriginConverter = ( + SovereignSignedViaLocation, + ChildParachainAsNative, + SignedAccountId32AsNative, + ChildSystemParachainAsSuperuser, +); + +parameter_types! { + pub const XcmInstructionWeight: Weight = Weight::from_parts(1_000, 1_000); + pub TokensPerSecondPerMegabyte: (AssetId, u128, u128) = + (AssetId(TokenLocation::get()), 1_000_000_000_000, 1024 * 1024); + pub const MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; +} + +pub struct ChildrenParachains; +impl Contains for ChildrenParachains { + fn contains(location: &Location) -> bool { + matches!(location.unpack(), (0, [Parachain(_)])) + } +} + +pub type XcmRouter = crate::RelayChainXcmRouter; +pub type Barrier = WithComputedOrigin< + ( + AllowExplicitUnpaidExecutionFrom, + AllowTopLevelPaidExecutionFrom, + AllowSubscriptionsFrom, + ), + UniversalLocation, + ConstU32<1>, +>; + +pub struct XcmConfig; +impl Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = XcmRouter; + type AssetTransactor = AssetTransactors; + type OriginConverter = LocalOriginConverter; + type IsReserve = (); + type IsTeleporter = (); + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = FixedRateOfFungible; + type ResponseHandler = XcmPallet; + type AssetTrap = XcmPallet; + type AssetLocker = XcmPallet; + type AssetExchanger = (); + type AssetClaims = XcmPallet; + type SubscriptionService = XcmPallet; + type PalletInstancesInfo = AllPalletsWithSystem; + type FeeManager = (); + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type MessageExporter = (); + type UniversalAliases = Nothing; + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Everything; + type Aliasers = Nothing; + type TransactionalProcessor = FrameTransactionalProcessor; + type HrmpNewChannelOpenRequestHandler = (); + type HrmpChannelAcceptedHandler = (); + type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; +} + +pub type LocalOriginToLocation = SignedToAccountId32; + +impl pallet_xcm::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmRouter = XcmRouter; + type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmExecuteFilter = Everything; + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = Everything; + type XcmReserveTransferFilter = Everything; + type Weigher = FixedWeightBounds; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; + type Currency = Balances; + type CurrencyMatcher = IsConcrete; + type TrustedLockers = (); + type SovereignAccountOf = SovereignAccountOf; + type MaxLockers = ConstU32<8>; + type MaxRemoteLockConsumers = ConstU32<0>; + type RemoteLockConsumerIdentifier = (); + type WeightInfo = pallet_xcm::TestWeightInfo; + type AdminOrigin = EnsureRoot; +} + +impl origin::Config for Runtime {} + +type Block = frame_system::mocking::MockBlock; + +impl pallet_message_queue::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Size = u32; + type HeapSize = MessageQueueHeapSize; + type MaxStale = MessageQueueMaxStale; + type ServiceWeight = MessageQueueServiceWeight; + type IdleMaxServiceWeight = (); + type MessageProcessor = MessageProcessor; + type QueueChangeHandler = (); + type WeightInfo = (); + type QueuePausedQuery = (); +} + +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + ParasOrigin: origin, + XcmPallet: pallet_xcm, + MessageQueue: pallet_message_queue, + } +); diff --git a/pallets/revive/mock-network/src/tests.rs b/pallets/revive/mock-network/src/tests.rs new file mode 100644 index 00000000..bd05726a --- /dev/null +++ b/pallets/revive/mock-network/src/tests.rs @@ -0,0 +1,204 @@ +// 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::{ + parachain, parachain_account_sovereign_account_id, primitives::CENTS, relay_chain, MockNet, + ParaA, ParachainBalances, Relay, ALICE, BOB, INITIAL_BALANCE, +}; +use codec::{Decode, Encode}; +use frame_support::traits::{fungibles::Mutate, Currency}; +use frame_system::RawOrigin; +use pallet_revive::{ + test_utils::{self, builder::*}, + Code, +}; +use pallet_revive_fixtures::compile_module; +use pallet_revive_uapi::ReturnErrorCode; +use sp_core::H160; +use xcm::{v4::prelude::*, VersionedLocation, VersionedXcm}; +use xcm_simulator::TestExt; + +macro_rules! assert_return_code { + ( $x:expr , $y:expr $(,)? ) => {{ + assert_eq!(u32::from_le_bytes($x.data[..].try_into().unwrap()), $y as u32); + }}; +} + +fn bare_call(dest: H160) -> BareCallBuilder { + BareCallBuilder::::bare_call(RawOrigin::Signed(ALICE).into(), dest) +} + +/// Instantiate the tests contract, and fund it with some balance and assets. +fn instantiate_test_contract(name: &str) -> Contract { + let (wasm, _) = compile_module(name).unwrap(); + + // Instantiate contract. + let contract = ParaA::execute_with(|| { + BareInstantiateBuilder::::bare_instantiate( + RawOrigin::Signed(ALICE).into(), + Code::Upload(wasm), + ) + .storage_deposit_limit(1_000_000_000_000) + .build_and_unwrap_contract() + }); + + // Funds contract account with some balance and assets. + ParaA::execute_with(|| { + parachain::Balances::make_free_balance_be(&contract.account_id, INITIAL_BALANCE); + parachain::Assets::mint_into(0u32.into(), &contract.account_id, INITIAL_BALANCE).unwrap(); + }); + Relay::execute_with(|| { + let sovereign_account = + parachain_account_sovereign_account_id(1u32, contract.account_id.clone()); + relay_chain::Balances::make_free_balance_be(&sovereign_account, INITIAL_BALANCE); + }); + + contract +} + +#[test] +fn test_xcm_execute() { + MockNet::reset(); + + let Contract { addr, account_id } = instantiate_test_contract("xcm_execute"); + + // Execute XCM instructions through the contract. + ParaA::execute_with(|| { + let amount: u128 = 10 * CENTS; + let assets: Asset = (Here, amount).into(); + let beneficiary = AccountId32 { network: None, id: BOB.clone().into() }; + + // The XCM used to transfer funds to Bob. + let message: Xcm<()> = Xcm::builder_unsafe() + .withdraw_asset(assets.clone()) + .deposit_asset(assets, beneficiary) + .build(); + + let result = bare_call(addr).data(VersionedXcm::V4(message).encode()).build(); + + assert_eq!(result.gas_consumed, result.gas_required); + assert_return_code!(&result.result.unwrap(), ReturnErrorCode::Success); + + // Check if the funds are subtracted from the account of Alice and added to the account of + // Bob. + let initial = INITIAL_BALANCE; + assert_eq!(ParachainBalances::free_balance(BOB), initial + amount); + assert_eq!(ParachainBalances::free_balance(&account_id), initial - amount); + }); +} + +#[test] +fn test_xcm_execute_incomplete() { + MockNet::reset(); + + let Contract { addr, account_id } = instantiate_test_contract("xcm_execute"); + let amount = 10 * CENTS; + + // Execute XCM instructions through the contract. + ParaA::execute_with(|| { + let assets: Asset = (Here, amount).into(); + let beneficiary = AccountId32 { network: None, id: BOB.clone().into() }; + + // The XCM used to transfer funds to Bob. + let message: Xcm<()> = Xcm::builder_unsafe() + .withdraw_asset(assets.clone()) + // This will fail as the contract does not have enough balance to complete both + // withdrawals. + .withdraw_asset((Here, INITIAL_BALANCE)) + .buy_execution(assets.clone(), Unlimited) + .deposit_asset(assets, beneficiary) + .build(); + + let result = bare_call(addr).data(VersionedXcm::V4(message).encode()).build(); + + assert_eq!(result.gas_consumed, result.gas_required); + assert_return_code!(&result.result.unwrap(), ReturnErrorCode::XcmExecutionFailed); + + assert_eq!(ParachainBalances::free_balance(BOB), INITIAL_BALANCE); + assert_eq!(ParachainBalances::free_balance(&account_id), INITIAL_BALANCE - amount); + }); +} + +#[test] +fn test_xcm_execute_reentrant_call() { + MockNet::reset(); + + let Contract { addr, .. } = instantiate_test_contract("xcm_execute"); + + ParaA::execute_with(|| { + let transact_call = parachain::RuntimeCall::Contracts(pallet_revive::Call::call { + dest: addr, + gas_limit: 1_000_000.into(), + storage_deposit_limit: test_utils::deposit_limit::(), + data: vec![], + value: 0u128, + }); + + // The XCM used to transfer funds to Bob. + let message: Xcm = Xcm::builder_unsafe() + .transact(OriginKind::Native, 1_000_000_000, transact_call.encode()) + .expect_transact_status(MaybeErrorCode::Success) + .build(); + + let result = bare_call(addr) + .data(VersionedXcm::V4(message).encode()) + .build_and_unwrap_result(); + + assert_return_code!(&result, ReturnErrorCode::XcmExecutionFailed); + + // Funds should not change hands as the XCM transact failed. + assert_eq!(ParachainBalances::free_balance(BOB), INITIAL_BALANCE); + }); +} + +#[test] +fn test_xcm_send() { + MockNet::reset(); + let Contract { addr, account_id } = instantiate_test_contract("xcm_send"); + let amount = 1_000 * CENTS; + let fee = parachain::estimate_message_fee(4); // Accounts for the `DescendOrigin` instruction added by `send_xcm` + + // Send XCM instructions through the contract, to transfer some funds from the contract + // derivative account to Alice on the relay chain. + ParaA::execute_with(|| { + let dest = VersionedLocation::V4(Parent.into()); + let assets: Asset = (Here, amount).into(); + let beneficiary = AccountId32 { network: None, id: ALICE.clone().into() }; + + let message: Xcm<()> = Xcm::builder() + .withdraw_asset(assets.clone()) + .buy_execution((Here, fee), Unlimited) + .deposit_asset(assets, beneficiary) + .build(); + + let result = bare_call(addr) + .data((dest, VersionedXcm::V4(message)).encode()) + .build_and_unwrap_result(); + + let mut data = &result.data[..]; + XcmHash::decode(&mut data).expect("Failed to decode xcm_send message_id"); + }); + + Relay::execute_with(|| { + let derived_contract_addr = ¶chain_account_sovereign_account_id(1, account_id); + assert_eq!( + INITIAL_BALANCE - amount, + relay_chain::Balances::free_balance(derived_contract_addr) + ); + assert_eq!(INITIAL_BALANCE + amount - fee, relay_chain::Balances::free_balance(ALICE)); + }); +} diff --git a/pallets/revive/proc-macro/Cargo.toml b/pallets/revive/proc-macro/Cargo.toml new file mode 100644 index 00000000..6d9622f2 --- /dev/null +++ b/pallets/revive/proc-macro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors.workspace = true +description = "Procedural macros used in pallet_revive" +edition.workspace = true +homepage.workspace = true +license = "Apache-2.0" +name = "pallet-revive-proc-macro" +repository.workspace = true +version = "0.1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { features = [ "full" ], workspace = true } diff --git a/pallets/revive/proc-macro/src/lib.rs b/pallets/revive/proc-macro/src/lib.rs new file mode 100644 index 00000000..1b6871c0 --- /dev/null +++ b/pallets/revive/proc-macro/src/lib.rs @@ -0,0 +1,617 @@ +// 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. + +//! Procedural macros used in the contracts module. +//! +//! Most likely you should use the [`#[define_env]`][`macro@define_env`] attribute macro which hides +//! boilerplate of defining external environment for a wasm module. + +use proc_macro::TokenStream; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens}; +use syn::{parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, FnArg, Ident}; + +/// Defines a host functions set that can be imported by contract wasm code. +/// +/// **NB**: Be advised that all functions defined by this macro +/// will panic if called with unexpected arguments. +/// +/// It's up to you as the user of this macro to check signatures of wasm code to be executed +/// and reject the code if any imported function has a mismatched signature. +/// +/// ## Example +/// +/// ```nocompile +/// #[define_env] +/// pub mod some_env { +/// fn foo(ctx: _, memory: _, key_ptr: u32, value_ptr: u32, value_len: u32) -> Result<(), TrapReason> { +/// ctx.some_host_fn(KeyType::Fix, key_ptr, value_ptr, value_len).map(|_| ()) +/// } +/// } +/// ``` +/// This example will expand to the `foo()` defined in the wasm module named `seal0`. This is +/// because the module `seal0` is the default when no module is specified. +/// +/// To define a host function in `seal2` and `seal3` modules, it should be annotated with the +/// appropriate attribute as follows: +/// +/// ## Example +/// +/// ```nocompile +/// #[define_env] +/// pub mod some_env { +/// #[version(2)] +/// fn foo(ctx: _, memory: _, key_ptr: u32, value_ptr: u32, value_len: u32) -> Result { +/// ctx.some_host_fn(KeyType::Fix, key_ptr, value_ptr, value_len).map(|_| ()) +/// } +/// +/// #[version(3)] +/// #[unstable] +/// fn bar(ctx: _, memory: _, key_ptr: u32, value_ptr: u32, value_len: u32) -> Result { +/// ctx.some_host_fn(KeyType::Fix, key_ptr, value_ptr, value_len).map(|_| ()) +/// } +/// } +/// ``` +/// The function `bar` is additionally annotated with `unstable` which removes it from the stable +/// interface. Check out the README to learn about unstable functions. +/// +/// +/// In this example, the following host functions will be generated by the macro: +/// - `foo()` in module `seal1`, +/// - `seal_foo()` in module `seal1`, +/// - `bar()` in module `seal42`. +/// +/// Only following return types are allowed for the host functions defined with the macro: +/// - `Result<(), TrapReason>`, +/// - `Result`, +/// - `Result`. +/// +/// The macro expands to `pub struct Env` declaration, with the following traits implementations: +/// - `pallet_revive::wasm::Environment> where E: Ext` +/// - `pallet_revive::wasm::Environment<()>` +/// +/// The implementation on `()` can be used in places where no `Ext` exists, yet. This is useful +/// when only checking whether a code can be instantiated without actually executing any code. +/// +/// +/// To build up these docs, run: +/// +/// ```nocompile +/// cargo doc +/// ``` +#[proc_macro_attribute] +pub fn define_env(attr: TokenStream, item: TokenStream) -> TokenStream { + if !attr.is_empty() { + let msg = r#"Invalid `define_env` attribute macro: expected no attributes: + - `#[define_env]`"#; + let span = TokenStream2::from(attr).span(); + return syn::Error::new(span, msg).to_compile_error().into() + } + + let item = syn::parse_macro_input!(item as syn::ItemMod); + + match EnvDef::try_from(item) { + Ok(mut def) => expand_env(&mut def).into(), + Err(e) => e.to_compile_error().into(), + } +} + +/// Parsed environment definition. +struct EnvDef { + host_funcs: Vec, +} + +/// Parsed host function definition. +struct HostFn { + item: syn::ItemFn, + api_version: Option, + name: String, + returns: HostFnReturn, + cfg: Option, +} + +enum HostFnReturn { + Unit, + U32, + ReturnCode, +} + +impl HostFnReturn { + fn map_output(&self) -> TokenStream2 { + match self { + Self::Unit => quote! { |_| None }, + Self::U32 => quote! { |ret_val| Some(ret_val) }, + Self::ReturnCode => quote! { |ret_code| Some(ret_code.into()) }, + } + } + + fn success_type(&self) -> syn::ReturnType { + match self { + Self::Unit => syn::ReturnType::Default, + Self::U32 => parse_quote! { -> u32 }, + Self::ReturnCode => parse_quote! { -> ReturnErrorCode }, + } + } +} + +impl EnvDef { + pub fn try_from(item: syn::ItemMod) -> syn::Result { + let span = item.span(); + let err = |msg| syn::Error::new(span, msg); + let items = &item + .content + .as_ref() + .ok_or(err("Invalid environment definition, expected `mod` to be inlined."))? + .1; + + let extract_fn = |i: &syn::Item| match i { + syn::Item::Fn(i_fn) => Some(i_fn.clone()), + _ => None, + }; + + let host_funcs = items + .iter() + .filter_map(extract_fn) + .map(HostFn::try_from) + .collect::, _>>()?; + + Ok(Self { host_funcs }) + } +} + +impl HostFn { + pub fn try_from(mut item: syn::ItemFn) -> syn::Result { + let err = |span, msg| { + let msg = format!("Invalid host function definition.\n{}", msg); + syn::Error::new(span, msg) + }; + + // process attributes + let msg = "Only #[api_version()], #[cfg] and #[mutating] attributes are allowed."; + let span = item.span(); + let mut attrs = item.attrs.clone(); + attrs.retain(|a| !a.path().is_ident("doc")); + let mut api_version = None; + let mut mutating = false; + let mut cfg = None; + while let Some(attr) = attrs.pop() { + let ident = attr.path().get_ident().ok_or(err(span, msg))?.to_string(); + match ident.as_str() { + "api_version" => { + if api_version.is_some() { + return Err(err(span, "#[api_version] can only be specified once")) + } + api_version = + Some(attr.parse_args::().and_then(|lit| lit.base10_parse())?); + }, + "mutating" => { + if mutating { + return Err(err(span, "#[mutating] can only be specified once")) + } + mutating = true; + }, + "cfg" => { + if cfg.is_some() { + return Err(err(span, "#[cfg] can only be specified once")) + } + cfg = Some(attr); + }, + id => return Err(err(span, &format!("Unsupported attribute \"{id}\". {msg}"))), + } + } + + if mutating { + let stmt = syn::parse_quote! { + if self.ext().is_read_only() { + return Err(Error::::StateChangeDenied.into()); + } + }; + item.block.stmts.insert(0, stmt); + } + + let name = item.sig.ident.to_string(); + + let msg = "Every function must start with these two parameters: &mut self, memory: &mut M"; + let special_args = item + .sig + .inputs + .iter() + .take(2) + .enumerate() + .map(|(i, arg)| is_valid_special_arg(i, arg)) + .fold(0u32, |acc, valid| if valid { acc + 1 } else { acc }); + + if special_args != 2 { + return Err(err(span, msg)) + } + + // process return type + let msg = r#"Should return one of the following: + - Result<(), TrapReason>, + - Result, + - Result"#; + let ret_ty = match item.clone().sig.output { + syn::ReturnType::Type(_, ty) => Ok(ty.clone()), + _ => Err(err(span, &msg)), + }?; + match *ret_ty { + syn::Type::Path(tp) => { + let result = &tp.path.segments.last().ok_or(err(span, &msg))?; + let (id, span) = (result.ident.to_string(), result.ident.span()); + id.eq(&"Result".to_string()).then_some(()).ok_or(err(span, &msg))?; + + match &result.arguments { + syn::PathArguments::AngleBracketed(group) => { + if group.args.len() != 2 { + return Err(err(span, &msg)) + }; + + let arg2 = group.args.last().ok_or(err(span, &msg))?; + + let err_ty = match arg2 { + syn::GenericArgument::Type(ty) => Ok(ty.clone()), + _ => Err(err(arg2.span(), &msg)), + }?; + + match err_ty { + syn::Type::Path(tp) => Ok(tp + .path + .segments + .first() + .ok_or(err(arg2.span(), &msg))? + .ident + .to_string()), + _ => Err(err(tp.span(), &msg)), + }? + .eq("TrapReason") + .then_some(()) + .ok_or(err(span, &msg))?; + + let arg1 = group.args.first().ok_or(err(span, &msg))?; + let ok_ty = match arg1 { + syn::GenericArgument::Type(ty) => Ok(ty.clone()), + _ => Err(err(arg1.span(), &msg)), + }?; + let ok_ty_str = match ok_ty { + syn::Type::Path(tp) => Ok(tp + .path + .segments + .first() + .ok_or(err(arg1.span(), &msg))? + .ident + .to_string()), + syn::Type::Tuple(tt) => { + if !tt.elems.is_empty() { + return Err(err(arg1.span(), &msg)) + }; + Ok("()".to_string()) + }, + _ => Err(err(ok_ty.span(), &msg)), + }?; + let returns = match ok_ty_str.as_str() { + "()" => Ok(HostFnReturn::Unit), + "u32" => Ok(HostFnReturn::U32), + "ReturnErrorCode" => Ok(HostFnReturn::ReturnCode), + _ => Err(err(arg1.span(), &msg)), + }?; + + Ok(Self { item, api_version, name, returns, cfg }) + }, + _ => Err(err(span, &msg)), + } + }, + _ => Err(err(span, &msg)), + } + } +} + +fn is_valid_special_arg(idx: usize, arg: &FnArg) -> bool { + match (idx, arg) { + (0, FnArg::Receiver(rec)) => rec.reference.is_some() && rec.mutability.is_some(), + (1, FnArg::Typed(pat)) => { + let ident = + if let syn::Pat::Ident(ref ident) = *pat.pat { &ident.ident } else { return false }; + if !(ident == "memory" || ident == "_memory") { + return false + } + matches!(*pat.ty, syn::Type::Reference(_)) + }, + _ => false, + } +} + +fn arg_decoder<'a, P, I>(param_names: P, param_types: I) -> TokenStream2 +where + P: Iterator> + Clone, + I: Iterator> + Clone, +{ + const ALLOWED_REGISTERS: u32 = 6; + let mut registers_used = 0; + let mut bindings = vec![]; + for (idx, (name, ty)) in param_names.clone().zip(param_types.clone()).enumerate() { + let syn::Type::Path(path) = &**ty else { + panic!("Type needs to be path"); + }; + let Some(ident) = path.path.get_ident() else { + panic!("Type needs to be ident"); + }; + let size = if ident == "i8" || + ident == "i16" || + ident == "i32" || + ident == "u8" || + ident == "u16" || + ident == "u32" + { + 1 + } else if ident == "i64" || ident == "u64" { + 2 + } else { + panic!("Pass by value only supports primitives"); + }; + registers_used += size; + if registers_used > ALLOWED_REGISTERS { + return quote! { + let (#( #param_names, )*): (#( #param_types, )*) = memory.read_as(__a0__)?; + } + } + let this_reg = quote::format_ident!("__a{}__", idx); + let next_reg = quote::format_ident!("__a{}__", idx + 1); + let binding = if size == 1 { + quote! { + let #name = #this_reg as #ty; + } + } else { + quote! { + let #name = (#this_reg as #ty) | ((#next_reg as #ty) << 32); + } + }; + bindings.push(binding); + } + quote! { + #( #bindings )* + } +} + +/// Expands environment definition. +/// Should generate source code for: +/// - implementations of the host functions to be added to the wasm runtime environment (see +/// `expand_impls()`). +fn expand_env(def: &EnvDef) -> TokenStream2 { + let impls = expand_functions(def); + let bench_impls = expand_bench_functions(def); + let docs = expand_func_doc(def); + let highest_api_version = + def.host_funcs.iter().filter_map(|f| f.api_version).max().unwrap_or_default(); + + quote! { + #[cfg(test)] + pub const HIGHEST_API_VERSION: u16 = #highest_api_version; + + impl<'a, E: Ext, M: PolkaVmInstance> Runtime<'a, E, M> { + fn handle_ecall( + &mut self, + memory: &mut M, + __syscall_symbol__: &[u8], + __available_api_version__: ApiVersion, + ) -> Result, TrapReason> + { + #impls + } + } + + #[cfg(feature = "runtime-benchmarks")] + impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { + #bench_impls + } + + /// Documentation of the syscalls (host functions) available to contracts. + /// + /// Each of the functions in this trait represent a function that is callable + /// by the contract. Guests use the function name as the import symbol. + /// + /// # Note + /// + /// This module is not meant to be used by any code. Rather, it is meant to be + /// consumed by humans through rustdoc. + #[cfg(doc)] + pub trait SyscallDoc { + #docs + } + } +} + +fn expand_functions(def: &EnvDef) -> TokenStream2 { + let impls = def.host_funcs.iter().map(|f| { + // skip the self and memory argument + let params = f.item.sig.inputs.iter().skip(2); + let param_names = params.clone().filter_map(|arg| { + let FnArg::Typed(arg) = arg else { + return None; + }; + Some(&arg.pat) + }); + let param_types = params.clone().filter_map(|arg| { + let FnArg::Typed(arg) = arg else { + return None; + }; + Some(&arg.ty) + }); + let arg_decoder = arg_decoder(param_names, param_types); + let cfg = &f.cfg; + let name = &f.name; + let syscall_symbol = Literal::byte_string(name.as_bytes()); + let body = &f.item.block; + let map_output = f.returns.map_output(); + let output = &f.item.sig.output; + let api_version = match f.api_version { + Some(version) => quote! { Some(#version) }, + None => quote! { None }, + }; + + // wrapped host function body call with host function traces + // see https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/contracts#host-function-tracing + let wrapped_body_with_trace = { + let trace_fmt_args = params.clone().filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(p) => match *p.pat.clone() { + syn::Pat::Ident(ref pat_ident) => Some(pat_ident.ident.clone()), + _ => None, + }, + }); + + let params_fmt_str = trace_fmt_args + .clone() + .map(|s| format!("{s}: {{:?}}")) + .collect::>() + .join(", "); + let trace_fmt_str = format!("{}({}) = {{:?}}\n", name, params_fmt_str); + + quote! { + // wrap body in closure to make sure the tracing is always executed + let result = (|| #body)(); + if ::log::log_enabled!(target: "runtime::revive::strace", ::log::Level::Trace) { + use core::fmt::Write; + let mut w = sp_std::Writer::default(); + let _ = core::write!(&mut w, #trace_fmt_str, #( #trace_fmt_args, )* result); + let msg = core::str::from_utf8(&w.inner()).unwrap_or_default(); + self.ext().append_debug_buffer(msg); + } + result + } + }; + + quote! { + #cfg + #syscall_symbol if __is_available__(#api_version) => { + // closure is needed so that "?" can infere the correct type + (|| #output { + #arg_decoder + #wrapped_body_with_trace + })().map(#map_output) + }, + } + }); + + quote! { + // Write gas from polkavm into pallet-revive before entering the host function. + let __gas_left_before__ = self + .ext + .gas_meter_mut() + .sync_from_executor(memory.gas()) + .map_err(TrapReason::from)?; + + // This is the overhead to call an empty syscall that always needs to be charged. + self.charge_gas(crate::wasm::RuntimeCosts::HostFn).map_err(TrapReason::from)?; + + // Not all APIs are available depending on configuration or when the code was deployed. + // This closure will be used by syscall specific code to perform this check. + let __is_available__ = |syscall_version: Option| { + match __available_api_version__ { + ApiVersion::UnsafeNewest => true, + ApiVersion::Versioned(max_available_version) => + syscall_version + .map(|required_version| max_available_version >= required_version) + .unwrap_or(false), + } + }; + + // They will be mapped to variable names by the syscall specific code. + let (__a0__, __a1__, __a2__, __a3__, __a4__, __a5__) = memory.read_input_regs(); + + // Execute the syscall specific logic in a closure so that the gas metering code is always executed. + let result = (|| match __syscall_symbol__ { + #( #impls )* + _ => Err(TrapReason::SupervisorError(Error::::InvalidSyscall.into())) + })(); + + // Write gas from pallet-revive into polkavm after leaving the host function. + let gas = self.ext.gas_meter_mut().sync_to_executor(__gas_left_before__).map_err(TrapReason::from)?; + memory.set_gas(gas.into()); + result + } +} + +fn expand_bench_functions(def: &EnvDef) -> TokenStream2 { + let impls = def.host_funcs.iter().map(|f| { + // skip the context and memory argument + let params = f.item.sig.inputs.iter().skip(2); + let cfg = &f.cfg; + let name = &f.name; + let body = &f.item.block; + let output = &f.item.sig.output; + + let name = Ident::new(&format!("bench_{name}"), Span::call_site()); + quote! { + #cfg + pub fn #name(&mut self, memory: &mut M, #(#params),*) #output { + #body + } + } + }); + + quote! { + #( #impls )* + } +} + +fn expand_func_doc(def: &EnvDef) -> TokenStream2 { + let docs = def.host_funcs.iter().map(|func| { + // Remove auxiliary args: `ctx: _` and `memory: _` + let func_decl = { + let mut sig = func.item.sig.clone(); + sig.inputs = sig + .inputs + .iter() + .skip(2) + .map(|p| p.clone()) + .collect::>(); + sig.output = func.returns.success_type(); + sig.to_token_stream() + }; + let func_doc = { + let func_docs = { + let docs = func.item.attrs.iter().filter(|a| a.path().is_ident("doc")).map(|d| { + let docs = d.to_token_stream(); + quote! { #docs } + }); + quote! { #( #docs )* } + }; + let availability = if let Some(version) = func.api_version { + let info = format!( + "\n# Required API version\nThis API was added in version **{version}**.", + ); + quote! { #[doc = #info] } + } else { + let info = "\n# Unstable API\nThis API is not standardized and only available for \ + testing."; + quote! { #[doc = #info] } + }; + quote! { + #func_docs + #availability + } + }; + quote! { + #func_doc + #func_decl; + } + }); + + quote! { + #( #docs )* + } +} diff --git a/pallets/revive/src/address.rs b/pallets/revive/src/address.rs new file mode 100644 index 00000000..928896e8 --- /dev/null +++ b/pallets/revive/src/address.rs @@ -0,0 +1,130 @@ +// 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. + +//! Functions that deal contract addresses. + +use alloc::vec::Vec; + +use sp_core::H160; +use sp_io::hashing::keccak_256; +use sp_runtime::AccountId32; + +/// Map between the native chain account id `T` and an Ethereum [`H160`]. +/// +/// This trait exists only to emulate specialization for different concrete +/// native account ids. **Not** to make the mapping user configurable. Hence +/// the trait is `Sealed` and only one mandatory implementor [`DefaultAddressMapper`] +/// exists. +/// +/// Please note that we assume that the native account is at least 20 bytes and +/// only implement this type for a `T` where this is the case. Luckily, this is the +/// case for all existing runtimes as of right now. Reasing is that this will allow +/// us to reverse an address -> account_id mapping by just stripping the prefix. +pub trait AddressMapper: private::Sealed { + /// Convert an account id to an ethereum adress. + /// + /// This mapping is **not** required to be reversible. + fn to_address(account_id: &T) -> H160; + + /// Convert an ethereum address to a native account id. + /// + /// This mapping is **required** to be reversible. + fn to_account_id(address: &H160) -> T; + + /// Same as [`Self::to_account_id`] but when we know the address is a contract. + /// + /// This is only the case when we just generated the new address. + fn to_account_id_contract(address: &H160) -> T; +} + +mod private { + pub trait Sealed {} + impl Sealed for super::DefaultAddressMapper {} +} + +/// The only implementor for `AddressMapper`. +pub enum DefaultAddressMapper {} + +impl AddressMapper for DefaultAddressMapper { + fn to_address(account_id: &AccountId32) -> H160 { + H160::from_slice(&>::as_ref(&account_id)[..20]) + } + + fn to_account_id(address: &H160) -> AccountId32 { + let mut account_id = AccountId32::new([0xEE; 32]); + >::as_mut(&mut account_id)[..20] + .copy_from_slice(address.as_bytes()); + account_id + } + + fn to_account_id_contract(address: &H160) -> AccountId32 { + Self::to_account_id(address) + } +} + +/// Determine the address of a contract using CREATE semantics. +pub fn create1(deployer: &H160, nonce: u64) -> H160 { + let mut list = rlp::RlpStream::new_list(2); + list.append(&deployer.as_bytes()); + list.append(&nonce); + let hash = keccak_256(&list.out()); + H160::from_slice(&hash[12..]) +} + +/// Determine the address of a contract using the CREATE2 semantics. +pub fn create2(deployer: &H160, code: &[u8], input_data: &[u8], salt: &[u8; 32]) -> H160 { + let init_code_hash = { + let init_code: Vec = code.into_iter().chain(input_data).cloned().collect(); + keccak_256(init_code.as_ref()) + }; + let mut bytes = [0; 85]; + bytes[0] = 0xff; + bytes[1..21].copy_from_slice(deployer.as_bytes()); + bytes[21..53].copy_from_slice(salt); + bytes[53..85].copy_from_slice(&init_code_hash); + let hash = keccak_256(&bytes); + H160::from_slice(&hash[12..]) +} + +#[cfg(test)] +mod test { + use sp_core::{hex2array, H160}; + + use super::*; + use crate::test_utils::ALICE_ADDR; + + #[test] + fn create1_works() { + assert_eq!( + create1(&ALICE_ADDR, 1u64), + H160(hex2array!("c851da37e4e8d3a20d8d56be2963934b4ad71c3b")), + ) + } + + #[test] + fn create2_works() { + assert_eq!( + create2( + &ALICE_ADDR, + &hex2array!("600060005560016000"), + &hex2array!("55"), + &hex2array!("1234567890123456789012345678901234567890123456789012345678901234") + ), + H160(hex2array!("7f31e795e5836a19a8f919ab5a9de9a197ecd2b6")), + ) + } +} diff --git a/pallets/revive/src/benchmarking/call_builder.rs b/pallets/revive/src/benchmarking/call_builder.rs new file mode 100644 index 00000000..a0d1ca2f --- /dev/null +++ b/pallets/revive/src/benchmarking/call_builder.rs @@ -0,0 +1,224 @@ +// 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 alloc::{vec, vec::Vec}; + +use frame_benchmarking::benchmarking; +use sp_core::U256; + +use crate::{ + address::AddressMapper, + benchmarking::{default_deposit_limit, Contract, WasmModule}, + exec::{ExportedFunction, Ext, Key, Stack}, + storage::meter::Meter, + transient_storage::MeterEntry, + wasm::{ApiVersion, PreparedCall, Runtime}, + BalanceOf, Config, DebugBuffer, Error, GasMeter, MomentOf, Origin, WasmBlob, Weight, +}; + +type StackExt<'a, T> = Stack<'a, T, WasmBlob>; + +/// A builder used to prepare a contract call. +pub struct CallSetup { + contract: Contract, + dest: T::AccountId, + origin: Origin, + gas_meter: GasMeter, + storage_meter: Meter, + value: BalanceOf, + debug_message: Option, + data: Vec, + transient_storage_size: u32, +} + +impl Default for CallSetup +where + T: Config + pallet_balances::Config, + BalanceOf: Into + TryFrom, + MomentOf: Into, +{ + fn default() -> Self { + Self::new(WasmModule::dummy()) + } +} + +impl CallSetup +where + T: Config + pallet_balances::Config, + BalanceOf: Into + TryFrom, + MomentOf: Into, +{ + /// Setup a new call for the given module. + pub fn new(module: WasmModule) -> Self { + let contract = Contract::::new(module.clone(), vec![]).unwrap(); + let dest = contract.account_id.clone(); + let origin = Origin::from_account_id(contract.caller.clone()); + + let storage_meter = Meter::new(&origin, default_deposit_limit::(), 0u32.into()).unwrap(); + + // Whitelist contract account, as it is already accounted for in the call benchmark + benchmarking::add_to_whitelist( + frame_system::Account::::hashed_key_for(&contract.account_id).into(), + ); + + // Whitelist the contract's contractInfo as it is already accounted for in the call + // benchmark + benchmarking::add_to_whitelist( + crate::ContractInfoOf::::hashed_key_for(&T::AddressMapper::to_address( + &contract.account_id, + )) + .into(), + ); + + Self { + contract, + dest, + origin, + gas_meter: GasMeter::new(Weight::MAX), + storage_meter, + value: 0u32.into(), + debug_message: None, + data: vec![], + transient_storage_size: 0, + } + } + + /// Set the meter's storage deposit limit. + pub fn set_storage_deposit_limit(&mut self, balance: BalanceOf) { + self.storage_meter = Meter::new(&self.origin, balance, 0u32.into()).unwrap(); + } + + /// Set the call's origin. + pub fn set_origin(&mut self, origin: Origin) { + self.origin = origin; + } + + /// Set the contract's balance. + pub fn set_balance(&mut self, value: BalanceOf) { + self.contract.set_balance(value); + } + + /// Set the call's input data. + pub fn set_data(&mut self, value: Vec) { + self.data = value; + } + + /// Set the transient storage size. + pub fn set_transient_storage_size(&mut self, size: u32) { + self.transient_storage_size = size; + } + + /// Set the debug message. + pub fn enable_debug_message(&mut self) { + self.debug_message = Some(Default::default()); + } + + /// Get the debug message. + pub fn debug_message(&self) -> Option { + self.debug_message.clone() + } + + /// Get the call's input data. + pub fn data(&self) -> Vec { + self.data.clone() + } + + /// Get the call's contract. + pub fn contract(&self) -> Contract { + self.contract.clone() + } + + /// Build the call stack. + pub fn ext(&mut self) -> (StackExt<'_, T>, WasmBlob) { + let mut ext = StackExt::bench_new_call( + T::AddressMapper::to_address(&self.dest), + self.origin.clone(), + &mut self.gas_meter, + &mut self.storage_meter, + self.value, + self.debug_message.as_mut(), + ); + if self.transient_storage_size > 0 { + Self::with_transient_storage(&mut ext.0, self.transient_storage_size).unwrap(); + } + ext + } + + /// Prepare a call to the module. + pub fn prepare_call<'a>( + ext: &'a mut StackExt<'a, T>, + module: WasmBlob, + input: Vec, + ) -> PreparedCall<'a, StackExt<'a, T>> { + module + .prepare_call( + Runtime::new(ext, input), + ExportedFunction::Call, + ApiVersion::UnsafeNewest, + ) + .unwrap() + } + + /// Add transient_storage + fn with_transient_storage(ext: &mut StackExt, size: u32) -> Result<(), &'static str> { + let &MeterEntry { amount, limit } = ext.transient_storage().meter().current(); + ext.transient_storage().meter().current_mut().limit = size; + for i in 1u32.. { + let mut key_data = i.to_le_bytes().to_vec(); + while key_data.last() == Some(&0) { + key_data.pop(); + } + let key = Key::try_from_var(key_data).unwrap(); + if let Err(e) = ext.set_transient_storage(&key, Some(Vec::new()), false) { + // Restore previous settings. + ext.transient_storage().meter().current_mut().limit = limit; + ext.transient_storage().meter().current_mut().amount = amount; + if e == Error::::OutOfTransientStorage.into() { + break; + } else { + return Err("Initialization of the transient storage failed"); + } + } + } + Ok(()) + } +} + +#[macro_export] +macro_rules! memory( + ($($bytes:expr,)*) => {{ + vec![].iter()$(.chain($bytes.iter()))*.cloned().collect::>() + }}; +); + +#[macro_export] +macro_rules! build_runtime( + ($runtime:ident, $memory:ident: [$($segment:expr,)*]) => { + $crate::build_runtime!($runtime, _contract, $memory: [$($segment,)*]); + }; + ($runtime:ident, $contract:ident, $memory:ident: [$($bytes:expr,)*]) => { + $crate::build_runtime!($runtime, $contract); + let mut $memory = $crate::memory!($($bytes,)*); + }; + ($runtime:ident, $contract:ident) => { + let mut setup = CallSetup::::default(); + let $contract = setup.contract(); + let input = setup.data(); + let (mut ext, _) = setup.ext(); + let mut $runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, input); + }; +); diff --git a/pallets/revive/src/benchmarking/code.rs b/pallets/revive/src/benchmarking/code.rs new file mode 100644 index 00000000..22bb2391 --- /dev/null +++ b/pallets/revive/src/benchmarking/code.rs @@ -0,0 +1,70 @@ +// 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. + +//! Functions to procedurally construct contract code used for benchmarking. +//! +//! In order to be able to benchmark events that are triggered by contract execution +//! (API calls into seal, individual instructions), we need to generate contracts that +//! perform those events. Because those contracts can get very big we cannot simply define +//! them as text (.wat) as this will be too slow and consume too much memory. Therefore +//! we define this simple definition of a contract that can be passed to `create_code` that +//! compiles it down into a `WasmModule` that can be used as a contract's code. + +use alloc::vec::Vec; + +use pallet_revive_fixtures::bench as bench_fixtures; +use sp_core::H256; +use sp_io::hashing::keccak_256; + +/// A wasm module ready to be put on chain. +#[derive(Clone)] +pub struct WasmModule { + pub code: Vec, + pub hash: H256, +} + +impl WasmModule { + /// Return a contract code that does nothing. + pub fn dummy() -> Self { + Self::new(bench_fixtures::DUMMY.to_vec()) + } + + /// Same as [`Self::dummy`] but uses `replace_with` to make the code unique. + pub fn dummy_unique(replace_with: u32) -> Self { + Self::new(bench_fixtures::dummy_unique(replace_with)) + } + + /// A contract code of specified sizte that does nothing. + pub fn sized(_size: u32) -> Self { + Self::dummy() + } + + /// A contract code that calls the "noop" host function in a loop depending in the input. + pub fn noop() -> Self { + Self::new(bench_fixtures::NOOP.to_vec()) + } + + /// A contract code that executes some ALU instructions in a loop. + pub fn instr() -> Self { + Self::new(bench_fixtures::INSTR.to_vec()) + } + + fn new(code: Vec) -> Self { + let hash = keccak_256(&code); + Self { code, hash: H256(hash) } + } +} diff --git a/pallets/revive/src/benchmarking/mod.rs b/pallets/revive/src/benchmarking/mod.rs new file mode 100644 index 00000000..9bd12b19 --- /dev/null +++ b/pallets/revive/src/benchmarking/mod.rs @@ -0,0 +1,1748 @@ +// 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 the contracts pallet + +#![cfg(all(feature = "runtime-benchmarks", feature = "riscv"))] + +mod call_builder; +mod code; +use alloc::{vec, vec::Vec}; + +use codec::{Encode, MaxEncodedLen}; +use frame_benchmarking::v2::*; +use frame_support::{ + self, assert_ok, + storage::child, + traits::{fungible::InspectHold, Currency}, + weights::{Weight, WeightMeter}, +}; +use frame_system::RawOrigin; +use pallet_balances; +use pallet_revive_uapi::{CallFlags, ReturnErrorCode, StorageFlags}; +use sp_runtime::traits::{Bounded, Hash}; + +use self::{call_builder::CallSetup, code::WasmModule}; +use crate::{ + exec::{Key, MomentOf}, + limits, + storage::WriteOutcome, + Pallet as Contracts, *, +}; + +/// How many runs we do per API benchmark. +/// +/// This is picked more or less arbitrary. We experimented with different numbers until +/// the results appeared to be stable. Reducing the number would speed up the benchmarks +/// but might make the results less precise. +const API_BENCHMARK_RUNS: u32 = 1600; + +/// How many runs we do per instruction benchmark. +/// +/// Same rationale as for [`API_BENCHMARK_RUNS`]. The number is bigger because instruction +/// benchmarks are faster. +const INSTR_BENCHMARK_RUNS: u32 = 5000; + +/// Number of layers in a Radix16 unbalanced trie. +const UNBALANCED_TRIE_LAYERS: u32 = 20; + +/// An instantiated and deployed contract. +#[derive(Clone)] +struct Contract { + caller: T::AccountId, + account_id: T::AccountId, +} + +impl Contract +where + T: Config + pallet_balances::Config, + BalanceOf: Into + TryFrom, + MomentOf: Into, +{ + /// Returns the address of the contract. + fn address(&self) -> H160 { + T::AddressMapper::to_address(&self.account_id) + } + + /// Create new contract and use a default account id as instantiator. + fn new(module: WasmModule, data: Vec) -> Result, &'static str> { + Self::with_index(0, module, data) + } + + /// Create new contract and use an account id derived from the supplied index as instantiator. + fn with_index( + index: u32, + module: WasmModule, + data: Vec, + ) -> Result, &'static str> { + Self::with_caller(account("instantiator", index, 0), module, data) + } + + /// Create new contract and use the supplied `caller` as instantiator. + fn with_caller( + caller: T::AccountId, + module: WasmModule, + data: Vec, + ) -> Result, &'static str> { + T::Currency::set_balance(&caller, caller_funding::()); + let salt = Some([0xffu8; 32]); + + let outcome = Contracts::::bare_instantiate( + RawOrigin::Signed(caller.clone()).into(), + 0u32.into(), + Weight::MAX, + default_deposit_limit::(), + Code::Upload(module.code), + data, + salt, + DebugInfo::Skip, + CollectEvents::Skip, + ); + + let address = outcome.result?.addr; + let account_id = T::AddressMapper::to_account_id_contract(&address); + let result = Contract { caller, account_id: account_id.clone() }; + + ContractInfoOf::::insert(&address, result.info()?); + + Ok(result) + } + + /// Create a new contract with the supplied storage item count and size each. + fn with_storage(code: WasmModule, stor_num: u32, stor_size: u32) -> Result { + let contract = Contract::::new(code, vec![])?; + let storage_items = (0..stor_num) + .map(|i| { + let hash = T::Hashing::hash_of(&i) + .as_ref() + .try_into() + .map_err(|_| "Hash too big for storage key")?; + Ok((hash, vec![42u8; stor_size as usize])) + }) + .collect::, &'static str>>()?; + contract.store(&storage_items)?; + Ok(contract) + } + + /// Store the supplied storage items into this contracts storage. + fn store(&self, items: &Vec<([u8; 32], Vec)>) -> Result<(), &'static str> { + let info = self.info()?; + for item in items { + info.write(&Key::Fix(item.0), Some(item.1.clone()), None, false) + .map_err(|_| "Failed to write storage to restoration dest")?; + } + >::insert(T::AddressMapper::to_address(&self.account_id), info); + Ok(()) + } + + /// Create a new contract with the specified unbalanced storage trie. + fn with_unbalanced_storage_trie(code: WasmModule, key: &[u8]) -> Result { + if (key.len() as u32) < (UNBALANCED_TRIE_LAYERS + 1) / 2 { + return Err("Key size too small to create the specified trie"); + } + + let value = vec![16u8; limits::PAYLOAD_BYTES as usize]; + let contract = Contract::::new(code, vec![])?; + let info = contract.info()?; + let child_trie_info = info.child_trie_info(); + child::put_raw(&child_trie_info, &key, &value); + for l in 0..UNBALANCED_TRIE_LAYERS { + let pos = l as usize / 2; + let mut key_new = key.to_vec(); + for i in 0u8..16 { + key_new[pos] = if l % 2 == 0 { + (key_new[pos] & 0xF0) | i + } else { + (key_new[pos] & 0x0F) | (i << 4) + }; + + if key == &key_new { + continue + } + child::put_raw(&child_trie_info, &key_new, &value); + } + } + Ok(contract) + } + + /// Get the `ContractInfo` of the `addr` or an error if it no longer exists. + fn address_info(addr: &T::AccountId) -> Result, &'static str> { + ContractInfoOf::::get(T::AddressMapper::to_address(addr)) + .ok_or("Expected contract to exist at this point.") + } + + /// Get the `ContractInfo` of this contract or an error if it no longer exists. + fn info(&self) -> Result, &'static str> { + Self::address_info(&self.account_id) + } + + /// Set the balance of the contract to the supplied amount. + fn set_balance(&self, balance: BalanceOf) { + T::Currency::set_balance(&self.account_id, balance); + } + + /// Returns `true` iff all storage entries related to code storage exist. + fn code_exists(hash: &sp_core::H256) -> bool { + >::contains_key(hash) && >::contains_key(&hash) + } + + /// Returns `true` iff no storage entry related to code storage exist. + fn code_removed(hash: &sp_core::H256) -> bool { + !>::contains_key(hash) && !>::contains_key(&hash) + } +} + +/// The funding that each account that either calls or instantiates contracts is funded with. +fn caller_funding() -> BalanceOf { + // Minting can overflow, so we can't abuse of the funding. This value happens to be big enough, + // but not too big to make the total supply overflow. + BalanceOf::::max_value() / 10_000u32.into() +} + +/// The deposit limit we use for benchmarks. +fn default_deposit_limit() -> BalanceOf { + (T::DepositPerByte::get() * 1024u32.into() * 1024u32.into()) + + T::DepositPerItem::get() * 1024u32.into() +} + +#[benchmarks( + where + BalanceOf: Into + TryFrom, + T: Config + pallet_balances::Config, + MomentOf: Into, + ::RuntimeEvent: From>, + as Currency>::Balance: From>, +)] +mod benchmarks { + use super::*; + + // The base weight consumed on processing contracts deletion queue. + #[benchmark(pov_mode = Measured)] + fn on_process_deletion_queue_batch() { + #[block] + { + ContractInfo::::process_deletion_queue_batch(&mut WeightMeter::new()) + } + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn on_initialize_per_trie_key(k: Linear<0, 1024>) -> Result<(), BenchmarkError> { + let instance = Contract::::with_storage(WasmModule::dummy(), k, limits::PAYLOAD_BYTES)?; + instance.info()?.queue_trie_for_deletion(); + + #[block] + { + ContractInfo::::process_deletion_queue_batch(&mut WeightMeter::new()) + } + + Ok(()) + } + + // This benchmarks the overhead of loading a code of size `c` byte from storage and into + // the execution engine. This does **not** include the actual execution for which the gas meter + // is responsible. This is achieved by generating all code to the `deploy` function + // which is in the wasm module but not executed on `call`. + // The results are supposed to be used as `call_with_code_per_byte(c) - + // call_with_code_per_byte(0)`. + #[benchmark(pov_mode = Measured)] + fn call_with_code_per_byte( + c: Linear<0, { limits::code::BLOB_BYTES }>, + ) -> Result<(), BenchmarkError> { + let instance = + Contract::::with_caller(whitelisted_caller(), WasmModule::sized(c), vec![])?; + let value = Pallet::::min_balance(); + let callee = T::AddressMapper::to_address(&instance.account_id); + let storage_deposit = default_deposit_limit::(); + + #[extrinsic_call] + call( + RawOrigin::Signed(instance.caller.clone()), + callee, + value, + Weight::MAX, + storage_deposit, + vec![], + ); + + Ok(()) + } + + // `c`: Size of the code in bytes. + // `i`: Size of the input in bytes. + #[benchmark(pov_mode = Measured)] + fn instantiate_with_code( + c: Linear<0, { limits::code::BLOB_BYTES }>, + i: Linear<0, { limits::code::BLOB_BYTES }>, + ) { + let input = vec![42u8; i as usize]; + let salt = [42u8; 32]; + let value = Pallet::::min_balance(); + let caller = whitelisted_caller(); + T::Currency::set_balance(&caller, caller_funding::()); + let WasmModule { code, .. } = WasmModule::sized(c); + let origin = RawOrigin::Signed(caller.clone()); + let deployer = T::AddressMapper::to_address(&caller); + let addr = crate::address::create2(&deployer, &code, &input, &salt); + let account_id = T::AddressMapper::to_account_id_contract(&addr); + let storage_deposit = default_deposit_limit::(); + #[extrinsic_call] + _(origin, value, Weight::MAX, storage_deposit, code, input, Some(salt)); + + let deposit = + T::Currency::balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account_id); + // uploading the code reserves some balance in the callers account + let code_deposit = + T::Currency::balance_on_hold(&HoldReason::CodeUploadDepositReserve.into(), &caller); + assert_eq!( + T::Currency::balance(&caller), + caller_funding::() - value - deposit - code_deposit - Pallet::::min_balance(), + ); + // contract has the full value + assert_eq!(T::Currency::balance(&account_id), value + Pallet::::min_balance()); + } + + // `i`: Size of the input in bytes. + // `s`: Size of e salt in bytes. + #[benchmark(pov_mode = Measured)] + fn instantiate(i: Linear<0, { limits::code::BLOB_BYTES }>) -> Result<(), BenchmarkError> { + let input = vec![42u8; i as usize]; + let salt = [42u8; 32]; + let value = Pallet::::min_balance(); + let caller = whitelisted_caller(); + T::Currency::set_balance(&caller, caller_funding::()); + let origin = RawOrigin::Signed(caller.clone()); + let WasmModule { code, .. } = WasmModule::dummy(); + let storage_deposit = default_deposit_limit::(); + let deployer = T::AddressMapper::to_address(&caller); + let addr = crate::address::create2(&deployer, &code, &input, &salt); + let hash = + Contracts::::bare_upload_code(origin.into(), code, storage_deposit)?.code_hash; + let account_id = T::AddressMapper::to_account_id_contract(&addr); + + #[extrinsic_call] + _( + RawOrigin::Signed(caller.clone()), + value, + Weight::MAX, + storage_deposit, + hash, + input, + Some(salt), + ); + + let deposit = + T::Currency::balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account_id); + let code_deposit = + T::Currency::balance_on_hold(&HoldReason::CodeUploadDepositReserve.into(), &account_id); + // value was removed from the caller + assert_eq!( + T::Currency::total_balance(&caller), + caller_funding::() - value - deposit - code_deposit - Pallet::::min_balance(), + ); + // contract has the full value + assert_eq!(T::Currency::balance(&account_id), value + Pallet::::min_balance()); + + Ok(()) + } + + // We just call a dummy contract to measure the overhead of the call extrinsic. + // The size of the data has no influence on the costs of this extrinsic as long as the contract + // won't call `seal_input` in its constructor to copy the data to contract memory. + // The dummy contract used here does not do this. The costs for the data copy is billed as + // part of `seal_input`. The costs for invoking a contract of a specific size are not part + // of this benchmark because we cannot know the size of the contract when issuing a call + // transaction. See `call_with_code_per_byte` for this. + #[benchmark(pov_mode = Measured)] + fn call() -> Result<(), BenchmarkError> { + let data = vec![42u8; 1024]; + let instance = + Contract::::with_caller(whitelisted_caller(), WasmModule::dummy(), vec![])?; + let value = Pallet::::min_balance(); + let origin = RawOrigin::Signed(instance.caller.clone()); + let callee = T::AddressMapper::to_address(&instance.account_id); + let before = T::Currency::balance(&instance.account_id); + let storage_deposit = default_deposit_limit::(); + #[extrinsic_call] + _(origin, callee, value, Weight::MAX, storage_deposit, data); + let deposit = T::Currency::balance_on_hold( + &HoldReason::StorageDepositReserve.into(), + &instance.account_id, + ); + let code_deposit = T::Currency::balance_on_hold( + &HoldReason::CodeUploadDepositReserve.into(), + &instance.caller, + ); + // value and value transferred via call should be removed from the caller + assert_eq!( + T::Currency::balance(&instance.caller), + caller_funding::() - value - deposit - code_deposit - Pallet::::min_balance(), + ); + // contract should have received the value + assert_eq!(T::Currency::balance(&instance.account_id), before + value); + // contract should still exist + instance.info()?; + + Ok(()) + } + + // This constructs a contract that is maximal expensive to instrument. + // It creates a maximum number of metering blocks per byte. + // `c`: Size of the code in bytes. + #[benchmark(pov_mode = Measured)] + fn upload_code(c: Linear<0, { limits::code::BLOB_BYTES }>) { + let caller = whitelisted_caller(); + T::Currency::set_balance(&caller, caller_funding::()); + let WasmModule { code, hash, .. } = WasmModule::sized(c); + let origin = RawOrigin::Signed(caller.clone()); + let storage_deposit = default_deposit_limit::(); + #[extrinsic_call] + _(origin, code, storage_deposit); + // uploading the code reserves some balance in the callers account + assert!(T::Currency::total_balance_on_hold(&caller) > 0u32.into()); + assert!(>::code_exists(&hash)); + } + + // Removing code does not depend on the size of the contract because all the information + // needed to verify the removal claim (refcount, owner) is stored in a separate storage + // item (`CodeInfoOf`). + #[benchmark(pov_mode = Measured)] + fn remove_code() -> Result<(), BenchmarkError> { + let caller = whitelisted_caller(); + T::Currency::set_balance(&caller, caller_funding::()); + let WasmModule { code, hash, .. } = WasmModule::dummy(); + let origin = RawOrigin::Signed(caller.clone()); + let storage_deposit = default_deposit_limit::(); + let uploaded = + >::bare_upload_code(origin.clone().into(), code, storage_deposit)?; + assert_eq!(uploaded.code_hash, hash); + assert_eq!(uploaded.deposit, T::Currency::total_balance_on_hold(&caller)); + assert!(>::code_exists(&hash)); + #[extrinsic_call] + _(origin, hash); + // removing the code should have unreserved the deposit + assert_eq!(T::Currency::total_balance_on_hold(&caller), 0u32.into()); + assert!(>::code_removed(&hash)); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn set_code() -> Result<(), BenchmarkError> { + let instance = + >::with_caller(whitelisted_caller(), WasmModule::dummy(), vec![])?; + // we just add some bytes so that the code hash is different + let WasmModule { code, .. } = WasmModule::dummy_unique(128); + let origin = RawOrigin::Signed(instance.caller.clone()); + let storage_deposit = default_deposit_limit::(); + let hash = + >::bare_upload_code(origin.into(), code, storage_deposit)?.code_hash; + let callee = T::AddressMapper::to_address(&instance.account_id); + assert_ne!(instance.info()?.code_hash, hash); + #[extrinsic_call] + _(RawOrigin::Root, callee, hash); + assert_eq!(instance.info()?.code_hash, hash); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn noop_host_fn(r: Linear<0, API_BENCHMARK_RUNS>) { + let mut setup = CallSetup::::new(WasmModule::noop()); + let (mut ext, module) = setup.ext(); + let prepared = CallSetup::::prepare_call(&mut ext, module, r.encode()); + #[block] + { + prepared.call().unwrap(); + } + } + + #[benchmark(pov_mode = Measured)] + fn seal_caller() { + let len = H160::len_bytes(); + build_runtime!(runtime, memory: [vec![0u8; len as _], ]); + + let result; + #[block] + { + result = runtime.bench_caller(memory.as_mut_slice(), 0); + } + + assert_ok!(result); + assert_eq!( + ::decode(&mut &memory[..]).unwrap(), + T::AddressMapper::to_address(&runtime.ext().caller().account_id().unwrap()) + ); + } + + #[benchmark(pov_mode = Measured)] + fn seal_is_contract() { + let Contract { account_id, .. } = + Contract::::with_index(1, WasmModule::dummy(), vec![]).unwrap(); + + build_runtime!(runtime, memory: [account_id.encode(), ]); + + let result; + #[block] + { + result = runtime.bench_is_contract(memory.as_mut_slice(), 0); + } + + assert_eq!(result.unwrap(), 1); + } + + #[benchmark(pov_mode = Measured)] + fn seal_code_hash() { + let contract = Contract::::with_index(1, WasmModule::dummy(), vec![]).unwrap(); + let len = ::max_encoded_len() as u32; + build_runtime!(runtime, memory: [vec![0u8; len as _], contract.account_id.encode(), ]); + + let result; + #[block] + { + result = runtime.bench_code_hash(memory.as_mut_slice(), len, 0); + } + + assert_ok!(result); + assert_eq!( + ::decode(&mut &memory[..]).unwrap(), + contract.info().unwrap().code_hash + ); + } + + #[benchmark(pov_mode = Measured)] + fn seal_own_code_hash() { + let len = ::max_encoded_len() as u32; + build_runtime!(runtime, contract, memory: [vec![0u8; len as _], ]); + let result; + #[block] + { + result = runtime.bench_own_code_hash(memory.as_mut_slice(), 0); + } + + assert_ok!(result); + assert_eq!( + ::decode(&mut &memory[..]).unwrap(), + contract.info().unwrap().code_hash + ); + } + + #[benchmark(pov_mode = Measured)] + fn seal_caller_is_origin() { + build_runtime!(runtime, memory: []); + + let result; + #[block] + { + result = runtime.bench_caller_is_origin(memory.as_mut_slice()); + } + assert_eq!(result.unwrap(), 1u32); + } + + #[benchmark(pov_mode = Measured)] + fn seal_caller_is_root() { + let mut setup = CallSetup::::default(); + setup.set_origin(Origin::Root); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::new(&mut ext, vec![]); + + let result; + #[block] + { + result = runtime.bench_caller_is_root([0u8; 0].as_mut_slice()); + } + assert_eq!(result.unwrap(), 1u32); + } + + #[benchmark(pov_mode = Measured)] + fn seal_address() { + let len = H160::len_bytes(); + build_runtime!(runtime, memory: [vec![0u8; len as _], ]); + + let result; + #[block] + { + result = runtime.bench_address(memory.as_mut_slice(), 0); + } + assert_ok!(result); + assert_eq!(::decode(&mut &memory[..]).unwrap(), runtime.ext().address()); + } + + #[benchmark(pov_mode = Measured)] + fn seal_weight_left() { + // use correct max_encoded_len when new version of parity-scale-codec is released + let len = 18u32; + assert!(::max_encoded_len() as u32 != len); + build_runtime!(runtime, memory: [32u32.to_le_bytes(), vec![0u8; len as _], ]); + + let result; + #[block] + { + result = runtime.bench_weight_left(memory.as_mut_slice(), 4, 0); + } + assert_ok!(result); + assert_eq!( + ::decode(&mut &memory[4..]).unwrap(), + runtime.ext().gas_meter().gas_left() + ); + } + + #[benchmark(pov_mode = Measured)] + fn seal_balance() { + build_runtime!(runtime, memory: [[0u8;32], ]); + let result; + #[block] + { + result = runtime.bench_balance(memory.as_mut_slice(), 0); + } + assert_ok!(result); + assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().balance()); + } + + #[benchmark(pov_mode = Measured)] + fn seal_balance_of() { + let len = ::max_encoded_len(); + let account = account::("target", 0, 0); + let address = T::AddressMapper::to_address(&account); + let balance = Pallet::::min_balance() * 2u32.into(); + T::Currency::set_balance(&account, balance); + + build_runtime!(runtime, memory: [vec![0u8; len], address.0, ]); + + let result; + #[block] + { + result = runtime.bench_balance_of(memory.as_mut_slice(), len as u32, 0); + } + + assert_ok!(result); + assert_eq!(U256::from_little_endian(&memory[..len]), runtime.ext().balance_of(&address)); + } + + #[benchmark(pov_mode = Measured)] + fn seal_value_transferred() { + build_runtime!(runtime, memory: [[0u8;32], ]); + let result; + #[block] + { + result = runtime.bench_value_transferred(memory.as_mut_slice(), 0); + } + assert_ok!(result); + assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().value_transferred()); + } + + #[benchmark(pov_mode = Measured)] + fn seal_minimum_balance() { + build_runtime!(runtime, memory: [[0u8;32], ]); + let result; + #[block] + { + result = runtime.bench_minimum_balance(memory.as_mut_slice(), 0); + } + assert_ok!(result); + assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().minimum_balance()); + } + + #[benchmark(pov_mode = Measured)] + fn seal_block_number() { + build_runtime!(runtime, memory: [[0u8;32], ]); + let result; + #[block] + { + result = runtime.bench_block_number(memory.as_mut_slice(), 0); + } + assert_ok!(result); + assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().block_number()); + } + + #[benchmark(pov_mode = Measured)] + fn seal_now() { + build_runtime!(runtime, memory: [[0u8;32], ]); + let result; + #[block] + { + result = runtime.bench_now(memory.as_mut_slice(), 0); + } + assert_ok!(result); + assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().now()); + } + + #[benchmark(pov_mode = Measured)] + fn seal_weight_to_fee() { + build_runtime!(runtime, memory: [[0u8;32], ]); + let weight = Weight::from_parts(500_000, 300_000); + let result; + #[block] + { + result = runtime.bench_weight_to_fee( + memory.as_mut_slice(), + weight.ref_time(), + weight.proof_size(), + 0, + ); + } + assert_ok!(result); + assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().get_weight_price(weight)); + } + + #[benchmark(pov_mode = Measured)] + fn seal_input(n: Linear<0, { limits::code::BLOB_BYTES - 4 }>) { + let mut setup = CallSetup::::default(); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::new(&mut ext, vec![42u8; n as usize]); + let mut memory = memory!(n.to_le_bytes(), vec![0u8; n as usize],); + let result; + #[block] + { + result = runtime.bench_input(memory.as_mut_slice(), 4, 0); + } + assert_ok!(result); + assert_eq!(&memory[4..], &vec![42u8; n as usize]); + } + + #[benchmark(pov_mode = Measured)] + fn seal_return(n: Linear<0, { limits::code::BLOB_BYTES - 4 }>) { + build_runtime!(runtime, memory: [n.to_le_bytes(), vec![42u8; n as usize], ]); + + let result; + #[block] + { + result = runtime.bench_seal_return(memory.as_mut_slice(), 0, 0, n); + } + + assert!(matches!( + result, + Err(crate::wasm::TrapReason::Return(crate::wasm::ReturnData { .. })) + )); + } + + #[benchmark(pov_mode = Measured)] + fn seal_terminate( + n: Linear<0, { limits::DELEGATE_DEPENDENCIES }>, + ) -> Result<(), BenchmarkError> { + let beneficiary = account::("beneficiary", 0, 0); + let caller = whitelisted_caller(); + T::Currency::set_balance(&caller, caller_funding::()); + let origin = RawOrigin::Signed(caller); + let storage_deposit = default_deposit_limit::(); + + build_runtime!(runtime, memory: [beneficiary.encode(),]); + + (0..n).for_each(|i| { + let new_code = WasmModule::dummy_unique(65 + i); + Contracts::::bare_upload_code(origin.clone().into(), new_code.code, storage_deposit) + .unwrap(); + runtime.ext().lock_delegate_dependency(new_code.hash).unwrap(); + }); + + let result; + #[block] + { + result = runtime.bench_terminate(memory.as_mut_slice(), 0); + } + + assert!(matches!(result, Err(crate::wasm::TrapReason::Termination))); + + Ok(()) + } + + // Benchmark the overhead that topics generate. + // `t`: Number of topics + // `n`: Size of event payload in bytes + #[benchmark(pov_mode = Measured)] + fn seal_deposit_event( + t: Linear<0, { limits::NUM_EVENT_TOPICS as u32 }>, + n: Linear<0, { limits::PAYLOAD_BYTES }>, + ) { + let num_topic = t as u32; + let topics = (0..t).map(|i| H256::repeat_byte(i as u8)).collect::>(); + let topics_data = + topics.iter().flat_map(|hash| hash.as_bytes().to_vec()).collect::>(); + let data = vec![42u8; n as _]; + build_runtime!(runtime, instance, memory: [ topics_data, data, ]); + + let result; + #[block] + { + result = runtime.bench_deposit_event( + memory.as_mut_slice(), + 0, // topics_ptr + num_topic, + topics_data.len() as u32, // data_ptr + n, // data_len + ); + } + assert_ok!(result); + + let events = System::::events(); + let record = &events[events.len() - 1]; + + assert_eq!( + record.event, + crate::Event::ContractEmitted { contract: instance.address(), data, topics }.into(), + ); + } + + // Benchmark debug_message call + // Whereas this function is used in RPC mode only, it still should be secured + // against an excessive use. + // + // i: size of input in bytes up to maximum allowed contract memory or maximum allowed debug + // buffer size, whichever is less. + #[benchmark] + fn seal_debug_message( + i: Linear<0, { (limits::code::BLOB_BYTES).min(limits::DEBUG_BUFFER_BYTES) }>, + ) { + let mut setup = CallSetup::::default(); + setup.enable_debug_message(); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + // Fill memory with printable ASCII bytes. + let mut memory = (0..i).zip((32..127).cycle()).map(|i| i.1).collect::>(); + + let result; + #[block] + { + result = runtime.bench_debug_message(memory.as_mut_slice(), 0, i); + } + assert_ok!(result); + assert_eq!(setup.debug_message().unwrap().len() as u32, i); + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn get_storage_empty() -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = vec![0u8; max_key_len as usize]; + let max_value_len = limits::PAYLOAD_BYTES as usize; + let value = vec![1u8; max_value_len]; + + let instance = Contract::::new(WasmModule::dummy(), vec![])?; + let info = instance.info()?; + let child_trie_info = info.child_trie_info(); + info.bench_write_raw(&key, Some(value.clone()), false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let result; + #[block] + { + result = child::get_raw(&child_trie_info, &key); + } + + assert_eq!(result, Some(value)); + Ok(()) + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn get_storage_full() -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = vec![0u8; max_key_len as usize]; + let max_value_len = limits::PAYLOAD_BYTES; + let value = vec![1u8; max_value_len as usize]; + + let instance = Contract::::with_unbalanced_storage_trie(WasmModule::dummy(), &key)?; + let info = instance.info()?; + let child_trie_info = info.child_trie_info(); + info.bench_write_raw(&key, Some(value.clone()), false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let result; + #[block] + { + result = child::get_raw(&child_trie_info, &key); + } + + assert_eq!(result, Some(value)); + Ok(()) + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn set_storage_empty() -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = vec![0u8; max_key_len as usize]; + let max_value_len = limits::PAYLOAD_BYTES as usize; + let value = vec![1u8; max_value_len]; + + let instance = Contract::::new(WasmModule::dummy(), vec![])?; + let info = instance.info()?; + let child_trie_info = info.child_trie_info(); + info.bench_write_raw(&key, Some(vec![42u8; max_value_len]), false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let val = Some(value.clone()); + let result; + #[block] + { + result = info.bench_write_raw(&key, val, true); + } + + assert_ok!(result); + assert_eq!(child::get_raw(&child_trie_info, &key).unwrap(), value); + Ok(()) + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn set_storage_full() -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = vec![0u8; max_key_len as usize]; + let max_value_len = limits::PAYLOAD_BYTES; + let value = vec![1u8; max_value_len as usize]; + + let instance = Contract::::with_unbalanced_storage_trie(WasmModule::dummy(), &key)?; + let info = instance.info()?; + let child_trie_info = info.child_trie_info(); + info.bench_write_raw(&key, Some(vec![42u8; max_value_len as usize]), false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let val = Some(value.clone()); + let result; + #[block] + { + result = info.bench_write_raw(&key, val, true); + } + + assert_ok!(result); + assert_eq!(child::get_raw(&child_trie_info, &key).unwrap(), value); + Ok(()) + } + + // n: new byte size + // o: old byte size + #[benchmark(skip_meta, pov_mode = Measured)] + fn seal_set_storage( + n: Linear<0, { limits::PAYLOAD_BYTES }>, + o: Linear<0, { limits::PAYLOAD_BYTES }>, + ) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + let value = vec![1u8; n as usize]; + + build_runtime!(runtime, instance, memory: [ key.unhashed(), value.clone(), ]); + let info = instance.info()?; + + info.write(&key, Some(vec![42u8; o as usize]), None, false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let result; + #[block] + { + result = runtime.bench_set_storage( + memory.as_mut_slice(), + StorageFlags::empty().bits(), + 0, // key_ptr + max_key_len, // key_len + max_key_len, // value_ptr + n, // value_len + ); + } + + assert_ok!(result); + assert_eq!(info.read(&key).unwrap(), value); + Ok(()) + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn seal_clear_storage(n: Linear<0, { limits::PAYLOAD_BYTES }>) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, instance, memory: [ key.unhashed(), ]); + let info = instance.info()?; + + info.write(&key, Some(vec![42u8; n as usize]), None, false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let result; + #[block] + { + result = runtime.bench_clear_storage( + memory.as_mut_slice(), + StorageFlags::empty().bits(), + 0, + max_key_len, + ); + } + + assert_ok!(result); + assert!(info.read(&key).is_none()); + Ok(()) + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn seal_get_storage(n: Linear<0, { limits::PAYLOAD_BYTES }>) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, instance, memory: [ key.unhashed(), n.to_le_bytes(), vec![0u8; n as _], ]); + let info = instance.info()?; + + info.write(&key, Some(vec![42u8; n as usize]), None, false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let out_ptr = max_key_len + 4; + let result; + #[block] + { + result = runtime.bench_get_storage( + memory.as_mut_slice(), + StorageFlags::empty().bits(), + 0, // key_ptr + max_key_len, // key_len + out_ptr, // out_ptr + max_key_len, // out_len_ptr + ); + } + + assert_ok!(result); + assert_eq!(&info.read(&key).unwrap(), &memory[out_ptr as usize..]); + Ok(()) + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn seal_contains_storage( + n: Linear<0, { limits::PAYLOAD_BYTES }>, + ) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, instance, memory: [ key.unhashed(), ]); + let info = instance.info()?; + + info.write(&key, Some(vec![42u8; n as usize]), None, false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let result; + #[block] + { + result = runtime.bench_contains_storage( + memory.as_mut_slice(), + StorageFlags::empty().bits(), + 0, + max_key_len, + ); + } + + assert_eq!(result.unwrap(), n); + Ok(()) + } + + #[benchmark(skip_meta, pov_mode = Measured)] + fn seal_take_storage(n: Linear<0, { limits::PAYLOAD_BYTES }>) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, instance, memory: [ key.unhashed(), n.to_le_bytes(), vec![0u8; n as _], ]); + let info = instance.info()?; + + let value = vec![42u8; n as usize]; + info.write(&key, Some(value.clone()), None, false) + .map_err(|_| "Failed to write to storage during setup.")?; + + let out_ptr = max_key_len + 4; + let result; + #[block] + { + result = runtime.bench_take_storage( + memory.as_mut_slice(), + StorageFlags::empty().bits(), + 0, // key_ptr + max_key_len, // key_len + out_ptr, // out_ptr + max_key_len, // out_len_ptr + ); + } + + assert_ok!(result); + assert!(&info.read(&key).is_none()); + assert_eq!(&value, &memory[out_ptr as usize..]); + Ok(()) + } + + // We use both full and empty benchmarks here instead of benchmarking transient_storage + // (BTreeMap) directly. This approach is necessary because benchmarking this BTreeMap is very + // slow. Additionally, we use linear regression for our benchmarks, and the BTreeMap's log(n) + // complexity can introduce approximation errors. + #[benchmark(pov_mode = Ignored)] + fn set_transient_storage_empty() -> Result<(), BenchmarkError> { + let max_value_len = limits::PAYLOAD_BYTES; + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + let value = Some(vec![42u8; max_value_len as _]); + let mut setup = CallSetup::::default(); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + let result; + #[block] + { + result = runtime.ext().set_transient_storage(&key, value, false); + } + + assert_eq!(result, Ok(WriteOutcome::New)); + assert_eq!(runtime.ext().get_transient_storage(&key), Some(vec![42u8; max_value_len as _])); + Ok(()) + } + + #[benchmark(pov_mode = Ignored)] + fn set_transient_storage_full() -> Result<(), BenchmarkError> { + let max_value_len = limits::PAYLOAD_BYTES; + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + let value = Some(vec![42u8; max_value_len as _]); + let mut setup = CallSetup::::default(); + setup.set_transient_storage_size(limits::TRANSIENT_STORAGE_BYTES); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + let result; + #[block] + { + result = runtime.ext().set_transient_storage(&key, value, false); + } + + assert_eq!(result, Ok(WriteOutcome::New)); + assert_eq!(runtime.ext().get_transient_storage(&key), Some(vec![42u8; max_value_len as _])); + Ok(()) + } + + #[benchmark(pov_mode = Ignored)] + fn get_transient_storage_empty() -> Result<(), BenchmarkError> { + let max_value_len = limits::PAYLOAD_BYTES; + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + + let mut setup = CallSetup::::default(); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + runtime + .ext() + .set_transient_storage(&key, Some(vec![42u8; max_value_len as _]), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + let result; + #[block] + { + result = runtime.ext().get_transient_storage(&key); + } + + assert_eq!(result, Some(vec![42u8; max_value_len as _])); + Ok(()) + } + + #[benchmark(pov_mode = Ignored)] + fn get_transient_storage_full() -> Result<(), BenchmarkError> { + let max_value_len = limits::PAYLOAD_BYTES; + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + + let mut setup = CallSetup::::default(); + setup.set_transient_storage_size(limits::TRANSIENT_STORAGE_BYTES); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + runtime + .ext() + .set_transient_storage(&key, Some(vec![42u8; max_value_len as _]), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + let result; + #[block] + { + result = runtime.ext().get_transient_storage(&key); + } + + assert_eq!(result, Some(vec![42u8; max_value_len as _])); + Ok(()) + } + + // The weight of journal rollbacks should be taken into account when setting storage. + #[benchmark(pov_mode = Ignored)] + fn rollback_transient_storage() -> Result<(), BenchmarkError> { + let max_value_len = limits::PAYLOAD_BYTES; + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + + let mut setup = CallSetup::::default(); + setup.set_transient_storage_size(limits::TRANSIENT_STORAGE_BYTES); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + runtime.ext().transient_storage().start_transaction(); + runtime + .ext() + .set_transient_storage(&key, Some(vec![42u8; max_value_len as _]), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + #[block] + { + runtime.ext().transient_storage().rollback_transaction(); + } + + assert_eq!(runtime.ext().get_transient_storage(&key), None); + Ok(()) + } + + // n: new byte size + // o: old byte size + #[benchmark(pov_mode = Measured)] + fn seal_set_transient_storage( + n: Linear<0, { limits::PAYLOAD_BYTES }>, + o: Linear<0, { limits::PAYLOAD_BYTES }>, + ) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + let value = vec![1u8; n as usize]; + build_runtime!(runtime, memory: [ key.unhashed(), value.clone(), ]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + runtime + .ext() + .set_transient_storage(&key, Some(vec![42u8; o as usize]), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + + let result; + #[block] + { + result = runtime.bench_set_storage( + memory.as_mut_slice(), + StorageFlags::TRANSIENT.bits(), + 0, // key_ptr + max_key_len, // key_len + max_key_len, // value_ptr + n, // value_len + ); + } + + assert_ok!(result); + assert_eq!(runtime.ext().get_transient_storage(&key).unwrap(), value); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn seal_clear_transient_storage( + n: Linear<0, { limits::PAYLOAD_BYTES }>, + ) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, memory: [ key.unhashed(), ]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + runtime + .ext() + .set_transient_storage(&key, Some(vec![42u8; n as usize]), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + + let result; + #[block] + { + result = runtime.bench_clear_storage( + memory.as_mut_slice(), + StorageFlags::TRANSIENT.bits(), + 0, + max_key_len, + ); + } + + assert_ok!(result); + assert!(runtime.ext().get_transient_storage(&key).is_none()); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn seal_get_transient_storage( + n: Linear<0, { limits::PAYLOAD_BYTES }>, + ) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, memory: [ key.unhashed(), n.to_le_bytes(), vec![0u8; n as _], ]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + runtime + .ext() + .set_transient_storage(&key, Some(vec![42u8; n as usize]), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + + let out_ptr = max_key_len + 4; + let result; + #[block] + { + result = runtime.bench_get_storage( + memory.as_mut_slice(), + StorageFlags::TRANSIENT.bits(), + 0, // key_ptr + max_key_len, // key_len + out_ptr, // out_ptr + max_key_len, // out_len_ptr + ); + } + + assert_ok!(result); + assert_eq!( + &runtime.ext().get_transient_storage(&key).unwrap(), + &memory[out_ptr as usize..] + ); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn seal_contains_transient_storage( + n: Linear<0, { limits::PAYLOAD_BYTES }>, + ) -> Result<(), BenchmarkError> { + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, memory: [ key.unhashed(), ]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + runtime + .ext() + .set_transient_storage(&key, Some(vec![42u8; n as usize]), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + + let result; + #[block] + { + result = runtime.bench_contains_storage( + memory.as_mut_slice(), + StorageFlags::TRANSIENT.bits(), + 0, + max_key_len, + ); + } + + assert_eq!(result.unwrap(), n); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn seal_take_transient_storage( + n: Linear<0, { limits::PAYLOAD_BYTES }>, + ) -> Result<(), BenchmarkError> { + let n = limits::PAYLOAD_BYTES; + let max_key_len = limits::STORAGE_KEY_BYTES; + let key = Key::try_from_var(vec![0u8; max_key_len as usize]) + .map_err(|_| "Key has wrong length")?; + build_runtime!(runtime, memory: [ key.unhashed(), n.to_le_bytes(), vec![0u8; n as _], ]); + runtime.ext().transient_storage().meter().current_mut().limit = u32::MAX; + let value = vec![42u8; n as usize]; + runtime + .ext() + .set_transient_storage(&key, Some(value.clone()), false) + .map_err(|_| "Failed to write to transient storage during setup.")?; + + let out_ptr = max_key_len + 4; + let result; + #[block] + { + result = runtime.bench_take_storage( + memory.as_mut_slice(), + StorageFlags::TRANSIENT.bits(), + 0, // key_ptr + max_key_len, // key_len + out_ptr, // out_ptr + max_key_len, // out_len_ptr + ); + } + + assert_ok!(result); + assert!(&runtime.ext().get_transient_storage(&key).is_none()); + assert_eq!(&value, &memory[out_ptr as usize..]); + Ok(()) + } + + // We transfer to unique accounts. + #[benchmark(pov_mode = Measured)] + fn seal_transfer() { + let account = account::("receiver", 0, 0); + let value = Pallet::::min_balance(); + assert!(value > 0u32.into()); + + let mut setup = CallSetup::::default(); + setup.set_balance(value); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + + let account_bytes = account.encode(); + let account_len = account_bytes.len() as u32; + let value_bytes = Into::::into(value).encode(); + let mut memory = memory!(account_bytes, value_bytes,); + + let result; + #[block] + { + result = runtime.bench_transfer( + memory.as_mut_slice(), + 0, // account_ptr + account_len, // value_ptr + ); + } + + assert_ok!(result); + } + + // t: with or without some value to transfer + // i: size of the input data + #[benchmark(pov_mode = Measured)] + fn seal_call(t: Linear<0, 1>, i: Linear<0, { limits::code::BLOB_BYTES }>) { + let Contract { account_id: callee, .. } = + Contract::::with_index(1, WasmModule::dummy(), vec![]).unwrap(); + let callee_bytes = callee.encode(); + let callee_len = callee_bytes.len() as u32; + + let value: BalanceOf = t.into(); + let value_bytes = Into::::into(value).encode(); + + let deposit: BalanceOf = (u32::MAX - 100).into(); + let deposit_bytes = Into::::into(deposit).encode(); + let deposit_len = deposit_bytes.len() as u32; + + let mut setup = CallSetup::::default(); + setup.set_storage_deposit_limit(deposit); + setup.set_data(vec![42; i as usize]); + setup.set_origin(Origin::from_account_id(setup.contract().account_id.clone())); + + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + let mut memory = memory!(callee_bytes, deposit_bytes, value_bytes,); + + let result; + #[block] + { + result = runtime.bench_call( + memory.as_mut_slice(), + CallFlags::CLONE_INPUT.bits(), // flags + 0, // callee_ptr + 0, // ref_time_limit + 0, // proof_size_limit + callee_len, // deposit_ptr + callee_len + deposit_len, // value_ptr + 0, // input_data_ptr + 0, // input_data_len + SENTINEL, // output_ptr + 0, // output_len_ptr + ); + } + + assert_ok!(result); + } + + #[benchmark(pov_mode = Measured)] + fn seal_delegate_call() -> Result<(), BenchmarkError> { + let hash = Contract::::with_index(1, WasmModule::dummy(), vec![])?.info()?.code_hash; + + let mut setup = CallSetup::::default(); + setup.set_origin(Origin::from_account_id(setup.contract().account_id.clone())); + + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + let mut memory = memory!(hash.encode(),); + + let result; + #[block] + { + result = runtime.bench_delegate_call( + memory.as_mut_slice(), + 0, // flags + 0, // code_hash_ptr + 0, // input_data_ptr + 0, // input_data_len + SENTINEL, // output_ptr + 0, + ); + } + + assert_ok!(result); + Ok(()) + } + + // t: value to transfer + // i: size of input in bytes + #[benchmark(pov_mode = Measured)] + fn seal_instantiate(i: Linear<0, { limits::code::BLOB_BYTES }>) -> Result<(), BenchmarkError> { + let code = WasmModule::dummy(); + let hash = Contract::::with_index(1, WasmModule::dummy(), vec![])?.info()?.code_hash; + let hash_bytes = hash.encode(); + let hash_len = hash_bytes.len() as u32; + + let value: BalanceOf = 1u32.into(); + let value_bytes = Into::::into(value).encode(); + let value_len = value_bytes.len() as u32; + + let deposit: BalanceOf = 0u32.into(); + let deposit_bytes = Into::::into(deposit).encode(); + let deposit_len = deposit_bytes.len() as u32; + + let mut setup = CallSetup::::default(); + setup.set_origin(Origin::from_account_id(setup.contract().account_id.clone())); + setup.set_balance(value + (Pallet::::min_balance() * 2u32.into())); + + let account_id = &setup.contract().account_id.clone(); + let (mut ext, _) = setup.ext(); + let mut runtime = crate::wasm::Runtime::<_, [u8]>::new(&mut ext, vec![]); + + let input = vec![42u8; i as _]; + let salt = [42u8; 32]; + let deployer = T::AddressMapper::to_address(&account_id); + let addr = crate::address::create2(&deployer, &code.code, &input, &salt); + let account_id = T::AddressMapper::to_account_id_contract(&addr); + let mut memory = memory!(hash_bytes, deposit_bytes, value_bytes, input, salt,); + + let mut offset = { + let mut current = 0u32; + move |after: u32| { + current += after; + current + } + }; + + assert!(ContractInfoOf::::get(&addr).is_none()); + + let result; + #[block] + { + result = runtime.bench_instantiate( + memory.as_mut_slice(), + 0, // code_hash_ptr + 0, // ref_time_limit + 0, // proof_size_limit + offset(hash_len), // deposit_ptr + offset(deposit_len), // value_ptr + offset(value_len), // input_data_ptr + i, // input_data_len + SENTINEL, // address_ptr + SENTINEL, // output_ptr + 0, // output_len_ptr + offset(i), // salt_ptr + ); + } + + assert_ok!(result); + assert!(ContractInfoOf::::get(&addr).is_some()); + assert_eq!(T::Currency::balance(&account_id), Pallet::::min_balance() + value); + Ok(()) + } + + // `n`: Input to hash in bytes + #[benchmark(pov_mode = Measured)] + fn seal_hash_sha2_256(n: Linear<0, { limits::code::BLOB_BYTES }>) { + build_runtime!(runtime, memory: [[0u8; 32], vec![0u8; n as usize], ]); + + let result; + #[block] + { + result = runtime.bench_hash_sha2_256(memory.as_mut_slice(), 32, n, 0); + } + assert_eq!(sp_io::hashing::sha2_256(&memory[32..]), &memory[0..32]); + assert_ok!(result); + } + + // `n`: Input to hash in bytes + #[benchmark(pov_mode = Measured)] + fn seal_hash_keccak_256(n: Linear<0, { limits::code::BLOB_BYTES }>) { + build_runtime!(runtime, memory: [[0u8; 32], vec![0u8; n as usize], ]); + + let result; + #[block] + { + result = runtime.bench_hash_keccak_256(memory.as_mut_slice(), 32, n, 0); + } + assert_eq!(sp_io::hashing::keccak_256(&memory[32..]), &memory[0..32]); + assert_ok!(result); + } + + // `n`: Input to hash in bytes + #[benchmark(pov_mode = Measured)] + fn seal_hash_blake2_256(n: Linear<0, { limits::code::BLOB_BYTES }>) { + build_runtime!(runtime, memory: [[0u8; 32], vec![0u8; n as usize], ]); + + let result; + #[block] + { + result = runtime.bench_hash_blake2_256(memory.as_mut_slice(), 32, n, 0); + } + assert_eq!(sp_io::hashing::blake2_256(&memory[32..]), &memory[0..32]); + assert_ok!(result); + } + + // `n`: Input to hash in bytes + #[benchmark(pov_mode = Measured)] + fn seal_hash_blake2_128(n: Linear<0, { limits::code::BLOB_BYTES }>) { + build_runtime!(runtime, memory: [[0u8; 16], vec![0u8; n as usize], ]); + + let result; + #[block] + { + result = runtime.bench_hash_blake2_128(memory.as_mut_slice(), 16, n, 0); + } + assert_eq!(sp_io::hashing::blake2_128(&memory[16..]), &memory[0..16]); + assert_ok!(result); + } + + // `n`: Message input length to verify in bytes. + // need some buffer so the code size does not exceed the max code size. + #[benchmark(pov_mode = Measured)] + fn seal_sr25519_verify(n: Linear<0, { limits::code::BLOB_BYTES - 255 }>) { + let message = (0..n).zip((32u8..127u8).cycle()).map(|(_, c)| c).collect::>(); + let message_len = message.len() as u32; + + let key_type = sp_core::crypto::KeyTypeId(*b"code"); + let pub_key = sp_io::crypto::sr25519_generate(key_type, None); + let sig = + sp_io::crypto::sr25519_sign(key_type, &pub_key, &message).expect("Generates signature"); + let sig = AsRef::<[u8; 64]>::as_ref(&sig).to_vec(); + let sig_len = sig.len() as u32; + + build_runtime!(runtime, memory: [sig, pub_key.to_vec(), message, ]); + + let result; + #[block] + { + result = runtime.bench_sr25519_verify( + memory.as_mut_slice(), + 0, // signature_ptr + sig_len, // pub_key_ptr + message_len, // message_len + sig_len + pub_key.len() as u32, // message_ptr + ); + } + + assert_eq!(result.unwrap(), ReturnErrorCode::Success); + } + + #[benchmark(pov_mode = Measured)] + fn seal_ecdsa_recover() { + let message_hash = sp_io::hashing::blake2_256("Hello world".as_bytes()); + let key_type = sp_core::crypto::KeyTypeId(*b"code"); + let signature = { + let pub_key = sp_io::crypto::ecdsa_generate(key_type, None); + let sig = sp_io::crypto::ecdsa_sign_prehashed(key_type, &pub_key, &message_hash) + .expect("Generates signature"); + AsRef::<[u8; 65]>::as_ref(&sig).to_vec() + }; + + build_runtime!(runtime, memory: [signature, message_hash, [0u8; 33], ]); + + let result; + #[block] + { + result = runtime.bench_ecdsa_recover( + memory.as_mut_slice(), + 0, // signature_ptr + 65, // message_hash_ptr + 65 + 32, // output_ptr + ); + } + + assert_eq!(result.unwrap(), ReturnErrorCode::Success); + } + + // Only calling the function itself for the list of + // generated different ECDSA keys. + // This is a slow call: We reduce the number of runs. + #[benchmark(pov_mode = Measured)] + fn seal_ecdsa_to_eth_address() { + let key_type = sp_core::crypto::KeyTypeId(*b"code"); + let pub_key_bytes = sp_io::crypto::ecdsa_generate(key_type, None).0; + build_runtime!(runtime, memory: [[0u8; 20], pub_key_bytes,]); + + let result; + #[block] + { + result = runtime.bench_ecdsa_to_eth_address( + memory.as_mut_slice(), + 20, // key_ptr + 0, // output_ptr + ); + } + + assert_ok!(result); + assert_eq!(&memory[..20], runtime.ext().ecdsa_to_eth_address(&pub_key_bytes).unwrap()); + } + + #[benchmark(pov_mode = Measured)] + fn seal_set_code_hash() -> Result<(), BenchmarkError> { + let code_hash = + Contract::::with_index(1, WasmModule::dummy(), vec![])?.info()?.code_hash; + + build_runtime!(runtime, memory: [ code_hash.encode(),]); + + let result; + #[block] + { + result = runtime.bench_set_code_hash(memory.as_mut_slice(), 0); + } + + assert_ok!(result); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn lock_delegate_dependency() -> Result<(), BenchmarkError> { + let code_hash = Contract::::with_index(1, WasmModule::dummy_unique(1), vec![])? + .info()? + .code_hash; + + build_runtime!(runtime, memory: [ code_hash.encode(),]); + + let result; + #[block] + { + result = runtime.bench_lock_delegate_dependency(memory.as_mut_slice(), 0); + } + + assert_ok!(result); + Ok(()) + } + + #[benchmark] + fn unlock_delegate_dependency() -> Result<(), BenchmarkError> { + let code_hash = Contract::::with_index(1, WasmModule::dummy_unique(1), vec![])? + .info()? + .code_hash; + + build_runtime!(runtime, memory: [ code_hash.encode(),]); + runtime.bench_lock_delegate_dependency(memory.as_mut_slice(), 0).unwrap(); + + let result; + #[block] + { + result = runtime.bench_unlock_delegate_dependency(memory.as_mut_slice(), 0); + } + + assert_ok!(result); + Ok(()) + } + + // Benchmark the execution of instructions. + #[benchmark(pov_mode = Ignored)] + fn instr(r: Linear<0, INSTR_BENCHMARK_RUNS>) { + // (round, start, div, mult, add) + let input = (r, 1_000u32, 2u32, 3u32, 100u32).encode(); + let mut setup = CallSetup::::new(WasmModule::instr()); + let (mut ext, module) = setup.ext(); + let prepared = CallSetup::::prepare_call(&mut ext, module, input); + #[block] + { + prepared.call().unwrap(); + } + } + + impl_benchmark_test_suite!( + Contracts, + crate::tests::ExtBuilder::default().build(), + crate::tests::Test, + ); +} diff --git a/pallets/revive/src/benchmarking_dummy.rs b/pallets/revive/src/benchmarking_dummy.rs new file mode 100644 index 00000000..b0b5769d --- /dev/null +++ b/pallets/revive/src/benchmarking_dummy.rs @@ -0,0 +1,38 @@ +// 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. +// + +//! Defines a dummy benchmarking suite so that the build doesn't fail in case +//! no RISC-V toolchain is available. + +#![cfg(feature = "runtime-benchmarks")] +#![cfg(not(feature = "riscv"))] + +use frame_benchmarking::v2::*; + +use crate::{Config, *}; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark(pov_mode = Ignored)] + fn enable_riscv_feature_to_unlock_benchmarks() { + #[block] + {} + } +} diff --git a/pallets/revive/src/chain_extension.rs b/pallets/revive/src/chain_extension.rs new file mode 100644 index 00000000..190d72a4 --- /dev/null +++ b/pallets/revive/src/chain_extension.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. + +//! A mechanism for runtime authors to augment the functionality of contracts. +//! +//! The runtime is able to call into any contract and retrieve the result using +//! [`bare_call`](crate::Pallet::bare_call). This already allows customization of runtime +//! behaviour by user generated code (contracts). However, often it is more straightforward +//! to allow the reverse behaviour: The contract calls into the runtime. We call the latter +//! one a "chain extension" because it allows the chain to extend the set of functions that are +//! callable by a contract. +//! +//! In order to create a chain extension the runtime author implements the [`ChainExtension`] +//! trait and declares it in this pallet's [configuration Trait](Config). All types +//! required for this endeavour are defined or re-exported in this module. There is an +//! implementation on `()` which can be used to signal that no chain extension is available. +//! +//! # Using multiple chain extensions +//! +//! Often there is a need for having multiple chain extensions. This is often the case when +//! some generally useful off-the-shelf extensions should be included. To have multiple chain +//! extensions they can be put into a tuple which is then passed to [`Config::ChainExtension`] like +//! this `type Extensions = (ExtensionA, ExtensionB)`. +//! +//! However, only extensions implementing [`RegisteredChainExtension`] can be put into a tuple. +//! This is because the [`RegisteredChainExtension::ID`] is used to decide which of those extensions +//! should be used when the contract calls a chain extensions. Extensions which are generally +//! useful should claim their `ID` with [the registry](https://github.com/paritytech/chainextension-registry) +//! so that no collisions with other vendors will occur. +//! +//! **Chain specific extensions must use the reserved `ID = 0` so that they can't be registered with +//! the registry.** +//! +//! # Security +//! +//! The chain author alone is responsible for the security of the chain extension. +//! This includes avoiding the exposure of exploitable functions and charging the +//! appropriate amount of weight. In order to do so benchmarks must be written and the +//! [`charge_weight`](Environment::charge_weight) function must be called **before** +//! carrying out any action that causes the consumption of the chargeable weight. +//! It cannot be overstated how delicate of a process the creation of a chain extension +//! is. Check whether using [`bare_call`](crate::Pallet::bare_call) suffices for the +//! use case at hand. +//! +//! # Benchmarking +//! +//! The builtin contract callable functions that pallet-revive provides all have +//! benchmarks that determine the correct weight that an invocation of these functions +//! induces. In order to be able to charge the correct weight for the functions defined +//! by a chain extension benchmarks must be written, too. In the near future this crate +//! will provide the means for easier creation of those specialized benchmarks. +//! +//! # Example +//! +//! The ink-examples repository maintains an +//! [end-to-end example](https://github.com/paritytech/ink-examples/tree/main/rand-extension) +//! on how to use a chain extension in order to provide new features to ink! contracts. + +use alloc::vec::Vec; + +use codec::{Decode, MaxEncodedLen}; +use frame_support::weights::Weight; +pub use frame_system::Config as SysConfig; +pub use pallet_revive_uapi::ReturnFlags; +use sp_runtime::DispatchError; + +pub use crate::{exec::Ext, gas::ChargedAmount, storage::meter::Diff, Config}; +use crate::{ + wasm::{Memory, Runtime, RuntimeCosts}, + Error, +}; + +/// Result that returns a [`DispatchError`] on error. +pub type Result = core::result::Result; + +/// A trait used to extend the set of contract callable functions. +/// +/// In order to create a custom chain extension this trait must be implemented and supplied +/// to the pallet contracts configuration trait as the associated type of the same name. +/// Consult the [module documentation](self) for a general explanation of chain extensions. +/// +/// # Lifetime +/// +/// The extension will be [`Default`] initialized at the beginning of each call +/// (**not** per call stack) and dropped afterwards. Hence any value held inside the extension +/// can be used as a per-call scratch buffer. +pub trait ChainExtension { + /// Call the chain extension logic. + /// + /// This is the only function that needs to be implemented in order to write a + /// chain extensions. It is called whenever a contract calls the `seal_call_chain_extension` + /// imported wasm function. + /// + /// # Parameters + /// - `env`: Access to the remaining arguments and the execution environment. + /// + /// # Return + /// + /// In case of `Err` the contract execution is immediately suspended and the passed error + /// is returned to the caller. Otherwise the value of [`RetVal`] determines the exit + /// behaviour. + /// + /// # Note + /// + /// The [`Self::call`] can be invoked within a read-only context, where any state-changing calls + /// are disallowed. This information can be obtained using `env.ext().is_read_only()`. It is + /// crucial for the implementer to handle this scenario appropriately. + fn call, M: ?Sized + Memory>( + &mut self, + env: Environment, + ) -> Result; + + /// Determines whether chain extensions are enabled for this chain. + /// + /// The default implementation returns `true`. Therefore it is not necessary to overwrite + /// this function when implementing a chain extension. In case of `false` the deployment of + /// a contract that references `seal_call_chain_extension` will be denied and calling this + /// function will return [`NoChainExtension`](Error::NoChainExtension) without first calling + /// into [`call`](Self::call). + fn enabled() -> bool { + true + } +} + +/// A [`ChainExtension`] that can be composed with other extensions using a tuple. +/// +/// An extension that implements this trait can be put in a tuple in order to have multiple +/// extensions available. The tuple implementation routes requests based on the first two +/// most significant bytes of the `id` passed to `call`. +/// +/// If this extensions is to be used by multiple runtimes consider +/// [registering it](https://github.com/paritytech/chainextension-registry) to ensure that there +/// are no collisions with other vendors. +/// +/// # Note +/// +/// Currently, we support tuples of up to ten registered chain extensions. If more chain extensions +/// are needed consider opening an issue. +pub trait RegisteredChainExtension: ChainExtension { + /// The extensions globally unique identifier. + const ID: u16; +} + +#[impl_trait_for_tuples::impl_for_tuples(10)] +#[tuple_types_custom_trait_bound(RegisteredChainExtension)] +impl ChainExtension for Tuple { + fn call, M: ?Sized + Memory>( + &mut self, + mut env: Environment, + ) -> Result { + for_tuples!( + #( + if (Tuple::ID == env.ext_id()) && Tuple::enabled() { + return Tuple.call(env); + } + )* + ); + Err(Error::::NoChainExtension.into()) + } + + fn enabled() -> bool { + for_tuples!( + #( + if Tuple::enabled() { + return true; + } + )* + ); + false + } +} + +/// Determines the exit behaviour and return value of a chain extension. +pub enum RetVal { + /// The chain extensions returns the supplied value to its calling contract. + Converging(u32), + /// The control does **not** return to the calling contract. + /// + /// Use this to stop the execution of the contract when the chain extension returns. + /// The semantic is the same as for calling `seal_return`: The control returns to + /// the caller of the currently executing contract yielding the supplied buffer and + /// flags. + Diverging { flags: ReturnFlags, data: Vec }, +} + +/// Grants the chain extension access to its parameters and execution environment. +pub struct Environment<'a, 'b, E: Ext, M: ?Sized> { + /// The runtime contains all necessary functions to interact with the running contract. + runtime: &'a mut Runtime<'b, E, M>, + /// Reference to the contract's memory. + memory: &'a mut M, + /// Verbatim argument passed to `seal_call_chain_extension`. + id: u32, + /// Verbatim argument passed to `seal_call_chain_extension`. + input_ptr: u32, + /// Verbatim argument passed to `seal_call_chain_extension`. + input_len: u32, + /// Verbatim argument passed to `seal_call_chain_extension`. + output_ptr: u32, + /// Verbatim argument passed to `seal_call_chain_extension`. + output_len_ptr: u32, +} + +/// Functions that are available in every state of this type. +impl<'a, 'b, E: Ext, M: ?Sized + Memory> Environment<'a, 'b, E, M> { + /// Creates a new environment for consumption by a chain extension. + pub fn new( + runtime: &'a mut Runtime<'b, E, M>, + memory: &'a mut M, + id: u32, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + output_len_ptr: u32, + ) -> Self { + Self { runtime, memory, id, input_ptr, input_len, output_ptr, output_len_ptr } + } + + /// The function id within the `id` passed by a contract. + /// + /// It returns the two least significant bytes of the `id` passed by a contract as the other + /// two bytes represent the chain extension itself (the code which is calling this function). + pub fn func_id(&self) -> u16 { + (self.id & 0x0000FFFF) as u16 + } + + /// The chain extension id within the `id` passed by a contract. + /// + /// It returns the two most significant bytes of the `id` passed by a contract which represent + /// the chain extension itself (the code which is calling this function). + pub fn ext_id(&self) -> u16 { + (self.id >> 16) as u16 + } + + /// Charge the passed `amount` of weight from the overall limit. + /// + /// It returns `Ok` when there the remaining weight budget is larger than the passed + /// `weight`. It returns `Err` otherwise. In this case the chain extension should + /// abort the execution and pass through the error. + /// + /// The returned value can be used to with [`Self::adjust_weight`]. Other than that + /// it has no purpose. + /// + /// # Note + /// + /// Weight is synonymous with gas in substrate. + pub fn charge_weight(&mut self, amount: Weight) -> Result { + self.runtime.charge_gas(RuntimeCosts::ChainExtension(amount)) + } + + /// Adjust a previously charged amount down to its actual amount. + /// + /// This is when a maximum a priori amount was charged and then should be partially + /// refunded to match the actual amount. + pub fn adjust_weight(&mut self, charged: ChargedAmount, actual_weight: Weight) { + self.runtime.adjust_gas(charged, RuntimeCosts::ChainExtension(actual_weight)) + } + + /// Grants access to the execution environment of the current contract call. + /// + /// Consult the functions on the returned type before re-implementing those functions. + pub fn ext(&mut self) -> &mut E { + self.runtime.ext() + } + + /// Reads `min(max_len, in_len)` from contract memory. + /// + /// This does **not** charge any weight. The caller must make sure that the an + /// appropriate amount of weight is charged **before** reading from contract memory. + /// The reason for that is that usually the costs for reading data and processing + /// said data cannot be separated in a benchmark. Therefore a chain extension would + /// charge the overall costs either using `max_len` (worst case approximation) or using + /// [`in_len()`](Self::in_len). + pub fn read(&self, max_len: u32) -> Result> { + self.memory.read(self.input_ptr, self.input_len.min(max_len)) + } + + /// Reads `min(buffer.len(), in_len) from contract memory. + /// + /// This takes a mutable pointer to a buffer fills it with data and shrinks it to + /// the size of the actual data. Apart from supporting pre-allocated buffers it is + /// equivalent to to [`read()`](Self::read). + pub fn read_into(&self, buffer: &mut &mut [u8]) -> Result<()> { + let len = buffer.len(); + let sliced = { + let buffer = core::mem::take(buffer); + &mut buffer[..len.min(self.input_len as usize)] + }; + self.memory.read_into_buf(self.input_ptr, sliced)?; + *buffer = sliced; + Ok(()) + } + + /// Reads and decodes a type with a size fixed at compile time from contract memory. + /// + /// This function is secure and recommended for all input types of fixed size + /// as long as the cost of reading the memory is included in the overall already charged + /// weight of the chain extension. This should usually be the case when fixed input types + /// are used. + pub fn read_as(&mut self) -> Result { + self.memory.read_as(self.input_ptr) + } + + /// Reads and decodes a type with a dynamic size from contract memory. + /// + /// Make sure to include `len` in your weight calculations. + pub fn read_as_unbounded(&mut self, len: u32) -> Result { + self.memory.read_as_unbounded(self.input_ptr, len) + } + + /// The length of the input as passed in as `input_len`. + /// + /// A chain extension would use this value to calculate the dynamic part of its + /// weight. For example a chain extension that calculates the hash of some passed in + /// bytes would use `in_len` to charge the costs of hashing that amount of bytes. + /// This also subsumes the act of copying those bytes as a benchmarks measures both. + pub fn in_len(&self) -> u32 { + self.input_len + } + + /// Write the supplied buffer to contract memory. + /// + /// If the contract supplied buffer is smaller than the passed `buffer` an `Err` is returned. + /// If `allow_skip` is set to true the contract is allowed to skip the copying of the buffer + /// by supplying the guard value of `pallet-revive::SENTINEL` as `out_ptr`. The + /// `weight_per_byte` is only charged when the write actually happens and is not skipped or + /// failed due to a too small output buffer. + pub fn write( + &mut self, + buffer: &[u8], + allow_skip: bool, + weight_per_byte: Option, + ) -> Result<()> { + self.runtime.write_sandbox_output( + self.memory, + self.output_ptr, + self.output_len_ptr, + buffer, + allow_skip, + |len| { + weight_per_byte.map(|w| RuntimeCosts::ChainExtension(w.saturating_mul(len.into()))) + }, + ) + } +} diff --git a/pallets/revive/src/debug.rs b/pallets/revive/src/debug.rs new file mode 100644 index 00000000..9002a9cc --- /dev/null +++ b/pallets/revive/src/debug.rs @@ -0,0 +1,110 @@ +// 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_core::H160; + +pub use crate::{ + exec::{ExecResult, ExportedFunction}, + primitives::ExecReturnValue, +}; +use crate::{Config, LOG_TARGET}; + +/// Umbrella trait for all interfaces that serves for debugging. +pub trait Debugger: Tracing + CallInterceptor {} + +impl Debugger for V where V: Tracing + CallInterceptor {} + +/// Defines methods to capture contract calls, enabling external observers to +/// measure, trace, and react to contract interactions. +pub trait Tracing { + /// The type of [`CallSpan`] that is created by this trait. + type CallSpan: CallSpan; + + /// Creates a new call span to encompass the upcoming contract execution. + /// + /// This method should be invoked just before the execution of a contract and + /// marks the beginning of a traceable span of execution. + /// + /// # Arguments + /// + /// * `contract_address` - The address of the contract that is about to be executed. + /// * `entry_point` - Describes whether the call is the constructor or a regular call. + /// * `input_data` - The raw input data of the call. + fn new_call_span( + contract_address: &H160, + entry_point: ExportedFunction, + input_data: &[u8], + ) -> Self::CallSpan; +} + +/// Defines a span of execution for a contract call. +pub trait CallSpan { + /// Called just after the execution of a contract. + /// + /// # Arguments + /// + /// * `output` - The raw output of the call. + fn after_call(self, output: &ExecReturnValue); +} + +impl Tracing for () { + type CallSpan = (); + + fn new_call_span(contract_address: &H160, entry_point: ExportedFunction, input_data: &[u8]) { + log::trace!(target: LOG_TARGET, "call {entry_point:?} address: {contract_address:?}, input_data: {input_data:?}") + } +} + +impl CallSpan for () { + fn after_call(self, output: &ExecReturnValue) { + log::trace!(target: LOG_TARGET, "call result {output:?}") + } +} + +/// Provides an interface for intercepting contract calls. +pub trait CallInterceptor { + /// Allows to intercept contract calls and decide whether they should be executed or not. + /// If the call is intercepted, the mocked result of the call is returned. + /// + /// # Arguments + /// + /// * `contract_address` - The address of the contract that is about to be executed. + /// * `entry_point` - Describes whether the call is the constructor or a regular call. + /// * `input_data` - The raw input data of the call. + /// + /// # Expected behavior + /// + /// This method should return: + /// * `Some(ExecResult)` - if the call should be intercepted and the mocked result of the call + /// is returned. + /// * `None` - otherwise, i.e. the call should be executed normally. + fn intercept_call( + contract_address: &H160, + entry_point: ExportedFunction, + input_data: &[u8], + ) -> Option; +} + +impl CallInterceptor for () { + fn intercept_call( + _contract_address: &H160, + _entry_point: ExportedFunction, + _input_data: &[u8], + ) -> Option { + None + } +} diff --git a/pallets/revive/src/exec.rs b/pallets/revive/src/exec.rs new file mode 100644 index 00000000..4d57422b --- /dev/null +++ b/pallets/revive/src/exec.rs @@ -0,0 +1,4290 @@ +// 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 alloc::vec::Vec; +use core::{fmt::Debug, marker::PhantomData, mem}; + +use frame_support::{ + crypto::ecdsa::ECDSAExt, + dispatch::{DispatchResult, DispatchResultWithPostInfo}, + ensure, + storage::{with_transaction, TransactionOutcome}, + traits::{ + fungible::{Inspect, Mutate}, + tokens::{Fortitude, Preservation}, + Contains, OriginTrait, Time, + }, + weights::Weight, + Blake2_128Concat, BoundedVec, StorageHasher, +}; +use frame_system::{ + pallet_prelude::{BlockNumberFor, OriginFor}, + Pallet as System, RawOrigin, +}; +use sp_core::{ + ecdsa::Public as ECDSAPublic, + sr25519::{Public as SR25519Public, Signature as SR25519Signature}, + ConstU32, Get, H160, H256, U256, +}; +use sp_io::{crypto::secp256k1_ecdsa_recover_compressed, hashing::blake2_256}; +use sp_runtime::{ + traits::{BadOrigin, Convert, Dispatchable, Zero}, + DispatchError, SaturatedConversion, +}; + +use crate::{ + address::{self, AddressMapper}, + debug::{CallInterceptor, CallSpan, Tracing}, + gas::GasMeter, + limits, + primitives::{ExecReturnValue, StorageDeposit}, + runtime_decl_for_revive_api::{Decode, Encode, RuntimeDebugNoBound, TypeInfo}, + storage::{self, meter::Diff, WriteOutcome}, + transient_storage::TransientStorage, + BalanceOf, CodeInfo, CodeInfoOf, Config, ContractInfo, ContractInfoOf, DebugBuffer, Error, + Event, Pallet as Contracts, LOG_TARGET, +}; + +pub type AccountIdOf = ::AccountId; +pub type MomentOf = <::Time as Time>::Moment; +pub type ExecResult = Result; + +/// Type for variable sized storage key. Used for transparent hashing. +type VarSizedKey = BoundedVec>; + +const FRAME_ALWAYS_EXISTS_ON_INSTANTIATE: &str = "The return value is only `None` if no contract \ + exists at the specified address. This cannot \ + happen on instantiate or delegate; qed"; + +/// Combined key type for both fixed and variable sized storage keys. +pub enum Key { + /// Variant for fixed sized keys. + Fix([u8; 32]), + /// Variant for variable sized keys. + Var(VarSizedKey), +} + +impl Key { + /// Reference to the raw unhashed key. + /// + /// # Note + /// + /// Only used by benchmarking in order to generate storage collisions on purpose. + #[cfg(feature = "runtime-benchmarks")] + pub fn unhashed(&self) -> &[u8] { + match self { + Key::Fix(v) => v.as_ref(), + Key::Var(v) => v.as_ref(), + } + } + + /// The hashed key that has be used as actual key to the storage trie. + pub fn hash(&self) -> Vec { + match self { + Key::Fix(v) => blake2_256(v.as_slice()).to_vec(), + Key::Var(v) => Blake2_128Concat::hash(v.as_slice()), + } + } + + pub fn from_fixed(v: [u8; 32]) -> Self { + Self::Fix(v) + } + + pub fn try_from_var(v: Vec) -> Result { + VarSizedKey::try_from(v).map(Self::Var).map_err(|_| ()) + } +} + +/// Origin of the error. +/// +/// Call or instantiate both called into other contracts and pass through errors happening +/// in those to the caller. This enum is for the caller to distinguish whether the error +/// happened during the execution of the callee or in the current execution context. +#[derive(Copy, Clone, PartialEq, Eq, Debug, codec::Decode, codec::Encode)] +pub enum ErrorOrigin { + /// Caller error origin. + /// + /// The error happened in the current execution context rather than in the one + /// of the contract that is called into. + Caller, + /// The error happened during execution of the called contract. + Callee, +} + +/// Error returned by contract execution. +#[derive(Copy, Clone, PartialEq, Eq, Debug, codec::Decode, codec::Encode)] +pub struct ExecError { + /// The reason why the execution failed. + pub error: DispatchError, + /// Origin of the error. + pub origin: ErrorOrigin, +} + +impl> From for ExecError { + fn from(error: T) -> Self { + Self { error: error.into(), origin: ErrorOrigin::Caller } + } +} + +/// The type of origins supported by the contracts pallet. +#[derive(Clone, Encode, Decode, PartialEq, TypeInfo, RuntimeDebugNoBound)] +pub enum Origin { + Root, + Signed(T::AccountId), +} + +impl Origin { + /// Creates a new Signed Caller from an AccountId. + pub fn from_account_id(account_id: T::AccountId) -> Self { + Origin::Signed(account_id) + } + + /// Creates a new Origin from a `RuntimeOrigin`. + pub fn from_runtime_origin(o: OriginFor) -> Result { + match o.into() { + Ok(RawOrigin::Root) => Ok(Self::Root), + Ok(RawOrigin::Signed(t)) => Ok(Self::Signed(t)), + _ => Err(BadOrigin.into()), + } + } + + /// Returns the AccountId of a Signed Origin or an error if the origin is Root. + pub fn account_id(&self) -> Result<&T::AccountId, DispatchError> { + match self { + Origin::Signed(id) => Ok(id), + Origin::Root => Err(DispatchError::RootNotAllowed), + } + } +} + +/// An interface that provides access to the external environment in which the +/// smart-contract is executed. +/// +/// This interface is specialized to an account of the executing code, so all +/// operations are implicitly performed on that account. +/// +/// # Note +/// +/// This trait is sealed and cannot be implemented by downstream crates. +pub trait Ext: sealing::Sealed { + type T: Config; + + /// Call (possibly transferring some amount of funds) into the specified account. + /// + /// Returns the code size of the called contract. + fn call( + &mut self, + gas_limit: Weight, + deposit_limit: U256, + to: &H160, + value: U256, + input_data: Vec, + allows_reentry: bool, + read_only: bool, + ) -> Result<(), ExecError>; + + /// Execute code in the current frame. + /// + /// Returns the code size of the called contract. + fn delegate_call(&mut self, code: H256, input_data: Vec) -> Result<(), ExecError>; + + /// Instantiate a contract from the given code. + /// + /// Returns the original code size of the called contract. + /// The newly created account will be associated with `code`. `value` specifies the amount of + /// value transferred from the caller to the newly created account. + fn instantiate( + &mut self, + gas_limit: Weight, + deposit_limit: U256, + code: H256, + value: U256, + input_data: Vec, + salt: Option<&[u8; 32]>, + ) -> Result; + + /// Transfer all funds to `beneficiary` and delete the contract. + /// + /// Since this function removes the self contract eagerly, if succeeded, no further actions + /// should be performed on this `Ext` instance. + /// + /// This function will fail if the same contract is present on the contract + /// call stack. + fn terminate(&mut self, beneficiary: &H160) -> DispatchResult; + + /// Transfer some amount of funds into the specified account. + fn transfer(&mut self, to: &H160, value: U256) -> DispatchResult; + + /// Returns the storage entry of the executing account by the given `key`. + /// + /// Returns `None` if the `key` wasn't previously set by `set_storage` or + /// was deleted. + fn get_storage(&mut self, key: &Key) -> Option>; + + /// Returns `Some(len)` (in bytes) if a storage item exists at `key`. + /// + /// Returns `None` if the `key` wasn't previously set by `set_storage` or + /// was deleted. + fn get_storage_size(&mut self, key: &Key) -> Option; + + /// Sets the storage entry by the given key to the specified value. If `value` is `None` then + /// the storage entry is deleted. + fn set_storage( + &mut self, + key: &Key, + value: Option>, + take_old: bool, + ) -> Result; + + /// Returns the transient storage entry of the executing account for the given `key`. + /// + /// Returns `None` if the `key` wasn't previously set by `set_transient_storage` or + /// was deleted. + fn get_transient_storage(&self, key: &Key) -> Option>; + + /// Returns `Some(len)` (in bytes) if a transient storage item exists at `key`. + /// + /// Returns `None` if the `key` wasn't previously set by `set_transient_storage` or + /// was deleted. + fn get_transient_storage_size(&self, key: &Key) -> Option; + + /// Sets the transient storage entry for the given key to the specified value. If `value` is + /// `None` then the storage entry is deleted. + fn set_transient_storage( + &mut self, + key: &Key, + value: Option>, + take_old: bool, + ) -> Result; + + /// Returns the caller. + fn caller(&self) -> Origin; + + /// Check if a contract lives at the specified `address`. + fn is_contract(&self, address: &H160) -> bool; + + /// Returns the code hash of the contract for the given `address`. + /// + /// Returns `None` if the `address` does not belong to a contract. + fn code_hash(&self, address: &H160) -> Option; + + /// Returns the code hash of the contract being executed. + fn own_code_hash(&mut self) -> &H256; + + /// Check if the caller of the current contract is the origin of the whole call stack. + /// + /// This can be checked with `is_contract(self.caller())` as well. + /// However, this function does not require any storage lookup and therefore uses less weight. + fn caller_is_origin(&self) -> bool; + + /// Check if the caller is origin, and this origin is root. + fn caller_is_root(&self) -> bool; + + /// Returns a reference to the account id of the current contract. + fn account_id(&self) -> &AccountIdOf; + + /// Returns a reference to the [`H160`] address of the current contract. + fn address(&self) -> H160 { + ::AddressMapper::to_address(self.account_id()) + } + + /// Returns the balance of the current contract. + /// + /// The `value_transferred` is already added. + fn balance(&self) -> U256; + + /// Returns the balance of the supplied account. + /// + /// The `value_transferred` is already added. + fn balance_of(&self, address: &H160) -> U256; + + /// Returns the value transferred along with this call. + fn value_transferred(&self) -> U256; + + /// Returns the timestamp of the current block + fn now(&self) -> U256; + + /// Returns the minimum balance that is required for creating an account. + fn minimum_balance(&self) -> U256; + + /// Deposit an event with the given topics. + /// + /// There should not be any duplicates in `topics`. + fn deposit_event(&mut self, topics: Vec, data: Vec); + + /// Returns the current block number. + fn block_number(&self) -> U256; + + /// Returns the maximum allowed size of a storage item. + fn max_value_size(&self) -> u32; + + /// Returns the price for the specified amount of weight. + fn get_weight_price(&self, weight: Weight) -> U256; + + /// Get an immutable reference to the nested gas meter. + fn gas_meter(&self) -> &GasMeter; + + /// Get a mutable reference to the nested gas meter. + fn gas_meter_mut(&mut self) -> &mut GasMeter; + + /// Charges `diff` from the meter. + fn charge_storage(&mut self, diff: &Diff); + + /// Append a string to the debug buffer. + /// + /// It is added as-is without any additional new line. + /// + /// This is a no-op if debug message recording is disabled which is always the case + /// when the code is executing on-chain. + /// + /// Returns `true` if debug message recording is enabled. Otherwise `false` is returned. + fn append_debug_buffer(&mut self, msg: &str) -> bool; + + /// Returns `true` if debug message recording is enabled. Otherwise `false` is returned. + fn debug_buffer_enabled(&self) -> bool; + + /// Call some dispatchable and return the result. + fn call_runtime(&self, call: ::RuntimeCall) -> DispatchResultWithPostInfo; + + /// Recovers ECDSA compressed public key based on signature and message hash. + fn ecdsa_recover(&self, signature: &[u8; 65], message_hash: &[u8; 32]) -> Result<[u8; 33], ()>; + + /// Verify a sr25519 signature. + fn sr25519_verify(&self, signature: &[u8; 64], message: &[u8], pub_key: &[u8; 32]) -> bool; + + /// Returns Ethereum address from the ECDSA compressed public key. + fn ecdsa_to_eth_address(&self, pk: &[u8; 33]) -> Result<[u8; 20], ()>; + + /// Tests sometimes need to modify and inspect the contract info directly. + #[cfg(any(test, feature = "runtime-benchmarks"))] + fn contract_info(&mut self) -> &mut ContractInfo; + + /// Get a mutable reference to the transient storage. + /// Useful in benchmarks when it is sometimes necessary to modify and inspect the transient + /// storage directly. + #[cfg(feature = "runtime-benchmarks")] + fn transient_storage(&mut self) -> &mut TransientStorage; + + /// Sets new code hash for existing contract. + fn set_code_hash(&mut self, hash: H256) -> DispatchResult; + + /// Returns the number of times the specified contract exists on the call stack. Delegated calls + /// Increment the reference count of a of a stored code by one. + /// + /// # Errors + /// + /// [`Error::CodeNotFound`] is returned if no stored code found having the specified + /// `code_hash`. + fn increment_refcount(code_hash: H256) -> DispatchResult; + + /// Decrement the reference count of a stored code by one. + /// + /// # Note + /// + /// A contract whose reference count dropped to zero isn't automatically removed. A + /// `remove_code` transaction must be submitted by the original uploader to do so. + fn decrement_refcount(code_hash: H256); + + /// Adds a delegate dependency to [`ContractInfo`]'s `delegate_dependencies` field. + /// + /// This ensures that the delegated contract is not removed while it is still in use. It + /// increases the reference count of the code hash and charges a fraction (see + /// [`Config::CodeHashLockupDepositPercent`]) of the code deposit. + /// + /// # Errors + /// + /// - [`Error::MaxDelegateDependenciesReached`] + /// - [`Error::CannotAddSelfAsDelegateDependency`] + /// - [`Error::DelegateDependencyAlreadyExists`] + fn lock_delegate_dependency(&mut self, code_hash: H256) -> DispatchResult; + + /// Removes a delegate dependency from [`ContractInfo`]'s `delegate_dependencies` field. + /// + /// This is the counterpart of [`Self::lock_delegate_dependency`]. It decreases the reference + /// count and refunds the deposit that was charged by [`Self::lock_delegate_dependency`]. + /// + /// # Errors + /// + /// - [`Error::DelegateDependencyNotFound`] + fn unlock_delegate_dependency(&mut self, code_hash: &H256) -> DispatchResult; + + /// Returns the number of locked delegate dependencies. + /// + /// Note: Requires &mut self to access the contract info. + fn locked_delegate_dependencies_count(&mut self) -> usize; + + /// Check if running in read-only context. + fn is_read_only(&self) -> bool; + + /// Returns an immutable reference to the output of the last executed call frame. + fn last_frame_output(&self) -> &ExecReturnValue; + + /// Returns a mutable reference to the output of the last executed call frame. + fn last_frame_output_mut(&mut self) -> &mut ExecReturnValue; +} + +/// Describes the different functions that can be exported by an [`Executable`]. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + sp_core::RuntimeDebug, + codec::Decode, + codec::Encode, + codec::MaxEncodedLen, + scale_info::TypeInfo, +)] +pub enum ExportedFunction { + /// The constructor function which is executed on deployment of a contract. + Constructor, + /// The function which is executed when a contract is called. + Call, +} + +/// A trait that represents something that can be executed. +/// +/// In the on-chain environment this would be represented by a wasm module. This trait exists in +/// order to be able to mock the wasm logic for testing. +pub trait Executable: Sized { + /// Load the executable from storage. + /// + /// # Note + /// Charges size base load weight from the gas meter. + fn from_storage(code_hash: H256, gas_meter: &mut GasMeter) -> Result; + + /// Execute the specified exported function and return the result. + /// + /// When the specified function is `Constructor` the executable is stored and its + /// refcount incremented. + /// + /// # Note + /// + /// This functions expects to be executed in a storage transaction that rolls back + /// all of its emitted storage changes. + fn execute>( + self, + ext: &mut E, + function: ExportedFunction, + input_data: Vec, + ) -> ExecResult; + + /// The code info of the executable. + fn code_info(&self) -> &CodeInfo; + + /// The raw code of the executable. + fn code(&self) -> &[u8]; + + /// The code hash of the executable. + fn code_hash(&self) -> &H256; +} + +/// The complete call stack of a contract execution. +/// +/// The call stack is initiated by either a signed origin or one of the contract RPC calls. +/// This type implements `Ext` and by that exposes the business logic of contract execution to +/// the runtime module which interfaces with the contract (the wasm blob) itself. +pub struct Stack<'a, T: Config, E> { + /// The origin that initiated the call stack. It could either be a Signed plain account that + /// holds an account id or Root. + /// + /// # Note + /// + /// Please note that it is possible that the id of a Signed origin belongs to a contract rather + /// than a plain account when being called through one of the contract RPCs where the + /// client can freely choose the origin. This usually makes no sense but is still possible. + origin: Origin, + /// The gas meter where costs are charged to. + gas_meter: &'a mut GasMeter, + /// The storage meter makes sure that the storage deposit limit is obeyed. + storage_meter: &'a mut storage::meter::Meter, + /// The timestamp at the point of call stack instantiation. + timestamp: MomentOf, + /// The block number at the time of call stack instantiation. + block_number: BlockNumberFor, + /// The actual call stack. One entry per nested contract called/instantiated. + /// This does **not** include the [`Self::first_frame`]. + frames: BoundedVec, ConstU32<{ limits::CALL_STACK_DEPTH }>>, + /// Statically guarantee that each call stack has at least one frame. + first_frame: Frame, + /// A text buffer used to output human readable information. + /// + /// All the bytes added to this field should be valid UTF-8. The buffer has no defined + /// structure and is intended to be shown to users as-is for debugging purposes. + debug_message: Option<&'a mut DebugBuffer>, + /// Transient storage used to store data, which is kept for the duration of a transaction. + transient_storage: TransientStorage, + /// No executable is held by the struct but influences its behaviour. + _phantom: PhantomData, +} + +/// Represents one entry in the call stack. +/// +/// For each nested contract call or instantiate one frame is created. It holds specific +/// information for the said call and caches the in-storage `ContractInfo` data structure. +struct Frame { + /// The address of the executing contract. + account_id: T::AccountId, + /// The cached in-storage data of the contract. + contract_info: CachedContract, + /// The amount of balance transferred by the caller as part of the call. + value_transferred: BalanceOf, + /// Determines whether this is a call or instantiate frame. + entry_point: ExportedFunction, + /// The gas meter capped to the supplied gas limit. + nested_gas: GasMeter, + /// The storage meter for the individual call. + nested_storage: storage::meter::NestedMeter, + /// If `false` the contract enabled its defense against reentrance attacks. + allows_reentry: bool, + /// If `true` subsequent calls cannot modify storage. + read_only: bool, + /// The caller of the currently executing frame which was spawned by `delegate_call`. + delegate_caller: Option>, + /// The output of the last executed call frame. + last_frame_output: ExecReturnValue, +} + +/// Used in a delegate call frame arguments in order to override the executable and caller. +struct DelegatedCall { + /// The executable which is run instead of the contracts own `executable`. + executable: E, + /// The caller of the contract. + caller: Origin, +} + +/// Parameter passed in when creating a new `Frame`. +/// +/// It determines whether the new frame is for a call or an instantiate. +enum FrameArgs<'a, T: Config, E> { + Call { + /// The account id of the contract that is to be called. + dest: T::AccountId, + /// If `None` the contract info needs to be reloaded from storage. + cached_info: Option>, + /// This frame was created by `seal_delegate_call` and hence uses different code than + /// what is stored at [`Self::Call::dest`]. Its caller ([`DelegatedCall::caller`]) is the + /// account which called the caller contract + delegated_call: Option>, + }, + Instantiate { + /// The contract or signed origin which instantiates the new contract. + sender: T::AccountId, + /// The executable whose `deploy` function is run. + executable: E, + /// A salt used in the contract address derivation of the new contract. + salt: Option<&'a [u8; 32]>, + /// The input data is used in the contract address derivation of the new contract. + input_data: &'a [u8], + }, +} + +/// Describes the different states of a contract as contained in a `Frame`. +enum CachedContract { + /// The cached contract is up to date with the in-storage value. + Cached(ContractInfo), + /// A recursive call into the same contract did write to the contract info. + /// + /// In this case the cached contract is stale and needs to be reloaded from storage. + Invalidated, + /// The current contract executed `terminate` and removed the contract. + /// + /// In this case a reload is neither allowed nor possible. Please note that recursive + /// calls cannot remove a contract as this is checked and denied. + Terminated, +} + +impl Frame { + /// Return the `contract_info` of the current contract. + fn contract_info(&mut self) -> &mut ContractInfo { + self.contract_info.get(&self.account_id) + } + + /// Terminate and return the `contract_info` of the current contract. + /// + /// # Note + /// + /// Under no circumstances the contract is allowed to access the `contract_info` after + /// a call to this function. This would constitute a programming error in the exec module. + fn terminate(&mut self) -> ContractInfo { + self.contract_info.terminate(&self.account_id) + } +} + +/// Extract the contract info after loading it from storage. +/// +/// This assumes that `load` was executed before calling this macro. +macro_rules! get_cached_or_panic_after_load { + ($c:expr) => {{ + if let CachedContract::Cached(contract) = $c { + contract + } else { + panic!( + "It is impossible to remove a contract that is on the call stack;See \ + implementations of terminate;Therefore fetching a contract will never fail while \ + using an account id + that is currently active on the call stack;qed" + ); + } + }}; +} + +/// Same as [`Stack::top_frame`]. +/// +/// We need this access as a macro because sometimes hiding the lifetimes behind +/// a function won't work out. +macro_rules! top_frame { + ($stack:expr) => { + $stack.frames.last().unwrap_or(&$stack.first_frame) + }; +} + +/// Same as [`Stack::top_frame_mut`]. +/// +/// We need this access as a macro because sometimes hiding the lifetimes behind +/// a function won't work out. +macro_rules! top_frame_mut { + ($stack:expr) => { + $stack.frames.last_mut().unwrap_or(&mut $stack.first_frame) + }; +} + +impl CachedContract { + /// Return `Some(ContractInfo)` if the contract is in cached state. `None` otherwise. + fn into_contract(self) -> Option> { + if let CachedContract::Cached(contract) = self { + Some(contract) + } else { + None + } + } + + /// Return `Some(&mut ContractInfo)` if the contract is in cached state. `None` otherwise. + fn as_contract(&mut self) -> Option<&mut ContractInfo> { + if let CachedContract::Cached(contract) = self { + Some(contract) + } else { + None + } + } + + /// Load the `contract_info` from storage if necessary. + fn load(&mut self, account_id: &T::AccountId) { + if let CachedContract::Invalidated = self { + let contract = >::get(T::AddressMapper::to_address(account_id)); + if let Some(contract) = contract { + *self = CachedContract::Cached(contract); + } + } + } + + /// Return the cached contract_info. + fn get(&mut self, account_id: &T::AccountId) -> &mut ContractInfo { + self.load(account_id); + get_cached_or_panic_after_load!(self) + } + + /// Terminate and return the contract info. + fn terminate(&mut self, account_id: &T::AccountId) -> ContractInfo { + self.load(account_id); + get_cached_or_panic_after_load!(mem::replace(self, Self::Terminated)) + } +} + +impl<'a, T, E> Stack<'a, T, E> +where + T: Config, + BalanceOf: Into + TryFrom, + MomentOf: Into, + E: Executable, +{ + /// Create and run a new call stack by calling into `dest`. + /// + /// # Note + /// + /// `debug_message` should only ever be set to `Some` when executing as an RPC because + /// it adds allocations and could be abused to drive the runtime into an OOM panic. + /// + /// # Return Value + /// + /// Result<(ExecReturnValue, CodeSize), (ExecError, CodeSize)> + pub fn run_call( + origin: Origin, + dest: H160, + gas_meter: &'a mut GasMeter, + storage_meter: &'a mut storage::meter::Meter, + value: BalanceOf, + input_data: Vec, + debug_message: Option<&'a mut DebugBuffer>, + ) -> ExecResult { + let dest = T::AddressMapper::to_account_id(&dest); + if let Some((mut stack, executable)) = Self::new( + FrameArgs::Call { dest: dest.clone(), cached_info: None, delegated_call: None }, + origin.clone(), + gas_meter, + storage_meter, + value, + debug_message, + )? { + stack.run(executable, input_data).map(|_| stack.first_frame.last_frame_output) + } else { + Self::transfer_no_contract(&origin, &dest, value) + } + } + + /// Create and run a new call stack by instantiating a new contract. + /// + /// # Note + /// + /// `debug_message` should only ever be set to `Some` when executing as an RPC because + /// it adds allocations and could be abused to drive the runtime into an OOM panic. + /// + /// # Return Value + /// + /// Result<(NewContractAccountId, ExecReturnValue), ExecError)> + pub fn run_instantiate( + origin: T::AccountId, + executable: E, + gas_meter: &'a mut GasMeter, + storage_meter: &'a mut storage::meter::Meter, + value: BalanceOf, + input_data: Vec, + salt: Option<&[u8; 32]>, + debug_message: Option<&'a mut DebugBuffer>, + ) -> Result<(H160, ExecReturnValue), ExecError> { + let (mut stack, executable) = Self::new( + FrameArgs::Instantiate { + sender: origin.clone(), + executable, + salt, + input_data: input_data.as_ref(), + }, + Origin::from_account_id(origin), + gas_meter, + storage_meter, + value, + debug_message, + )? + .expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE); + let address = T::AddressMapper::to_address(&stack.top_frame().account_id); + stack + .run(executable, input_data) + .map(|_| (address, stack.first_frame.last_frame_output)) + } + + #[cfg(all(feature = "runtime-benchmarks", feature = "riscv"))] + pub fn bench_new_call( + dest: H160, + origin: Origin, + gas_meter: &'a mut GasMeter, + storage_meter: &'a mut storage::meter::Meter, + value: BalanceOf, + debug_message: Option<&'a mut DebugBuffer>, + ) -> (Self, E) { + Self::new( + FrameArgs::Call { + dest: T::AddressMapper::to_account_id(&dest), + cached_info: None, + delegated_call: None, + }, + origin, + gas_meter, + storage_meter, + value, + debug_message, + ) + .unwrap() + .unwrap() + } + + /// Create a new call stack. + /// + /// Returns `None` when calling a non existant contract. This is not an error case + /// since this will result in a value transfer. + fn new( + args: FrameArgs, + origin: Origin, + gas_meter: &'a mut GasMeter, + storage_meter: &'a mut storage::meter::Meter, + value: BalanceOf, + debug_message: Option<&'a mut DebugBuffer>, + ) -> Result, ExecError> { + let Some((first_frame, executable)) = Self::new_frame( + args, + value, + gas_meter, + Weight::zero(), + storage_meter, + BalanceOf::::zero(), + false, + )? + else { + return Ok(None); + }; + + let stack = Self { + origin, + gas_meter, + storage_meter, + timestamp: T::Time::now(), + block_number: >::block_number(), + first_frame, + frames: Default::default(), + debug_message, + transient_storage: TransientStorage::new(limits::TRANSIENT_STORAGE_BYTES), + _phantom: Default::default(), + }; + + Ok(Some((stack, executable))) + } + + /// Construct a new frame. + /// + /// This does not take `self` because when constructing the first frame `self` is + /// not initialized, yet. + fn new_frame( + frame_args: FrameArgs, + value_transferred: BalanceOf, + gas_meter: &mut GasMeter, + gas_limit: Weight, + storage_meter: &mut storage::meter::GenericMeter, + deposit_limit: BalanceOf, + read_only: bool, + ) -> Result, E)>, ExecError> { + let (account_id, contract_info, executable, delegate_caller, entry_point) = match frame_args + { + FrameArgs::Call { dest, cached_info, delegated_call } => { + let contract = if let Some(contract) = cached_info { + contract + } else { + if let Some(contract) = + >::get(T::AddressMapper::to_address(&dest)) + { + contract + } else { + return Ok(None); + } + }; + + let (executable, delegate_caller) = + if let Some(DelegatedCall { executable, caller }) = delegated_call { + (executable, Some(caller)) + } else { + (E::from_storage(contract.code_hash, gas_meter)?, None) + }; + + (dest, contract, executable, delegate_caller, ExportedFunction::Call) + }, + FrameArgs::Instantiate { sender, executable, salt, input_data } => { + let deployer = T::AddressMapper::to_address(&sender); + let account_nonce = >::account_nonce(&sender); + let address = if let Some(salt) = salt { + address::create2(&deployer, executable.code(), input_data, salt) + } else { + address::create1(&deployer, account_nonce.saturated_into()) + }; + let contract = ContractInfo::new( + &address, + >::account_nonce(&sender), + *executable.code_hash(), + )?; + ( + T::AddressMapper::to_account_id_contract(&address), + contract, + executable, + None, + ExportedFunction::Constructor, + ) + }, + }; + + let frame = Frame { + delegate_caller, + value_transferred, + contract_info: CachedContract::Cached(contract_info), + account_id, + entry_point, + nested_gas: gas_meter.nested(gas_limit), + nested_storage: storage_meter.nested(deposit_limit), + allows_reentry: true, + read_only, + last_frame_output: Default::default(), + }; + + Ok(Some((frame, executable))) + } + + /// Create a subsequent nested frame. + fn push_frame( + &mut self, + frame_args: FrameArgs, + value_transferred: BalanceOf, + gas_limit: Weight, + deposit_limit: BalanceOf, + read_only: bool, + ) -> Result, ExecError> { + if self.frames.len() as u32 == limits::CALL_STACK_DEPTH { + return Err(Error::::MaxCallDepthReached.into()); + } + + // We need to make sure that changes made to the contract info are not discarded. + // See the `in_memory_changes_not_discarded` test for more information. + // We do not store on instantiate because we do not allow to call into a contract + // from its own constructor. + let frame = self.top_frame(); + if let (CachedContract::Cached(contract), ExportedFunction::Call) = + (&frame.contract_info, frame.entry_point) + { + >::insert( + T::AddressMapper::to_address(&frame.account_id), + contract.clone(), + ); + } + + let frame = top_frame_mut!(self); + let nested_gas = &mut frame.nested_gas; + let nested_storage = &mut frame.nested_storage; + if let Some((frame, executable)) = Self::new_frame( + frame_args, + value_transferred, + nested_gas, + gas_limit, + nested_storage, + deposit_limit, + read_only, + )? { + self.frames.try_push(frame).map_err(|_| Error::::MaxCallDepthReached)?; + Ok(Some(executable)) + } else { + Ok(None) + } + } + + /// Run the current (top) frame. + /// + /// This can be either a call or an instantiate. + fn run(&mut self, executable: E, input_data: Vec) -> Result<(), ExecError> { + let frame = self.top_frame(); + let entry_point = frame.entry_point; + let delegated_code_hash = + if frame.delegate_caller.is_some() { Some(*executable.code_hash()) } else { None }; + + // The output of the caller frame will be replaced by the output of this run. + // It is also not accessible from nested frames. + // Hence we drop it early to save the memory. + let frames_len = self.frames.len(); + if let Some(caller_frame) = match frames_len { + 0 => None, + 1 => Some(&mut self.first_frame.last_frame_output), + _ => self.frames.get_mut(frames_len - 2).map(|frame| &mut frame.last_frame_output), + } { + *caller_frame = Default::default(); + } + + self.transient_storage.start_transaction(); + + let do_transaction = || { + let caller = self.caller(); + let frame = top_frame_mut!(self); + + // We need to charge the storage deposit before the initial transfer so that + // it can create the account in case the initial transfer is < ed. + if entry_point == ExportedFunction::Constructor { + // Root origin can't be used to instantiate a contract, so it is safe to assume that + // if we reached this point the origin has an associated account. + let origin = &self.origin.account_id()?; + frame.nested_storage.charge_instantiate( + origin, + &frame.account_id, + frame.contract_info.get(&frame.account_id), + executable.code_info(), + )?; + // Needs to be incremented before calling into the code so that it is visible + // in case of recursion. + >::inc_account_nonce(caller.account_id()?); + } + + // Every non delegate call or instantiate also optionally transfers the balance. + // If it is a delegate call, then we've already transferred tokens in the + // last non-delegate frame. + if delegated_code_hash.is_none() { + Self::transfer_from_origin(&caller, &frame.account_id, frame.value_transferred)?; + } + + let contract_address = T::AddressMapper::to_address(&top_frame!(self).account_id); + + let call_span = T::Debug::new_call_span(&contract_address, entry_point, &input_data); + + let output = T::Debug::intercept_call(&contract_address, entry_point, &input_data) + .unwrap_or_else(|| { + executable + .execute(self, entry_point, input_data) + .map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee }) + })?; + + call_span.after_call(&output); + + // Avoid useless work that would be reverted anyways. + if output.did_revert() { + return Ok(output); + } + + // Storage limit is normally enforced as late as possible (when the last frame returns) + // so that the ordering of storage accesses does not matter. + // (However, if a special limit was set for a sub-call, it should be enforced right + // after the sub-call returned. See below for this case of enforcement). + if self.frames.is_empty() { + let frame = &mut self.first_frame; + frame.contract_info.load(&frame.account_id); + let contract = frame.contract_info.as_contract(); + frame.nested_storage.enforce_limit(contract)?; + } + + let frame = self.top_frame(); + let account_id = T::AddressMapper::to_address(&frame.account_id); + match (entry_point, delegated_code_hash) { + (ExportedFunction::Constructor, _) => { + // It is not allowed to terminate a contract inside its constructor. + if matches!(frame.contract_info, CachedContract::Terminated) { + return Err(Error::::TerminatedInConstructor.into()); + } + + // If a special limit was set for the sub-call, we enforce it here. + // This is needed because contract constructor might write to storage. + // The sub-call will be rolled back in case the limit is exhausted. + let frame = self.top_frame_mut(); + let contract = frame.contract_info.as_contract(); + frame.nested_storage.enforce_subcall_limit(contract)?; + + let caller = T::AddressMapper::to_address(self.caller().account_id()?); + + // Deposit an instantiation event. + Contracts::::deposit_event(Event::Instantiated { + deployer: caller, + contract: account_id, + }); + }, + (ExportedFunction::Call, Some(code_hash)) => { + Contracts::::deposit_event(Event::DelegateCalled { + contract: account_id, + code_hash, + }); + }, + (ExportedFunction::Call, None) => { + // If a special limit was set for the sub-call, we enforce it here. + // The sub-call will be rolled back in case the limit is exhausted. + let frame = self.top_frame_mut(); + let contract = frame.contract_info.as_contract(); + frame.nested_storage.enforce_subcall_limit(contract)?; + + let caller = self.caller(); + Contracts::::deposit_event(Event::Called { + caller: caller.clone(), + contract: account_id, + }); + }, + } + + Ok(output) + }; + + // All changes performed by the contract are executed under a storage transaction. + // This allows for roll back on error. Changes to the cached contract_info are + // committed or rolled back when popping the frame. + // + // `with_transactional` may return an error caused by a limit in the + // transactional storage depth. + let transaction_outcome = + with_transaction(|| -> TransactionOutcome> { + let output = do_transaction(); + match &output { + Ok(result) if !result.did_revert() => + TransactionOutcome::Commit(Ok((true, output))), + _ => TransactionOutcome::Rollback(Ok((false, output))), + } + }); + + let (success, output) = match transaction_outcome { + // `with_transactional` executed successfully, and we have the expected output. + Ok((success, output)) => (success, output), + // `with_transactional` returned an error, and we propagate that error and note no state + // has changed. + Err(error) => (false, Err(error.into())), + }; + + if success { + self.transient_storage.commit_transaction(); + } else { + self.transient_storage.rollback_transaction(); + } + + self.pop_frame(success); + output.map(|output| { + self.top_frame_mut().last_frame_output = output; + }) + } + + /// Remove the current (top) frame from the stack. + /// + /// This is called after running the current frame. It commits cached values to storage + /// and invalidates all stale references to it that might exist further down the call stack. + fn pop_frame(&mut self, persist: bool) { + // Pop the current frame from the stack and return it in case it needs to interact + // with duplicates that might exist on the stack. + // A `None` means that we are returning from the `first_frame`. + let frame = self.frames.pop(); + + // Both branches do essentially the same with the exception. The difference is that + // the else branch does consume the hardcoded `first_frame`. + if let Some(mut frame) = frame { + let account_id = &frame.account_id; + let prev = top_frame_mut!(self); + + prev.nested_gas.absorb_nested(frame.nested_gas); + + // Only gas counter changes are persisted in case of a failure. + if !persist { + return; + } + + // Record the storage meter changes of the nested call into the parent meter. + // If the dropped frame's contract wasn't terminated we update the deposit counter + // in its contract info. The load is necessary to pull it from storage in case + // it was invalidated. + frame.contract_info.load(account_id); + let mut contract = frame.contract_info.into_contract(); + prev.nested_storage.absorb(frame.nested_storage, account_id, contract.as_mut()); + + // In case the contract wasn't terminated we need to persist changes made to it. + if let Some(contract) = contract { + // optimization: Predecessor is the same contract. + // We can just copy the contract into the predecessor without a storage write. + // This is possible when there is no other contract in-between that could + // trigger a rollback. + if prev.account_id == *account_id { + prev.contract_info = CachedContract::Cached(contract); + return; + } + + // Predecessor is a different contract: We persist the info and invalidate the first + // stale cache we find. This triggers a reload from storage on next use. We skip(1) + // because that case is already handled by the optimization above. Only the first + // cache needs to be invalidated because that one will invalidate the next cache + // when it is popped from the stack. + >::insert(T::AddressMapper::to_address(account_id), contract); + if let Some(c) = self.frames_mut().skip(1).find(|f| f.account_id == *account_id) { + c.contract_info = CachedContract::Invalidated; + } + } + } else { + if let Some((msg, false)) = self.debug_message.as_ref().map(|m| (m, m.is_empty())) { + log::debug!( + target: LOG_TARGET, + "Execution finished with debug buffer: {}", + core::str::from_utf8(msg).unwrap_or(""), + ); + } + self.gas_meter.absorb_nested(mem::take(&mut self.first_frame.nested_gas)); + if !persist { + return; + } + let mut contract = self.first_frame.contract_info.as_contract(); + self.storage_meter.absorb( + mem::take(&mut self.first_frame.nested_storage), + &self.first_frame.account_id, + contract.as_deref_mut(), + ); + if let Some(contract) = contract { + >::insert( + T::AddressMapper::to_address(&self.first_frame.account_id), + contract, + ); + } + } + } + + /// Transfer some funds from `from` to `to`. + fn transfer(from: &T::AccountId, to: &T::AccountId, value: BalanceOf) -> DispatchResult { + // this avoids events to be emitted for zero balance transfers + if !value.is_zero() { + T::Currency::transfer(from, to, value, Preservation::Preserve) + .map_err(|_| Error::::TransferFailed)?; + } + Ok(()) + } + + /// Same as `transfer` but `from` is an `Origin`. + fn transfer_from_origin( + from: &Origin, + to: &T::AccountId, + value: BalanceOf, + ) -> DispatchResult { + // If the from address is root there is no account to transfer from, and therefore we can't + // take any `value` other than 0. + let from = match from { + Origin::Signed(caller) => caller, + Origin::Root if value.is_zero() => return Ok(()), + Origin::Root => return DispatchError::RootNotAllowed.into(), + }; + Self::transfer(from, to, value) + } + + /// Same as `transfer_from_origin` but creates an `ExecReturnValue` on success. + fn transfer_no_contract( + from: &Origin, + to: &T::AccountId, + value: BalanceOf, + ) -> ExecResult { + Self::transfer_from_origin(from, to, value) + .map(|_| ExecReturnValue::default()) + .map_err(Into::into) + } + + /// Reference to the current (top) frame. + fn top_frame(&self) -> &Frame { + top_frame!(self) + } + + /// Mutable reference to the current (top) frame. + fn top_frame_mut(&mut self) -> &mut Frame { + top_frame_mut!(self) + } + + /// Iterator over all frames. + /// + /// The iterator starts with the top frame and ends with the root frame. + fn frames(&self) -> impl Iterator> { + core::iter::once(&self.first_frame).chain(&self.frames).rev() + } + + /// Same as `frames` but with a mutable reference as iterator item. + fn frames_mut(&mut self) -> impl Iterator> { + core::iter::once(&mut self.first_frame).chain(&mut self.frames).rev() + } + + /// Returns whether the current contract is on the stack multiple times. + fn is_recursive(&self) -> bool { + let account_id = &self.top_frame().account_id; + self.frames().skip(1).any(|f| &f.account_id == account_id) + } + + /// Returns whether the specified contract allows to be reentered right now. + fn allows_reentry(&self, id: &T::AccountId) -> bool { + !self.frames().any(|f| &f.account_id == id && !f.allows_reentry) + } + + /// Returns the *free* balance of the supplied AccountId. + fn account_balance(&self, who: &T::AccountId) -> U256 { + T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Polite).into() + } +} + +impl<'a, T, E> Ext for Stack<'a, T, E> +where + T: Config, + E: Executable, + BalanceOf: Into + TryFrom, + MomentOf: Into, +{ + type T = T; + + fn call( + &mut self, + gas_limit: Weight, + deposit_limit: U256, + dest: &H160, + value: U256, + input_data: Vec, + allows_reentry: bool, + read_only: bool, + ) -> Result<(), ExecError> { + // Before pushing the new frame: Protect the caller contract against reentrancy attacks. + // It is important to do this before calling `allows_reentry` so that a direct recursion + // is caught by it. + self.top_frame_mut().allows_reentry = allows_reentry; + + let dest = T::AddressMapper::to_account_id(dest); + let value = value.try_into().map_err(|_| Error::::BalanceConversionFailed)?; + + let try_call = || { + if !self.allows_reentry(&dest) { + return Err(>::ReentranceDenied.into()); + } + + // We ignore instantiate frames in our search for a cached contract. + // Otherwise it would be possible to recursively call a contract from its own + // constructor: We disallow calling not fully constructed contracts. + let cached_info = self + .frames() + .find(|f| f.entry_point == ExportedFunction::Call && f.account_id == dest) + .and_then(|f| match &f.contract_info { + CachedContract::Cached(contract) => Some(contract.clone()), + _ => None, + }); + if let Some(executable) = self.push_frame( + FrameArgs::Call { dest: dest.clone(), cached_info, delegated_call: None }, + value, + gas_limit, + deposit_limit.try_into().map_err(|_| Error::::BalanceConversionFailed)?, + // Enable read-only access if requested; cannot disable it if already set. + read_only || self.is_read_only(), + )? { + self.run(executable, input_data) + } else { + Self::transfer_no_contract( + &Origin::from_account_id(self.account_id().clone()), + &dest, + value, + )?; + Ok(()) + } + }; + + // We need to make sure to reset `allows_reentry` even on failure. + let result = try_call(); + + // Protection is on a per call basis. + self.top_frame_mut().allows_reentry = true; + + result + } + + fn delegate_call(&mut self, code_hash: H256, input_data: Vec) -> Result<(), ExecError> { + let executable = E::from_storage(code_hash, self.gas_meter_mut())?; + let top_frame = self.top_frame_mut(); + let contract_info = top_frame.contract_info().clone(); + let account_id = top_frame.account_id.clone(); + let value = top_frame.value_transferred; + let executable = self.push_frame( + FrameArgs::Call { + dest: account_id, + cached_info: Some(contract_info), + delegated_call: Some(DelegatedCall { executable, caller: self.caller().clone() }), + }, + value, + Weight::zero(), + BalanceOf::::zero(), + self.is_read_only(), + )?; + self.run(executable.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE), input_data) + } + + fn instantiate( + &mut self, + gas_limit: Weight, + deposit_limit: U256, + code_hash: H256, + value: U256, + input_data: Vec, + salt: Option<&[u8; 32]>, + ) -> Result { + let executable = E::from_storage(code_hash, self.gas_meter_mut())?; + let sender = &self.top_frame().account_id; + let executable = self.push_frame( + FrameArgs::Instantiate { + sender: sender.clone(), + executable, + salt, + input_data: input_data.as_ref(), + }, + value.try_into().map_err(|_| Error::::BalanceConversionFailed)?, + gas_limit, + deposit_limit.try_into().map_err(|_| Error::::BalanceConversionFailed)?, + self.is_read_only(), + )?; + let address = T::AddressMapper::to_address(&self.top_frame().account_id); + self.run(executable.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE), input_data) + .map(|_| address) + } + + fn terminate(&mut self, beneficiary: &H160) -> DispatchResult { + if self.is_recursive() { + return Err(Error::::TerminatedWhileReentrant.into()); + } + let frame = self.top_frame_mut(); + let info = frame.terminate(); + let beneficiary_account = T::AddressMapper::to_account_id(beneficiary); + frame.nested_storage.terminate(&info, beneficiary_account); + + info.queue_trie_for_deletion(); + let account_address = T::AddressMapper::to_address(&frame.account_id); + ContractInfoOf::::remove(&account_address); + Self::decrement_refcount(info.code_hash); + + for (code_hash, deposit) in info.delegate_dependencies() { + Self::decrement_refcount(*code_hash); + frame + .nested_storage + .charge_deposit(frame.account_id.clone(), StorageDeposit::Refund(*deposit)); + } + + Contracts::::deposit_event(Event::Terminated { + contract: account_address, + beneficiary: *beneficiary, + }); + Ok(()) + } + + fn transfer(&mut self, to: &H160, value: U256) -> DispatchResult { + Self::transfer( + &self.top_frame().account_id, + &T::AddressMapper::to_account_id(to), + value.try_into().map_err(|_| Error::::BalanceConversionFailed)?, + ) + } + + fn get_storage(&mut self, key: &Key) -> Option> { + self.top_frame_mut().contract_info().read(key) + } + + fn get_storage_size(&mut self, key: &Key) -> Option { + self.top_frame_mut().contract_info().size(key.into()) + } + + fn set_storage( + &mut self, + key: &Key, + value: Option>, + take_old: bool, + ) -> Result { + let frame = self.top_frame_mut(); + frame.contract_info.get(&frame.account_id).write( + key.into(), + value, + Some(&mut frame.nested_storage), + take_old, + ) + } + + fn get_transient_storage(&self, key: &Key) -> Option> { + self.transient_storage.read(self.account_id(), key) + } + + fn get_transient_storage_size(&self, key: &Key) -> Option { + self.transient_storage + .read(self.account_id(), key) + .map(|value| value.len() as _) + } + + fn set_transient_storage( + &mut self, + key: &Key, + value: Option>, + take_old: bool, + ) -> Result { + let account_id = self.account_id().clone(); + self.transient_storage.write(&account_id, key, value, take_old) + } + + fn account_id(&self) -> &T::AccountId { + &self.top_frame().account_id + } + + fn caller(&self) -> Origin { + if let Some(caller) = &self.top_frame().delegate_caller { + caller.clone() + } else { + self.frames() + .nth(1) + .map(|f| Origin::from_account_id(f.account_id.clone())) + .unwrap_or(self.origin.clone()) + } + } + + fn is_contract(&self, address: &H160) -> bool { + ContractInfoOf::::contains_key(&address) + } + + fn code_hash(&self, address: &H160) -> Option { + >::get(&address).map(|contract| contract.code_hash) + } + + fn own_code_hash(&mut self) -> &H256 { + &self.top_frame_mut().contract_info().code_hash + } + + fn caller_is_origin(&self) -> bool { + self.origin == self.caller() + } + + fn caller_is_root(&self) -> bool { + // if the caller isn't origin, then it can't be root. + self.caller_is_origin() && self.origin == Origin::Root + } + + fn balance(&self) -> U256 { + self.account_balance(&self.top_frame().account_id) + } + + fn balance_of(&self, address: &H160) -> U256 { + self.account_balance(&::AddressMapper::to_account_id(address)) + } + + fn value_transferred(&self) -> U256 { + self.top_frame().value_transferred.into() + } + + fn now(&self) -> U256 { + self.timestamp.into() + } + + fn minimum_balance(&self) -> U256 { + T::Currency::minimum_balance().into() + } + + fn deposit_event(&mut self, topics: Vec, data: Vec) { + Contracts::::deposit_event(Event::ContractEmitted { + contract: T::AddressMapper::to_address(self.account_id()), + data, + topics, + }); + } + + fn block_number(&self) -> U256 { + self.block_number.into() + } + + fn max_value_size(&self) -> u32 { + limits::PAYLOAD_BYTES + } + + fn get_weight_price(&self, weight: Weight) -> U256 { + T::WeightPrice::convert(weight).into() + } + + fn gas_meter(&self) -> &GasMeter { + &self.top_frame().nested_gas + } + + fn gas_meter_mut(&mut self) -> &mut GasMeter { + &mut self.top_frame_mut().nested_gas + } + + fn charge_storage(&mut self, diff: &Diff) { + self.top_frame_mut().nested_storage.charge(diff) + } + + fn debug_buffer_enabled(&self) -> bool { + self.debug_message.is_some() + } + + fn append_debug_buffer(&mut self, msg: &str) -> bool { + if let Some(buffer) = &mut self.debug_message { + buffer + .try_extend(&mut msg.bytes()) + .map_err(|_| { + log::debug!( + target: LOG_TARGET, + "Debug buffer (of {} bytes) exhausted!", + limits::DEBUG_BUFFER_BYTES, + ) + }) + .ok(); + true + } else { + false + } + } + + fn call_runtime(&self, call: ::RuntimeCall) -> DispatchResultWithPostInfo { + let mut origin: T::RuntimeOrigin = RawOrigin::Signed(self.account_id().clone()).into(); + origin.add_filter(T::CallFilter::contains); + call.dispatch(origin) + } + + fn ecdsa_recover(&self, signature: &[u8; 65], message_hash: &[u8; 32]) -> Result<[u8; 33], ()> { + secp256k1_ecdsa_recover_compressed(signature, message_hash).map_err(|_| ()) + } + + fn sr25519_verify(&self, signature: &[u8; 64], message: &[u8], pub_key: &[u8; 32]) -> bool { + sp_io::crypto::sr25519_verify( + &SR25519Signature::from(*signature), + message, + &SR25519Public::from(*pub_key), + ) + } + + fn ecdsa_to_eth_address(&self, pk: &[u8; 33]) -> Result<[u8; 20], ()> { + ECDSAPublic::from(*pk).to_eth_address() + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + fn contract_info(&mut self) -> &mut ContractInfo { + self.top_frame_mut().contract_info() + } + + #[cfg(feature = "runtime-benchmarks")] + fn transient_storage(&mut self) -> &mut TransientStorage { + &mut self.transient_storage + } + + fn set_code_hash(&mut self, hash: H256) -> DispatchResult { + let frame = top_frame_mut!(self); + + let info = frame.contract_info(); + + let prev_hash = info.code_hash; + info.code_hash = hash; + + let code_info = CodeInfoOf::::get(hash).ok_or(Error::::CodeNotFound)?; + + let old_base_deposit = info.storage_base_deposit(); + let new_base_deposit = info.update_base_deposit(&code_info); + let deposit = StorageDeposit::Charge(new_base_deposit) + .saturating_sub(&StorageDeposit::Charge(old_base_deposit)); + + frame.nested_storage.charge_deposit(frame.account_id.clone(), deposit); + + Self::increment_refcount(hash)?; + Self::decrement_refcount(prev_hash); + Contracts::::deposit_event(Event::ContractCodeUpdated { + contract: T::AddressMapper::to_address(&frame.account_id), + new_code_hash: hash, + old_code_hash: prev_hash, + }); + Ok(()) + } + + fn increment_refcount(code_hash: H256) -> DispatchResult { + >::mutate(code_hash, |existing| -> Result<(), DispatchError> { + if let Some(info) = existing { + *info.refcount_mut() = info.refcount().saturating_add(1); + Ok(()) + } else { + Err(Error::::CodeNotFound.into()) + } + }) + } + + fn decrement_refcount(code_hash: H256) { + >::mutate(code_hash, |existing| { + if let Some(info) = existing { + *info.refcount_mut() = info.refcount().saturating_sub(1); + } + }); + } + + fn lock_delegate_dependency(&mut self, code_hash: H256) -> DispatchResult { + let frame = self.top_frame_mut(); + let info = frame.contract_info.get(&frame.account_id); + ensure!(code_hash != info.code_hash, Error::::CannotAddSelfAsDelegateDependency); + + let code_info = CodeInfoOf::::get(code_hash).ok_or(Error::::CodeNotFound)?; + let deposit = T::CodeHashLockupDepositPercent::get().mul_ceil(code_info.deposit()); + + info.lock_delegate_dependency(code_hash, deposit)?; + Self::increment_refcount(code_hash)?; + frame + .nested_storage + .charge_deposit(frame.account_id.clone(), StorageDeposit::Charge(deposit)); + Ok(()) + } + + fn unlock_delegate_dependency(&mut self, code_hash: &H256) -> DispatchResult { + let frame = self.top_frame_mut(); + let info = frame.contract_info.get(&frame.account_id); + + let deposit = info.unlock_delegate_dependency(code_hash)?; + Self::decrement_refcount(*code_hash); + frame + .nested_storage + .charge_deposit(frame.account_id.clone(), StorageDeposit::Refund(deposit)); + Ok(()) + } + + fn locked_delegate_dependencies_count(&mut self) -> usize { + self.top_frame_mut().contract_info().delegate_dependencies_count() + } + + fn is_read_only(&self) -> bool { + self.top_frame().read_only + } + + fn last_frame_output(&self) -> &ExecReturnValue { + &self.top_frame().last_frame_output + } + + fn last_frame_output_mut(&mut self) -> &mut ExecReturnValue { + &mut self.top_frame_mut().last_frame_output + } +} + +mod sealing { + use super::*; + + pub trait Sealed {} + + impl<'a, T: Config, E> Sealed for Stack<'a, T, E> {} +} + +/// These tests exercise the executive layer. +/// +/// In these tests the VM/loader are mocked. Instead of dealing with wasm bytecode they use simple +/// closures. This allows you to tackle executive logic more thoroughly without writing a +/// wasm VM code. +#[cfg(test)] +mod tests { + use std::{cell::RefCell, collections::hash_map::HashMap, rc::Rc}; + + use assert_matches::assert_matches; + use frame_support::{assert_err, assert_ok, parameter_types}; + use frame_system::{EventRecord, Phase}; + use pallet_revive_uapi::ReturnFlags; + use pretty_assertions::assert_eq; + use sp_runtime::{traits::Hash, DispatchError}; + + use super::*; + use crate::{ + exec::ExportedFunction::*, + gas::GasMeter, + test_utils::*, + tests::{ + test_utils::{get_balance, place_contract, set_balance}, + ExtBuilder, RuntimeCall, RuntimeEvent as MetaEvent, Test, TestFilter, + }, + AddressMapper, Error, + }; + + type System = frame_system::Pallet; + + type MockStack<'a> = Stack<'a, Test, MockExecutable>; + + parameter_types! { + static Loader: MockLoader = MockLoader::default(); + } + + fn events() -> Vec> { + System::events() + .into_iter() + .filter_map(|meta| match meta.event { + MetaEvent::Contracts(contract_event) => Some(contract_event), + _ => None, + }) + .collect() + } + + struct MockCtx<'a> { + ext: &'a mut MockStack<'a>, + input_data: Vec, + } + + #[derive(Clone)] + struct MockExecutable { + func: Rc Fn(MockCtx<'a>, &Self) -> ExecResult + 'static>, + func_type: ExportedFunction, + code_hash: H256, + code_info: CodeInfo, + } + + #[derive(Default, Clone)] + pub struct MockLoader { + map: HashMap, + counter: u64, + } + + impl MockLoader { + fn code_hashes() -> Vec { + Loader::get().map.keys().copied().collect() + } + + fn insert( + func_type: ExportedFunction, + f: impl Fn(MockCtx, &MockExecutable) -> ExecResult + 'static, + ) -> H256 { + Loader::mutate(|loader| { + // Generate code hashes as monotonically increasing values. + let hash = ::Hash::from_low_u64_be(loader.counter); + loader.counter += 1; + loader.map.insert( + hash, + MockExecutable { + func: Rc::new(f), + func_type, + code_hash: hash, + code_info: CodeInfo::::new(ALICE), + }, + ); + hash + }) + } + } + + impl Executable for MockExecutable { + fn from_storage( + code_hash: H256, + _gas_meter: &mut GasMeter, + ) -> Result { + Loader::mutate(|loader| { + loader.map.get(&code_hash).cloned().ok_or(Error::::CodeNotFound.into()) + }) + } + + fn execute>( + self, + ext: &mut E, + function: ExportedFunction, + input_data: Vec, + ) -> ExecResult { + if let Constructor = function { + E::increment_refcount(self.code_hash).unwrap(); + } + // # Safety + // + // We know that we **always** call execute with a `MockStack` in this test. + // + // # Note + // + // The transmute is necessary because `execute` has to be generic over all + // `E: Ext`. However, `MockExecutable` can't be generic over `E` as it would + // constitute a cycle. + let ext = unsafe { mem::transmute(ext) }; + if function == self.func_type { + (self.func)(MockCtx { ext, input_data }, &self) + } else { + exec_success() + } + } + + fn code(&self) -> &[u8] { + // The mock executable doesn't have code", so we return the code hash. + self.code_hash.as_ref() + } + + fn code_hash(&self) -> &H256 { + &self.code_hash + } + + fn code_info(&self) -> &CodeInfo { + &self.code_info + } + } + + fn exec_success() -> ExecResult { + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() }) + } + + fn exec_trapped() -> ExecResult { + Err(ExecError { error: >::ContractTrapped.into(), origin: ErrorOrigin::Callee }) + } + + #[test] + fn it_works() { + parameter_types! { + static TestData: Vec = vec![0]; + } + + let value = Default::default(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let exec_ch = MockLoader::insert(Call, |_ctx, _executable| { + TestData::mutate(|data| data.push(1)); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, exec_ch); + let mut storage_meter = + storage::meter::Meter::new(&Origin::from_account_id(ALICE), 0, value).unwrap(); + + assert_matches!( + MockStack::run_call( + Origin::from_account_id(ALICE), + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + value, + vec![], + None, + ), + Ok(_) + ); + }); + + assert_eq!(TestData::get(), vec![0, 1]); + } + + #[test] + fn transfer_works() { + // This test verifies that a contract is able to transfer + // some funds to another account. + ExtBuilder::default().build().execute_with(|| { + set_balance(&ALICE, 100); + set_balance(&BOB, 0); + + MockStack::transfer(&ALICE, &BOB, 55).unwrap(); + + assert_eq!(get_balance(&ALICE), 45); + assert_eq!(get_balance(&BOB), 55); + }); + } + + #[test] + fn correct_transfer_on_call() { + let value = 55; + + let success_ch = MockLoader::insert(Call, move |ctx, _| { + assert_eq!(ctx.ext.value_transferred(), U256::from(value)); + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() }) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, success_ch); + set_balance(&ALICE, 100); + let balance = get_balance(&BOB_CONTRACT_ID); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, value).unwrap(); + + let _ = MockStack::run_call( + origin.clone(), + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + value, + vec![], + None, + ) + .unwrap(); + + assert_eq!(get_balance(&ALICE), 100 - value); + assert_eq!(get_balance(&BOB_CONTRACT_ID), balance + value); + }); + } + + #[test] + fn correct_transfer_on_delegate_call() { + let value = 35; + + let success_ch = MockLoader::insert(Call, move |ctx, _| { + assert_eq!(ctx.ext.value_transferred(), U256::from(value)); + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() }) + }); + + let delegate_ch = MockLoader::insert(Call, move |ctx, _| { + assert_eq!(ctx.ext.value_transferred(), U256::from(value)); + let _ = ctx.ext.delegate_call(success_ch, Vec::new())?; + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() }) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, delegate_ch); + set_balance(&ALICE, 100); + let balance = get_balance(&BOB_CONTRACT_ID); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 55).unwrap(); + + let _ = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + value, + vec![], + None, + ) + .unwrap(); + + assert_eq!(get_balance(&ALICE), 100 - value); + assert_eq!(get_balance(&BOB_CONTRACT_ID), balance + value); + }); + } + + #[test] + fn changes_are_reverted_on_failing_call() { + // This test verifies that changes are reverted on a call which fails (or equally, returns + // a non-zero status code). + + let return_ch = MockLoader::insert(Call, |_, _| { + Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: Vec::new() }) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, return_ch); + set_balance(&ALICE, 100); + let balance = get_balance(&BOB); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 55).unwrap(); + + let output = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 55, + vec![], + None, + ) + .unwrap(); + + assert!(output.did_revert()); + assert_eq!(get_balance(&ALICE), 100); + assert_eq!(get_balance(&BOB), balance); + }); + } + + #[test] + fn balance_too_low() { + // This test verifies that a contract can't send value if it's + // balance is too low. + let origin = ALICE; + let dest = BOB; + + ExtBuilder::default().build().execute_with(|| { + set_balance(&origin, 0); + + let result = MockStack::transfer(&origin, &dest, 100); + + assert_eq!(result, Err(Error::::TransferFailed.into())); + assert_eq!(get_balance(&origin), 0); + assert_eq!(get_balance(&dest), 0); + }); + } + + #[test] + fn output_is_returned_on_success() { + // Verifies that if a contract returns data with a successful exit status, this data + // is returned from the execution context. + let return_ch = MockLoader::insert(Call, |_, _| { + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: vec![1, 2, 3, 4] }) + }); + + ExtBuilder::default().build().execute_with(|| { + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + place_contract(&BOB, return_ch); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ); + + let output = result.unwrap(); + assert!(!output.did_revert()); + assert_eq!(output.data, vec![1, 2, 3, 4]); + }); + } + + #[test] + fn output_is_returned_on_failure() { + // Verifies that if a contract returns data with a failing exit status, this data + // is returned from the execution context. + let return_ch = MockLoader::insert(Call, |_, _| { + Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![1, 2, 3, 4] }) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, return_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ); + + let output = result.unwrap(); + assert!(output.did_revert()); + assert_eq!(output.data, vec![1, 2, 3, 4]); + }); + } + + #[test] + fn input_data_to_call() { + let input_data_ch = MockLoader::insert(Call, |ctx, _| { + assert_eq!(ctx.input_data, &[1, 2, 3, 4]); + exec_success() + }); + + // This one tests passing the input data into a contract via call. + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, input_data_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![1, 2, 3, 4], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn input_data_to_instantiate() { + let input_data_ch = MockLoader::insert(Constructor, |ctx, _| { + assert_eq!(ctx.input_data, &[1, 2, 3, 4]); + exec_success() + }); + + // This one tests passing the input data into a contract via instantiate. + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .build() + .execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let executable = + MockExecutable::from_storage(input_data_ch, &mut gas_meter).unwrap(); + set_balance(&ALICE, min_balance * 10_000); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), min_balance) + .unwrap(); + + let result = MockStack::run_instantiate( + ALICE, + executable, + &mut gas_meter, + &mut storage_meter, + min_balance, + vec![1, 2, 3, 4], + Some(&[0; 32]), + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn max_depth() { + // This test verifies that when we reach the maximal depth creation of an + // yet another context fails. + parameter_types! { + static ReachedBottom: bool = false; + } + let value = Default::default(); + let recurse_ch = MockLoader::insert(Call, |ctx, _| { + // Try to call into yourself. + let r = ctx.ext.call( + Weight::zero(), + U256::zero(), + &BOB_ADDR, + U256::zero(), + vec![], + true, + false, + ); + + ReachedBottom::mutate(|reached_bottom| { + if !*reached_bottom { + // We are first time here, it means we just reached bottom. + // Verify that we've got proper error and set `reached_bottom`. + assert_eq!(r, Err(Error::::MaxCallDepthReached.into())); + *reached_bottom = true; + } else { + // We just unwinding stack here. + assert_matches!(r, Ok(_)); + } + }); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + set_balance(&BOB, 1); + place_contract(&BOB, recurse_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, value).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + value, + vec![], + None, + ); + + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn caller_returns_proper_values() { + parameter_types! { + static WitnessedCallerBob: Option = None; + static WitnessedCallerCharlie: Option = None; + } + + let bob_ch = MockLoader::insert(Call, |ctx, _| { + // Record the caller for bob. + WitnessedCallerBob::mutate(|caller| { + let origin = ctx.ext.caller(); + *caller = Some(::AddressMapper::to_address( + &origin.account_id().unwrap(), + )); + }); + + // Call into CHARLIE contract. + assert_matches!( + ctx.ext.call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false + ), + Ok(_) + ); + exec_success() + }); + let charlie_ch = MockLoader::insert(Call, |ctx, _| { + // Record the caller for charlie. + WitnessedCallerCharlie::mutate(|caller| { + let origin = ctx.ext.caller(); + *caller = Some(::AddressMapper::to_address( + &origin.account_id().unwrap(), + )); + }); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, bob_ch); + place_contract(&CHARLIE, charlie_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ); + + assert_matches!(result, Ok(_)); + }); + + assert_eq!(WitnessedCallerBob::get(), Some(ALICE_ADDR)); + assert_eq!(WitnessedCallerCharlie::get(), Some(BOB_ADDR)); + } + + #[test] + fn is_contract_returns_proper_values() { + let bob_ch = MockLoader::insert(Call, |ctx, _| { + // Verify that BOB is a contract + assert!(ctx.ext.is_contract(&BOB_ADDR)); + // Verify that ALICE is not a contract + assert!(!ctx.ext.is_contract(&ALICE_ADDR)); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, bob_ch); + + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn code_hash_returns_proper_values() { + let code_bob = MockLoader::insert(Call, |ctx, _| { + // ALICE is not a contract and hence they do not have a code_hash + assert!(ctx.ext.code_hash(&ALICE_ADDR).is_none()); + // BOB is a contract and hence it has a code_hash + assert!(ctx.ext.code_hash(&BOB_ADDR).is_some()); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + // ALICE (not contract) -> BOB (contract) + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn own_code_hash_returns_proper_values() { + let bob_ch = MockLoader::insert(Call, |ctx, _| { + let code_hash = ctx.ext.code_hash(&BOB_ADDR).unwrap(); + assert_eq!(*ctx.ext.own_code_hash(), code_hash); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, bob_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + // ALICE (not contract) -> BOB (contract) + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn caller_is_origin_returns_proper_values() { + let code_charlie = MockLoader::insert(Call, |ctx, _| { + // BOB is not the origin of the stack call + assert!(!ctx.ext.caller_is_origin()); + exec_success() + }); + + let code_bob = MockLoader::insert(Call, |ctx, _| { + // ALICE is the origin of the call stack + assert!(ctx.ext.caller_is_origin()); + // BOB calls CHARLIE + ctx.ext + .call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false, + ) + .map(|_| ctx.ext.last_frame_output().clone()) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + // ALICE -> BOB (caller is origin) -> CHARLIE (caller is not origin) + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn root_caller_succeeds() { + let code_bob = MockLoader::insert(Call, |ctx, _| { + // root is the origin of the call stack. + assert!(ctx.ext.caller_is_root()); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + let origin = Origin::Root; + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + // root -> BOB (caller is root) + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn root_caller_does_not_succeed_when_value_not_zero() { + let code_bob = MockLoader::insert(Call, |ctx, _| { + // root is the origin of the call stack. + assert!(ctx.ext.caller_is_root()); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + let origin = Origin::Root; + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + // root -> BOB (caller is root) + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 1, + vec![0], + None, + ); + assert_matches!(result, Err(_)); + }); + } + + #[test] + fn root_caller_succeeds_with_consecutive_calls() { + let code_charlie = MockLoader::insert(Call, |ctx, _| { + // BOB is not root, even though the origin is root. + assert!(!ctx.ext.caller_is_root()); + exec_success() + }); + + let code_bob = MockLoader::insert(Call, |ctx, _| { + // root is the origin of the call stack. + assert!(ctx.ext.caller_is_root()); + // BOB calls CHARLIE. + ctx.ext + .call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false, + ) + .map(|_| ctx.ext.last_frame_output().clone()) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::Root; + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + // root -> BOB (caller is root) -> CHARLIE (caller is not root) + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn address_returns_proper_values() { + let bob_ch = MockLoader::insert(Call, |ctx, _| { + // Verify that address matches BOB. + assert_eq!(ctx.ext.address(), BOB_ADDR); + + // Call into charlie contract. + assert_matches!( + ctx.ext.call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false + ), + Ok(_) + ); + exec_success() + }); + let charlie_ch = MockLoader::insert(Call, |ctx, _| { + assert_eq!(ctx.ext.address(), CHARLIE_ADDR); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, bob_ch); + place_contract(&CHARLIE, charlie_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ); + + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn refuse_instantiate_with_value_below_existential_deposit() { + let dummy_ch = MockLoader::insert(Constructor, |_, _| exec_success()); + + ExtBuilder::default().existential_deposit(15).build().execute_with(|| { + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let executable = MockExecutable::from_storage(dummy_ch, &mut gas_meter).unwrap(); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + assert_matches!( + MockStack::run_instantiate( + ALICE, + executable, + &mut gas_meter, + &mut storage_meter, + 0, // <- zero value + vec![], + Some(&[0; 32]), + None, + ), + Err(_) + ); + }); + } + + #[test] + fn instantiation_work_with_success_output() { + let dummy_ch = MockLoader::insert(Constructor, |_, _| { + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: vec![80, 65, 83, 83] }) + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .existential_deposit(15) + .build() + .execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let executable = MockExecutable::from_storage(dummy_ch, &mut gas_meter).unwrap(); + set_balance(&ALICE, min_balance * 1000); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, min_balance * 100, min_balance).unwrap(); + + let instantiated_contract_address = assert_matches!( + MockStack::run_instantiate( + ALICE, + executable, + &mut gas_meter, + &mut storage_meter, + + min_balance, + vec![], + Some(&[0 ;32]), + None, + ), + Ok((address, ref output)) if output.data == vec![80, 65, 83, 83] => address + ); + let instantiated_contract_id = + ::AddressMapper::to_account_id_contract( + &instantiated_contract_address, + ); + + // Check that the newly created account has the expected code hash and + // there are instantiation event. + assert_eq!( + ContractInfo::::load_code_hash(&instantiated_contract_id).unwrap(), + dummy_ch + ); + assert_eq!( + &events(), + &[Event::Instantiated { + deployer: ALICE_ADDR, + contract: instantiated_contract_address + }] + ); + }); + } + + #[test] + fn instantiation_fails_with_failing_output() { + let dummy_ch = MockLoader::insert(Constructor, |_, _| { + Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![70, 65, 73, 76] }) + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .existential_deposit(15) + .build() + .execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let executable = MockExecutable::from_storage(dummy_ch, &mut gas_meter).unwrap(); + set_balance(&ALICE, min_balance * 1000); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, min_balance * 100, min_balance).unwrap(); + + let instantiated_contract_address = assert_matches!( + MockStack::run_instantiate( + ALICE, + executable, + &mut gas_meter, + &mut storage_meter, + + min_balance, + vec![], + Some(&[0; 32]), + None, + ), + Ok((address, ref output)) if output.data == vec![70, 65, 73, 76] => address + ); + + let instantiated_contract_id = + ::AddressMapper::to_account_id_contract( + &instantiated_contract_address, + ); + + // Check that the account has not been created. + assert!(ContractInfo::::load_code_hash(&instantiated_contract_id).is_none()); + assert!(events().is_empty()); + }); + } + + #[test] + fn instantiation_from_contract() { + let dummy_ch = MockLoader::insert(Call, |_, _| exec_success()); + let instantiated_contract_address = Rc::new(RefCell::new(None::)); + let instantiator_ch = MockLoader::insert(Call, { + let instantiated_contract_address = Rc::clone(&instantiated_contract_address); + move |ctx, _| { + // Instantiate a contract and save it's address in `instantiated_contract_address`. + let (address, output) = ctx + .ext + .instantiate( + Weight::zero(), + U256::zero(), + dummy_ch, + ::Currency::minimum_balance().into(), + vec![], + Some(&[48; 32]), + ) + .map(|address| (address, ctx.ext.last_frame_output().clone())) + .unwrap(); + + *instantiated_contract_address.borrow_mut() = Some(address); + Ok(output) + } + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .existential_deposit(15) + .build() + .execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + set_balance(&ALICE, min_balance * 100); + place_contract(&BOB, instantiator_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, min_balance * 10, min_balance * 10) + .unwrap(); + + assert_matches!( + MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + min_balance * 10, + vec![], + None, + ), + Ok(_) + ); + + let instantiated_contract_address = + *instantiated_contract_address.borrow().as_ref().unwrap(); + + let instantiated_contract_id = + ::AddressMapper::to_account_id_contract( + &instantiated_contract_address, + ); + + // Check that the newly created account has the expected code hash and + // there are instantiation event. + assert_eq!( + ContractInfo::::load_code_hash(&instantiated_contract_id).unwrap(), + dummy_ch + ); + assert_eq!( + &events(), + &[ + Event::Instantiated { + deployer: BOB_ADDR, + contract: instantiated_contract_address + }, + Event::Called { + caller: Origin::from_account_id(ALICE), + contract: BOB_ADDR + }, + ] + ); + }); + } + + #[test] + fn instantiation_traps() { + let dummy_ch = MockLoader::insert(Constructor, |_, _| Err("It's a trap!".into())); + let instantiator_ch = MockLoader::insert(Call, { + move |ctx, _| { + // Instantiate a contract and save it's address in `instantiated_contract_address`. + assert_matches!( + ctx.ext.instantiate( + Weight::zero(), + U256::zero(), + dummy_ch, + ::Currency::minimum_balance().into(), + vec![], + Some(&[0; 32]), + ), + Err(ExecError { + error: DispatchError::Other("It's a trap!"), + origin: ErrorOrigin::Callee, + }) + ); + + exec_success() + } + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .existential_deposit(15) + .build() + .execute_with(|| { + set_balance(&ALICE, 1000); + set_balance(&BOB_CONTRACT_ID, 100); + place_contract(&BOB, instantiator_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 200, 0).unwrap(); + + assert_matches!( + MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ), + Ok(_) + ); + + // The contract wasn't instantiated so we don't expect to see an instantiation + // event here. + assert_eq!( + &events(), + &[Event::Called { caller: Origin::from_account_id(ALICE), contract: BOB_ADDR },] + ); + }); + } + + #[test] + fn termination_from_instantiate_fails() { + let terminate_ch = MockLoader::insert(Constructor, |ctx, _| { + ctx.ext.terminate(&ALICE_ADDR).unwrap(); + exec_success() + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .existential_deposit(15) + .build() + .execute_with(|| { + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let executable = + MockExecutable::from_storage(terminate_ch, &mut gas_meter).unwrap(); + set_balance(&ALICE, 10_000); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 100).unwrap(); + + assert_eq!( + MockStack::run_instantiate( + ALICE, + executable, + &mut gas_meter, + &mut storage_meter, + 100, + vec![], + Some(&[0; 32]), + None, + ), + Err(Error::::TerminatedInConstructor.into()) + ); + + assert_eq!(&events(), &[]); + }); + } + + #[test] + fn in_memory_changes_not_discarded() { + // Call stack: BOB -> CHARLIE (trap) -> BOB' (success) + // This tests verifies some edge case of the contract info cache: + // We change some value in our contract info before calling into a contract + // that calls into ourself. This triggers a case where BOBs contract info + // is written to storage and invalidated by the successful execution of BOB'. + // The trap of CHARLIE reverts the storage changes to BOB. When the root BOB regains + // control it reloads its contract info from storage. We check that changes that + // are made before calling into CHARLIE are not discarded. + let code_bob = MockLoader::insert(Call, |ctx, _| { + if ctx.input_data[0] == 0 { + let info = ctx.ext.contract_info(); + assert_eq!(info.storage_byte_deposit, 0); + info.storage_byte_deposit = 42; + assert_eq!( + ctx.ext + .call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false + ) + .map(|_| ctx.ext.last_frame_output().clone()), + exec_trapped() + ); + assert_eq!(ctx.ext.contract_info().storage_byte_deposit, 42); + } + exec_success() + }); + let code_charlie = MockLoader::insert(Call, |ctx, _| { + assert!(ctx + .ext + .call(Weight::zero(), U256::zero(), &BOB_ADDR, U256::zero(), vec![99], true, false) + .is_ok()); + exec_trapped() + }); + + // This one tests passing the input data into a contract via call. + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn recursive_call_during_constructor_is_balance_transfer() { + let code = MockLoader::insert(Constructor, |ctx, _| { + let account_id = ctx.ext.account_id().clone(); + let addr = ::AddressMapper::to_address(&account_id); + let balance = ctx.ext.balance(); + + // Calling ourselves during the constructor will trigger a balance + // transfer since no contract exist yet. + assert_ok!(ctx.ext.call( + Weight::zero(), + U256::zero(), + &addr, + (balance - 1).into(), + vec![], + true, + false + )); + + // Should also work with call data set as it is ignored when no + // contract is deployed. + assert_ok!(ctx.ext.call( + Weight::zero(), + U256::zero(), + &addr, + 1u32.into(), + vec![1, 2, 3, 4], + true, + false + )); + exec_success() + }); + + // This one tests passing the input data into a contract via instantiate. + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .build() + .execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let executable = MockExecutable::from_storage(code, &mut gas_meter).unwrap(); + set_balance(&ALICE, min_balance * 10_000); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), min_balance) + .unwrap(); + + let result = MockStack::run_instantiate( + ALICE, + executable, + &mut gas_meter, + &mut storage_meter, + 10, + vec![], + Some(&[0; 32]), + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn cannot_send_more_balance_than_available_to_self() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + let account_id = ctx.ext.account_id().clone(); + let addr = ::AddressMapper::to_address(&account_id); + let balance = ctx.ext.balance(); + + assert_err!( + ctx.ext.call( + Weight::zero(), + U256::zero(), + &addr, + (balance + 1).into(), + vec![], + true, + false + ), + >::TransferFailed + ); + exec_success() + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .build() + .execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 10); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + ) + .unwrap(); + }); + } + + #[test] + fn printing_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + ctx.ext.append_debug_buffer("This is a test"); + ctx.ext.append_debug_buffer("More text"); + exec_success() + }); + + let mut debug_buffer = DebugBuffer::try_from(Vec::new()).unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 10); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + Some(&mut debug_buffer), + ) + .unwrap(); + }); + + assert_eq!(&String::from_utf8(debug_buffer.to_vec()).unwrap(), "This is a testMore text"); + } + + #[test] + fn printing_works_on_fail() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + ctx.ext.append_debug_buffer("This is a test"); + ctx.ext.append_debug_buffer("More text"); + exec_trapped() + }); + + let mut debug_buffer = DebugBuffer::try_from(Vec::new()).unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 10); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + Some(&mut debug_buffer), + ); + assert!(result.is_err()); + }); + + assert_eq!(&String::from_utf8(debug_buffer.to_vec()).unwrap(), "This is a testMore text"); + } + + #[test] + fn debug_buffer_is_limited() { + let code_hash = MockLoader::insert(Call, move |ctx, _| { + ctx.ext.append_debug_buffer("overflowing bytes"); + exec_success() + }); + + // Pre-fill the buffer almost up to its limit, leaving not enough space to the message + let debug_buf_before = DebugBuffer::try_from(vec![0u8; DebugBuffer::bound() - 5]).unwrap(); + let mut debug_buf_after = debug_buf_before.clone(); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 10); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + Some(&mut debug_buf_after), + ) + .unwrap(); + assert_eq!(debug_buf_before, debug_buf_after); + }); + } + + #[test] + fn call_reentry_direct_recursion() { + // call the contract passed as input with disabled reentry + let code_bob = MockLoader::insert(Call, |ctx, _| { + let dest = H160::from_slice(ctx.input_data.as_ref()); + ctx.ext + .call(Weight::zero(), U256::zero(), &dest, U256::zero(), vec![], false, false) + .map(|_| ctx.ext.last_frame_output().clone()) + }); + + let code_charlie = MockLoader::insert(Call, |_, _| exec_success()); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + // Calling another contract should succeed + assert_ok!(MockStack::run_call( + origin.clone(), + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + CHARLIE_ADDR.as_bytes().to_vec(), + None, + )); + + // Calling into oneself fails + assert_err!( + MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + BOB_ADDR.as_bytes().to_vec(), + None, + ) + .map_err(|e| e.error), + >::ReentranceDenied, + ); + }); + } + + #[test] + fn call_deny_reentry() { + let code_bob = MockLoader::insert(Call, |ctx, _| { + if ctx.input_data[0] == 0 { + ctx.ext + .call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + false, + false, + ) + .map(|_| ctx.ext.last_frame_output().clone()) + } else { + exec_success() + } + }); + + // call BOB with input set to '1' + let code_charlie = MockLoader::insert(Call, |ctx, _| { + ctx.ext + .call(Weight::zero(), U256::zero(), &BOB_ADDR, U256::zero(), vec![1], true, false) + .map(|_| ctx.ext.last_frame_output().clone()) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + // BOB -> CHARLIE -> BOB fails as BOB denies reentry. + assert_err!( + MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ) + .map_err(|e| e.error), + >::ReentranceDenied, + ); + }); + } + + #[test] + fn call_runtime_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + let call = RuntimeCall::System(frame_system::Call::remark_with_event { + remark: b"Hello World".to_vec(), + }); + ctx.ext.call_runtime(call).unwrap(); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 10); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + System::reset_events(); + MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + ) + .unwrap(); + + let remark_hash = ::Hashing::hash(b"Hello World"); + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: MetaEvent::System(frame_system::Event::Remarked { + sender: BOB_CONTRACT_ID, + hash: remark_hash + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: MetaEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: BOB_ADDR, + }), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn call_runtime_filter() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + use frame_system::Call as SysCall; + use pallet_balances::Call as BalanceCall; + use pallet_utility::Call as UtilCall; + + // remark should still be allowed + let allowed_call = + RuntimeCall::System(SysCall::remark_with_event { remark: b"Hello".to_vec() }); + + // transfers are disallowed by the `TestFiler` (see below) + let forbidden_call = RuntimeCall::Balances(BalanceCall::transfer_allow_death { + dest: CHARLIE, + value: 22, + }); + + // simple cases: direct call + assert_err!( + ctx.ext.call_runtime(forbidden_call.clone()), + frame_system::Error::::CallFiltered + ); + + // as part of a patch: return is OK (but it interrupted the batch) + assert_ok!(ctx.ext.call_runtime(RuntimeCall::Utility(UtilCall::batch { + calls: vec![allowed_call.clone(), forbidden_call, allowed_call] + })),); + + // the transfer wasn't performed + assert_eq!(get_balance(&CHARLIE), 0); + + exec_success() + }); + + TestFilter::set_filter(|call| match call { + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) => false, + _ => true, + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 10); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + System::reset_events(); + MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + ) + .unwrap(); + + let remark_hash = ::Hashing::hash(b"Hello"); + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: MetaEvent::System(frame_system::Event::Remarked { + sender: BOB_CONTRACT_ID, + hash: remark_hash + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: MetaEvent::Utility(pallet_utility::Event::ItemCompleted), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: MetaEvent::Utility(pallet_utility::Event::BatchInterrupted { + index: 1, + error: frame_system::Error::::CallFiltered.into() + },), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: MetaEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: BOB_ADDR, + }), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn nonce() { + let fail_code = MockLoader::insert(Constructor, |_, _| exec_trapped()); + let success_code = MockLoader::insert(Constructor, |_, _| exec_success()); + let succ_fail_code = MockLoader::insert(Constructor, move |ctx, _| { + ctx.ext + .instantiate( + Weight::zero(), + U256::zero(), + fail_code, + ctx.ext.minimum_balance() * 100, + vec![], + Some(&[0; 32]), + ) + .ok(); + exec_success() + }); + let succ_succ_code = MockLoader::insert(Constructor, move |ctx, _| { + let alice_nonce = System::account_nonce(&ALICE); + assert_eq!(System::account_nonce(ctx.ext.account_id()), 0); + assert_eq!(ctx.ext.caller().account_id().unwrap(), &ALICE); + let addr = ctx + .ext + .instantiate( + Weight::zero(), + U256::zero(), + success_code, + ctx.ext.minimum_balance() * 100, + vec![], + Some(&[0; 32]), + ) + .unwrap(); + + let account_id = ::AddressMapper::to_account_id_contract(&addr); + + assert_eq!(System::account_nonce(&ALICE), alice_nonce); + assert_eq!(System::account_nonce(ctx.ext.account_id()), 1); + assert_eq!(System::account_nonce(&account_id), 0); + + // a plain call should not influence the account counter + ctx.ext + .call(Weight::zero(), U256::zero(), &addr, U256::zero(), vec![], false, false) + .unwrap(); + + assert_eq!(System::account_nonce(ALICE), alice_nonce); + assert_eq!(System::account_nonce(ctx.ext.account_id()), 1); + assert_eq!(System::account_nonce(&account_id), 0); + + exec_success() + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .build() + .execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + let fail_executable = + MockExecutable::from_storage(fail_code, &mut gas_meter).unwrap(); + let success_executable = + MockExecutable::from_storage(success_code, &mut gas_meter).unwrap(); + let succ_fail_executable = + MockExecutable::from_storage(succ_fail_code, &mut gas_meter).unwrap(); + let succ_succ_executable = + MockExecutable::from_storage(succ_succ_code, &mut gas_meter).unwrap(); + set_balance(&ALICE, min_balance * 10_000); + set_balance(&BOB, min_balance * 10_000); + let origin = Origin::from_account_id(BOB); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), min_balance * 100) + .unwrap(); + + // fail should not increment + MockStack::run_instantiate( + ALICE, + fail_executable, + &mut gas_meter, + &mut storage_meter, + min_balance * 100, + vec![], + Some(&[0; 32]), + None, + ) + .ok(); + assert_eq!(System::account_nonce(&ALICE), 0); + + assert_ok!(MockStack::run_instantiate( + ALICE, + success_executable, + &mut gas_meter, + &mut storage_meter, + min_balance * 100, + vec![], + Some(&[0; 32]), + None, + )); + assert_eq!(System::account_nonce(&ALICE), 1); + + assert_ok!(MockStack::run_instantiate( + ALICE, + succ_fail_executable, + &mut gas_meter, + &mut storage_meter, + min_balance * 200, + vec![], + Some(&[0; 32]), + None, + )); + assert_eq!(System::account_nonce(&ALICE), 2); + + assert_ok!(MockStack::run_instantiate( + ALICE, + succ_succ_executable, + &mut gas_meter, + &mut storage_meter, + min_balance * 200, + vec![], + Some(&[0; 32]), + None, + )); + assert_eq!(System::account_nonce(&ALICE), 3); + }); + } + + #[test] + fn set_storage_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + // Write + assert_eq!( + ctx.ext.set_storage(&Key::Fix([1; 32]), Some(vec![1, 2, 3]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([2; 32]), Some(vec![4, 5, 6]), true), + Ok(WriteOutcome::New) + ); + assert_eq!(ctx.ext.set_storage(&Key::Fix([3; 32]), None, false), Ok(WriteOutcome::New)); + assert_eq!(ctx.ext.set_storage(&Key::Fix([4; 32]), None, true), Ok(WriteOutcome::New)); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([5; 32]), Some(vec![]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([6; 32]), Some(vec![]), true), + Ok(WriteOutcome::New) + ); + + // Overwrite + assert_eq!( + ctx.ext.set_storage(&Key::Fix([1; 32]), Some(vec![42]), false), + Ok(WriteOutcome::Overwritten(3)) + ); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([2; 32]), Some(vec![48]), true), + Ok(WriteOutcome::Taken(vec![4, 5, 6])) + ); + assert_eq!(ctx.ext.set_storage(&Key::Fix([3; 32]), None, false), Ok(WriteOutcome::New)); + assert_eq!(ctx.ext.set_storage(&Key::Fix([4; 32]), None, true), Ok(WriteOutcome::New)); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([5; 32]), Some(vec![]), false), + Ok(WriteOutcome::Overwritten(0)) + ); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([6; 32]), Some(vec![]), true), + Ok(WriteOutcome::Taken(vec![])) + ); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 1000); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn set_storage_varsized_key_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + // Write + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([1; 64].to_vec()).unwrap(), + Some(vec![1, 2, 3]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([2; 19].to_vec()).unwrap(), + Some(vec![4, 5, 6]), + true + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage(&Key::try_from_var([3; 19].to_vec()).unwrap(), None, false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage(&Key::try_from_var([4; 64].to_vec()).unwrap(), None, true), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([5; 30].to_vec()).unwrap(), + Some(vec![]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([6; 128].to_vec()).unwrap(), + Some(vec![]), + true + ), + Ok(WriteOutcome::New) + ); + + // Overwrite + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([1; 64].to_vec()).unwrap(), + Some(vec![42, 43, 44]), + false + ), + Ok(WriteOutcome::Overwritten(3)) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([2; 19].to_vec()).unwrap(), + Some(vec![48]), + true + ), + Ok(WriteOutcome::Taken(vec![4, 5, 6])) + ); + assert_eq!( + ctx.ext.set_storage(&Key::try_from_var([3; 19].to_vec()).unwrap(), None, false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage(&Key::try_from_var([4; 64].to_vec()).unwrap(), None, true), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([5; 30].to_vec()).unwrap(), + Some(vec![]), + false + ), + Ok(WriteOutcome::Overwritten(0)) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([6; 128].to_vec()).unwrap(), + Some(vec![]), + true + ), + Ok(WriteOutcome::Taken(vec![])) + ); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 1000); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn get_storage_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + assert_eq!( + ctx.ext.set_storage(&Key::Fix([1; 32]), Some(vec![1, 2, 3]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([2; 32]), Some(vec![]), false), + Ok(WriteOutcome::New) + ); + assert_eq!(ctx.ext.get_storage(&Key::Fix([1; 32])), Some(vec![1, 2, 3])); + assert_eq!(ctx.ext.get_storage(&Key::Fix([2; 32])), Some(vec![])); + assert_eq!(ctx.ext.get_storage(&Key::Fix([3; 32])), None); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 1000); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn get_storage_size_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + assert_eq!( + ctx.ext.set_storage(&Key::Fix([1; 32]), Some(vec![1, 2, 3]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage(&Key::Fix([2; 32]), Some(vec![]), false), + Ok(WriteOutcome::New) + ); + assert_eq!(ctx.ext.get_storage_size(&Key::Fix([1; 32])), Some(3)); + assert_eq!(ctx.ext.get_storage_size(&Key::Fix([2; 32])), Some(0)); + assert_eq!(ctx.ext.get_storage_size(&Key::Fix([3; 32])), None); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 1000); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn get_storage_varsized_key_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([1; 19].to_vec()).unwrap(), + Some(vec![1, 2, 3]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([2; 16].to_vec()).unwrap(), + Some(vec![]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.get_storage(&Key::try_from_var([1; 19].to_vec()).unwrap()), + Some(vec![1, 2, 3]) + ); + assert_eq!( + ctx.ext.get_storage(&Key::try_from_var([2; 16].to_vec()).unwrap()), + Some(vec![]) + ); + assert_eq!(ctx.ext.get_storage(&Key::try_from_var([3; 8].to_vec()).unwrap()), None); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 1000); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn get_storage_size_varsized_key_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([1; 19].to_vec()).unwrap(), + Some(vec![1, 2, 3]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_storage( + &Key::try_from_var([2; 16].to_vec()).unwrap(), + Some(vec![]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.get_storage_size(&Key::try_from_var([1; 19].to_vec()).unwrap()), + Some(3) + ); + assert_eq!( + ctx.ext.get_storage_size(&Key::try_from_var([2; 16].to_vec()).unwrap()), + Some(0) + ); + assert_eq!( + ctx.ext.get_storage_size(&Key::try_from_var([3; 8].to_vec()).unwrap()), + None + ); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let min_balance = ::Currency::minimum_balance(); + + let mut gas_meter = GasMeter::::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 1000); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn set_transient_storage_works() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + // Write + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([1; 32]), Some(vec![1, 2, 3]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([2; 32]), Some(vec![4, 5, 6]), true), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([3; 32]), None, false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([4; 32]), None, true), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([5; 32]), Some(vec![]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([6; 32]), Some(vec![]), true), + Ok(WriteOutcome::New) + ); + + // Overwrite + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([1; 32]), Some(vec![42]), false), + Ok(WriteOutcome::Overwritten(3)) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([2; 32]), Some(vec![48]), true), + Ok(WriteOutcome::Taken(vec![4, 5, 6])) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([3; 32]), None, false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([4; 32]), None, true), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([5; 32]), Some(vec![]), false), + Ok(WriteOutcome::Overwritten(0)) + ); + assert_eq!( + ctx.ext.set_transient_storage(&Key::Fix([6; 32]), Some(vec![]), true), + Ok(WriteOutcome::Taken(vec![])) + ); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = + storage::meter::Meter::new(&origin, deposit_limit::(), 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn get_transient_storage_works() { + // Call stack: BOB -> CHARLIE(success) -> BOB' (success) + let storage_key_1 = &Key::Fix([1; 32]); + let storage_key_2 = &Key::Fix([2; 32]); + let storage_key_3 = &Key::Fix([3; 32]); + let code_bob = MockLoader::insert(Call, |ctx, _| { + if ctx.input_data[0] == 0 { + assert_eq!( + ctx.ext.set_transient_storage(storage_key_1, Some(vec![1, 2]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext + .call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false, + ) + .map(|_| ctx.ext.last_frame_output().clone()), + exec_success() + ); + assert_eq!(ctx.ext.get_transient_storage(storage_key_1), Some(vec![3])); + assert_eq!(ctx.ext.get_transient_storage(storage_key_2), Some(vec![])); + assert_eq!(ctx.ext.get_transient_storage(storage_key_3), None); + } else { + assert_eq!( + ctx.ext.set_transient_storage(storage_key_1, Some(vec![3]), true), + Ok(WriteOutcome::Taken(vec![1, 2])) + ); + assert_eq!( + ctx.ext.set_transient_storage(storage_key_2, Some(vec![]), false), + Ok(WriteOutcome::New) + ); + } + exec_success() + }); + let code_charlie = MockLoader::insert(Call, |ctx, _| { + assert!(ctx + .ext + .call(Weight::zero(), U256::zero(), &BOB_ADDR, U256::zero(), vec![99], true, false) + .is_ok()); + // CHARLIE can not read BOB`s storage. + assert_eq!(ctx.ext.get_transient_storage(storage_key_1), None); + exec_success() + }); + + // This one tests passing the input data into a contract via call. + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn get_transient_storage_size_works() { + let storage_key_1 = &Key::Fix([1; 32]); + let storage_key_2 = &Key::Fix([2; 32]); + let storage_key_3 = &Key::Fix([3; 32]); + let code_hash = MockLoader::insert(Call, |ctx, _| { + assert_eq!( + ctx.ext.set_transient_storage(storage_key_1, Some(vec![1, 2, 3]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext.set_transient_storage(storage_key_2, Some(vec![]), false), + Ok(WriteOutcome::New) + ); + assert_eq!(ctx.ext.get_transient_storage_size(storage_key_1), Some(3)); + assert_eq!(ctx.ext.get_transient_storage_size(storage_key_2), Some(0)); + assert_eq!(ctx.ext.get_transient_storage_size(storage_key_3), None); + + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + assert_ok!(MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + )); + }); + } + + #[test] + fn rollback_transient_storage_works() { + // Call stack: BOB -> CHARLIE (trap) -> BOB' (success) + let storage_key = &Key::Fix([1; 32]); + let code_bob = MockLoader::insert(Call, |ctx, _| { + if ctx.input_data[0] == 0 { + assert_eq!( + ctx.ext.set_transient_storage(storage_key, Some(vec![1, 2]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + ctx.ext + .call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false + ) + .map(|_| ctx.ext.last_frame_output().clone()), + exec_trapped() + ); + assert_eq!(ctx.ext.get_transient_storage(storage_key), Some(vec![1, 2])); + } else { + let overwritten_length = ctx.ext.get_transient_storage_size(storage_key).unwrap(); + assert_eq!( + ctx.ext.set_transient_storage(storage_key, Some(vec![3]), false), + Ok(WriteOutcome::Overwritten(overwritten_length)) + ); + assert_eq!(ctx.ext.get_transient_storage(storage_key), Some(vec![3])); + } + exec_success() + }); + let code_charlie = MockLoader::insert(Call, |ctx, _| { + assert!(ctx + .ext + .call(Weight::zero(), U256::zero(), &BOB_ADDR, U256::zero(), vec![99], true, false) + .is_ok()); + exec_trapped() + }); + + // This one tests passing the input data into a contract via call. + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn ecdsa_to_eth_address_returns_proper_value() { + let bob_ch = MockLoader::insert(Call, |ctx, _| { + let pubkey_compressed = array_bytes::hex2array_unchecked( + "028db55b05db86c0b1786ca49f095d76344c9e6056b2f02701a7e7f3c20aabfd91", + ); + assert_eq!( + ctx.ext.ecdsa_to_eth_address(&pubkey_compressed).unwrap(), + array_bytes::hex2array_unchecked::<_, 20>( + "09231da7b19A016f9e576d23B16277062F4d46A8" + ) + ); + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, bob_ch); + + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ); + assert_matches!(result, Ok(_)); + }); + } + + #[test] + fn last_frame_output_works_on_instantiate() { + let ok_ch = MockLoader::insert(Constructor, move |_, _| { + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: vec![127] }) + }); + let revert_ch = MockLoader::insert(Constructor, move |_, _| { + Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![70] }) + }); + let trap_ch = MockLoader::insert(Constructor, |_, _| Err("It's a trap!".into())); + let instantiator_ch = MockLoader::insert(Call, { + move |ctx, _| { + let value = ::Currency::minimum_balance().into(); + + // Successful instantiation should set the output + let address = ctx + .ext + .instantiate(Weight::zero(), U256::zero(), ok_ch, value, vec![], None) + .unwrap(); + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::empty(), data: vec![127] } + ); + + // Plain transfers should not set the output + ctx.ext.transfer(&address, U256::from(1)).unwrap(); + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::empty(), data: vec![127] } + ); + + // Reverted instantiation should set the output + ctx.ext + .instantiate(Weight::zero(), U256::zero(), revert_ch, value, vec![], None) + .unwrap(); + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![70] } + ); + + // Trapped instantiation should clear the output + ctx.ext + .instantiate(Weight::zero(), U256::zero(), trap_ch, value, vec![], None) + .unwrap_err(); + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::empty(), data: vec![] } + ); + + exec_success() + } + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .existential_deposit(15) + .build() + .execute_with(|| { + set_balance(&ALICE, 1000); + set_balance(&BOB_CONTRACT_ID, 100); + place_contract(&BOB, instantiator_ch); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 200, 0).unwrap(); + + MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![], + None, + ) + .unwrap() + }); + } + + #[test] + fn last_frame_output_works_on_nested_call() { + // Call stack: BOB -> CHARLIE(revert) -> BOB' (success) + let code_bob = MockLoader::insert(Call, |ctx, _| { + if ctx.input_data.is_empty() { + // We didn't do anything yet + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::empty(), data: vec![] } + ); + + ctx.ext + .call( + Weight::zero(), + U256::zero(), + &CHARLIE_ADDR, + U256::zero(), + vec![], + true, + false, + ) + .unwrap(); + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![70] } + ); + } + + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: vec![127] }) + }); + let code_charlie = MockLoader::insert(Call, |ctx, _| { + // We didn't do anything yet + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::empty(), data: vec![] } + ); + + assert!(ctx + .ext + .call(Weight::zero(), U256::zero(), &BOB_ADDR, U256::zero(), vec![99], true, false) + .is_ok()); + assert_eq!( + ctx.ext.last_frame_output(), + &ExecReturnValue { flags: ReturnFlags::empty(), data: vec![127] } + ); + + Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![70] }) + }); + + ExtBuilder::default().build().execute_with(|| { + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + + let result = MockStack::run_call( + origin, + BOB_ADDR, + &mut GasMeter::::new(GAS_LIMIT), + &mut storage_meter, + 0, + vec![0], + None, + ); + assert_matches!(result, Ok(_)); + }); + } +} diff --git a/pallets/revive/src/gas.rs b/pallets/revive/src/gas.rs new file mode 100644 index 00000000..9d06127e --- /dev/null +++ b/pallets/revive/src/gas.rs @@ -0,0 +1,417 @@ +// 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 core::marker::PhantomData; +#[cfg(test)] +use std::{any::Any, fmt::Debug}; + +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, PostDispatchInfo}, + weights::Weight, + DefaultNoBound, +}; +use sp_runtime::{traits::Zero, DispatchError}; + +use crate::{exec::ExecError, weights::WeightInfo, Config, Error}; + +#[derive(Debug, PartialEq, Eq)] +pub struct ChargedAmount(Weight); + +impl ChargedAmount { + pub fn amount(&self) -> Weight { + self.0 + } +} + +/// Meter for syncing the gas between the executor and the gas meter. +#[derive(DefaultNoBound)] +struct EngineMeter { + fuel: u64, + _phantom: PhantomData, +} + +impl EngineMeter { + /// Create a meter with the given fuel limit. + fn new(limit: Weight) -> Self { + Self { + fuel: limit.ref_time().saturating_div(Self::ref_time_per_fuel()), + _phantom: PhantomData, + } + } + + /// Set the fuel left to the given value. + /// Returns the amount of Weight consumed since the last update. + fn set_fuel(&mut self, fuel: u64) -> Weight { + let consumed = self.fuel.saturating_sub(fuel).saturating_mul(Self::ref_time_per_fuel()); + self.fuel = fuel; + Weight::from_parts(consumed, 0) + } + + /// Charge the given amount of gas. + /// Returns the amount of fuel left. + fn charge_ref_time(&mut self, ref_time: u64) -> Result { + let amount = ref_time + .checked_div(Self::ref_time_per_fuel()) + .ok_or(Error::::InvalidSchedule)?; + + self.fuel.checked_sub(amount).ok_or_else(|| Error::::OutOfGas)?; + Ok(Syncable(self.fuel.try_into().map_err(|_| Error::::OutOfGas)?)) + } + + /// How much ref time does each PolkaVM gas correspond to. + fn ref_time_per_fuel() -> u64 { + // We execute 6 different instructions therefore we have to divide the actual + // computed gas costs by 6 to have a rough estimate as to how expensive each + // single executed instruction is going to be. + let instr_cost = T::WeightInfo::instr_i64_load_store(1) + .saturating_sub(T::WeightInfo::instr_i64_load_store(0)) + .ref_time(); + instr_cost / 6 + } +} + +/// Used to capture the gas left before entering a host function. +/// +/// Has to be consumed in order to sync back the gas after leaving the host function. +#[must_use] +pub struct RefTimeLeft(u64); + +/// Resource that needs to be synced to the executor. +/// +/// Wrapped to make sure that the resource will be synced back the the executor. +#[must_use] +pub struct Syncable(polkavm::Gas); + +impl From for polkavm::Gas { + fn from(from: Syncable) -> Self { + from.0 + } +} + +#[cfg(not(test))] +pub trait TestAuxiliaries {} +#[cfg(not(test))] +impl TestAuxiliaries for T {} + +#[cfg(test)] +pub trait TestAuxiliaries: Any + Debug + PartialEq + Eq {} +#[cfg(test)] +impl TestAuxiliaries for T {} + +/// This trait represents a token that can be used for charging `GasMeter`. +/// There is no other way of charging it. +/// +/// Implementing type is expected to be super lightweight hence `Copy` (`Clone` is added +/// for consistency). If inlined there should be no observable difference compared +/// to a hand-written code. +pub trait Token: Copy + Clone + TestAuxiliaries { + /// Return the amount of gas that should be taken by this token. + /// + /// This function should be really lightweight and must not fail. It is not + /// expected that implementors will query the storage or do any kinds of heavy operations. + /// + /// That said, implementors of this function still can run into overflows + /// while calculating the amount. In this case it is ok to use saturating operations + /// since on overflow they will return `max_value` which should consume all gas. + fn weight(&self) -> Weight; + + /// Returns true if this token is expected to influence the lowest gas limit. + fn influence_lowest_gas_limit(&self) -> bool { + true + } +} + +/// A wrapper around a type-erased trait object of what used to be a `Token`. +#[cfg(test)] +pub struct ErasedToken { + pub description: String, + pub token: Box, +} + +#[derive(DefaultNoBound)] +pub struct GasMeter { + gas_limit: Weight, + /// Amount of gas left from initial gas limit. Can reach zero. + gas_left: Weight, + /// Due to `adjust_gas` and `nested` the `gas_left` can temporarily dip below its final value. + gas_left_lowest: Weight, + /// The amount of resources that was consumed by the execution engine. + /// We have to track it separately in order to avoid the loss of precision that happens when + /// converting from ref_time to the execution engine unit. + engine_meter: EngineMeter, + _phantom: PhantomData, + #[cfg(test)] + tokens: Vec, +} + +impl GasMeter { + pub fn new(gas_limit: Weight) -> Self { + GasMeter { + gas_limit, + gas_left: gas_limit, + gas_left_lowest: gas_limit, + engine_meter: EngineMeter::new(gas_limit), + _phantom: PhantomData, + #[cfg(test)] + tokens: Vec::new(), + } + } + + /// Create a new gas meter by removing gas from the current meter. + /// + /// # Note + /// + /// Passing `0` as amount is interpreted as "all remaining gas". + pub fn nested(&mut self, amount: Weight) -> Self { + let amount = Weight::from_parts( + if amount.ref_time().is_zero() { + self.gas_left().ref_time() + } else { + amount.ref_time() + }, + if amount.proof_size().is_zero() { + self.gas_left().proof_size() + } else { + amount.proof_size() + }, + ) + .min(self.gas_left); + self.gas_left -= amount; + GasMeter::new(amount) + } + + /// Absorb the remaining gas of a nested meter after we are done using it. + pub fn absorb_nested(&mut self, nested: Self) { + self.gas_left_lowest = (self.gas_left + nested.gas_limit) + .saturating_sub(nested.gas_required()) + .min(self.gas_left_lowest); + self.gas_left += nested.gas_left; + } + + /// Account for used gas. + /// + /// Amount is calculated by the given `token`. + /// + /// Returns `OutOfGas` if there is not enough gas or addition of the specified + /// amount of gas has lead to overflow. + /// + /// NOTE that amount isn't consumed if there is not enough gas. This is considered + /// safe because we always charge gas before performing any resource-spending action. + #[inline] + pub fn charge>(&mut self, token: Tok) -> Result { + #[cfg(test)] + { + // Unconditionally add the token to the storage. + let erased_tok = + ErasedToken { description: format!("{:?}", token), token: Box::new(token) }; + self.tokens.push(erased_tok); + } + let amount = token.weight(); + // It is OK to not charge anything on failure because we always charge _before_ we perform + // any action + self.gas_left = self.gas_left.checked_sub(&amount).ok_or_else(|| Error::::OutOfGas)?; + Ok(ChargedAmount(amount)) + } + + /// Adjust a previously charged amount down to its actual amount. + /// + /// This is when a maximum a priori amount was charged and then should be partially + /// refunded to match the actual amount. + pub fn adjust_gas>(&mut self, charged_amount: ChargedAmount, token: Tok) { + if token.influence_lowest_gas_limit() { + self.gas_left_lowest = self.gas_left_lowest(); + } + let adjustment = charged_amount.0.saturating_sub(token.weight()); + self.gas_left = self.gas_left.saturating_add(adjustment).min(self.gas_limit); + } + + /// Hand over the gas metering responsibility from the executor to this meter. + /// + /// Needs to be called when entering a host function to update this meter with the + /// gas that was tracked by the executor. It tracks the latest seen total value + /// in order to compute the delta that needs to be charged. + pub fn sync_from_executor( + &mut self, + engine_fuel: polkavm::Gas, + ) -> Result { + let weight_consumed = self + .engine_meter + .set_fuel(engine_fuel.try_into().map_err(|_| Error::::OutOfGas)?); + self.gas_left + .checked_reduce(weight_consumed) + .ok_or_else(|| Error::::OutOfGas)?; + Ok(RefTimeLeft(self.gas_left.ref_time())) + } + + /// Hand over the gas metering responsibility from this meter to the executor. + /// + /// Needs to be called when leaving a host function in order to calculate how much + /// gas needs to be charged from the **executor**. It updates the last seen executor + /// total value so that it is correct when `sync_from_executor` is called the next time. + /// + /// It is important that this does **not** actually sync with the executor. That has + /// to be done by the caller. + pub fn sync_to_executor(&mut self, before: RefTimeLeft) -> Result { + let ref_time_consumed = before.0.saturating_sub(self.gas_left().ref_time()); + self.engine_meter.charge_ref_time(ref_time_consumed) + } + + /// Returns the amount of gas that is required to run the same call. + /// + /// This can be different from `gas_spent` because due to `adjust_gas` the amount of + /// spent gas can temporarily drop and be refunded later. + pub fn gas_required(&self) -> Weight { + self.gas_limit.saturating_sub(self.gas_left_lowest()) + } + + /// Returns how much gas was spent + pub fn gas_consumed(&self) -> Weight { + self.gas_limit.saturating_sub(self.gas_left) + } + + /// Returns how much gas left from the initial budget. + pub fn gas_left(&self) -> Weight { + self.gas_left + } + + /// The amount of gas in terms of engine gas. + pub fn engine_fuel_left(&self) -> Result { + self.engine_meter.fuel.try_into().map_err(|_| >::OutOfGas.into()) + } + + /// Turn this GasMeter into a DispatchResult that contains the actually used gas. + pub fn into_dispatch_result( + self, + result: Result, + base_weight: Weight, + ) -> DispatchResultWithPostInfo + where + E: Into, + { + let post_info = PostDispatchInfo { + actual_weight: Some(self.gas_consumed().saturating_add(base_weight)), + pays_fee: Default::default(), + }; + + result + .map(|_| post_info) + .map_err(|e| DispatchErrorWithPostInfo { post_info, error: e.into().error }) + } + + fn gas_left_lowest(&self) -> Weight { + self.gas_left_lowest.min(self.gas_left) + } + + #[cfg(test)] + pub fn tokens(&self) -> &[ErasedToken] { + &self.tokens + } +} + +#[cfg(test)] +mod tests { + use super::{GasMeter, Token, Weight}; + use crate::tests::Test; + + /// A simple utility macro that helps to match against a + /// list of tokens. + macro_rules! match_tokens { + ($tokens_iter:ident,) => { + }; + ($tokens_iter:ident, $x:expr, $($rest:tt)*) => { + { + let next = ($tokens_iter).next().unwrap(); + let pattern = $x; + + // Note that we don't specify the type name directly in this macro, + // we only have some expression $x of some type. At the same time, we + // have an iterator of Box and to downcast we need to specify + // the type which we want downcast to. + // + // So what we do is we assign `_pattern_typed_next_ref` to a variable which has + // the required type. + // + // Then we make `_pattern_typed_next_ref = token.downcast_ref()`. This makes + // rustc infer the type `T` (in `downcast_ref`) to be the same as in $x. + + let mut _pattern_typed_next_ref = &pattern; + _pattern_typed_next_ref = match next.token.downcast_ref() { + Some(p) => { + assert_eq!(p, &pattern); + p + } + None => { + panic!("expected type {} got {}", stringify!($x), next.description); + } + }; + } + + match_tokens!($tokens_iter, $($rest)*); + }; + } + + /// A trivial token that charges the specified number of gas units. + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + struct SimpleToken(u64); + impl Token for SimpleToken { + fn weight(&self) -> Weight { + Weight::from_parts(self.0, 0) + } + } + + #[test] + fn it_works() { + let gas_meter = GasMeter::::new(Weight::from_parts(50000, 0)); + assert_eq!(gas_meter.gas_left(), Weight::from_parts(50000, 0)); + } + + #[test] + fn tracing() { + let mut gas_meter = GasMeter::::new(Weight::from_parts(50000, 0)); + assert!(!gas_meter.charge(SimpleToken(1)).is_err()); + + let mut tokens = gas_meter.tokens().iter(); + match_tokens!(tokens, SimpleToken(1),); + } + + // This test makes sure that nothing can be executed if there is no gas. + #[test] + fn refuse_to_execute_anything_if_zero() { + let mut gas_meter = GasMeter::::new(Weight::zero()); + assert!(gas_meter.charge(SimpleToken(1)).is_err()); + } + + // Make sure that the gas meter does not charge in case of overcharge + #[test] + fn overcharge_does_not_charge() { + let mut gas_meter = GasMeter::::new(Weight::from_parts(200, 0)); + + // The first charge is should lead to OOG. + assert!(gas_meter.charge(SimpleToken(300)).is_err()); + + // The gas meter should still contain the full 200. + assert!(gas_meter.charge(SimpleToken(200)).is_ok()); + } + + // Charging the exact amount that the user paid for should be + // possible. + #[test] + fn charge_exact_amount() { + let mut gas_meter = GasMeter::::new(Weight::from_parts(25, 0)); + assert!(!gas_meter.charge(SimpleToken(25)).is_err()); + } +} diff --git a/pallets/revive/src/lib.rs b/pallets/revive/src/lib.rs new file mode 100644 index 00000000..3b764ed9 --- /dev/null +++ b/pallets/revive/src/lib.rs @@ -0,0 +1,1220 @@ +// 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. + +#![doc = include_str!("../README.md")] +#![allow(rustdoc::private_intra_doc_links)] +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(feature = "runtime-benchmarks", recursion_limit = "1024")] + +extern crate alloc; +mod address; +mod benchmarking; +mod benchmarking_dummy; +mod exec; +mod gas; +mod primitives; +use frame_support::traits::IsType; +pub use primitives::*; +use sp_core::U256; + +use crate::exec::MomentOf; + +mod limits; +mod storage; +mod transient_storage; +pub mod wasm; + +pub mod chain_extension; +pub mod debug; +pub mod test_utils; +pub mod weights; + +#[cfg(test)] +mod tests; +use codec::{Codec, Decode, Encode}; +use environmental::*; +use frame_support::{ + dispatch::{ + DispatchErrorWithPostInfo, DispatchResultWithPostInfo, GetDispatchInfo, Pays, + PostDispatchInfo, RawOrigin, + }, + ensure, + traits::{ + fungible::{Inspect, Mutate, MutateHold}, + ConstU32, ConstU64, Contains, EnsureOrigin, Get, Time, + }, + weights::{Weight, WeightMeter}, + BoundedVec, RuntimeDebugNoBound, +}; +use frame_system::{ + ensure_signed, + pallet_prelude::{BlockNumberFor, OriginFor}, + EventRecord, Pallet as System, +}; +use scale_info::TypeInfo; +use sp_core::{H160, H256}; +use sp_runtime::{ + traits::{BadOrigin, Convert, Dispatchable, Saturating}, + DispatchError, +}; +pub use weights::WeightInfo; + +#[cfg(doc)] +pub use crate::wasm::SyscallDoc; +pub use crate::{ + address::{AddressMapper, DefaultAddressMapper}, + debug::Tracing, + pallet::*, +}; +use crate::{ + exec::{AccountIdOf, ExecError, Executable, Ext, Key, Origin, Stack as ExecStack}, + gas::GasMeter, + storage::{meter::Meter as StorageMeter, ContractInfo, DeletionQueueManager}, + wasm::{CodeInfo, RuntimeCosts, WasmBlob}, +}; + +type TrieId = BoundedVec>; +type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; +type CodeVec = BoundedVec>; +type EventRecordOf = + EventRecord<::RuntimeEvent, ::Hash>; +type DebugBuffer = BoundedVec>; + +/// Used as a sentinel value when reading and writing contract memory. +/// +/// It is usually used to signal `None` to a contract when only a primitive is allowed +/// and we don't want to go through encoding a full Rust type. Using `u32::Max` is a safe +/// sentinel because contracts are never allowed to use such a large amount of resources +/// that this value makes sense for a memory location or length. +const SENTINEL: u32 = u32::MAX; + +/// The target that is used for the log output emitted by this crate. +/// +/// Hence you can use this target to selectively increase the log level for this crate. +/// +/// Example: `RUST_LOG=runtime::revive=debug my_code --dev` +const LOG_TARGET: &str = "runtime::revive"; + +/// This version determines which syscalls are available to contracts. +/// +/// Needs to be bumped every time a versioned syscall is added. +const API_VERSION: u16 = 0; + +#[test] +fn api_version_up_to_date() { + assert!( + API_VERSION == crate::wasm::HIGHEST_API_VERSION, + "A new versioned API has been added. The `API_VERSION` needs to be bumped." + ); +} + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_core::U256; + use sp_runtime::Perbill; + + use super::*; + use crate::debug::Debugger; + + /// The in-code storage version. + pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config(with_default)] + pub trait Config: frame_system::Config { + /// The time implementation used to supply timestamps to contracts through `seal_now`. + type Time: Time; + + /// The fungible in which fees are paid and contract balances are held. + #[pallet::no_default] + type Currency: Inspect + + Mutate + + MutateHold; + + /// The overarching event type. + #[pallet::no_default_bounds] + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The overarching call type. + #[pallet::no_default_bounds] + type RuntimeCall: Dispatchable + + GetDispatchInfo + + codec::Decode + + IsType<::RuntimeCall>; + + /// Overarching hold reason. + #[pallet::no_default_bounds] + type RuntimeHoldReason: From; + + /// Filter that is applied to calls dispatched by contracts. + /// + /// Use this filter to control which dispatchables are callable by contracts. + /// This is applied in **addition** to [`frame_system::Config::BaseCallFilter`]. + /// It is recommended to treat this as a whitelist. + /// + /// # Stability + /// + /// The runtime **must** make sure that all dispatchables that are callable by + /// contracts remain stable. In addition [`Self::RuntimeCall`] itself must remain stable. + /// This means that no existing variants are allowed to switch their positions. + /// + /// # Note + /// + /// Note that dispatchables that are called via contracts do not spawn their + /// own wasm instance for each call (as opposed to when called via a transaction). + /// Therefore please make sure to be restrictive about which dispatchables are allowed + /// in order to not introduce a new DoS vector like memory allocation patterns that can + /// be exploited to drive the runtime into a panic. + /// + /// This filter does not apply to XCM transact calls. To impose restrictions on XCM transact + /// calls, you must configure them separately within the XCM pallet itself. + #[pallet::no_default_bounds] + type CallFilter: Contains<::RuntimeCall>; + + /// Used to answer contracts' queries regarding the current weight price. This is **not** + /// used to calculate the actual fee and is only for informational purposes. + #[pallet::no_default_bounds] + type WeightPrice: Convert>; + + /// Describes the weights of the dispatchables of this module and is also used to + /// construct a default cost schedule. + type WeightInfo: WeightInfo; + + /// Type that allows the runtime authors to add new host functions for a contract to call. + #[pallet::no_default_bounds] + type ChainExtension: chain_extension::ChainExtension + Default; + + /// The amount of balance a caller has to pay for each byte of storage. + /// + /// # Note + /// + /// It is safe to change this value on a live chain as all refunds are pro rata. + #[pallet::constant] + #[pallet::no_default_bounds] + type DepositPerByte: Get>; + + /// The amount of balance a caller has to pay for each storage item. + /// + /// # Note + /// + /// It is safe to change this value on a live chain as all refunds are pro rata. + #[pallet::constant] + #[pallet::no_default_bounds] + type DepositPerItem: Get>; + + /// The percentage of the storage deposit that should be held for using a code hash. + /// Instantiating a contract, or calling [`chain_extension::Ext::lock_delegate_dependency`] + /// protects the code from being removed. In order to prevent abuse these actions are + /// protected with a percentage of the code deposit. + #[pallet::constant] + type CodeHashLockupDepositPercent: Get; + + /// Only valid type is [`DefaultAddressMapper`]. + #[pallet::no_default_bounds] + type AddressMapper: AddressMapper>; + + /// Make contract callable functions marked as `#[unstable]` available. + /// + /// Contracts that use `#[unstable]` functions won't be able to be uploaded unless + /// this is set to `true`. This is only meant for testnets and dev nodes in order to + /// experiment with new features. + /// + /// # Warning + /// + /// Do **not** set to `true` on productions chains. + #[pallet::constant] + type UnsafeUnstableInterface: Get; + + /// Origin allowed to upload code. + /// + /// By default, it is safe to set this to `EnsureSigned`, allowing anyone to upload contract + /// code. + #[pallet::no_default_bounds] + type UploadOrigin: EnsureOrigin; + + /// Origin allowed to instantiate code. + /// + /// # Note + /// + /// This is not enforced when a contract instantiates another contract. The + /// [`Self::UploadOrigin`] should make sure that no code is deployed that does unwanted + /// instantiations. + /// + /// By default, it is safe to set this to `EnsureSigned`, allowing anyone to instantiate + /// contract code. + #[pallet::no_default_bounds] + type InstantiateOrigin: EnsureOrigin; + + /// For most production chains, it's recommended to use the `()` implementation of this + /// trait. This implementation offers additional logging when the log target + /// "runtime::revive" is set to trace. + #[pallet::no_default_bounds] + type Debug: Debugger; + + /// A type that exposes XCM APIs, allowing contracts to interact with other parachains, and + /// execute XCM programs. + #[pallet::no_default_bounds] + type Xcm: xcm_builder::Controller< + OriginFor, + ::RuntimeCall, + BlockNumberFor, + >; + + /// The amount of memory in bytes that parachain nodes a lot to the runtime. + /// + /// This is used in [`Pallet::integrity_test`] to make sure that the runtime has enough + /// memory to support this pallet if set to the correct value. + type RuntimeMemory: Get; + + /// The amount of memory in bytes that relay chain validators a lot to the PoV. + /// + /// This is used in [`Pallet::integrity_test`] to make sure that the runtime has enough + /// memory to support this pallet if set to the correct value. + /// + /// This value is usually higher than [`Self::RuntimeMemory`] to account for the fact + /// that validators have to hold all storage items in PvF memory. + type PVFMemory: Get; + + /// The [EIP-155](https://eips.ethereum.org/EIPS/eip-155) chain ID. + /// + /// This is a unique identifier assigned to each blockchain network, + /// preventing replay attacks. + #[pallet::constant] + type ChainId: Get; + } + + /// Container for different types that implement [`DefaultConfig`]` of this pallet. + pub mod config_preludes { + use frame_support::{ + derive_impl, + traits::{ConstBool, ConstU32}, + }; + use frame_system::EnsureSigned; + use sp_core::parameter_types; + + use super::*; + + type AccountId = sp_runtime::AccountId32; + type Balance = u64; + const UNITS: Balance = 10_000_000_000; + const CENTS: Balance = UNITS / 100; + + pub const fn deposit(items: u32, bytes: u32) -> Balance { + items as Balance * 1 * CENTS + (bytes as Balance) * 1 * CENTS + } + + parameter_types! { + pub const DepositPerItem: Balance = deposit(1, 0); + pub const DepositPerByte: Balance = deposit(0, 1); + pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); + } + + /// A type providing default configurations for this pallet in testing environment. + pub struct TestDefaultConfig; + + impl Time for TestDefaultConfig { + type Moment = u64; + + fn now() -> Self::Moment { + unimplemented!( + "No default `now` implementation in `TestDefaultConfig` provide a custom \ + `T::Time` type." + ) + } + } + + impl> Convert for TestDefaultConfig { + fn convert(w: Weight) -> T { + w.ref_time().into() + } + } + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig, no_aggregated_types)] + impl frame_system::DefaultConfig for TestDefaultConfig {} + + #[frame_support::register_default_impl(TestDefaultConfig)] + impl DefaultConfig for TestDefaultConfig { + type AddressMapper = DefaultAddressMapper; + type CallFilter = (); + type ChainExtension = (); + type ChainId = ConstU64<{ 0 }>; + type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; + type Debug = (); + type DepositPerByte = DepositPerByte; + type DepositPerItem = DepositPerItem; + type InstantiateOrigin = EnsureSigned; + type PVFMemory = ConstU32<{ 512 * 1024 * 1024 }>; + #[inject_runtime_type] + type RuntimeCall = (); + #[inject_runtime_type] + type RuntimeEvent = (); + #[inject_runtime_type] + type RuntimeHoldReason = (); + type RuntimeMemory = ConstU32<{ 128 * 1024 * 1024 }>; + type Time = Self; + type UnsafeUnstableInterface = ConstBool; + type UploadOrigin = EnsureSigned; + type WeightInfo = (); + type WeightPrice = Self; + type Xcm = (); + } + } + + #[pallet::event] + pub enum Event { + /// Contract deployed by address at the specified address. + Instantiated { deployer: H160, contract: H160 }, + + /// Contract has been removed. + /// + /// # Note + /// + /// The only way for a contract to be removed and emitting this event is by calling + /// `seal_terminate`. + Terminated { + /// The contract that was terminated. + contract: H160, + /// The account that received the contracts remaining balance + beneficiary: H160, + }, + + /// Code with the specified hash has been stored. + CodeStored { code_hash: H256, deposit_held: BalanceOf, uploader: H160 }, + + /// A custom event emitted by the contract. + ContractEmitted { + /// The contract that emitted the event. + contract: H160, + /// Data supplied by the contract. Metadata generated during contract compilation + /// is needed to decode it. + data: Vec, + /// A list of topics used to index the event. + /// Number of topics is capped by [`limits::NUM_EVENT_TOPICS`]. + topics: Vec, + }, + + /// A code with the specified hash was removed. + CodeRemoved { code_hash: H256, deposit_released: BalanceOf, remover: H160 }, + + /// A contract's code was updated. + ContractCodeUpdated { + /// The contract that has been updated. + contract: H160, + /// New code hash that was set for the contract. + new_code_hash: H256, + /// Previous code hash of the contract. + old_code_hash: H256, + }, + + /// A contract was called either by a plain account or another contract. + /// + /// # Note + /// + /// Please keep in mind that like all events this is only emitted for successful + /// calls. This is because on failure all storage changes including events are + /// rolled back. + Called { + /// The caller of the `contract`. + caller: Origin, + /// The contract that was called. + contract: H160, + }, + + /// A contract delegate called a code hash. + /// + /// # Note + /// + /// Please keep in mind that like all events this is only emitted for successful + /// calls. This is because on failure all storage changes including events are + /// rolled back. + DelegateCalled { + /// The contract that performed the delegate call and hence in whose context + /// the `code_hash` is executed. + contract: H160, + /// The code hash that was delegate called. + code_hash: H256, + }, + + /// Some funds have been transferred and held as storage deposit. + StorageDepositTransferredAndHeld { from: H160, to: H160, amount: BalanceOf }, + + /// Some storage deposit funds have been transferred and released. + StorageDepositTransferredAndReleased { from: H160, to: H160, amount: BalanceOf }, + } + + #[pallet::error] + pub enum Error { + /// Invalid schedule supplied, e.g. with zero weight of a basic operation. + InvalidSchedule, + /// Invalid combination of flags supplied to `seal_call` or `seal_delegate_call`. + InvalidCallFlags, + /// The executed contract exhausted its gas limit. + OutOfGas, + /// Performing the requested transfer failed. Probably because there isn't enough + /// free balance in the sender's account. + TransferFailed, + /// Performing a call was denied because the calling depth reached the limit + /// of what is specified in the schedule. + MaxCallDepthReached, + /// No contract was found at the specified address. + ContractNotFound, + /// No code could be found at the supplied code hash. + CodeNotFound, + /// No code info could be found at the supplied code hash. + CodeInfoNotFound, + /// A buffer outside of sandbox memory was passed to a contract API function. + OutOfBounds, + /// Input passed to a contract API function failed to decode as expected type. + DecodingFailed, + /// Contract trapped during execution. + ContractTrapped, + /// The size defined in `T::MaxValueSize` was exceeded. + ValueTooLarge, + /// Termination of a contract is not allowed while the contract is already + /// on the call stack. Can be triggered by `seal_terminate`. + TerminatedWhileReentrant, + /// `seal_call` forwarded this contracts input. It therefore is no longer available. + InputForwarded, + /// The amount of topics passed to `seal_deposit_events` exceeds the limit. + TooManyTopics, + /// The chain does not provide a chain extension. Calling the chain extension results + /// in this error. Note that this usually shouldn't happen as deploying such contracts + /// is rejected. + NoChainExtension, + /// Failed to decode the XCM program. + XCMDecodeFailed, + /// A contract with the same AccountId already exists. + DuplicateContract, + /// A contract self destructed in its constructor. + /// + /// This can be triggered by a call to `seal_terminate`. + TerminatedInConstructor, + /// A call tried to invoke a contract that is flagged as non-reentrant. + ReentranceDenied, + /// A contract called into the runtime which then called back into this pallet. + ReenteredPallet, + /// A contract attempted to invoke a state modifying API while being in read-only mode. + StateChangeDenied, + /// Origin doesn't have enough balance to pay the required storage deposits. + StorageDepositNotEnoughFunds, + /// More storage was created than allowed by the storage deposit limit. + StorageDepositLimitExhausted, + /// Code removal was denied because the code is still in use by at least one contract. + CodeInUse, + /// The contract ran to completion but decided to revert its storage changes. + /// Please note that this error is only returned from extrinsics. When called directly + /// or via RPC an `Ok` will be returned. In this case the caller needs to inspect the flags + /// to determine whether a reversion has taken place. + ContractReverted, + /// The contract failed to compile or is missing the correct entry points. + /// + /// A more detailed error can be found on the node console if debug messages are enabled + /// by supplying `-lruntime::revive=debug`. + CodeRejected, + /// The code blob supplied is larger than [`limits::code::BLOB_BYTES`]. + BlobTooLarge, + /// The static memory consumption of the blob will be larger than + /// [`limits::code::STATIC_MEMORY_BYTES`]. + StaticMemoryTooLarge, + /// The contract has reached its maximum number of delegate dependencies. + MaxDelegateDependenciesReached, + /// The dependency was not found in the contract's delegate dependencies. + DelegateDependencyNotFound, + /// The contract already depends on the given delegate dependency. + DelegateDependencyAlreadyExists, + /// Can not add a delegate dependency to the code hash of the contract itself. + CannotAddSelfAsDelegateDependency, + /// Can not add more data to transient storage. + OutOfTransientStorage, + /// The contract tried to call a syscall which does not exist (at its current api level). + InvalidSyscall, + /// Invalid storage flags were passed to one of the storage syscalls. + InvalidStorageFlags, + /// PolkaVM failed during code execution. Probably due to a malformed program. + ExecutionFailed, + /// Failed to convert a U256 to a Balance. + BalanceConversionFailed, + } + + /// A reason for the pallet contracts placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// The Pallet has reserved it for storing code on-chain. + CodeUploadDepositReserve, + /// The Pallet has reserved it for storage deposit. + StorageDepositReserve, + } + + /// A mapping from a contract's code hash to its code. + #[pallet::storage] + pub(crate) type PristineCode = StorageMap<_, Identity, H256, CodeVec>; + + /// A mapping from a contract's code hash to its code info. + #[pallet::storage] + pub(crate) type CodeInfoOf = StorageMap<_, Identity, H256, CodeInfo>; + + /// The code associated with a given account. + #[pallet::storage] + pub(crate) type ContractInfoOf = StorageMap<_, Identity, H160, ContractInfo>; + + /// Evicted contracts that await child trie deletion. + /// + /// Child trie deletion is a heavy operation depending on the amount of storage items + /// stored in said trie. Therefore this operation is performed lazily in `on_idle`. + #[pallet::storage] + pub(crate) type DeletionQueue = StorageMap<_, Twox64Concat, u32, TrieId>; + + /// A pair of monotonic counters used to track the latest contract marked for deletion + /// and the latest deleted contract in queue. + #[pallet::storage] + pub(crate) type DeletionQueueCounter = + StorageValue<_, DeletionQueueManager, ValueQuery>; + + #[pallet::extra_constants] + impl Pallet { + #[pallet::constant_name(ApiVersion)] + fn api_version() -> u16 { + API_VERSION + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_idle(_block: BlockNumberFor, limit: Weight) -> Weight { + let mut meter = WeightMeter::with_limit(limit); + ContractInfo::::process_deletion_queue_batch(&mut meter); + meter.consumed() + } + + fn integrity_test() { + use limits::code::STATIC_MEMORY_BYTES; + + // The memory available in the block building runtime + let max_runtime_mem: u32 = T::RuntimeMemory::get(); + // The root frame is not accounted in CALL_STACK_DEPTH + let max_call_depth = + limits::CALL_STACK_DEPTH.checked_add(1).expect("CallStack size is too big"); + // Transient storage uses a BTreeMap, which has overhead compared to the raw size of + // key-value data. To ensure safety, a margin of 2x the raw key-value size is used. + let max_transient_storage_size = limits::TRANSIENT_STORAGE_BYTES + .checked_mul(2) + .expect("MaxTransientStorageSize is too large"); + + // We only allow 50% of the runtime memory to be utilized by the contracts call + // stack, keeping the rest for other facilities, such as PoV, etc. + const TOTAL_MEMORY_DEVIDER: u32 = 2; + + // The inefficiencies of the freeing-bump allocator + // being used in the client for the runtime memory allocations, could lead to possible + // memory allocations grow up to `x4` times in some extreme cases. + const MEMORY_ALLOCATOR_INEFFICENCY_DEVIDER: u32 = 4; + + // Check that the configured `STATIC_MEMORY_BYTES` fits into runtime memory. + // + // `STATIC_MEMORY_BYTES` is the amount of memory that a contract can consume + // in memory and is enforced at upload time. + // + // Dynamic allocations are not available, yet. Hence are not taken into consideration + // here. + let static_memory_limit = max_runtime_mem + .saturating_div(TOTAL_MEMORY_DEVIDER) + .saturating_sub(max_transient_storage_size) + .saturating_div(max_call_depth) + .saturating_sub(STATIC_MEMORY_BYTES) + .saturating_div(MEMORY_ALLOCATOR_INEFFICENCY_DEVIDER); + + assert!( + STATIC_MEMORY_BYTES < static_memory_limit, + "Given `CallStack` height {:?}, `STATIC_MEMORY_LIMIT` should be set less than \ + {:?} (current value is {:?}), to avoid possible runtime oom issues.", + max_call_depth, + static_memory_limit, + STATIC_MEMORY_BYTES, + ); + + // Validators are configured to be able to use more memory than block builders. This is + // because in addition to `max_runtime_mem` they need to hold additional data in + // memory: PoV in multiple copies (1x encoded + 2x decoded) and all storage which + // includes emitted events. The assumption is that storage/events size + // can be a maximum of half of the validator runtime memory - max_runtime_mem. + let max_block_ref_time = T::BlockWeights::get() + .get(DispatchClass::Normal) + .max_total + .unwrap_or_else(|| T::BlockWeights::get().max_block) + .ref_time(); + let max_payload_size = limits::PAYLOAD_BYTES; + let max_key_size = + Key::try_from_var(alloc::vec![0u8; limits::STORAGE_KEY_BYTES as usize]) + .expect("Key of maximal size shall be created") + .hash() + .len() as u32; + + // We can use storage to store items using the available block ref_time with the + // `set_storage` host function. + let max_storage_size: u32 = ((max_block_ref_time / + (>::weight(&RuntimeCosts::SetStorage { + new_bytes: max_payload_size, + old_bytes: 0, + }) + .ref_time())) + .saturating_mul(max_payload_size.saturating_add(max_key_size) as u64)) + .try_into() + .expect("Storage size too big"); + + let max_pvf_mem: u32 = T::PVFMemory::get(); + let storage_size_limit = max_pvf_mem.saturating_sub(max_runtime_mem) / 2; + + assert!( + max_storage_size < storage_size_limit, + "Maximal storage size {} exceeds the storage limit {}", + max_storage_size, + storage_size_limit + ); + + // We can use storage to store events using the available block ref_time with the + // `deposit_event` host function. The overhead of stored events, which is around 100B, + // is not taken into account to simplify calculations, as it does not change much. + let max_events_size: u32 = ((max_block_ref_time / + (>::weight(&RuntimeCosts::DepositEvent { + num_topic: 0, + len: max_payload_size, + }) + .ref_time())) + .saturating_mul(max_payload_size as u64)) + .try_into() + .expect("Events size too big"); + + assert!( + max_events_size < storage_size_limit, + "Maximal events size {} exceeds the events limit {}", + max_events_size, + storage_size_limit + ); + } + } + + #[pallet::call] + impl Pallet + where + BalanceOf: Into + TryFrom, + MomentOf: Into, + { + /// Makes a call to an account, optionally transferring some balance. + /// + /// # Parameters + /// + /// * `dest`: Address of the contract to call. + /// * `value`: The balance to transfer from the `origin` to `dest`. + /// * `gas_limit`: The gas limit enforced when executing the constructor. + /// * `storage_deposit_limit`: The maximum amount of balance that can be charged from the + /// caller to pay for the storage consumed. + /// * `data`: The input data to pass to the contract. + /// + /// * If the account is a smart-contract account, the associated code will be + /// executed and any value will be transferred. + /// * If the account is a regular account, any value will be transferred. + /// * If no account exists and the call value is not less than `existential_deposit`, + /// a regular account will be created and any value will be transferred. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::call().saturating_add(*gas_limit))] + pub fn call( + origin: OriginFor, + dest: H160, + #[pallet::compact] value: BalanceOf, + gas_limit: Weight, + #[pallet::compact] storage_deposit_limit: BalanceOf, + data: Vec, + ) -> DispatchResultWithPostInfo { + let mut output = Self::bare_call( + origin, + dest, + value, + gas_limit, + storage_deposit_limit, + data, + DebugInfo::Skip, + CollectEvents::Skip, + ); + if let Ok(return_value) = &output.result { + if return_value.did_revert() { + output.result = Err(>::ContractReverted.into()); + } + } + dispatch_result(output.result, output.gas_consumed, T::WeightInfo::call()) + } + + /// Instantiates a contract from a previously deployed wasm binary. + /// + /// This function is identical to [`Self::instantiate_with_code`] but without the + /// code deployment step. Instead, the `code_hash` of an on-chain deployed wasm binary + /// must be supplied. + #[pallet::call_index(1)] + #[pallet::weight( + T::WeightInfo::instantiate(data.len() as u32).saturating_add(*gas_limit) + )] + pub fn instantiate( + origin: OriginFor, + #[pallet::compact] value: BalanceOf, + gas_limit: Weight, + #[pallet::compact] storage_deposit_limit: BalanceOf, + code_hash: sp_core::H256, + data: Vec, + salt: Option<[u8; 32]>, + ) -> DispatchResultWithPostInfo { + let data_len = data.len() as u32; + let mut output = Self::bare_instantiate( + origin, + value, + gas_limit, + storage_deposit_limit, + Code::Existing(code_hash), + data, + salt, + DebugInfo::Skip, + CollectEvents::Skip, + ); + if let Ok(retval) = &output.result { + if retval.result.did_revert() { + output.result = Err(>::ContractReverted.into()); + } + } + dispatch_result( + output.result.map(|result| result.result), + output.gas_consumed, + T::WeightInfo::instantiate(data_len), + ) + } + + /// Instantiates a new contract from the supplied `code` optionally transferring + /// some balance. + /// + /// This dispatchable has the same effect as calling [`Self::upload_code`] + + /// [`Self::instantiate`]. Bundling them together provides efficiency gains. Please + /// also check the documentation of [`Self::upload_code`]. + /// + /// # Parameters + /// + /// * `value`: The balance to transfer from the `origin` to the newly created contract. + /// * `gas_limit`: The gas limit enforced when executing the constructor. + /// * `storage_deposit_limit`: The maximum amount of balance that can be charged/reserved + /// from the caller to pay for the storage consumed. + /// * `code`: The contract code to deploy in raw bytes. + /// * `data`: The input data to pass to the contract constructor. + /// * `salt`: Used for the address derivation. If `Some` is supplied then `CREATE2` + /// semantics are used. If `None` then `CRATE1` is used. + /// + /// + /// Instantiation is executed as follows: + /// + /// - The supplied `code` is deployed, and a `code_hash` is created for that code. + /// - If the `code_hash` already exists on the chain the underlying `code` will be shared. + /// - The destination address is computed based on the sender, code_hash and the salt. + /// - The smart-contract account is created at the computed address. + /// - The `value` is transferred to the new account. + /// - The `deploy` function is executed in the context of the newly-created account. + #[pallet::call_index(2)] + #[pallet::weight( + T::WeightInfo::instantiate_with_code(code.len() as u32, data.len() as u32) + .saturating_add(*gas_limit) + )] + pub fn instantiate_with_code( + origin: OriginFor, + #[pallet::compact] value: BalanceOf, + gas_limit: Weight, + #[pallet::compact] storage_deposit_limit: BalanceOf, + code: Vec, + data: Vec, + salt: Option<[u8; 32]>, + ) -> DispatchResultWithPostInfo { + let code_len = code.len() as u32; + let data_len = data.len() as u32; + let mut output = Self::bare_instantiate( + origin, + value, + gas_limit, + storage_deposit_limit, + Code::Upload(code), + data, + salt, + DebugInfo::Skip, + CollectEvents::Skip, + ); + if let Ok(retval) = &output.result { + if retval.result.did_revert() { + output.result = Err(>::ContractReverted.into()); + } + } + dispatch_result( + output.result.map(|result| result.result), + output.gas_consumed, + T::WeightInfo::instantiate_with_code(code_len, data_len), + ) + } + + /// Upload new `code` without instantiating a contract from it. + /// + /// If the code does not already exist a deposit is reserved from the caller + /// and unreserved only when [`Self::remove_code`] is called. The size of the reserve + /// depends on the size of the supplied `code`. + /// + /// # Note + /// + /// Anyone can instantiate a contract from any uploaded code and thus prevent its removal. + /// To avoid this situation a constructor could employ access control so that it can + /// only be instantiated by permissioned entities. The same is true when uploading + /// through [`Self::instantiate_with_code`]. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::upload_code_determinism_enforced(code.len() as u32))] + pub fn upload_code( + origin: OriginFor, + code: Vec, + #[pallet::compact] storage_deposit_limit: BalanceOf, + ) -> DispatchResult { + Self::bare_upload_code(origin, code, storage_deposit_limit).map(|_| ()) + } + + /// Remove the code stored under `code_hash` and refund the deposit to its owner. + /// + /// A code can only be removed by its original uploader (its owner) and only if it is + /// not used by any contract. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::remove_code())] + pub fn remove_code( + origin: OriginFor, + code_hash: sp_core::H256, + ) -> DispatchResultWithPostInfo { + let origin = ensure_signed(origin)?; + >::remove(&origin, code_hash)?; + // we waive the fee because removing unused code is beneficial + Ok(Pays::No.into()) + } + + /// Privileged function that changes the code of an existing contract. + /// + /// This takes care of updating refcounts and all other necessary operations. Returns + /// an error if either the `code_hash` or `dest` do not exist. + /// + /// # Note + /// + /// This does **not** change the address of the contract in question. This means + /// that the contract address is no longer derived from its code hash after calling + /// this dispatchable. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::set_code())] + pub fn set_code( + origin: OriginFor, + dest: H160, + code_hash: sp_core::H256, + ) -> DispatchResult { + ensure_root(origin)?; + >::try_mutate(&dest, |contract| { + let contract = if let Some(contract) = contract { + contract + } else { + return Err(>::ContractNotFound.into()); + }; + >>::increment_refcount(code_hash)?; + >>::decrement_refcount(contract.code_hash); + Self::deposit_event(Event::ContractCodeUpdated { + contract: dest, + new_code_hash: code_hash, + old_code_hash: contract.code_hash, + }); + contract.code_hash = code_hash; + Ok(()) + }) + } + } +} + +/// Create a dispatch result reflecting the amount of consumed gas. +fn dispatch_result( + result: Result, + gas_consumed: Weight, + base_weight: Weight, +) -> DispatchResultWithPostInfo { + let post_info = PostDispatchInfo { + actual_weight: Some(gas_consumed.saturating_add(base_weight)), + pays_fee: Default::default(), + }; + + result + .map(|_| post_info) + .map_err(|e| DispatchErrorWithPostInfo { post_info, error: e }) +} + +impl Pallet +where + BalanceOf: Into + TryFrom, + MomentOf: Into, +{ + /// A generalized version of [`Self::call`]. + /// + /// Identical to [`Self::call`] but tailored towards being called by other code within the + /// runtime as opposed to from an extrinsic. It returns more information and allows the + /// enablement of features that are not suitable for an extrinsic (debugging, event + /// collection). + pub fn bare_call( + origin: OriginFor, + dest: H160, + value: BalanceOf, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + data: Vec, + debug: DebugInfo, + collect_events: CollectEvents, + ) -> ContractExecResult, EventRecordOf> { + let mut gas_meter = GasMeter::new(gas_limit); + let mut storage_deposit = Default::default(); + let mut debug_message = if matches!(debug, DebugInfo::UnsafeDebug) { + Some(DebugBuffer::default()) + } else { + None + }; + let try_call = || { + let origin = Origin::from_runtime_origin(origin)?; + let mut storage_meter = StorageMeter::new(&origin, storage_deposit_limit, value)?; + let result = ExecStack::>::run_call( + origin.clone(), + dest, + &mut gas_meter, + &mut storage_meter, + value, + data, + debug_message.as_mut(), + )?; + storage_deposit = storage_meter.try_into_deposit(&origin)?; + Ok(result) + }; + let result = Self::run_guarded(try_call); + let events = if matches!(collect_events, CollectEvents::UnsafeCollect) { + Some(System::::read_events_no_consensus().map(|e| *e).collect()) + } else { + None + }; + ContractExecResult { + result: result.map_err(|r| r.error), + gas_consumed: gas_meter.gas_consumed(), + gas_required: gas_meter.gas_required(), + storage_deposit, + debug_message: debug_message.unwrap_or_default().to_vec(), + events, + } + } + + /// A generalized version of [`Self::instantiate`] or [`Self::instantiate_with_code`]. + /// + /// Identical to [`Self::instantiate`] or [`Self::instantiate_with_code`] but tailored towards + /// being called by other code within the runtime as opposed to from an extrinsic. It returns + /// more information and allows the enablement of features that are not suitable for an + /// extrinsic (debugging, event collection). + pub fn bare_instantiate( + origin: OriginFor, + value: BalanceOf, + gas_limit: Weight, + mut storage_deposit_limit: BalanceOf, + code: Code, + data: Vec, + salt: Option<[u8; 32]>, + debug: DebugInfo, + collect_events: CollectEvents, + ) -> ContractInstantiateResult, EventRecordOf> { + let mut gas_meter = GasMeter::new(gas_limit); + let mut storage_deposit = Default::default(); + let mut debug_message = + if debug == DebugInfo::UnsafeDebug { Some(DebugBuffer::default()) } else { None }; + let try_instantiate = || { + let instantiate_account = T::InstantiateOrigin::ensure_origin(origin.clone())?; + let (executable, upload_deposit) = match code { + Code::Upload(code) => { + let upload_account = T::UploadOrigin::ensure_origin(origin)?; + let (executable, upload_deposit) = + Self::try_upload_code(upload_account, code, storage_deposit_limit)?; + storage_deposit_limit.saturating_reduce(upload_deposit); + (executable, upload_deposit) + }, + Code::Existing(code_hash) => + (WasmBlob::from_storage(code_hash, &mut gas_meter)?, Default::default()), + }; + let instantiate_origin = Origin::from_account_id(instantiate_account.clone()); + let mut storage_meter = + StorageMeter::new(&instantiate_origin, storage_deposit_limit, value)?; + let result = ExecStack::>::run_instantiate( + instantiate_account, + executable, + &mut gas_meter, + &mut storage_meter, + value, + data, + salt.as_ref(), + debug_message.as_mut(), + ); + storage_deposit = storage_meter + .try_into_deposit(&instantiate_origin)? + .saturating_add(&StorageDeposit::Charge(upload_deposit)); + result + }; + let output = Self::run_guarded(try_instantiate); + let events = if matches!(collect_events, CollectEvents::UnsafeCollect) { + Some(System::::read_events_no_consensus().map(|e| *e).collect()) + } else { + None + }; + ContractInstantiateResult { + result: output + .map(|(addr, result)| InstantiateReturnValue { result, addr }) + .map_err(|e| e.error), + gas_consumed: gas_meter.gas_consumed(), + gas_required: gas_meter.gas_required(), + storage_deposit, + debug_message: debug_message.unwrap_or_default().to_vec(), + events, + } + } + + /// A generalized version of [`Self::upload_code`]. + /// + /// It is identical to [`Self::upload_code`] and only differs in the information it returns. + pub fn bare_upload_code( + origin: OriginFor, + code: Vec, + storage_deposit_limit: BalanceOf, + ) -> CodeUploadResult> { + let origin = T::UploadOrigin::ensure_origin(origin)?; + let (module, deposit) = Self::try_upload_code(origin, code, storage_deposit_limit)?; + Ok(CodeUploadReturnValue { code_hash: *module.code_hash(), deposit }) + } + + /// Query storage of a specified contract under a specified key. + pub fn get_storage(address: H160, key: [u8; 32]) -> GetStorageResult { + let contract_info = + ContractInfoOf::::get(&address).ok_or(ContractAccessError::DoesntExist)?; + + let maybe_value = contract_info.read(&Key::from_fixed(key)); + Ok(maybe_value) + } + + /// Uploads new code and returns the Wasm blob and deposit amount collected. + fn try_upload_code( + origin: T::AccountId, + code: Vec, + storage_deposit_limit: BalanceOf, + ) -> Result<(WasmBlob, BalanceOf), DispatchError> { + let mut module = WasmBlob::from_code(code, origin)?; + let deposit = module.store_code()?; + ensure!(storage_deposit_limit >= deposit, >::StorageDepositLimitExhausted); + Ok((module, deposit)) + } + + /// Run the supplied function `f` if no other instance of this pallet is on the stack. + fn run_guarded Result>(f: F) -> Result { + executing_contract::using_once(&mut false, || { + executing_contract::with(|f| { + // Fail if already entered contract execution + if *f { + return Err(()); + } + // We are entering contract execution + *f = true; + Ok(()) + }) + .expect( + "Returns `Ok` if called within `using_once`. It is syntactically obvious that \ + this is the case; qed", + ) + .map_err(|_| >::ReenteredPallet.into()) + .map(|_| f()) + .and_then(|r| r) + }) + } +} + +impl Pallet { + /// Return the existential deposit of [`Config::Currency`]. + fn min_balance() -> BalanceOf { + >>::minimum_balance() + } + + /// Deposit a pallet contracts event. + fn deposit_event(event: Event) { + >::deposit_event(::RuntimeEvent::from(event)) + } +} + +// Set up a global reference to the boolean flag used for the re-entrancy guard. +environmental!(executing_contract: bool); + +sp_api::decl_runtime_apis! { + /// The API used to dry-run contract interactions. + #[api_version(1)] + pub trait ReviveApi where + AccountId: Codec, + Balance: Codec, + BlockNumber: Codec, + EventRecord: Codec, + { + /// Perform a call from a specified account to a given contract. + /// + /// See [`crate::Pallet::bare_call`]. + fn call( + origin: AccountId, + dest: H160, + value: Balance, + gas_limit: Option, + storage_deposit_limit: Option, + input_data: Vec, + ) -> ContractExecResult; + + /// Instantiate a new contract. + /// + /// See `[crate::Pallet::bare_instantiate]`. + fn instantiate( + origin: AccountId, + value: Balance, + gas_limit: Option, + storage_deposit_limit: Option, + code: Code, + data: Vec, + salt: Option<[u8; 32]>, + ) -> ContractInstantiateResult; + + /// Upload new code without instantiating a contract from it. + /// + /// See [`crate::Pallet::bare_upload_code`]. + fn upload_code( + origin: AccountId, + code: Vec, + storage_deposit_limit: Option, + ) -> CodeUploadResult; + + /// Query a given storage key in a given contract. + /// + /// Returns `Ok(Some(Vec))` if the storage value exists under the given key in the + /// specified account and `Ok(None)` if it doesn't. If the account specified by the address + /// doesn't exist, or doesn't have a contract then `Err` is returned. + fn get_storage( + address: H160, + key: [u8; 32], + ) -> GetStorageResult; + } +} diff --git a/pallets/revive/src/limits.rs b/pallets/revive/src/limits.rs new file mode 100644 index 00000000..88c379e7 --- /dev/null +++ b/pallets/revive/src/limits.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. + +//! Limits that are observeable by contract code. +//! +//! It is important to never change this limits without supporting the old limits +//! for already deployed contracts. This is what the [`crate::Contract::behaviour_version`] +//! is meant for. This is true for either increasing or decreasing the limit. +//! +//! Limits in this file are different from the limits configured on the [`Config`] trait which are +//! generally only affect actions that cannot be performed by a contract: For example things related +//! to deposits and weights are allowed to be changed as they are paid by root callers which +//! are not contracts. +//! +//! Exceptions to this rule apply: Limits in the [`code`] module can be increased +//! without emulating the old values for existing contracts. Reason is that those limits are only +//! applied **once** at code upload time. Since this action cannot be performed by contracts we +//! can change those limits without breaking existing contracts. Please keep in mind that we should +//! only ever **increase** those values but never decrease. + +/// The maximum depth of the call stack. +/// +/// A 0 means that no callings of other contracts are possible. In other words only the origin +/// called "root contract" is allowed to execute then. +pub const CALL_STACK_DEPTH: u32 = 10; + +/// The maximum number of topics a call to [`crate::SyscallDoc::deposit_event`] can emit. +/// +/// We set it to the same limit that ethereum has. It is unlikely to change. +// TOOD: debug a proper value. The original at 4 was too small +pub const NUM_EVENT_TOPICS: u32 = 1000; + +/// The maximum number of code hashes a contract can lock. +pub const DELEGATE_DEPENDENCIES: u32 = 32; + +/// Maximum size of events (including topics) and storage values. +pub const PAYLOAD_BYTES: u32 = 512; + +/// The maximum size of the transient storage in bytes. +/// +/// This includes keys, values, and previous entries used for storage rollback. +pub const TRANSIENT_STORAGE_BYTES: u32 = 4 * 1024; + +/// The maximum allowable length in bytes for (transient) storage keys. +pub const STORAGE_KEY_BYTES: u32 = 128; + +/// The maximum size of the debug buffer contracts can write messages to. +/// +/// The buffer will always be disabled for on-chain execution. +pub const DEBUG_BUFFER_BYTES: u32 = 2 * 1024 * 1024; + +/// The page size in which PolkaVM should allocate memory chunks. +pub const PAGE_SIZE: u32 = 4 * 1024; + +/// Limits that are only enforced on code upload. +/// +/// # Note +/// +/// This limit can be increased later without breaking existing contracts +/// as it is only enforced at code upload time. Code already uploaded +/// will not be affected by those limits. +pub mod code { + use alloc::vec::Vec; + + use frame_support::ensure; + use sp_runtime::DispatchError; + + use super::PAGE_SIZE; + use crate::{CodeVec, Config, Error, LOG_TARGET}; + + /// The maximum length of a code blob in bytes. + /// + /// This mostly exist to prevent parsing too big blobs and to + /// have a maximum encoded length. The actual memory calculation + /// is purely based off [`STATIC_MEMORY_BYTES`]. + pub const BLOB_BYTES: u32 = 256 * 1024; + + /// Maximum size the program is allowed to take in memory. + /// + /// This includes data and code. Increasing this limit will allow + /// for more code or more data. However, since code will decompress + /// into a bigger representation on compilation it will only increase + /// the allowed code size by [`BYTE_PER_INSTRUCTION`]. + pub const STATIC_MEMORY_BYTES: u32 = 1024 * 1024 * 1024; + + /// How much memory each instruction will take in-memory after compilation. + /// + /// This is `size_of() + 16`. But we don't use `usize` here so it isn't + /// different on the native runtime (used for testing). + const BYTES_PER_INSTRUCTION: u32 = 20; + + /// The code is stored multiple times as part of the compiled program. + const EXTRA_OVERHEAD_PER_CODE_BYTE: u32 = 4; + + /// Make sure that the various program parts are within the defined limits. + pub fn enforce(blob: Vec) -> Result { + fn round_page(n: u32) -> u64 { + // performing the rounding in u64 in order to prevent overflow + u64::from(n).next_multiple_of(PAGE_SIZE.into()) + } + + let blob: CodeVec = blob.try_into().map_err(|_| >::BlobTooLarge)?; + + let program = polkavm::ProgramBlob::parse(blob.as_slice().into()).map_err(|err| { + log::debug!(target: LOG_TARGET, "failed to parse polkavm blob: {err:?}"); + Error::::CodeRejected + })?; + + // this is O(n) but it allows us to be more precise + let num_instructions = program.instructions().count() as u64; + + // The memory consumptions is the byte size of the whole blob, + // minus the RO data payload in the blob, + // minus the RW data payload in the blob, + // plus the RO data in memory (which is always equal or bigger than the RO payload), + // plus RW data in memory, plus stack size in memory. + // plus the overhead of instructions in memory which is derived from the code + // size itself and the number of instruction + let memory_size = (blob.len() as u64) + .saturating_add(round_page(program.ro_data_size())) + .saturating_sub(program.ro_data().len() as u64) + .saturating_add(round_page(program.rw_data_size())) + .saturating_sub(program.rw_data().len() as u64) + .saturating_add(round_page(program.stack_size())) + .saturating_add((num_instructions).saturating_mul(BYTES_PER_INSTRUCTION.into())) + .saturating_add( + (program.code().len() as u64).saturating_mul(EXTRA_OVERHEAD_PER_CODE_BYTE.into()), + ); + + ensure!(memory_size <= STATIC_MEMORY_BYTES as u64, >::StaticMemoryTooLarge); + + Ok(blob) + } +} diff --git a/pallets/revive/src/primitives.rs b/pallets/revive/src/primitives.rs new file mode 100644 index 00000000..ac2a7ca4 --- /dev/null +++ b/pallets/revive/src/primitives.rs @@ -0,0 +1,285 @@ +// 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. + +//! A crate that hosts a common definitions that are relevant for the pallet-revive. + +use alloc::vec::Vec; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::weights::Weight; +use pallet_revive_uapi::ReturnFlags; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Saturating, Zero}, + DispatchError, RuntimeDebug, +}; + +use crate::H160; + +/// Result type of a `bare_call` or `bare_instantiate` call as well as `ContractsApi::call` and +/// `ContractsApi::instantiate`. +/// +/// It contains the execution result together with some auxiliary information. +/// +/// #Note +/// +/// It has been extended to include `events` at the end of the struct while not bumping the +/// `ContractsApi` version. Therefore when SCALE decoding a `ContractResult` its trailing data +/// should be ignored to avoid any potential compatibility issues. +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct ContractResult { + /// How much weight was consumed during execution. + pub gas_consumed: Weight, + /// How much weight is required as gas limit in order to execute this call. + /// + /// This value should be used to determine the weight limit for on-chain execution. + /// + /// # Note + /// + /// This can only different from [`Self::gas_consumed`] when weight pre charging + /// is used. Currently, only `seal_call_runtime` makes use of pre charging. + /// Additionally, any `seal_call` or `seal_instantiate` makes use of pre-charging + /// when a non-zero `gas_limit` argument is supplied. + pub gas_required: Weight, + /// How much balance was paid by the origin into the contract's deposit account in order to + /// pay for storage. + /// + /// The storage deposit is never actually charged from the origin in case of [`Self::result`] + /// is `Err`. This is because on error all storage changes are rolled back including the + /// payment of the deposit. + pub storage_deposit: StorageDeposit, + /// An optional debug message. This message is only filled when explicitly requested + /// by the code that calls into the contract. Otherwise it is empty. + /// + /// The contained bytes are valid UTF-8. This is not declared as `String` because + /// this type is not allowed within the runtime. + /// + /// Clients should not make any assumptions about the format of the buffer. + /// They should just display it as-is. It is **not** only a collection of log lines + /// provided by a contract but a formatted buffer with different sections. + /// + /// # Note + /// + /// The debug message is never generated during on-chain execution. It is reserved for + /// RPC calls. + pub debug_message: Vec, + /// The execution result of the wasm code. + pub result: R, + /// The events that were emitted during execution. It is an option as event collection is + /// optional. + pub events: Option>, +} + +/// Result type of a `bare_call` call as well as `ContractsApi::call`. +pub type ContractExecResult = + ContractResult, Balance, EventRecord>; + +/// Result type of a `bare_instantiate` call as well as `ContractsApi::instantiate`. +pub type ContractInstantiateResult = + ContractResult, Balance, EventRecord>; + +/// Result type of a `bare_code_upload` call. +pub type CodeUploadResult = Result, DispatchError>; + +/// Result type of a `get_storage` call. +pub type GetStorageResult = Result>, ContractAccessError>; + +/// The possible errors that can happen querying the storage of a contract. +#[derive(Copy, Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo)] +pub enum ContractAccessError { + /// The given address doesn't point to a contract. + DoesntExist, + /// Storage key cannot be decoded from the provided input data. + KeyDecodingFailed, +} + +/// Output of a contract call or instantiation which ran to completion. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Default)] +pub struct ExecReturnValue { + /// Flags passed along by `seal_return`. Empty when `seal_return` was never called. + pub flags: ReturnFlags, + /// Buffer passed along by `seal_return`. Empty when `seal_return` was never called. + pub data: Vec, +} + +impl ExecReturnValue { + /// The contract did revert all storage changes. + pub fn did_revert(&self) -> bool { + self.flags.contains(ReturnFlags::REVERT) + } +} + +/// The result of a successful contract instantiation. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct InstantiateReturnValue { + /// The output of the called constructor. + pub result: ExecReturnValue, + /// The address of the new contract. + pub addr: H160, +} + +/// The result of successfully uploading a contract. +#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo)] +pub struct CodeUploadReturnValue { + /// The key under which the new code is stored. + pub code_hash: sp_core::H256, + /// The deposit that was reserved at the caller. Is zero when the code already existed. + pub deposit: Balance, +} + +/// Reference to an existing code hash or a new wasm module. +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum Code { + /// A wasm module as raw bytes. + Upload(Vec), + /// The code hash of an on-chain wasm blob. + Existing(sp_core::H256), +} + +/// The amount of balance that was either charged or refunded in order to pay for storage. +#[derive( + Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo, +)] +pub enum StorageDeposit { + /// The transaction reduced storage consumption. + /// + /// This means that the specified amount of balance was transferred from the involved + /// deposit accounts to the origin. + Refund(Balance), + /// The transaction increased storage consumption. + /// + /// This means that the specified amount of balance was transferred from the origin + /// to the involved deposit accounts. + Charge(Balance), +} + +impl Default for StorageDeposit { + fn default() -> Self { + Self::Charge(Zero::zero()) + } +} + +impl StorageDeposit { + /// Returns how much balance is charged or `0` in case of a refund. + pub fn charge_or_zero(&self) -> Balance { + match self { + Self::Charge(amount) => *amount, + Self::Refund(_) => Zero::zero(), + } + } + + pub fn is_zero(&self) -> bool { + match self { + Self::Charge(amount) => amount.is_zero(), + Self::Refund(amount) => amount.is_zero(), + } + } +} + +impl StorageDeposit +where + Balance: Saturating + Ord + Copy, +{ + /// This is essentially a saturating signed add. + pub fn saturating_add(&self, rhs: &Self) -> Self { + use StorageDeposit::*; + match (self, rhs) { + (Charge(lhs), Charge(rhs)) => Charge(lhs.saturating_add(*rhs)), + (Refund(lhs), Refund(rhs)) => Refund(lhs.saturating_add(*rhs)), + (Charge(lhs), Refund(rhs)) => + if lhs >= rhs { + Charge(lhs.saturating_sub(*rhs)) + } else { + Refund(rhs.saturating_sub(*lhs)) + }, + (Refund(lhs), Charge(rhs)) => + if lhs > rhs { + Refund(lhs.saturating_sub(*rhs)) + } else { + Charge(rhs.saturating_sub(*lhs)) + }, + } + } + + /// This is essentially a saturating signed sub. + pub fn saturating_sub(&self, rhs: &Self) -> Self { + use StorageDeposit::*; + match (self, rhs) { + (Charge(lhs), Refund(rhs)) => Charge(lhs.saturating_add(*rhs)), + (Refund(lhs), Charge(rhs)) => Refund(lhs.saturating_add(*rhs)), + (Charge(lhs), Charge(rhs)) => + if lhs >= rhs { + Charge(lhs.saturating_sub(*rhs)) + } else { + Refund(rhs.saturating_sub(*lhs)) + }, + (Refund(lhs), Refund(rhs)) => + if lhs > rhs { + Refund(lhs.saturating_sub(*rhs)) + } else { + Charge(rhs.saturating_sub(*lhs)) + }, + } + } + + /// If the amount of deposit (this type) is constrained by a `limit` this calculates how + /// much balance (if any) is still available from this limit. + /// + /// # Note + /// + /// In case of a refund the return value can be larger than `limit`. + pub fn available(&self, limit: &Balance) -> Balance { + use StorageDeposit::*; + match self { + Charge(amount) => limit.saturating_sub(*amount), + Refund(amount) => limit.saturating_add(*amount), + } + } +} + +/// Determines whether events should be collected during execution. +#[derive( + Copy, Clone, PartialEq, Eq, RuntimeDebug, Decode, Encode, MaxEncodedLen, scale_info::TypeInfo, +)] +pub enum CollectEvents { + /// Collect events. + /// + /// # Note + /// + /// Events should only be collected when called off-chain, as this would otherwise + /// collect all the Events emitted in the block so far and put them into the PoV. + /// + /// **Never** use this mode for on-chain execution. + UnsafeCollect, + /// Skip event collection. + Skip, +} + +/// Determines whether debug messages will be collected. +#[derive( + Copy, Clone, PartialEq, Eq, RuntimeDebug, Decode, Encode, MaxEncodedLen, scale_info::TypeInfo, +)] +pub enum DebugInfo { + /// Collect debug messages. + /// # Note + /// + /// This should only ever be set to `UnsafeDebug` when executing as an RPC because + /// it adds allocations and could be abused to drive the runtime into an OOM panic. + UnsafeDebug, + /// Skip collection of debug messages. + Skip, +} diff --git a/pallets/revive/src/storage.rs b/pallets/revive/src/storage.rs new file mode 100644 index 00000000..edf5aa40 --- /dev/null +++ b/pallets/revive/src/storage.rs @@ -0,0 +1,486 @@ +// 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. + +//! This module contains routines for accessing and altering a contract related state. + +pub mod meter; + +use alloc::vec::Vec; +use core::marker::PhantomData; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + storage::child::{self, ChildInfo}, + weights::{Weight, WeightMeter}, + CloneNoBound, DefaultNoBound, +}; +use scale_info::TypeInfo; +use sp_core::{ConstU32, Get, H160}; +use sp_io::KillStorageResult; +use sp_runtime::{ + traits::{Hash, Saturating, Zero}, + BoundedBTreeMap, DispatchError, DispatchResult, RuntimeDebug, +}; + +use crate::{ + address::AddressMapper, + exec::{AccountIdOf, Key}, + limits, + storage::meter::Diff, + weights::WeightInfo, + BalanceOf, CodeInfo, Config, ContractInfoOf, DeletionQueue, DeletionQueueCounter, Error, + TrieId, SENTINEL, +}; + +type DelegateDependencyMap = + BoundedBTreeMap, ConstU32<{ limits::DELEGATE_DEPENDENCIES }>>; + +/// Information for managing an account and its sub trie abstraction. +/// This is the required info to cache for an account. +#[derive(Encode, Decode, CloneNoBound, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct ContractInfo { + /// Unique ID for the subtree encoded as a bytes vector. + pub trie_id: TrieId, + /// The code associated with a given account. + pub code_hash: sp_core::H256, + /// How many bytes of storage are accumulated in this contract's child trie. + storage_bytes: u32, + /// How many items of storage are accumulated in this contract's child trie. + storage_items: u32, + /// This records to how much deposit the accumulated `storage_bytes` amount to. + pub storage_byte_deposit: BalanceOf, + /// This records to how much deposit the accumulated `storage_items` amount to. + storage_item_deposit: BalanceOf, + /// This records how much deposit is put down in order to pay for the contract itself. + /// + /// We need to store this information separately so it is not used when calculating any refunds + /// since the base deposit can only ever be refunded on contract termination. + storage_base_deposit: BalanceOf, + /// Map of code hashes and deposit balances. + /// + /// Tracks the code hash and deposit held for locking delegate dependencies. Dependencies added + /// to the map can not be removed from the chain state and can be safely used for delegate + /// calls. + delegate_dependencies: DelegateDependencyMap, +} + +impl ContractInfo { + /// Constructs a new contract info **without** writing it to storage. + /// + /// This returns an `Err` if an contract with the supplied `account` already exists + /// in storage. + pub fn new( + address: &H160, + nonce: T::Nonce, + code_hash: sp_core::H256, + ) -> Result { + if >::contains_key(address) { + return Err(Error::::DuplicateContract.into()) + } + + let trie_id = { + let buf = ("bcontract_trie_v1", address, nonce).using_encoded(T::Hashing::hash); + buf.as_ref() + .to_vec() + .try_into() + .expect("Runtime uses a reasonable hash size. Hence sizeof(T::Hash) <= 128; qed") + }; + + let contract = Self { + trie_id, + code_hash, + storage_bytes: 0, + storage_items: 0, + storage_byte_deposit: Zero::zero(), + storage_item_deposit: Zero::zero(), + storage_base_deposit: Zero::zero(), + delegate_dependencies: Default::default(), + }; + + Ok(contract) + } + + /// Returns the number of locked delegate dependencies. + pub fn delegate_dependencies_count(&self) -> usize { + self.delegate_dependencies.len() + } + + /// Associated child trie unique id is built from the hash part of the trie id. + pub fn child_trie_info(&self) -> ChildInfo { + ChildInfo::new_default(self.trie_id.as_ref()) + } + + /// The deposit paying for the accumulated storage generated within the contract's child trie. + pub fn extra_deposit(&self) -> BalanceOf { + self.storage_byte_deposit.saturating_add(self.storage_item_deposit) + } + + /// Same as [`Self::extra_deposit`] but including the base deposit. + pub fn total_deposit(&self) -> BalanceOf { + self.extra_deposit().saturating_add(self.storage_base_deposit) + } + + /// Returns the storage base deposit of the contract. + pub fn storage_base_deposit(&self) -> BalanceOf { + self.storage_base_deposit + } + + /// Reads a storage kv pair of a contract. + /// + /// The read is performed from the `trie_id` only. The `address` is not necessary. If the + /// contract doesn't store under the given `key` `None` is returned. + pub fn read(&self, key: &Key) -> Option> { + child::get_raw(&self.child_trie_info(), key.hash().as_slice()) + } + + /// Returns `Some(len)` (in bytes) if a storage item exists at `key`. + /// + /// Returns `None` if the `key` wasn't previously set by `set_storage` or + /// was deleted. + pub fn size(&self, key: &Key) -> Option { + child::len(&self.child_trie_info(), key.hash().as_slice()) + } + + /// Update a storage entry into a contract's kv storage. + /// + /// If the `new_value` is `None` then the kv pair is removed. If `take` is true + /// a [`WriteOutcome::Taken`] is returned instead of a [`WriteOutcome::Overwritten`]. + /// + /// This function also records how much storage was created or removed if a `storage_meter` + /// is supplied. It should only be absent for testing or benchmarking code. + pub fn write( + &self, + key: &Key, + new_value: Option>, + storage_meter: Option<&mut meter::NestedMeter>, + take: bool, + ) -> Result { + let hashed_key = key.hash(); + self.write_raw(&hashed_key, new_value, storage_meter, take) + } + + /// Update a storage entry into a contract's kv storage. + /// Function used in benchmarks, which can simulate prefix collision in keys. + #[cfg(feature = "runtime-benchmarks")] + pub fn bench_write_raw( + &self, + key: &[u8], + new_value: Option>, + take: bool, + ) -> Result { + self.write_raw(key, new_value, None, take) + } + + fn write_raw( + &self, + key: &[u8], + new_value: Option>, + storage_meter: Option<&mut meter::NestedMeter>, + take: bool, + ) -> Result { + let child_trie_info = &self.child_trie_info(); + let (old_len, old_value) = if take { + let val = child::get_raw(child_trie_info, key); + (val.as_ref().map(|v| v.len() as u32), val) + } else { + (child::len(child_trie_info, key), None) + }; + + if let Some(storage_meter) = storage_meter { + let mut diff = meter::Diff::default(); + match (old_len, new_value.as_ref().map(|v| v.len() as u32)) { + (Some(old_len), Some(new_len)) => + if new_len > old_len { + diff.bytes_added = new_len - old_len; + } else { + diff.bytes_removed = old_len - new_len; + }, + (None, Some(new_len)) => { + diff.bytes_added = new_len; + diff.items_added = 1; + }, + (Some(old_len), None) => { + diff.bytes_removed = old_len; + diff.items_removed = 1; + }, + (None, None) => (), + } + storage_meter.charge(&diff); + } + + match &new_value { + Some(new_value) => child::put_raw(child_trie_info, key, new_value), + None => child::kill(child_trie_info, key), + } + + Ok(match (old_len, old_value) { + (None, _) => WriteOutcome::New, + (Some(old_len), None) => WriteOutcome::Overwritten(old_len), + (Some(_), Some(old_value)) => WriteOutcome::Taken(old_value), + }) + } + + /// Sets and returns the contract base deposit. + /// + /// The base deposit is updated when the `code_hash` of the contract changes, as it depends on + /// the deposit paid to upload the contract's code. + pub fn update_base_deposit(&mut self, code_info: &CodeInfo) -> BalanceOf { + let info_deposit = + Diff { bytes_added: self.encoded_size() as u32, items_added: 1, ..Default::default() } + .update_contract::(None) + .charge_or_zero(); + + // Instantiating the contract prevents its code to be deleted, therefore the base deposit + // includes a fraction (`T::CodeHashLockupDepositPercent`) of the original storage deposit + // to prevent abuse. + let upload_deposit = T::CodeHashLockupDepositPercent::get().mul_ceil(code_info.deposit()); + + let deposit = info_deposit.saturating_add(upload_deposit); + self.storage_base_deposit = deposit; + deposit + } + + /// Adds a new delegate dependency to the contract. + /// The `amount` is the amount of funds that will be reserved for the dependency. + /// + /// Returns an error if the maximum number of delegate_dependencies is reached or if + /// the delegate dependency already exists. + pub fn lock_delegate_dependency( + &mut self, + code_hash: sp_core::H256, + amount: BalanceOf, + ) -> DispatchResult { + self.delegate_dependencies + .try_insert(code_hash, amount) + .map_err(|_| Error::::MaxDelegateDependenciesReached)? + .map_or(Ok(()), |_| Err(Error::::DelegateDependencyAlreadyExists)) + .map_err(Into::into) + } + + /// Removes the delegate dependency from the contract and returns the deposit held for this + /// dependency. + /// + /// Returns an error if the entry doesn't exist. + pub fn unlock_delegate_dependency( + &mut self, + code_hash: &sp_core::H256, + ) -> Result, DispatchError> { + self.delegate_dependencies + .remove(code_hash) + .ok_or(Error::::DelegateDependencyNotFound.into()) + } + + /// Returns the delegate_dependencies of the contract. + pub fn delegate_dependencies(&self) -> &DelegateDependencyMap { + &self.delegate_dependencies + } + + /// Push a contract's trie to the deletion queue for lazy removal. + /// + /// You must make sure that the contract is also removed when queuing the trie for deletion. + pub fn queue_trie_for_deletion(&self) { + DeletionQueueManager::::load().insert(self.trie_id.clone()); + } + + /// Calculates the weight that is necessary to remove one key from the trie and how many + /// of those keys can be deleted from the deletion queue given the supplied weight limit. + pub fn deletion_budget(meter: &WeightMeter) -> (Weight, u32) { + let base_weight = T::WeightInfo::on_process_deletion_queue_batch(); + let weight_per_key = T::WeightInfo::on_initialize_per_trie_key(1) - + T::WeightInfo::on_initialize_per_trie_key(0); + + // `weight_per_key` being zero makes no sense and would constitute a failure to + // benchmark properly. We opt for not removing any keys at all in this case. + let key_budget = meter + .limit() + .saturating_sub(base_weight) + .checked_div_per_component(&weight_per_key) + .unwrap_or(0) as u32; + + (weight_per_key, key_budget) + } + + /// Delete as many items from the deletion queue possible within the supplied weight limit. + pub fn process_deletion_queue_batch(meter: &mut WeightMeter) { + if meter.try_consume(T::WeightInfo::on_process_deletion_queue_batch()).is_err() { + return + }; + + let mut queue = >::load(); + if queue.is_empty() { + return; + } + + let (weight_per_key, budget) = Self::deletion_budget(&meter); + let mut remaining_key_budget = budget; + while remaining_key_budget > 0 { + let Some(entry) = queue.next() else { break }; + + #[allow(deprecated)] + let outcome = child::kill_storage( + &ChildInfo::new_default(&entry.trie_id), + Some(remaining_key_budget), + ); + + match outcome { + // This happens when our budget wasn't large enough to remove all keys. + KillStorageResult::SomeRemaining(keys_removed) => { + remaining_key_budget.saturating_reduce(keys_removed); + break + }, + KillStorageResult::AllRemoved(keys_removed) => { + entry.remove(); + // charge at least one key even if none were removed. + remaining_key_budget = remaining_key_budget.saturating_sub(keys_removed.max(1)); + }, + }; + } + + meter.consume(weight_per_key.saturating_mul(u64::from(budget - remaining_key_budget))) + } + + /// Returns the code hash of the contract specified by `account` ID. + pub fn load_code_hash(account: &AccountIdOf) -> Option { + >::get(&T::AddressMapper::to_address(account)).map(|i| i.code_hash) + } +} + +/// Information about what happened to the pre-existing value when calling [`ContractInfo::write`]. +#[cfg_attr(any(test, feature = "runtime-benchmarks"), derive(Debug, PartialEq))] +pub enum WriteOutcome { + /// No value existed at the specified key. + New, + /// A value of the returned length was overwritten. + Overwritten(u32), + /// The returned value was taken out of storage before being overwritten. + /// + /// This is only returned when specifically requested because it causes additional work + /// depending on the size of the pre-existing value. When not requested [`Self::Overwritten`] + /// is returned instead. + Taken(Vec), +} + +impl WriteOutcome { + /// Extracts the size of the overwritten value or `0` if there + /// was no value in storage. + pub fn old_len(&self) -> u32 { + match self { + Self::New => 0, + Self::Overwritten(len) => *len, + Self::Taken(value) => value.len() as u32, + } + } + + /// Extracts the size of the overwritten value or `SENTINEL` if there + /// was no value in storage. + /// + /// # Note + /// + /// We cannot use `0` as sentinel value because there could be a zero sized + /// storage entry which is different from a non existing one. + pub fn old_len_with_sentinel(&self) -> u32 { + match self { + Self::New => SENTINEL, + Self::Overwritten(len) => *len, + Self::Taken(value) => value.len() as u32, + } + } +} + +/// Manage the removal of contracts storage that are marked for deletion. +/// +/// When a contract is deleted by calling `seal_terminate` it becomes inaccessible +/// immediately, but the deletion of the storage items it has accumulated is performed +/// later by pulling the contract from the queue in the `on_idle` hook. +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, DefaultNoBound, Clone)] +#[scale_info(skip_type_params(T))] +pub struct DeletionQueueManager { + /// Counter used as a key for inserting a new deleted contract in the queue. + /// The counter is incremented after each insertion. + insert_counter: u32, + /// The index used to read the next element to be deleted in the queue. + /// The counter is incremented after each deletion. + delete_counter: u32, + + _phantom: PhantomData, +} + +/// View on a contract that is marked for deletion. +struct DeletionQueueEntry<'a, T: Config> { + /// the trie id of the contract to delete. + trie_id: TrieId, + + /// A mutable reference on the queue so that the contract can be removed, and none can be added + /// or read in the meantime. + queue: &'a mut DeletionQueueManager, +} + +impl<'a, T: Config> DeletionQueueEntry<'a, T> { + /// Remove the contract from the deletion queue. + fn remove(self) { + >::remove(self.queue.delete_counter); + self.queue.delete_counter = self.queue.delete_counter.wrapping_add(1); + >::set(self.queue.clone()); + } +} + +impl DeletionQueueManager { + /// Load the `DeletionQueueCounter`, so we can perform read or write operations on the + /// DeletionQueue storage. + fn load() -> Self { + >::get() + } + + /// Returns `true` if the queue contains no elements. + fn is_empty(&self) -> bool { + self.insert_counter.wrapping_sub(self.delete_counter) == 0 + } + + /// Insert a contract in the deletion queue. + fn insert(&mut self, trie_id: TrieId) { + >::insert(self.insert_counter, trie_id); + self.insert_counter = self.insert_counter.wrapping_add(1); + >::set(self.clone()); + } + + /// Fetch the next contract to be deleted. + /// + /// Note: + /// we use the delete counter to get the next value to read from the queue and thus don't pay + /// the cost of an extra call to `sp_io::storage::next_key` to lookup the next entry in the map + fn next(&mut self) -> Option> { + if self.is_empty() { + return None + } + + let entry = >::get(self.delete_counter); + entry.map(|trie_id| DeletionQueueEntry { trie_id, queue: self }) + } +} + +#[cfg(test)] +#[cfg(feature = "riscv")] +impl DeletionQueueManager { + pub fn from_test_values(insert_counter: u32, delete_counter: u32) -> Self { + Self { insert_counter, delete_counter, _phantom: Default::default() } + } + + pub fn as_test_tuple(&self) -> (u32, u32) { + (self.insert_counter, self.delete_counter) + } +} diff --git a/pallets/revive/src/storage/meter.rs b/pallets/revive/src/storage/meter.rs new file mode 100644 index 00000000..1bb9b1de --- /dev/null +++ b/pallets/revive/src/storage/meter.rs @@ -0,0 +1,886 @@ +// 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. + +//! This module contains functions to meter the storage deposit. + +use alloc::vec::Vec; +use core::{fmt::Debug, marker::PhantomData}; + +use frame_support::{ + traits::{ + fungible::{Mutate, MutateHold}, + tokens::{Fortitude, Fortitude::Polite, Precision, Preservation, Restriction}, + Get, + }, + DefaultNoBound, RuntimeDebugNoBound, +}; +use sp_runtime::{ + traits::{Saturating, Zero}, + DispatchError, FixedPointNumber, FixedU128, +}; + +use crate::{ + address::AddressMapper, storage::ContractInfo, AccountIdOf, BalanceOf, CodeInfo, Config, Error, + Event, HoldReason, Inspect, Origin, Pallet, StorageDeposit as Deposit, System, LOG_TARGET, +}; + +/// Deposit that uses the native fungible's balance type. +pub type DepositOf = Deposit>; + +/// A production root storage meter that actually charges from its origin. +pub type Meter = RawMeter; + +/// A production nested storage meter that actually charges from its origin. +pub type NestedMeter = RawMeter; + +/// A production storage meter that actually charges from its origin. +/// +/// This can be used where we want to be generic over the state (Root vs. Nested). +pub type GenericMeter = RawMeter; + +/// A trait that allows to decouple the metering from the charging of balance. +/// +/// This mostly exists for testing so that the charging can be mocked. +pub trait Ext { + /// This checks whether `origin` is able to afford the storage deposit limit. + /// + /// It is necessary to do this check beforehand so that the charge won't fail later on. + /// + /// `origin`: The origin of the call stack from which is responsible for putting down a deposit. + /// `limit`: The limit with which the meter was constructed. + /// `min_leftover`: How much `free_balance` in addition to the existential deposit (ed) should + /// be left inside the `origin` account. + /// + /// Returns the limit that should be used by the meter. If origin can't afford the `limit` + /// it returns `Err`. + fn check_limit( + origin: &T::AccountId, + limit: BalanceOf, + min_leftover: BalanceOf, + ) -> Result, DispatchError>; + /// This is called to inform the implementer that some balance should be charged due to + /// some interaction of the `origin` with a `contract`. + /// + /// The balance transfer can either flow from `origin` to `contract` or the other way + /// around depending on whether `amount` constitutes a `Charge` or a `Refund`. + /// It should be used in combination with `check_limit` to check that no more balance than this + /// limit is ever charged. + fn charge( + origin: &T::AccountId, + contract: &T::AccountId, + amount: &DepositOf, + state: &ContractState, + ) -> Result<(), DispatchError>; +} + +/// This [`Ext`] is used for actual on-chain execution when balance needs to be charged. +/// +/// It uses [`frame_support::traits::fungible::Mutate`] in order to do accomplish the reserves. +pub enum ReservingExt {} + +/// Used to implement a type state pattern for the meter. +/// +/// It is sealed and cannot be implemented outside of this module. +pub trait State: private::Sealed {} + +/// State parameter that constitutes a meter that is in its root state. +#[derive(Default, Debug)] +pub struct Root; + +/// State parameter that constitutes a meter that is in its nested state. +/// Its value indicates whether the nested meter has its own limit. +#[derive(DefaultNoBound, RuntimeDebugNoBound)] +pub enum Nested { + #[default] + DerivedLimit, + OwnLimit, +} + +impl State for Root {} +impl State for Nested {} + +/// A type that allows the metering of consumed or freed storage of a single contract call stack. +#[derive(DefaultNoBound, RuntimeDebugNoBound)] +pub struct RawMeter { + /// The limit of how much balance this meter is allowed to consume. + limit: BalanceOf, + /// The amount of balance that was used in this meter and all of its already absorbed children. + total_deposit: DepositOf, + /// The amount of storage changes that were recorded in this meter alone. + own_contribution: Contribution, + /// List of charges that should be applied at the end of a contract stack execution. + /// + /// We only have one charge per contract hence the size of this vector is + /// limited by the maximum call depth. + charges: Vec>, + /// We store the nested state to determine if it has a special limit for sub-call. + nested: S, + /// Type parameter only used in impls. + _phantom: PhantomData, +} + +/// This type is used to describe a storage change when charging from the meter. +#[derive(Default, RuntimeDebugNoBound)] +pub struct Diff { + /// How many bytes were added to storage. + pub bytes_added: u32, + /// How many bytes were removed from storage. + pub bytes_removed: u32, + /// How many storage items were added to storage. + pub items_added: u32, + /// How many storage items were removed from storage. + pub items_removed: u32, +} + +impl Diff { + /// Calculate how much of a charge or refund results from applying the diff and store it + /// in the passed `info` if any. + /// + /// # Note + /// + /// In case `None` is passed for `info` only charges are calculated. This is because refunds + /// are calculated pro rata of the existing storage within a contract and hence need extract + /// this information from the passed `info`. + pub fn update_contract(&self, info: Option<&mut ContractInfo>) -> DepositOf { + let per_byte = T::DepositPerByte::get(); + let per_item = T::DepositPerItem::get(); + let bytes_added = self.bytes_added.saturating_sub(self.bytes_removed); + let items_added = self.items_added.saturating_sub(self.items_removed); + let mut bytes_deposit = Deposit::Charge(per_byte.saturating_mul((bytes_added).into())); + let mut items_deposit = Deposit::Charge(per_item.saturating_mul((items_added).into())); + + // Without any contract info we can only calculate diffs which add storage + let info = if let Some(info) = info { + info + } else { + debug_assert_eq!(self.bytes_removed, 0); + debug_assert_eq!(self.items_removed, 0); + return bytes_deposit.saturating_add(&items_deposit) + }; + + // Refunds are calculated pro rata based on the accumulated storage within the contract + let bytes_removed = self.bytes_removed.saturating_sub(self.bytes_added); + let items_removed = self.items_removed.saturating_sub(self.items_added); + let ratio = FixedU128::checked_from_rational(bytes_removed, info.storage_bytes) + .unwrap_or_default() + .min(FixedU128::from_u32(1)); + bytes_deposit = bytes_deposit + .saturating_add(&Deposit::Refund(ratio.saturating_mul_int(info.storage_byte_deposit))); + let ratio = FixedU128::checked_from_rational(items_removed, info.storage_items) + .unwrap_or_default() + .min(FixedU128::from_u32(1)); + items_deposit = items_deposit + .saturating_add(&Deposit::Refund(ratio.saturating_mul_int(info.storage_item_deposit))); + + // We need to update the contract info structure with the new deposits + info.storage_bytes = + info.storage_bytes.saturating_add(bytes_added).saturating_sub(bytes_removed); + info.storage_items = + info.storage_items.saturating_add(items_added).saturating_sub(items_removed); + match &bytes_deposit { + Deposit::Charge(amount) => + info.storage_byte_deposit = info.storage_byte_deposit.saturating_add(*amount), + Deposit::Refund(amount) => + info.storage_byte_deposit = info.storage_byte_deposit.saturating_sub(*amount), + } + match &items_deposit { + Deposit::Charge(amount) => + info.storage_item_deposit = info.storage_item_deposit.saturating_add(*amount), + Deposit::Refund(amount) => + info.storage_item_deposit = info.storage_item_deposit.saturating_sub(*amount), + } + + bytes_deposit.saturating_add(&items_deposit) + } +} + +impl Diff { + fn saturating_add(&self, rhs: &Self) -> Self { + Self { + bytes_added: self.bytes_added.saturating_add(rhs.bytes_added), + bytes_removed: self.bytes_removed.saturating_add(rhs.bytes_removed), + items_added: self.items_added.saturating_add(rhs.items_added), + items_removed: self.items_removed.saturating_add(rhs.items_removed), + } + } +} + +/// The state of a contract. +/// +/// In case of termination the beneficiary is indicated. +#[derive(RuntimeDebugNoBound, Clone, PartialEq, Eq)] +pub enum ContractState { + Alive, + Terminated { beneficiary: AccountIdOf }, +} + +/// Records information to charge or refund a plain account. +/// +/// All the charges are deferred to the end of a whole call stack. Reason is that by doing +/// this we can do all the refunds before doing any charge. This way a plain account can use +/// more deposit than it has balance as along as it is covered by a refund. This +/// essentially makes the order of storage changes irrelevant with regard to the deposit system. +/// The only exception is when a special (tougher) deposit limit is specified for a cross-contract +/// call. In that case the limit is enforced once the call is returned, rolling it back if +/// exhausted. +#[derive(RuntimeDebugNoBound, Clone)] +struct Charge { + contract: T::AccountId, + amount: DepositOf, + state: ContractState, +} + +/// Records the storage changes of a storage meter. +#[derive(RuntimeDebugNoBound)] +enum Contribution { + /// The contract the meter belongs to is alive and accumulates changes using a [`Diff`]. + Alive(Diff), + /// The meter was checked against its limit using [`RawMeter::enforce_limit`] at the end of + /// its execution. In this process the [`Diff`] was converted into a [`Deposit`]. + Checked(DepositOf), + /// The contract was terminated. In this process the [`Diff`] was converted into a [`Deposit`] + /// in order to calculate the refund. Upon termination the `reducible_balance` in the + /// contract's account is transferred to the [`beneficiary`]. + Terminated { deposit: DepositOf, beneficiary: AccountIdOf }, +} + +impl Contribution { + /// See [`Diff::update_contract`]. + fn update_contract(&self, info: Option<&mut ContractInfo>) -> DepositOf { + match self { + Self::Alive(diff) => diff.update_contract::(info), + Self::Terminated { deposit, beneficiary: _ } | Self::Checked(deposit) => + deposit.clone(), + } + } +} + +impl Default for Contribution { + fn default() -> Self { + Self::Alive(Default::default()) + } +} + +/// Functions that apply to all states. +impl RawMeter +where + T: Config, + E: Ext, + S: State + Default + Debug, +{ + /// Create a new child that has its `limit`. + /// Passing `0` as the limit is interpreted as to take whatever is remaining from its parent. + /// + /// This is called whenever a new subcall is initiated in order to track the storage + /// usage for this sub call separately. This is necessary because we want to exchange balance + /// with the current contract we are interacting with. + pub fn nested(&self, limit: BalanceOf) -> RawMeter { + debug_assert!(matches!(self.contract_state(), ContractState::Alive)); + // If a special limit is specified higher than it is available, + // we want to enforce the lesser limit to the nested meter, to fail in the sub-call. + let limit = self.available().min(limit); + if limit.is_zero() { + RawMeter { limit: self.available(), ..Default::default() } + } else { + RawMeter { limit, nested: Nested::OwnLimit, ..Default::default() } + } + } + + /// Absorb a child that was spawned to handle a sub call. + /// + /// This should be called whenever a sub call comes to its end and it is **not** reverted. + /// This does the actual balance transfer from/to `origin` and `contract` based on the + /// overall storage consumption of the call. It also updates the supplied contract info. + /// + /// In case a contract reverted the child meter should just be dropped in order to revert + /// any changes it recorded. + /// + /// # Parameters + /// + /// - `absorbed`: The child storage meter that should be absorbed. + /// - `origin`: The origin that spawned the original root meter. + /// - `contract`: The contract's account that this sub call belongs to. + /// - `info`: The info of the contract in question. `None` if the contract was terminated. + pub fn absorb( + &mut self, + absorbed: RawMeter, + contract: &T::AccountId, + info: Option<&mut ContractInfo>, + ) { + let own_deposit = absorbed.own_contribution.update_contract(info); + self.total_deposit = self + .total_deposit + .saturating_add(&absorbed.total_deposit) + .saturating_add(&own_deposit); + self.charges.extend_from_slice(&absorbed.charges); + if !own_deposit.is_zero() { + self.charges.push(Charge { + contract: contract.clone(), + amount: own_deposit, + state: absorbed.contract_state(), + }); + } + } + + /// The amount of balance that is still available from the original `limit`. + fn available(&self) -> BalanceOf { + self.total_deposit.available(&self.limit) + } + + /// Returns the state of the currently executed contract. + fn contract_state(&self) -> ContractState { + match &self.own_contribution { + Contribution::Terminated { deposit: _, beneficiary } => + ContractState::Terminated { beneficiary: beneficiary.clone() }, + _ => ContractState::Alive, + } + } +} + +/// Functions that only apply to the root state. +impl RawMeter +where + T: Config, + E: Ext, +{ + /// Create new storage meter for the specified `origin` and `limit`. + /// + /// This tries to [`Ext::check_limit`] on `origin` and fails if this is not possible. + pub fn new( + origin: &Origin, + limit: BalanceOf, + min_leftover: BalanceOf, + ) -> Result { + // Check the limit only if the origin is not root. + return match origin { + Origin::Root => Ok(Self { limit, ..Default::default() }), + Origin::Signed(o) => { + let limit = E::check_limit(o, limit, min_leftover)?; + Ok(Self { limit, ..Default::default() }) + }, + } + } + + /// The total amount of deposit that should change hands as result of the execution + /// that this meter was passed into. This will also perform all the charges accumulated + /// in the whole contract stack. + /// + /// This drops the root meter in order to make sure it is only called when the whole + /// execution did finish. + pub fn try_into_deposit(self, origin: &Origin) -> Result, DispatchError> { + // Only refund or charge deposit if the origin is not root. + let origin = match origin { + Origin::Root => return Ok(Deposit::Charge(Zero::zero())), + Origin::Signed(o) => o, + }; + for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Refund(_))) { + E::charge(origin, &charge.contract, &charge.amount, &charge.state)?; + } + for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Charge(_))) { + E::charge(origin, &charge.contract, &charge.amount, &charge.state)?; + } + Ok(self.total_deposit) + } +} + +/// Functions that only apply to the nested state. +impl> RawMeter { + /// Charges `diff` from the meter. + pub fn charge(&mut self, diff: &Diff) { + match &mut self.own_contribution { + Contribution::Alive(own) => *own = own.saturating_add(diff), + _ => panic!("Charge is never called after termination; qed"), + }; + } + + /// Adds a deposit charge. + /// + /// Use this method instead of [`Self::charge`] when the charge is not the result of a storage + /// change. This is the case when a `delegate_dependency` is added or removed, or when the + /// `code_hash` is updated. [`Self::charge`] cannot be used here because we keep track of the + /// deposit charge separately from the storage charge. + pub fn charge_deposit(&mut self, contract: T::AccountId, amount: DepositOf) { + self.total_deposit = self.total_deposit.saturating_add(&amount); + self.charges.push(Charge { contract, amount, state: ContractState::Alive }); + } + + /// Charges from `origin` a storage deposit for contract instantiation. + /// + /// This immediately transfers the balance in order to create the account. + pub fn charge_instantiate( + &mut self, + origin: &T::AccountId, + contract: &T::AccountId, + contract_info: &mut ContractInfo, + code_info: &CodeInfo, + ) -> Result<(), DispatchError> { + debug_assert!(matches!(self.contract_state(), ContractState::Alive)); + + // We need to make sure that the contract's account exists. + let ed = Pallet::::min_balance(); + self.total_deposit = Deposit::Charge(ed); + T::Currency::transfer(origin, contract, ed, Preservation::Preserve)?; + + // A consumer is added at account creation and removed it on termination, otherwise the + // runtime could remove the account. As long as a contract exists its account must exist. + // With the consumer, a correct runtime cannot remove the account. + System::::inc_consumers(contract)?; + + let deposit = contract_info.update_base_deposit(&code_info); + let deposit = Deposit::Charge(deposit); + + self.charge_deposit(contract.clone(), deposit); + Ok(()) + } + + /// Call to tell the meter that the currently executing contract was terminated. + /// + /// This will manipulate the meter so that all storage deposit accumulated in + /// `contract_info` will be refunded to the `origin` of the meter. And the free + /// (`reducible_balance`) will be sent to the `beneficiary`. + pub fn terminate(&mut self, info: &ContractInfo, beneficiary: T::AccountId) { + debug_assert!(matches!(self.contract_state(), ContractState::Alive)); + self.own_contribution = Contribution::Terminated { + deposit: Deposit::Refund(info.total_deposit()), + beneficiary, + }; + } + + /// [`Self::charge`] does not enforce the storage limit since we want to do this check as late + /// as possible to allow later refunds to offset earlier charges. + /// + /// # Note + /// + /// We normally need to call this **once** for every call stack and not for every cross contract + /// call. However, if a dedicated limit is specified for a sub-call, this needs to be called + /// once the sub-call has returned. For this, the [`Self::enforce_subcall_limit`] wrapper is + /// used. + pub fn enforce_limit( + &mut self, + info: Option<&mut ContractInfo>, + ) -> Result<(), DispatchError> { + let deposit = self.own_contribution.update_contract(info); + let total_deposit = self.total_deposit.saturating_add(&deposit); + // We don't want to override a `Terminated` with a `Checked`. + if matches!(self.contract_state(), ContractState::Alive) { + self.own_contribution = Contribution::Checked(deposit); + } + if let Deposit::Charge(amount) = total_deposit { + if amount > self.limit { + return Err(>::StorageDepositLimitExhausted.into()) + } + } + Ok(()) + } + + /// This is a wrapper around [`Self::enforce_limit`] to use on the exit from a sub-call to + /// enforce its special limit if needed. + pub fn enforce_subcall_limit( + &mut self, + info: Option<&mut ContractInfo>, + ) -> Result<(), DispatchError> { + match self.nested { + Nested::OwnLimit => self.enforce_limit(info), + Nested::DerivedLimit => Ok(()), + } + } +} + +impl Ext for ReservingExt { + fn check_limit( + origin: &T::AccountId, + limit: BalanceOf, + min_leftover: BalanceOf, + ) -> Result, DispatchError> { + let limit = T::Currency::reducible_balance(origin, Preservation::Preserve, Polite) + .saturating_sub(min_leftover) + .min(limit); + Ok(limit) + } + + fn charge( + origin: &T::AccountId, + contract: &T::AccountId, + amount: &DepositOf, + state: &ContractState, + ) -> Result<(), DispatchError> { + match amount { + Deposit::Charge(amount) | Deposit::Refund(amount) if amount.is_zero() => return Ok(()), + Deposit::Charge(amount) => { + // This could fail if the `origin` does not have enough liquidity. Ideally, though, + // this should have been checked before with `check_limit`. + T::Currency::transfer_and_hold( + &HoldReason::StorageDepositReserve.into(), + origin, + contract, + *amount, + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + )?; + + Pallet::::deposit_event(Event::StorageDepositTransferredAndHeld { + from: T::AddressMapper::to_address(origin), + to: T::AddressMapper::to_address(contract), + amount: *amount, + }); + }, + Deposit::Refund(amount) => { + let transferred = T::Currency::transfer_on_hold( + &HoldReason::StorageDepositReserve.into(), + contract, + origin, + *amount, + Precision::BestEffort, + Restriction::Free, + Fortitude::Polite, + )?; + + Pallet::::deposit_event(Event::StorageDepositTransferredAndReleased { + from: T::AddressMapper::to_address(contract), + to: T::AddressMapper::to_address(origin), + amount: transferred, + }); + + if transferred < *amount { + // This should never happen, if it does it means that there is a bug in the + // runtime logic. In the rare case this happens we try to refund as much as we + // can, thus the `Precision::BestEffort`. + log::error!( + target: LOG_TARGET, + "Failed to repatriate full storage deposit {:?} from contract {:?} to origin {:?}. Transferred {:?}.", + amount, contract, origin, transferred, + ); + } + }, + } + if let ContractState::::Terminated { beneficiary } = state { + System::::dec_consumers(&contract); + // Whatever is left in the contract is sent to the termination beneficiary. + T::Currency::transfer( + &contract, + &beneficiary, + T::Currency::reducible_balance(&contract, Preservation::Expendable, Polite), + Preservation::Expendable, + )?; + } + Ok(()) + } +} + +mod private { + pub trait Sealed {} + impl Sealed for super::Root {} + impl Sealed for super::Nested {} +} + +#[cfg(test)] +mod tests { + use frame_support::parameter_types; + use pretty_assertions::assert_eq; + + use super::*; + use crate::{exec::AccountIdOf, test_utils::*, tests::Test}; + + type TestMeter = RawMeter; + + parameter_types! { + static TestExtTestValue: TestExt = Default::default(); + } + + #[derive(Debug, PartialEq, Eq, Clone)] + struct LimitCheck { + origin: AccountIdOf, + limit: BalanceOf, + min_leftover: BalanceOf, + } + + #[derive(Debug, PartialEq, Eq, Clone)] + struct Charge { + origin: AccountIdOf, + contract: AccountIdOf, + amount: DepositOf, + state: ContractState, + } + + #[derive(Default, Debug, PartialEq, Eq, Clone)] + pub struct TestExt { + limit_checks: Vec, + charges: Vec, + } + + impl TestExt { + fn clear(&mut self) { + self.limit_checks.clear(); + self.charges.clear(); + } + } + + impl Ext for TestExt { + fn check_limit( + origin: &AccountIdOf, + limit: BalanceOf, + min_leftover: BalanceOf, + ) -> Result, DispatchError> { + TestExtTestValue::mutate(|ext| { + ext.limit_checks + .push(LimitCheck { origin: origin.clone(), limit, min_leftover }) + }); + Ok(limit) + } + + fn charge( + origin: &AccountIdOf, + contract: &AccountIdOf, + amount: &DepositOf, + state: &ContractState, + ) -> Result<(), DispatchError> { + TestExtTestValue::mutate(|ext| { + ext.charges.push(Charge { + origin: origin.clone(), + contract: contract.clone(), + amount: amount.clone(), + state: state.clone(), + }) + }); + Ok(()) + } + } + + fn clear_ext() { + TestExtTestValue::mutate(|ext| ext.clear()) + } + + struct ChargingTestCase { + origin: Origin, + deposit: DepositOf, + expected: TestExt, + } + + #[derive(Default)] + struct StorageInfo { + bytes: u32, + items: u32, + bytes_deposit: BalanceOf, + items_deposit: BalanceOf, + } + + fn new_info(info: StorageInfo) -> ContractInfo { + ContractInfo:: { + trie_id: Default::default(), + code_hash: Default::default(), + storage_bytes: info.bytes, + storage_items: info.items, + storage_byte_deposit: info.bytes_deposit, + storage_item_deposit: info.items_deposit, + storage_base_deposit: Default::default(), + delegate_dependencies: Default::default(), + } + } + + #[test] + fn new_reserves_balance_works() { + clear_ext(); + + TestMeter::new(&Origin::from_account_id(ALICE), 1_000, 0).unwrap(); + + assert_eq!( + TestExtTestValue::get(), + TestExt { + limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }], + ..Default::default() + } + ) + } + + #[test] + fn empty_charge_works() { + clear_ext(); + + let mut meter = TestMeter::new(&Origin::from_account_id(ALICE), 1_000, 0).unwrap(); + assert_eq!(meter.available(), 1_000); + + // an empty charge does not create a `Charge` entry + let mut nested0 = meter.nested(BalanceOf::::zero()); + nested0.charge(&Default::default()); + meter.absorb(nested0, &BOB, None); + + assert_eq!( + TestExtTestValue::get(), + TestExt { + limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }], + ..Default::default() + } + ) + } + + #[test] + fn charging_works() { + let test_cases = vec![ + ChargingTestCase { + origin: Origin::::from_account_id(ALICE), + deposit: Deposit::Refund(28), + expected: TestExt { + limit_checks: vec![LimitCheck { origin: ALICE, limit: 100, min_leftover: 0 }], + charges: vec![ + Charge { + origin: ALICE, + contract: CHARLIE, + amount: Deposit::Refund(10), + state: ContractState::Alive, + }, + Charge { + origin: ALICE, + contract: CHARLIE, + amount: Deposit::Refund(20), + state: ContractState::Alive, + }, + Charge { + origin: ALICE, + contract: BOB, + amount: Deposit::Charge(2), + state: ContractState::Alive, + }, + ], + }, + }, + ChargingTestCase { + origin: Origin::::Root, + deposit: Deposit::Charge(0), + expected: TestExt { limit_checks: vec![], charges: vec![] }, + }, + ]; + + for test_case in test_cases { + clear_ext(); + + let mut meter = TestMeter::new(&test_case.origin, 100, 0).unwrap(); + assert_eq!(meter.available(), 100); + + let mut nested0_info = new_info(StorageInfo { + bytes: 100, + items: 5, + bytes_deposit: 100, + items_deposit: 10, + }); + let mut nested0 = meter.nested(BalanceOf::::zero()); + nested0.charge(&Diff { + bytes_added: 108, + bytes_removed: 5, + items_added: 1, + items_removed: 2, + }); + nested0.charge(&Diff { bytes_removed: 99, ..Default::default() }); + + let mut nested1_info = new_info(StorageInfo { + bytes: 100, + items: 10, + bytes_deposit: 100, + items_deposit: 20, + }); + let mut nested1 = nested0.nested(BalanceOf::::zero()); + nested1.charge(&Diff { items_removed: 5, ..Default::default() }); + nested0.absorb(nested1, &CHARLIE, Some(&mut nested1_info)); + + let mut nested2_info = new_info(StorageInfo { + bytes: 100, + items: 7, + bytes_deposit: 100, + items_deposit: 20, + }); + let mut nested2 = nested0.nested(BalanceOf::::zero()); + nested2.charge(&Diff { items_removed: 7, ..Default::default() }); + nested0.absorb(nested2, &CHARLIE, Some(&mut nested2_info)); + + nested0.enforce_limit(Some(&mut nested0_info)).unwrap(); + meter.absorb(nested0, &BOB, Some(&mut nested0_info)); + + assert_eq!(meter.try_into_deposit(&test_case.origin).unwrap(), test_case.deposit); + + assert_eq!(nested0_info.extra_deposit(), 112); + assert_eq!(nested1_info.extra_deposit(), 110); + assert_eq!(nested2_info.extra_deposit(), 100); + + assert_eq!(TestExtTestValue::get(), test_case.expected) + } + } + + #[test] + fn termination_works() { + let test_cases = vec![ + ChargingTestCase { + origin: Origin::::from_account_id(ALICE), + deposit: Deposit::Refund(108), + expected: TestExt { + limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }], + charges: vec![ + Charge { + origin: ALICE, + contract: CHARLIE, + amount: Deposit::Refund(120), + state: ContractState::Terminated { beneficiary: CHARLIE }, + }, + Charge { + origin: ALICE, + contract: BOB, + amount: Deposit::Charge(12), + state: ContractState::Alive, + }, + ], + }, + }, + ChargingTestCase { + origin: Origin::::Root, + deposit: Deposit::Charge(0), + expected: TestExt { limit_checks: vec![], charges: vec![] }, + }, + ]; + + for test_case in test_cases { + clear_ext(); + + let mut meter = TestMeter::new(&test_case.origin, 1_000, 0).unwrap(); + assert_eq!(meter.available(), 1_000); + + let mut nested0 = meter.nested(BalanceOf::::zero()); + nested0.charge(&Diff { + bytes_added: 5, + bytes_removed: 1, + items_added: 3, + items_removed: 1, + }); + nested0.charge(&Diff { items_added: 2, ..Default::default() }); + + let mut nested1_info = new_info(StorageInfo { + bytes: 100, + items: 10, + bytes_deposit: 100, + items_deposit: 20, + }); + let mut nested1 = nested0.nested(BalanceOf::::zero()); + nested1.charge(&Diff { items_removed: 5, ..Default::default() }); + nested1.charge(&Diff { bytes_added: 20, ..Default::default() }); + nested1.terminate(&nested1_info, CHARLIE); + nested0.enforce_limit(Some(&mut nested1_info)).unwrap(); + nested0.absorb(nested1, &CHARLIE, None); + + meter.absorb(nested0, &BOB, None); + assert_eq!(meter.try_into_deposit(&test_case.origin).unwrap(), test_case.deposit); + assert_eq!(TestExtTestValue::get(), test_case.expected) + } + } +} diff --git a/pallets/revive/src/test_utils.rs b/pallets/revive/src/test_utils.rs new file mode 100644 index 00000000..b26021ff --- /dev/null +++ b/pallets/revive/src/test_utils.rs @@ -0,0 +1,67 @@ +// 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. + +//! Shared utilities for testing contracts. +//! This is not part of the tests module because it is made public for other crates to use. + +#![cfg(feature = "std")] + +pub mod builder; + +use frame_support::weights::Weight; +use sp_core::H160; +pub use sp_runtime::AccountId32; + +use crate::{BalanceOf, Config}; + +const fn ee_suffix(addr: H160) -> AccountId32 { + let mut id = [0u8; 32]; + let mut i = 0; + while i < 20 { + id[i] = addr.0[i]; + i += 1; + } + + let mut j = 20; + while j < 32 { + id[j] = 0xee; + j += 1; + } + + AccountId32::new(id) +} + +pub const ALICE: AccountId32 = AccountId32::new([1u8; 32]); +pub const ALICE_ADDR: H160 = H160([1u8; 20]); +pub const ETH_ALICE: AccountId32 = ee_suffix(ALICE_ADDR); + +pub const BOB: AccountId32 = AccountId32::new([2u8; 32]); +pub const BOB_ADDR: H160 = H160([2u8; 20]); +pub const BOB_CONTRACT_ID: AccountId32 = ee_suffix(BOB_ADDR); + +pub const CHARLIE: AccountId32 = AccountId32::new([3u8; 32]); +pub const CHARLIE_ADDR: H160 = H160([3u8; 20]); + +pub const DJANGO: AccountId32 = AccountId32::new([4u8; 32]); +pub const DJANGO_ADDR: H160 = H160([4u8; 20]); +pub const ETH_DJANGO: AccountId32 = ee_suffix(DJANGO_ADDR); + +pub const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024); + +pub fn deposit_limit() -> BalanceOf { + 10_000_000u32.into() +} diff --git a/pallets/revive/src/test_utils/builder.rs b/pallets/revive/src/test_utils/builder.rs new file mode 100644 index 00000000..2cd7aaff --- /dev/null +++ b/pallets/revive/src/test_utils/builder.rs @@ -0,0 +1,227 @@ +// 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::pallet_prelude::DispatchResultWithPostInfo; +use paste::paste; +use sp_core::H160; + +use super::{deposit_limit, GAS_LIMIT}; +use crate::{ + address::AddressMapper, AccountIdOf, BalanceOf, Code, CollectEvents, Config, + ContractExecResult, ContractInstantiateResult, DebugInfo, EventRecordOf, ExecReturnValue, + InstantiateReturnValue, OriginFor, Pallet, Weight, +}; + +/// Helper macro to generate a builder for contract API calls. +macro_rules! builder { + // Entry point to generate a builder for the given method. + ( + $method:ident($($field:ident: $type:ty,)*) -> $result:ty; + $($extra:item)* + ) => { + paste!{ + builder!([< $method:camel Builder >], $method($($field: $type,)* ) -> $result; $($extra)*); + } + }; + // Generate the builder struct and its methods. + ( + $name:ident, + $method:ident($($field:ident: $type:ty,)*) -> $result:ty; + $($extra:item)* + ) => { + #[doc = concat!("A builder to construct a ", stringify!($method), " call")] + pub struct $name { + $($field: $type,)* + } + + #[allow(dead_code)] + impl $name + where + BalanceOf: Into + TryFrom, + crate::MomentOf: Into, + T::Hash: frame_support::traits::IsType, + { + $( + #[doc = concat!("Set the ", stringify!($field))] + pub fn $field(mut self, value: $type) -> Self { + self.$field = value; + self + } + )* + + #[doc = concat!("Build the ", stringify!($method), " call")] + pub fn build(self) -> $result { + Pallet::::$method( + $(self.$field,)* + ) + } + + $($extra)* + } + } +} + +pub struct Contract { + pub account_id: AccountIdOf, + pub addr: H160, +} + +builder!( + instantiate_with_code( + origin: OriginFor, + value: BalanceOf, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + code: Vec, + data: Vec, + salt: Option<[u8; 32]>, + ) -> DispatchResultWithPostInfo; + + /// Create an [`InstantiateWithCodeBuilder`] with default values. + pub fn instantiate_with_code(origin: OriginFor, code: Vec) -> Self { + Self { + origin, + value: 0u32.into(), + gas_limit: GAS_LIMIT, + storage_deposit_limit: deposit_limit::(), + code, + data: vec![], + salt: Some([0; 32]), + } + } +); + +builder!( + instantiate( + origin: OriginFor, + value: BalanceOf, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + code_hash: sp_core::H256, + data: Vec, + salt: Option<[u8; 32]>, + ) -> DispatchResultWithPostInfo; + + /// Create an [`InstantiateBuilder`] with default values. + pub fn instantiate(origin: OriginFor, code_hash: sp_core::H256) -> Self { + Self { + origin, + value: 0u32.into(), + gas_limit: GAS_LIMIT, + storage_deposit_limit: deposit_limit::(), + code_hash, + data: vec![], + salt: Some([0; 32]), + } + } +); + +builder!( + bare_instantiate( + origin: OriginFor, + value: BalanceOf, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + code: Code, + data: Vec, + salt: Option<[u8; 32]>, + debug: DebugInfo, + collect_events: CollectEvents, + ) -> ContractInstantiateResult, EventRecordOf>; + + /// Build the instantiate call and unwrap the result. + pub fn build_and_unwrap_result(self) -> InstantiateReturnValue { + self.build().result.unwrap() + } + + /// Build the instantiate call and unwrap the account id. + pub fn build_and_unwrap_contract(self) -> Contract { + let addr = self.build().result.unwrap().addr; + let account_id = T::AddressMapper::to_account_id(&addr); + Contract{ account_id, addr } + } + + /// Create a [`BareInstantiateBuilder`] with default values. + pub fn bare_instantiate(origin: OriginFor, code: Code) -> Self { + Self { + origin, + value: 0u32.into(), + gas_limit: GAS_LIMIT, + storage_deposit_limit: deposit_limit::(), + code, + data: vec![], + salt: Some([0; 32]), + debug: DebugInfo::UnsafeDebug, + collect_events: CollectEvents::Skip, + } + } +); + +builder!( + call( + origin: OriginFor, + dest: H160, + value: BalanceOf, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + data: Vec, + ) -> DispatchResultWithPostInfo; + + /// Create a [`CallBuilder`] with default values. + pub fn call(origin: OriginFor, dest: H160) -> Self { + CallBuilder { + origin, + dest, + value: 0u32.into(), + gas_limit: GAS_LIMIT, + storage_deposit_limit: deposit_limit::(), + data: vec![], + } + } +); + +builder!( + bare_call( + origin: OriginFor, + dest: H160, + value: BalanceOf, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + data: Vec, + debug: DebugInfo, + collect_events: CollectEvents, + ) -> ContractExecResult, EventRecordOf>; + + /// Build the call and unwrap the result. + pub fn build_and_unwrap_result(self) -> ExecReturnValue { + self.build().result.unwrap() + } + + /// Create a [`BareCallBuilder`] with default values. + pub fn bare_call(origin: OriginFor, dest: H160) -> Self { + Self { + origin, + dest, + value: 0u32.into(), + gas_limit: GAS_LIMIT, + storage_deposit_limit: deposit_limit::(), + data: vec![], + debug: DebugInfo::UnsafeDebug, + collect_events: CollectEvents::Skip, + } + } +); diff --git a/pallets/revive/src/tests.rs b/pallets/revive/src/tests.rs new file mode 100644 index 00000000..5385a5d4 --- /dev/null +++ b/pallets/revive/src/tests.rs @@ -0,0 +1,4362 @@ +// 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. + +#![cfg_attr(not(feature = "riscv"), allow(dead_code, unused_imports, unused_macros))] + +mod pallet_dummy; +mod test_debug; + +use assert_matches::assert_matches; +use codec::{Decode, Encode}; +use frame_support::{ + assert_err, assert_err_ignore_postinfo, assert_err_with_weight, assert_noop, assert_ok, + derive_impl, + pallet_prelude::EnsureOrigin, + parameter_types, + storage::child, + traits::{ + fungible::{BalancedHold, Inspect, Mutate, MutateHold}, + tokens::Preservation, + ConstU32, ConstU64, Contains, OnIdle, OnInitialize, StorageVersion, + }, + weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight, WeightMeter}, +}; +use frame_system::{EventRecord, Phase}; +use pallet_revive_fixtures::{bench::dummy_unique, compile_module}; +use pallet_revive_uapi::ReturnErrorCode as RuntimeReturnCode; +use sp_io::hashing::blake2_256; +use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; +use sp_runtime::{ + testing::H256, + traits::{BlakeTwo256, Convert, IdentityLookup}, + AccountId32, BuildStorage, DispatchError, Perbill, TokenError, +}; + +use self::{ + test_debug::TestDebug, + test_utils::{ensure_stored, expected_deposit}, +}; +use crate::{ + self as pallet_revive, + address::{create1, create2, AddressMapper}, + chain_extension::{ + ChainExtension, Environment, Ext, RegisteredChainExtension, Result as ExtensionResult, + RetVal, ReturnFlags, + }, + exec::Key, + limits, + primitives::CodeUploadReturnValue, + storage::DeletionQueueManager, + test_utils::{builder::Contract, *}, + tests::test_utils::{get_contract, get_contract_checked}, + wasm::Memory, + weights::WeightInfo, + BalanceOf, Code, CodeInfoOf, CollectEvents, Config, ContractInfo, ContractInfoOf, DebugInfo, + DefaultAddressMapper, DeletionQueueCounter, Error, HoldReason, Origin, Pallet, PristineCode, + H160, +}; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Utility: pallet_utility, + Contracts: pallet_revive, + Proxy: pallet_proxy, + Dummy: pallet_dummy + } +); + +macro_rules! assert_return_code { + ( $x:expr , $y:expr $(,)? ) => {{ + assert_eq!(u32::from_le_bytes($x.data[..].try_into().unwrap()), $y as u32); + }}; +} + +macro_rules! assert_refcount { + ( $code_hash:expr , $should:expr $(,)? ) => {{ + let is = crate::CodeInfoOf::::get($code_hash).map(|m| m.refcount()).unwrap(); + assert_eq!(is, $should); + }}; +} + +pub mod test_utils { + use codec::{Encode, MaxEncodedLen}; + use frame_support::traits::fungible::{InspectHold, Mutate}; + use sp_core::H160; + + use super::{Contracts, DepositPerByte, DepositPerItem, Test}; + use crate::{ + address::AddressMapper, exec::AccountIdOf, BalanceOf, CodeInfo, CodeInfoOf, Config, + ContractInfo, ContractInfoOf, PristineCode, + }; + + pub fn place_contract(address: &AccountIdOf, code_hash: sp_core::H256) { + set_balance(address, Contracts::min_balance() * 10); + >::insert(code_hash, CodeInfo::new(address.clone())); + let address = ::AddressMapper::to_address(&address); + let contract = >::new(&address, 0, code_hash).unwrap(); + >::insert(address, contract); + } + pub fn set_balance(who: &AccountIdOf, amount: u64) { + let _ = ::Currency::set_balance(who, amount); + } + pub fn get_balance(who: &AccountIdOf) -> u64 { + ::Currency::free_balance(who) + } + pub fn get_balance_on_hold( + reason: &::RuntimeHoldReason, + who: &AccountIdOf, + ) -> u64 { + ::Currency::balance_on_hold(reason.into(), who) + } + pub fn get_contract(addr: &H160) -> ContractInfo { + get_contract_checked(addr).unwrap() + } + pub fn get_contract_checked(addr: &H160) -> Option> { + ContractInfoOf::::get(addr) + } + pub fn get_code_deposit(code_hash: &sp_core::H256) -> BalanceOf { + crate::CodeInfoOf::::get(code_hash).unwrap().deposit() + } + pub fn contract_info_storage_deposit(addr: &H160) -> BalanceOf { + let contract_info = self::get_contract(&addr); + let info_size = contract_info.encoded_size() as u64; + DepositPerByte::get() + .saturating_mul(info_size) + .saturating_add(DepositPerItem::get()) + } + pub fn expected_deposit(code_len: usize) -> u64 { + // For code_info, the deposit for max_encoded_len is taken. + let code_info_len = CodeInfo::::max_encoded_len() as u64; + // Calculate deposit to be reserved. + // We add 2 storage items: one for code, other for code_info + DepositPerByte::get().saturating_mul(code_len as u64 + code_info_len) + + DepositPerItem::get().saturating_mul(2) + } + pub fn ensure_stored(code_hash: sp_core::H256) -> usize { + // Assert that code_info is stored + assert!(CodeInfoOf::::contains_key(&code_hash)); + // Assert that contract code is stored, and get its size. + PristineCode::::try_get(&code_hash).unwrap().len() + } + pub fn u256_bytes(u: u64) -> [u8; 32] { + let mut buffer = [0u8; 32]; + let bytes = u.to_le_bytes(); + buffer[..8].copy_from_slice(&bytes); + buffer + } +} + +mod builder { + use sp_core::{H160, H256}; + + use super::Test; + use crate::{ + test_utils::{builder::*, ALICE}, + tests::RuntimeOrigin, + Code, + }; + + pub fn bare_instantiate(code: Code) -> BareInstantiateBuilder { + BareInstantiateBuilder::::bare_instantiate(RuntimeOrigin::signed(ALICE), code) + } + + pub fn bare_call(dest: H160) -> BareCallBuilder { + BareCallBuilder::::bare_call(RuntimeOrigin::signed(ALICE), dest) + } + + pub fn instantiate_with_code(code: Vec) -> InstantiateWithCodeBuilder { + InstantiateWithCodeBuilder::::instantiate_with_code( + RuntimeOrigin::signed(ALICE), + code, + ) + } + + pub fn instantiate(code_hash: H256) -> InstantiateBuilder { + InstantiateBuilder::::instantiate(RuntimeOrigin::signed(ALICE), code_hash) + } + + pub fn call(dest: H160) -> CallBuilder { + CallBuilder::::call(RuntimeOrigin::signed(ALICE), dest) + } +} + +impl Test { + pub fn set_unstable_interface(unstable_interface: bool) { + UNSTABLE_INTERFACE.with(|v| *v.borrow_mut() = unstable_interface); + } +} + +parameter_types! { + static TestExtensionTestValue: TestExtension = Default::default(); +} + +#[derive(Clone)] +pub struct TestExtension { + enabled: bool, + last_seen_buffer: Vec, + last_seen_input_len: u32, +} + +#[derive(Default)] +pub struct RevertingExtension; + +#[derive(Default)] +pub struct DisabledExtension; + +#[derive(Default)] +pub struct TempStorageExtension { + storage: u32, +} + +impl TestExtension { + fn disable() { + TestExtensionTestValue::mutate(|e| e.enabled = false) + } + + fn last_seen_buffer() -> Vec { + TestExtensionTestValue::get().last_seen_buffer.clone() + } + + fn last_seen_input_len() -> u32 { + TestExtensionTestValue::get().last_seen_input_len + } +} + +impl Default for TestExtension { + fn default() -> Self { + Self { enabled: true, last_seen_buffer: vec![], last_seen_input_len: 0 } + } +} + +impl ChainExtension for TestExtension { + fn call(&mut self, mut env: Environment) -> ExtensionResult + where + E: Ext, + M: ?Sized + Memory, + { + let func_id = env.func_id(); + let id = env.ext_id() as u32 | func_id as u32; + match func_id { + 0 => { + let input = env.read(8)?; + env.write(&input, false, None)?; + TestExtensionTestValue::mutate(|e| e.last_seen_buffer = input); + Ok(RetVal::Converging(id)) + }, + 1 => { + TestExtensionTestValue::mutate(|e| e.last_seen_input_len = env.in_len()); + Ok(RetVal::Converging(id)) + }, + 2 => { + let mut enc = &env.read(9)?[4..8]; + let weight = Weight::from_parts( + u32::decode(&mut enc).map_err(|_| Error::::ContractTrapped)?.into(), + 0, + ); + env.charge_weight(weight)?; + Ok(RetVal::Converging(id)) + }, + 3 => Ok(RetVal::Diverging { flags: ReturnFlags::REVERT, data: vec![42, 99] }), + _ => { + panic!("Passed unknown id to test chain extension: {}", func_id); + }, + } + } + + fn enabled() -> bool { + TestExtensionTestValue::get().enabled + } +} + +impl RegisteredChainExtension for TestExtension { + const ID: u16 = 0; +} + +impl ChainExtension for RevertingExtension { + fn call(&mut self, _env: Environment) -> ExtensionResult + where + E: Ext, + M: ?Sized + Memory, + { + Ok(RetVal::Diverging { flags: ReturnFlags::REVERT, data: vec![0x4B, 0x1D] }) + } + + fn enabled() -> bool { + TestExtensionTestValue::get().enabled + } +} + +impl RegisteredChainExtension for RevertingExtension { + const ID: u16 = 1; +} + +impl ChainExtension for DisabledExtension { + fn call(&mut self, _env: Environment) -> ExtensionResult + where + E: Ext, + M: ?Sized + Memory, + { + panic!("Disabled chain extensions are never called") + } + + fn enabled() -> bool { + false + } +} + +impl RegisteredChainExtension for DisabledExtension { + const ID: u16 = 2; +} + +impl ChainExtension for TempStorageExtension { + fn call(&mut self, env: Environment) -> ExtensionResult + where + E: Ext, + M: ?Sized + Memory, + { + let func_id = env.func_id(); + match func_id { + 0 => self.storage = 42, + 1 => assert_eq!(self.storage, 42, "Storage is preserved inside the same call."), + 2 => { + assert_eq!(self.storage, 0, "Storage is different for different calls."); + self.storage = 99; + }, + 3 => assert_eq!(self.storage, 99, "Storage is preserved inside the same call."), + _ => { + panic!("Passed unknown id to test chain extension: {}", func_id); + }, + } + Ok(RetVal::Converging(0)) + } + + fn enabled() -> bool { + TestExtensionTestValue::get().enabled + } +} + +impl RegisteredChainExtension for TempStorageExtension { + const ID: u16 = 3; +} + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max( + Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX), + ); + pub static ExistentialDeposit: u64 = 1; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId32; + type Block = Block; + type Lookup = IdentityLookup; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; + type ExistentialDeposit = ExistentialDeposit; + type ReserveIdentifier = [u8; 8]; +} + +#[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)] +impl pallet_timestamp::Config for Test {} + +impl pallet_utility::Config for Test { + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +impl pallet_proxy::Config for Test { + type AnnouncementDepositBase = ConstU64<1>; + type AnnouncementDepositFactor = ConstU64<1>; + type CallHasher = BlakeTwo256; + type Currency = Balances; + type MaxPending = ConstU32<32>; + type MaxProxies = ConstU32<32>; + type ProxyDepositBase = ConstU64<1>; + type ProxyDepositFactor = ConstU64<1>; + type ProxyType = (); + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +impl pallet_dummy::Config for Test {} + +parameter_types! { + pub static DepositPerByte: BalanceOf = 1; + pub const DepositPerItem: BalanceOf = 2; + pub static CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); + pub static ChainId: u64 = 384; +} + +impl Convert> for Test { + fn convert(w: Weight) -> BalanceOf { + w.ref_time() + } +} + +/// A filter whose filter function can be swapped at runtime. +pub struct TestFilter; + +#[derive(Clone)] +pub struct Filters { + filter: fn(&RuntimeCall) -> bool, +} + +impl Default for Filters { + fn default() -> Self { + Filters { filter: (|_| true) } + } +} + +parameter_types! { + static CallFilter: Filters = Default::default(); +} + +impl TestFilter { + pub fn set_filter(filter: fn(&RuntimeCall) -> bool) { + CallFilter::mutate(|fltr| fltr.filter = filter); + } +} + +impl Contains for TestFilter { + fn contains(call: &RuntimeCall) -> bool { + (CallFilter::get().filter)(call) + } +} + +parameter_types! { + pub static UploadAccount: Option<::AccountId> = None; + pub static InstantiateAccount: Option<::AccountId> = None; +} + +pub struct EnsureAccount(core::marker::PhantomData<(T, A)>); +impl>>> + EnsureOrigin<::RuntimeOrigin> for EnsureAccount +where + ::AccountId: From, +{ + type Success = T::AccountId; + + fn try_origin(o: T::RuntimeOrigin) -> Result { + let who = as EnsureOrigin<_>>::try_origin(o.clone())?; + if matches!(A::get(), Some(a) if who != a) { + return Err(o); + } + + Ok(who) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Err(()) + } +} +parameter_types! { + pub static UnstableInterface: bool = true; +} + +#[derive_impl(crate::config_preludes::TestDefaultConfig)] +impl Config for Test { + type AddressMapper = DefaultAddressMapper; + type CallFilter = TestFilter; + type ChainExtension = + (TestExtension, DisabledExtension, RevertingExtension, TempStorageExtension); + type ChainId = ChainId; + type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; + type Currency = Balances; + type Debug = TestDebug; + type DepositPerByte = DepositPerByte; + type DepositPerItem = DepositPerItem; + type InstantiateOrigin = EnsureAccount; + type Time = Timestamp; + type UnsafeUnstableInterface = UnstableInterface; + type UploadOrigin = EnsureAccount; +} + +pub struct ExtBuilder { + existential_deposit: u64, + storage_version: Option, + code_hashes: Vec, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + existential_deposit: ExistentialDeposit::get(), + storage_version: None, + code_hashes: vec![], + } + } +} + +impl ExtBuilder { + pub fn existential_deposit(mut self, existential_deposit: u64) -> Self { + self.existential_deposit = existential_deposit; + self + } + + pub fn with_code_hashes(mut self, code_hashes: Vec) -> Self { + self.code_hashes = code_hashes; + self + } + + pub fn set_associated_consts(&self) { + EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); + } + + pub fn build(self) -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + self.set_associated_consts(); + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.register_extension(KeystoreExt::new(MemoryKeystore::new())); + ext.execute_with(|| { + use frame_support::traits::OnGenesis; + + Pallet::::on_genesis(); + if let Some(storage_version) = self.storage_version { + storage_version.put::>(); + } + System::set_block_number(1) + }); + ext.execute_with(|| { + for code_hash in self.code_hashes { + CodeInfoOf::::insert(code_hash, crate::CodeInfo::new(ALICE)); + } + }); + ext + } +} + +fn initialize_block(number: u64) { + System::reset_events(); + System::initialize(&number, &[0u8; 32].into(), &Default::default()); +} + +struct ExtensionInput<'a> { + extension_id: u16, + func_id: u16, + extra: &'a [u8], +} + +impl<'a> ExtensionInput<'a> { + fn to_vec(&self) -> Vec { + ((self.extension_id as u32) << 16 | (self.func_id as u32)) + .to_le_bytes() + .iter() + .chain(self.extra) + .cloned() + .collect() + } +} + +impl<'a> From> for Vec { + fn from(input: ExtensionInput) -> Vec { + input.to_vec() + } +} + +impl Default for Origin { + fn default() -> Self { + Self::Signed(ALICE) + } +} + +/// We can only run the tests if we have a riscv toolchain installed +#[cfg(feature = "riscv")] +mod run_tests { + use pretty_assertions::{assert_eq, assert_ne}; + use sp_core::U256; + + use super::*; + + #[test] + fn calling_plain_account_is_balance_transfer() { + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000); + assert!(!>::contains_key(BOB_ADDR)); + assert_eq!(test_utils::get_balance(&BOB_CONTRACT_ID), 0); + let result = builder::bare_call(BOB_ADDR).value(42).build_and_unwrap_result(); + assert_eq!(test_utils::get_balance(&BOB_CONTRACT_ID), 42); + assert_eq!(result, Default::default()); + }); + } + + #[test] + fn instantiate_and_call_and_deposit_event() { + let (wasm, code_hash) = compile_module("event_and_return_on_deploy").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let value = 100; + + // We determine the storage deposit limit after uploading because it depends on ALICEs + // free balance which is changed by uploading a module. + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + wasm, + deposit_limit::(), + )); + + // Drop previous events + initialize_block(2); + + // Check at the end to get hash on error easily + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Existing(code_hash)) + .value(value) + .build_and_unwrap_contract(); + assert!(ContractInfoOf::::contains_key(&addr)); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: account_id.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: account_id.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: value, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::ContractEmitted { + contract: addr, + data: vec![1, 2, 3, 4], + topics: vec![H256::repeat_byte(42)], + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Instantiated { + deployer: ALICE_ADDR, + contract: addr + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndHeld { + from: ALICE_ADDR, + to: addr, + amount: test_utils::contract_info_storage_deposit(&addr), + } + ), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn create1_address_from_extrinsic() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + wasm.clone(), + deposit_limit::(), + )); + + assert_eq!(System::account_nonce(&ALICE), 0); + + for nonce in 0..3 { + let Contract { addr, .. } = builder::bare_instantiate(Code::Existing(code_hash)) + .salt(None) + .build_and_unwrap_contract(); + assert!(ContractInfoOf::::contains_key(&addr)); + assert_eq!( + addr, + create1(&::AddressMapper::to_address(&ALICE), nonce) + ); + } + assert_eq!(System::account_nonce(&ALICE), 3); + + for nonce in 3..6 { + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm.clone())) + .salt(None) + .build_and_unwrap_contract(); + assert!(ContractInfoOf::::contains_key(&addr)); + assert_eq!( + addr, + create1(&::AddressMapper::to_address(&ALICE), nonce) + ); + } + assert_eq!(System::account_nonce(&ALICE), 6); + }); + } + + #[test] + fn deposit_event_max_value_limit() { + let (wasm, _code_hash) = compile_module("event_size").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + // Create + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(30_000) + .build_and_unwrap_contract(); + + // Call contract with allowed storage value. + assert_ok!(builder::call(addr) + .gas_limit(GAS_LIMIT.set_ref_time(GAS_LIMIT.ref_time() * 2)) // we are copying a huge buffer, + .data(limits::PAYLOAD_BYTES.encode()) + .build()); + + // Call contract with too large a storage value. + assert_err_ignore_postinfo!( + builder::call(addr).data((limits::PAYLOAD_BYTES + 1).encode()).build(), + Error::::ValueTooLarge, + ); + }); + } + + // Fail out of fuel (ref_time weight) in the engine. + #[test] + fn run_out_of_fuel_engine() { + let (wasm, _code_hash) = compile_module("run_out_of_gas").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(100 * min_balance) + .build_and_unwrap_contract(); + + // Call the contract with a fixed gas limit. It must run out of gas because it just + // loops forever. + assert_err_ignore_postinfo!( + builder::call(addr) + .gas_limit(Weight::from_parts(10_000_000_000, u64::MAX)) + .build(), + Error::::OutOfGas, + ); + }); + } + + // Fail out of fuel (ref_time weight) in the host. + #[test] + fn run_out_of_fuel_host() { + let (code, _hash) = compile_module("chain_extension").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let gas_limit = Weight::from_parts(u32::MAX as u64, GAS_LIMIT.proof_size()); + + // Use chain extension to charge more ref_time than it is available. + let result = builder::bare_call(addr) + .gas_limit(gas_limit) + .data( + ExtensionInput { extension_id: 0, func_id: 2, extra: &u32::MAX.encode() } + .into(), + ) + .build() + .result; + assert_err!(result, >::OutOfGas); + }); + } + + #[test] + fn gas_syncs_work() { + let (code, _code_hash) = compile_module("caller_is_origin_n").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let contract = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let result = builder::bare_call(contract.addr).data(0u32.encode()).build(); + assert_ok!(result.result); + let engine_consumed_noop = result.gas_consumed.ref_time(); + + let result = builder::bare_call(contract.addr).data(1u32.encode()).build(); + assert_ok!(result.result); + let gas_consumed_once = result.gas_consumed.ref_time(); + let host_consumed_once = + ::WeightInfo::seal_caller_is_origin().ref_time(); + let engine_consumed_once = + gas_consumed_once - host_consumed_once - engine_consumed_noop; + + let result = builder::bare_call(contract.addr).data(2u32.encode()).build(); + assert_ok!(result.result); + let gas_consumed_twice = result.gas_consumed.ref_time(); + let host_consumed_twice = host_consumed_once * 2; + let engine_consumed_twice = + gas_consumed_twice - host_consumed_twice - engine_consumed_noop; + + // Second contract just repeats first contract's instructions twice. + // If runtime syncs gas with the engine properly, this should pass. + assert_eq!(engine_consumed_twice, engine_consumed_once * 2); + }); + } + + /// Check that contracts with the same account id have different trie ids. + /// Check the `Nonce` storage item for more information. + #[test] + fn instantiate_unique_trie_id() { + let (wasm, code_hash) = compile_module("self_destruct").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, deposit_limit::()) + .unwrap(); + + // Instantiate the contract and store its trie id for later comparison. + let Contract { addr, .. } = + builder::bare_instantiate(Code::Existing(code_hash)).build_and_unwrap_contract(); + let trie_id = get_contract(&addr).trie_id; + + // Try to instantiate it again without termination should yield an error. + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).build(), + >::DuplicateContract, + ); + + // Terminate the contract. + assert_ok!(builder::call(addr).build()); + + // Re-Instantiate after termination. + assert_ok!(builder::instantiate(code_hash).build()); + + // Trie ids shouldn't match or we might have a collision + assert_ne!(trie_id, get_contract(&addr).trie_id); + }); + } + + #[test] + fn storage_work() { + let (code, _code_hash) = compile_module("storage").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + builder::bare_call(addr).build_and_unwrap_result(); + }); + } + + #[test] + fn storage_max_value_limit() { + let (wasm, _code_hash) = compile_module("storage_size").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + // Create + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(30_000) + .build_and_unwrap_contract(); + get_contract(&addr); + + // Call contract with allowed storage value. + assert_ok!(builder::call(addr) + .gas_limit(GAS_LIMIT.set_ref_time(GAS_LIMIT.ref_time() * 2)) // we are copying a huge buffer + .data(limits::PAYLOAD_BYTES.encode()) + .build()); + + // Call contract with too large a storage value. + assert_err_ignore_postinfo!( + builder::call(addr).data((limits::PAYLOAD_BYTES + 1).encode()).build(), + Error::::ValueTooLarge, + ); + }); + } + + #[test] + fn transient_storage_work() { + let (code, _code_hash) = compile_module("transient_storage").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + builder::bare_call(addr).build_and_unwrap_result(); + }); + } + + #[test] + fn transient_storage_limit_in_call() { + let (wasm_caller, _code_hash_caller) = + compile_module("create_transient_storage_and_call").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module("set_transient_storage").unwrap(); + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(wasm_callee)).build_and_unwrap_contract(); + + // Call contracts with storage values within the limit. + // Caller and Callee contracts each set a transient storage value of size 100. + assert_ok!(builder::call(addr_caller) + .data((100u32, 100u32, &addr_callee).encode()) + .build(),); + + // Call a contract with a storage value that is too large. + // Limit exceeded in the caller contract. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data((4u32 * 1024u32, 200u32, &addr_callee).encode()) + .build(), + >::OutOfTransientStorage, + ); + + // Call a contract with a storage value that is too large. + // Limit exceeded in the callee contract. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data((50u32, 4 * 1024u32, &addr_callee).encode()) + .build(), + >::ContractTrapped + ); + }); + } + + #[test] + fn deploy_and_call_other_contract() { + let (caller_wasm, _caller_code_hash) = compile_module("caller_contract").unwrap(); + let (callee_wasm, callee_code_hash) = compile_module("return_with_data").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + + // Create + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr: caller_addr, account_id: caller_account } = + builder::bare_instantiate(Code::Upload(caller_wasm)) + .value(100_000) + .build_and_unwrap_contract(); + + let callee_addr = create2( + &caller_addr, + &callee_wasm, + &[0, 1, 34, 51, 68, 85, 102, 119], // hard coded in wasm + &[0u8; 32], + ); + let callee_account = ::AddressMapper::to_account_id(&callee_addr); + + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + callee_wasm, + deposit_limit::(), + ) + .unwrap(); + + // Drop previous events + initialize_block(2); + + // Call BOB contract, which attempts to instantiate and call the callee contract and + // makes various assertions on the results from those calls. + assert_ok!(builder::call(caller_addr).data(callee_code_hash.as_ref().to_vec()).build()); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: callee_account.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: callee_account.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: callee_account.clone(), + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: caller_account.clone(), + to: callee_account.clone(), + amount: 32768 // hardcoded in wasm + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Instantiated { + deployer: caller_addr, + contract: callee_addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: caller_account.clone(), + to: callee_account.clone(), + amount: 32768, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(caller_account.clone()), + contract: callee_addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: caller_addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndHeld { + from: ALICE_ADDR, + to: callee_addr, + amount: test_utils::contract_info_storage_deposit(&callee_addr), + } + ), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn delegate_call() { + let (caller_wasm, _caller_code_hash) = compile_module("delegate_call").unwrap(); + let (callee_wasm, callee_code_hash) = compile_module("delegate_call_lib").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the 'caller' + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_wasm)) + .value(300_000) + .build_and_unwrap_contract(); + // Only upload 'callee' code + assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), callee_wasm, 100_000,)); + + assert_ok!(builder::call(caller_addr) + .value(1337) + .data(callee_code_hash.as_ref().to_vec()) + .build()); + }); + } + + #[test] + fn transfer_expendable_cannot_kill_account() { + let (wasm, _code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the BOB contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(1_000) + .build_and_unwrap_contract(); + + // Check that the BOB contract has been instantiated. + get_contract(&addr); + + let account = ::AddressMapper::to_account_id(&addr); + let total_balance = ::Currency::total_balance(&account); + + assert_eq!( + test_utils::get_balance_on_hold( + &HoldReason::StorageDepositReserve.into(), + &account + ), + test_utils::contract_info_storage_deposit(&addr) + ); + + // Some ot the total balance is held, so it can't be transferred. + assert_err!( + <::Currency as Mutate>::transfer( + &account, + &ALICE, + total_balance, + Preservation::Expendable, + ), + TokenError::FundsUnavailable, + ); + + assert_eq!(::Currency::total_balance(&account), total_balance); + }); + } + + #[test] + fn cannot_self_destruct_through_draining() { + let (wasm, _code_hash) = compile_module("drain").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let value = 1_000; + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(value) + .build_and_unwrap_contract(); + let account = ::AddressMapper::to_account_id(&addr); + + // Check that the BOB contract has been instantiated. + get_contract(&addr); + + // Call BOB which makes it send all funds to the zero address + // The contract code asserts that the transfer fails with the correct error code + assert_ok!(builder::call(addr).build()); + + // Make sure the account wasn't remove by sending all free balance away. + assert_eq!( + ::Currency::total_balance(&account), + value + test_utils::contract_info_storage_deposit(&addr) + min_balance, + ); + }); + } + + #[test] + fn cannot_self_destruct_through_storage_refund_after_price_change() { + let (wasm, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let contract = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + let info_deposit = test_utils::contract_info_storage_deposit(&contract.addr); + + // Check that the contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&contract.addr).total_deposit(), info_deposit); + assert_eq!(get_contract(&contract.addr).extra_deposit(), 0); + assert_eq!( + ::Currency::total_balance(&contract.account_id), + info_deposit + min_balance + ); + + // Create 100 bytes of storage with a price of per byte and a single storage item of + // price 2 + assert_ok!(builder::call(contract.addr).data(100u32.to_le_bytes().to_vec()).build()); + assert_eq!(get_contract(&contract.addr).total_deposit(), info_deposit + 102); + + // Increase the byte price and trigger a refund. This should not have any influence + // because the removal is pro rata and exactly those 100 bytes should have been + // removed. + DEPOSIT_PER_BYTE.with(|c| *c.borrow_mut() = 500); + assert_ok!(builder::call(contract.addr).data(0u32.to_le_bytes().to_vec()).build()); + + // Make sure the account wasn't removed by the refund + assert_eq!( + ::Currency::total_balance(&contract.account_id), + get_contract(&contract.addr).total_deposit() + min_balance, + ); + assert_eq!(get_contract(&contract.addr).extra_deposit(), 2); + }); + } + + #[test] + fn cannot_self_destruct_while_live() { + let (wasm, _code_hash) = compile_module("self_destruct").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the BOB contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(100_000) + .build_and_unwrap_contract(); + + // Check that the BOB contract has been instantiated. + get_contract(&addr); + + // Call BOB with input data, forcing it make a recursive call to itself to + // self-destruct, resulting in a trap. + assert_err_ignore_postinfo!( + builder::call(addr).data(vec![0]).build(), + Error::::ContractTrapped, + ); + + // Check that BOB is still there. + get_contract(&addr); + }); + } + + #[test] + fn self_destruct_works() { + let (wasm, code_hash) = compile_module("self_destruct").unwrap(); + ExtBuilder::default().existential_deposit(1_000).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(Ð_DJANGO, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let contract = builder::bare_instantiate(Code::Upload(wasm)) + .value(100_000) + .build_and_unwrap_contract(); + + // Check that the BOB contract has been instantiated. + let _ = get_contract(&contract.addr); + + let info_deposit = test_utils::contract_info_storage_deposit(&contract.addr); + + // Drop all previous events + initialize_block(2); + + // Call BOB without input data which triggers termination. + assert_matches!(builder::call(contract.addr).build(), Ok(_)); + + // Check that code is still there but refcount dropped to zero. + assert_refcount!(&code_hash, 0); + + // Check that account is gone + assert!(get_contract_checked(&contract.addr).is_none()); + assert_eq!(::Currency::total_balance(&contract.account_id), 0); + + // Check that the beneficiary (django) got remaining balance. + assert_eq!( + ::Currency::free_balance(ETH_DJANGO), + 1_000_000 + 100_000 + min_balance + ); + + // Check that the Alice is missing Django's benefit. Within ALICE's total balance + // there's also the code upload deposit held. + assert_eq!( + ::Currency::total_balance(&ALICE), + 1_000_000 - (100_000 + min_balance) + ); + + pretty_assertions::assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Terminated { + contract: contract.addr, + beneficiary: DJANGO_ADDR, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: contract.addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndReleased { + from: contract.addr, + to: ALICE_ADDR, + amount: info_deposit, + } + ), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::KilledAccount { + account: contract.account_id.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: contract.account_id.clone(), + to: ETH_DJANGO, + amount: 100_000 + min_balance, + }), + topics: vec![], + }, + ], + ); + }); + } + + // This tests that one contract cannot prevent another from self-destructing by sending it + // additional funds after it has been drained. + #[test] + fn destroy_contract_and_transfer_funds() { + let (callee_wasm, callee_code_hash) = compile_module("self_destruct").unwrap(); + let (caller_wasm, _caller_code_hash) = compile_module("destroy_and_transfer").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + // Create code hash for bob to instantiate + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + callee_wasm.clone(), + deposit_limit::(), + ) + .unwrap(); + + // This deploys the BOB contract, which in turn deploys the CHARLIE contract during + // construction. + let Contract { addr: addr_bob, .. } = + builder::bare_instantiate(Code::Upload(caller_wasm)) + .value(200_000) + .data(callee_code_hash.as_ref().to_vec()) + .build_and_unwrap_contract(); + + // Check that the CHARLIE contract has been instantiated. + let salt = [47; 32]; // hard coded in fixture. + let addr_charlie = create2(&addr_bob, &callee_wasm, &[], &salt); + get_contract(&addr_charlie); + + // Call BOB, which calls CHARLIE, forcing CHARLIE to self-destruct. + assert_ok!(builder::call(addr_bob).data(addr_charlie.encode()).build()); + + // Check that CHARLIE has moved on to the great beyond (ie. died). + assert!(get_contract_checked(&addr_charlie).is_none()); + }); + } + + #[test] + fn cannot_self_destruct_in_constructor() { + let (wasm, _) = compile_module("self_destructing_constructor").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Fail to instantiate the BOB because the constructor calls seal_terminate. + assert_err_ignore_postinfo!( + builder::instantiate_with_code(wasm).value(100_000).build(), + Error::::TerminatedInConstructor, + ); + }); + } + + #[test] + fn crypto_hashes() { + let (wasm, _code_hash) = compile_module("crypto_hashes").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the CRYPTO_HASHES contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(100_000) + .build_and_unwrap_contract(); + // Perform the call. + let input = b"_DEAD_BEEF"; + use sp_io::hashing::*; + // Wraps a hash function into a more dynamic form usable for testing. + macro_rules! dyn_hash_fn { + ($name:ident) => { + Box::new(|input| $name(input).as_ref().to_vec().into_boxed_slice()) + }; + } + // All hash functions and their associated output byte lengths. + let test_cases: &[(Box Box<[u8]>>, usize)] = &[ + (dyn_hash_fn!(sha2_256), 32), + (dyn_hash_fn!(keccak_256), 32), + (dyn_hash_fn!(blake2_256), 32), + (dyn_hash_fn!(blake2_128), 16), + ]; + // Test the given hash functions for the input: "_DEAD_BEEF" + for (n, (hash_fn, expected_size)) in test_cases.iter().enumerate() { + // We offset data in the contract tables by 1. + let mut params = vec![(n + 1) as u8]; + params.extend_from_slice(input); + let result = builder::bare_call(addr).data(params).build_and_unwrap_result(); + assert!(!result.did_revert()); + let expected = hash_fn(input.as_ref()); + assert_eq!(&result.data[..*expected_size], &*expected); + } + }) + } + + #[test] + fn transfer_return_code() { + let (wasm, _code_hash) = compile_module("transfer_return_code").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let contract = builder::bare_instantiate(Code::Upload(wasm)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + // Contract has only the minimal balance so any transfer will fail. + ::Currency::set_balance(&contract.account_id, min_balance); + let result = builder::bare_call(contract.addr).build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + }); + } + + #[test] + fn call_return_code() { + use test_utils::u256_bytes; + + let (caller_code, _caller_hash) = compile_module("call_return_code").unwrap(); + let (callee_code, _callee_hash) = compile_module("ok_trap_revert").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let _ = ::Currency::set_balance(&CHARLIE, 1000 * min_balance); + + let bob = builder::bare_instantiate(Code::Upload(caller_code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + // Contract calls into Django which is no valid contract + // This will be a balance transfer into a new account + // with more than the contract has which will make the transfer fail + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&u256_bytes(min_balance * 200)) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Sending less than the minimum balance will also make the transfer fail + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&u256_bytes(42)) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Sending at least the minimum balance should result in success but + // no code called. + assert_eq!(test_utils::get_balance(Ð_DJANGO), 0); + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&u256_bytes(55)) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::Success); + assert_eq!(test_utils::get_balance(Ð_DJANGO), 55); + + let django = builder::bare_instantiate(Code::Upload(callee_code)) + .origin(RuntimeOrigin::signed(CHARLIE)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + // Sending more than the contract has will make the transfer fail. + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&django.addr) + .iter() + .chain(&u256_bytes(min_balance * 300)) + .chain(&0u32.to_le_bytes()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Contract has enough balance but callee reverts because "1" is passed. + ::Currency::set_balance(&bob.account_id, min_balance + 1000); + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&django.addr) + .iter() + .chain(&u256_bytes(5)) + .chain(&1u32.to_le_bytes()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeReverted); + + // Contract has enough balance but callee traps because "2" is passed. + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&django.addr) + .iter() + .chain(&u256_bytes(5)) + .chain(&2u32.to_le_bytes()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeTrapped); + }); + } + + #[test] + fn instantiate_return_code() { + let (caller_code, _caller_hash) = compile_module("instantiate_return_code").unwrap(); + let (callee_code, callee_hash) = compile_module("ok_trap_revert").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let _ = ::Currency::set_balance(&CHARLIE, 1000 * min_balance); + let callee_hash = callee_hash.as_ref().to_vec(); + + assert_ok!(builder::instantiate_with_code(callee_code) + .value(min_balance * 100) + .build()); + + let contract = builder::bare_instantiate(Code::Upload(caller_code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + // Contract has only the minimal balance so any transfer will fail. + ::Currency::set_balance(&contract.account_id, min_balance); + let result = builder::bare_call(contract.addr) + .data(callee_hash.clone()) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Contract has enough balance but the passed code hash is invalid + ::Currency::set_balance(&contract.account_id, min_balance + 10_000); + let result = + builder::bare_call(contract.addr).data(vec![0; 33]).build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CodeNotFound); + + // Contract has enough balance but callee reverts because "1" is passed. + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&1u32.to_le_bytes()).cloned().collect()) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeReverted); + + // Contract has enough balance but callee traps because "2" is passed. + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&2u32.to_le_bytes()).cloned().collect()) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeTrapped); + }); + } + + #[test] + fn disabled_chain_extension_errors_on_call() { + let (code, _hash) = compile_module("chain_extension").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let contract = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + TestExtension::disable(); + assert_err_ignore_postinfo!( + builder::call(contract.addr).data(vec![7u8; 8]).build(), + Error::::NoChainExtension, + ); + }); + } + + #[test] + fn chain_extension_works() { + let (code, _hash) = compile_module("chain_extension").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let contract = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + // 0 = read input buffer and pass it through as output + let input: Vec = + ExtensionInput { extension_id: 0, func_id: 0, extra: &[99] }.into(); + let result = builder::bare_call(contract.addr).data(input.clone()).build(); + assert_eq!(TestExtension::last_seen_buffer(), input); + assert_eq!(result.result.unwrap().data, input); + + // 1 = treat inputs as integer primitives and store the supplied integers + builder::bare_call(contract.addr) + .data(ExtensionInput { extension_id: 0, func_id: 1, extra: &[] }.into()) + .build_and_unwrap_result(); + assert_eq!(TestExtension::last_seen_input_len(), 4); + + // 2 = charge some extra weight (amount supplied in the fifth byte) + let result = builder::bare_call(contract.addr) + .data(ExtensionInput { extension_id: 0, func_id: 2, extra: &0u32.encode() }.into()) + .build(); + assert_ok!(result.result); + let gas_consumed = result.gas_consumed; + let result = builder::bare_call(contract.addr) + .data(ExtensionInput { extension_id: 0, func_id: 2, extra: &42u32.encode() }.into()) + .build(); + assert_ok!(result.result); + assert_eq!(result.gas_consumed.ref_time(), gas_consumed.ref_time() + 42); + let result = builder::bare_call(contract.addr) + .data(ExtensionInput { extension_id: 0, func_id: 2, extra: &95u32.encode() }.into()) + .build(); + assert_ok!(result.result); + assert_eq!(result.gas_consumed.ref_time(), gas_consumed.ref_time() + 95); + + // 3 = diverging chain extension call that sets flags to 0x1 and returns a fixed buffer + let result = builder::bare_call(contract.addr) + .data(ExtensionInput { extension_id: 0, func_id: 3, extra: &[] }.into()) + .build_and_unwrap_result(); + assert_eq!(result.flags, ReturnFlags::REVERT); + assert_eq!(result.data, vec![42, 99]); + + // diverging to second chain extension that sets flags to 0x1 and returns a fixed buffer + // We set the MSB part to 1 (instead of 0) which routes the request into the second + // extension + let result = builder::bare_call(contract.addr) + .data(ExtensionInput { extension_id: 1, func_id: 0, extra: &[] }.into()) + .build_and_unwrap_result(); + assert_eq!(result.flags, ReturnFlags::REVERT); + assert_eq!(result.data, vec![0x4B, 0x1D]); + + // Diverging to third chain extension that is disabled + // We set the MSB part to 2 (instead of 0) which routes the request into the third + // extension + assert_err_ignore_postinfo!( + builder::call(contract.addr) + .data(ExtensionInput { extension_id: 2, func_id: 0, extra: &[] }.into()) + .build(), + Error::::NoChainExtension, + ); + }); + } + + #[test] + fn chain_extension_temp_storage_works() { + let (code, _hash) = compile_module("chain_extension_temp_storage").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let contract = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + // Call func 0 and func 1 back to back. + let stop_recursion = 0u8; + let mut input: Vec = + ExtensionInput { extension_id: 3, func_id: 0, extra: &[] }.into(); + input.extend_from_slice( + ExtensionInput { extension_id: 3, func_id: 1, extra: &[stop_recursion] } + .to_vec() + .as_ref(), + ); + + assert_ok!(builder::bare_call(contract.addr).data(input.clone()).build().result); + }) + } + + #[test] + fn lazy_removal_works() { + let (code, _hash) = compile_module("self_destruct").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let contract = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&contract.addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract + assert_ok!(builder::call(contract.addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&contract.addr)); + + // But value should be still there as the lazy removal did not run, yet. + assert_matches!(child::get(trie, &[99]), Some(42)); + + // Run the lazy removal + Contracts::on_idle(System::block_number(), Weight::MAX); + + // Value should be gone now + assert_matches!(child::get::(trie, &[99]), None); + }); + } + + #[test] + fn lazy_batch_removal_works() { + let (code, _hash) = compile_module("self_destruct").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let mut tries: Vec = vec![]; + + for i in 0..3u8 { + let contract = builder::bare_instantiate(Code::Upload(code.clone())) + .value(min_balance * 100) + .salt(Some([i; 32])) + .build_and_unwrap_contract(); + + let info = get_contract(&contract.addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract. Contract info should be gone, but value should be still + // there as the lazy removal did not run, yet. + assert_ok!(builder::call(contract.addr).build()); + + assert!(!>::contains_key(&contract.addr)); + assert_matches!(child::get(trie, &[99]), Some(42)); + + tries.push(trie.clone()) + } + + // Run single lazy removal + Contracts::on_idle(System::block_number(), Weight::MAX); + + // The single lazy removal should have removed all queued tries + for trie in tries.iter() { + assert_matches!(child::get::(trie, &[99]), None); + } + }); + } + + #[test] + fn lazy_removal_partial_remove_works() { + let (code, _hash) = compile_module("self_destruct").unwrap(); + + // We create a contract with some extra keys above the weight limit + let extra_keys = 7u32; + let mut meter = WeightMeter::with_limit(Weight::from_parts(5_000_000_000, 100 * 1024)); + let (weight_per_key, max_keys) = ContractInfo::::deletion_budget(&meter); + let vals: Vec<_> = (0..max_keys + extra_keys) + .map(|i| (blake2_256(&i.encode()), (i as u32), (i as u32).encode())) + .collect(); + + let mut ext = ExtBuilder::default().existential_deposit(50).build(); + + let trie = ext.execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + + // Put value into the contracts child trie + for val in &vals { + info.write(&Key::Fix(val.0), Some(val.2.clone()), None, false).unwrap(); + } + >::insert(&addr, info.clone()); + + // Terminate the contract + assert_ok!(builder::call(addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&addr)); + + let trie = info.child_trie_info(); + + // But value should be still there as the lazy removal did not run, yet. + for val in &vals { + assert_eq!(child::get::(&trie, &blake2_256(&val.0)), Some(val.1)); + } + + trie.clone() + }); + + // The lazy removal limit only applies to the backend but not to the overlay. + // This commits all keys from the overlay to the backend. + ext.commit_all().unwrap(); + + ext.execute_with(|| { + // Run the lazy removal + ContractInfo::::process_deletion_queue_batch(&mut meter); + + // Weight should be exhausted because we could not even delete all keys + assert!(!meter.can_consume(weight_per_key)); + + let mut num_deleted = 0u32; + let mut num_remaining = 0u32; + + for val in &vals { + match child::get::(&trie, &blake2_256(&val.0)) { + None => num_deleted += 1, + Some(x) if x == val.1 => num_remaining += 1, + Some(_) => panic!("Unexpected value in contract storage"), + } + } + + // All but one key is removed + assert_eq!(num_deleted + num_remaining, vals.len() as u32); + assert_eq!(num_deleted, max_keys); + assert_eq!(num_remaining, extra_keys); + }); + } + + #[test] + fn lazy_removal_does_no_run_on_low_remaining_weight() { + let (code, _hash) = compile_module("self_destruct").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract + assert_ok!(builder::call(addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&addr)); + + // But value should be still there as the lazy removal did not run, yet. + assert_matches!(child::get(trie, &[99]), Some(42)); + + // Assign a remaining weight which is too low for a successful deletion of the contract + let low_remaining_weight = + <::WeightInfo as WeightInfo>::on_process_deletion_queue_batch(); + + // Run the lazy removal + Contracts::on_idle(System::block_number(), low_remaining_weight); + + // Value should still be there, since remaining weight was too low for removal + assert_matches!(child::get::(trie, &[99]), Some(42)); + + // Run the lazy removal while deletion_queue is not full + Contracts::on_initialize(System::block_number()); + + // Value should still be there, since deletion_queue was not full + assert_matches!(child::get::(trie, &[99]), Some(42)); + + // Run on_idle with max remaining weight, this should remove the value + Contracts::on_idle(System::block_number(), Weight::MAX); + + // Value should be gone + assert_matches!(child::get::(trie, &[99]), None); + }); + } + + #[test] + fn lazy_removal_does_not_use_all_weight() { + let (code, _hash) = compile_module("self_destruct").unwrap(); + + let mut meter = WeightMeter::with_limit(Weight::from_parts(5_000_000_000, 100 * 1024)); + let mut ext = ExtBuilder::default().existential_deposit(50).build(); + + let (trie, vals, weight_per_key) = ext.execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + let (weight_per_key, max_keys) = ContractInfo::::deletion_budget(&meter); + assert!(max_keys > 0); + + // We create a contract with one less storage item than we can remove within the limit + let vals: Vec<_> = (0..max_keys - 1) + .map(|i| (blake2_256(&i.encode()), (i as u32), (i as u32).encode())) + .collect(); + + // Put value into the contracts child trie + for val in &vals { + info.write(&Key::Fix(val.0), Some(val.2.clone()), None, false).unwrap(); + } + >::insert(&addr, info.clone()); + + // Terminate the contract + assert_ok!(builder::call(addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&addr)); + + let trie = info.child_trie_info(); + + // But value should be still there as the lazy removal did not run, yet. + for val in &vals { + assert_eq!(child::get::(&trie, &blake2_256(&val.0)), Some(val.1)); + } + + (trie, vals, weight_per_key) + }); + + // The lazy removal limit only applies to the backend but not to the overlay. + // This commits all keys from the overlay to the backend. + ext.commit_all().unwrap(); + + ext.execute_with(|| { + // Run the lazy removal + ContractInfo::::process_deletion_queue_batch(&mut meter); + let base_weight = + <::WeightInfo as WeightInfo>::on_process_deletion_queue_batch(); + assert_eq!(meter.consumed(), weight_per_key.mul(vals.len() as _) + base_weight); + + // All the keys are removed + for val in vals { + assert_eq!(child::get::(&trie, &blake2_256(&val.0)), None); + } + }); + } + + #[test] + fn deletion_queue_ring_buffer_overflow() { + let (code, _hash) = compile_module("self_destruct").unwrap(); + let mut ext = ExtBuilder::default().existential_deposit(50).build(); + + // setup the deletion queue with custom counters + ext.execute_with(|| { + let queue = DeletionQueueManager::from_test_values(u32::MAX - 1, u32::MAX - 1); + >::set(queue); + }); + + // commit the changes to the storage + ext.commit_all().unwrap(); + + ext.execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let mut tries: Vec = vec![]; + + // add 3 contracts to the deletion queue + for i in 0..3u8 { + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code.clone())) + .value(min_balance * 100) + .salt(Some([i; 32])) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract. Contract info should be gone, but value should be still + // there as the lazy removal did not run, yet. + assert_ok!(builder::call(addr).build()); + + assert!(!>::contains_key(&addr)); + assert_matches!(child::get(trie, &[99]), Some(42)); + + tries.push(trie.clone()) + } + + // Run single lazy removal + Contracts::on_idle(System::block_number(), Weight::MAX); + + // The single lazy removal should have removed all queued tries + for trie in tries.iter() { + assert_matches!(child::get::(trie, &[99]), None); + } + + // insert and delete counter values should go from u32::MAX - 1 to 1 + assert_eq!(>::get().as_test_tuple(), (1, 1)); + }) + } + #[test] + fn refcounter() { + let (wasm, code_hash) = compile_module("self_destruct").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Create two contracts with the same code and check that they do in fact share it. + let Contract { addr: addr0, .. } = + builder::bare_instantiate(Code::Upload(wasm.clone())) + .value(min_balance * 100) + .salt(Some([0; 32])) + .build_and_unwrap_contract(); + let Contract { addr: addr1, .. } = + builder::bare_instantiate(Code::Upload(wasm.clone())) + .value(min_balance * 100) + .salt(Some([1; 32])) + .build_and_unwrap_contract(); + assert_refcount!(code_hash, 2); + + // Sharing should also work with the usual instantiate call + let Contract { addr: addr2, .. } = builder::bare_instantiate(Code::Existing(code_hash)) + .value(min_balance * 100) + .salt(Some([2; 32])) + .build_and_unwrap_contract(); + assert_refcount!(code_hash, 3); + + // Terminating one contract should decrement the refcount + assert_ok!(builder::call(addr0).build()); + assert_refcount!(code_hash, 2); + + // remove another one + assert_ok!(builder::call(addr1).build()); + assert_refcount!(code_hash, 1); + + // Pristine code should still be there + PristineCode::::get(code_hash).unwrap(); + + // remove the last contract + assert_ok!(builder::call(addr2).build()); + assert_refcount!(code_hash, 0); + + // refcount is `0` but code should still exists because it needs to be removed manually + assert!(crate::PristineCode::::contains_key(&code_hash)); + }); + } + + #[test] + fn debug_message_works() { + let (wasm, _code_hash) = compile_module("debug_message_works").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(30_000) + .build_and_unwrap_contract(); + let result = builder::bare_call(addr).debug(DebugInfo::UnsafeDebug).build(); + + assert_matches!(result.result, Ok(_)); + assert_eq!(std::str::from_utf8(&result.debug_message).unwrap(), "Hello World!"); + }); + } + + #[test] + fn debug_message_logging_disabled() { + let (wasm, _code_hash) = compile_module("debug_message_logging_disabled").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(30_000) + .build_and_unwrap_contract(); + // the dispatchables always run without debugging + assert_ok!(Contracts::call( + RuntimeOrigin::signed(ALICE), + addr, + 0, + GAS_LIMIT, + deposit_limit::(), + vec![] + )); + }); + } + + #[test] + fn debug_message_invalid_utf8() { + let (wasm, _code_hash) = compile_module("debug_message_invalid_utf8").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(30_000) + .build_and_unwrap_contract(); + let result = builder::bare_call(addr).debug(DebugInfo::UnsafeDebug).build(); + assert_ok!(result.result); + assert!(result.debug_message.is_empty()); + }); + } + + #[test] + fn gas_estimation_for_subcalls() { + let (caller_code, _caller_hash) = compile_module("call_with_limit").unwrap(); + let (call_runtime_code, _caller_hash) = compile_module("call_runtime").unwrap(); + let (dummy_code, _callee_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 2_000 * min_balance); + + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(caller_code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let Contract { addr: addr_dummy, .. } = + builder::bare_instantiate(Code::Upload(dummy_code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let Contract { addr: addr_call_runtime, .. } = + builder::bare_instantiate(Code::Upload(call_runtime_code)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + // Run the test for all of those weight limits for the subcall + let weights = [ + Weight::zero(), + GAS_LIMIT, + GAS_LIMIT * 2, + GAS_LIMIT / 5, + Weight::from_parts(0, GAS_LIMIT.proof_size()), + Weight::from_parts(GAS_LIMIT.ref_time(), 0), + ]; + + // This call is passed to the sub call in order to create a large `required_weight` + let runtime_call = RuntimeCall::Dummy(pallet_dummy::Call::overestimate_pre_charge { + pre_charge: Weight::from_parts(10_000_000_000, 512 * 1024), + actual_weight: Weight::from_parts(1, 1), + }) + .encode(); + + // Encodes which contract should be sub called with which input + let sub_calls: [(&[u8], Vec<_>, bool); 2] = [ + (addr_dummy.as_ref(), vec![], false), + (addr_call_runtime.as_ref(), runtime_call, true), + ]; + + for weight in weights { + for (sub_addr, sub_input, out_of_gas_in_subcall) in &sub_calls { + let input: Vec = sub_addr + .iter() + .cloned() + .chain(weight.ref_time().to_le_bytes()) + .chain(weight.proof_size().to_le_bytes()) + .chain(sub_input.clone()) + .collect(); + + // Call in order to determine the gas that is required for this call + let result_orig = builder::bare_call(addr_caller).data(input.clone()).build(); + assert_ok!(&result_orig.result); + + // If the out of gas happens in the subcall the caller contract + // will just trap. Otherwise we would need to forward an error + // code to signal that the sub contract ran out of gas. + let error: DispatchError = if *out_of_gas_in_subcall { + assert!(result_orig.gas_required.all_gt(result_orig.gas_consumed)); + >::ContractTrapped.into() + } else { + assert_eq!(result_orig.gas_required, result_orig.gas_consumed); + >::OutOfGas.into() + }; + + // Make the same call using the estimated gas. Should succeed. + let result = builder::bare_call(addr_caller) + .gas_limit(result_orig.gas_required) + .storage_deposit_limit(result_orig.storage_deposit.charge_or_zero()) + .data(input.clone()) + .build(); + assert_ok!(&result.result); + + // Check that it fails with too little ref_time + let result = builder::bare_call(addr_caller) + .gas_limit(result_orig.gas_required.sub_ref_time(1)) + .storage_deposit_limit(result_orig.storage_deposit.charge_or_zero()) + .data(input.clone()) + .build(); + assert_err!(result.result, error); + + // Check that it fails with too little proof_size + let result = builder::bare_call(addr_caller) + .gas_limit(result_orig.gas_required.sub_proof_size(1)) + .storage_deposit_limit(result_orig.storage_deposit.charge_or_zero()) + .data(input.clone()) + .build(); + assert_err!(result.result, error); + } + } + }); + } + + #[test] + fn gas_estimation_call_runtime() { + let (caller_code, _caller_hash) = compile_module("call_runtime").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let _ = ::Currency::set_balance(&CHARLIE, 1000 * min_balance); + + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(caller_code)) + .value(min_balance * 100) + .salt(Some([0; 32])) + .build_and_unwrap_contract(); + + // Call something trivial with a huge gas limit so that we can observe the effects + // of pre-charging. This should create a difference between consumed and required. + let call = RuntimeCall::Dummy(pallet_dummy::Call::overestimate_pre_charge { + pre_charge: Weight::from_parts(10_000_000, 1_000), + actual_weight: Weight::from_parts(100, 100), + }); + let result = builder::bare_call(addr_caller).data(call.encode()).build(); + // contract encodes the result of the dispatch runtime + let outcome = u32::decode(&mut result.result.unwrap().data.as_ref()).unwrap(); + assert_eq!(outcome, 0); + assert!(result.gas_required.all_gt(result.gas_consumed)); + + // Make the same call using the required gas. Should succeed. + assert_ok!( + builder::bare_call(addr_caller) + .gas_limit(result.gas_required) + .data(call.encode()) + .build() + .result + ); + }); + } + + #[test] + fn call_runtime_reentrancy_guarded() { + let (caller_code, _caller_hash) = compile_module("call_runtime").unwrap(); + let (callee_code, _callee_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let _ = ::Currency::set_balance(&CHARLIE, 1000 * min_balance); + + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(caller_code)) + .value(min_balance * 100) + .salt(Some([0; 32])) + .build_and_unwrap_contract(); + + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(callee_code)) + .value(min_balance * 100) + .salt(Some([1; 32])) + .build_and_unwrap_contract(); + + // Call pallet_revive call() dispatchable + let call = RuntimeCall::Contracts(crate::Call::call { + dest: addr_callee, + value: 0, + gas_limit: GAS_LIMIT / 3, + storage_deposit_limit: deposit_limit::(), + data: vec![], + }); + + // Call runtime to re-enter back to contracts engine by + // calling dummy contract + let result = + builder::bare_call(addr_caller).data(call.encode()).build_and_unwrap_result(); + // Call to runtime should fail because of the re-entrancy guard + assert_return_code!(result, RuntimeReturnCode::CallRuntimeFailed); + }); + } + + #[test] + fn ecdsa_recover() { + let (wasm, _code_hash) = compile_module("ecdsa_recover").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the ecdsa_recover contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(100_000) + .build_and_unwrap_contract(); + + #[rustfmt::skip] + let signature: [u8; 65] = [ + 161, 234, 203, 74, 147, 96, 51, 212, 5, 174, 231, 9, 142, 48, 137, 201, + 162, 118, 192, 67, 239, 16, 71, 216, 125, 86, 167, 139, 70, 7, 86, 241, + 33, 87, 154, 251, 81, 29, 160, 4, 176, 239, 88, 211, 244, 232, 232, 52, + 211, 234, 100, 115, 230, 47, 80, 44, 152, 166, 62, 50, 8, 13, 86, 175, + 28, + ]; + #[rustfmt::skip] + let message_hash: [u8; 32] = [ + 162, 28, 244, 179, 96, 76, 244, 178, 188, 83, 230, 248, 143, 106, 77, 117, + 239, 95, 244, 171, 65, 95, 62, 153, 174, 166, 182, 28, 130, 73, 196, 208 + ]; + #[rustfmt::skip] + const EXPECTED_COMPRESSED_PUBLIC_KEY: [u8; 33] = [ + 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, + 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, + 152, + ]; + let mut params = vec![]; + params.extend_from_slice(&signature); + params.extend_from_slice(&message_hash); + assert!(params.len() == 65 + 32); + let result = builder::bare_call(addr).data(params).build_and_unwrap_result(); + assert!(!result.did_revert()); + assert_eq!(result.data, EXPECTED_COMPRESSED_PUBLIC_KEY); + }) + } + + #[test] + fn bare_instantiate_returns_events() { + let (wasm, _code_hash) = compile_module("transfer_return_code").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let result = builder::bare_instantiate(Code::Upload(wasm)) + .value(min_balance * 100) + .collect_events(CollectEvents::UnsafeCollect) + .build(); + + let events = result.events.unwrap(); + assert!(!events.is_empty()); + assert_eq!(events, System::events()); + }); + } + + #[test] + fn bare_instantiate_does_not_return_events() { + let (wasm, _code_hash) = compile_module("transfer_return_code").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let result = + builder::bare_instantiate(Code::Upload(wasm)).value(min_balance * 100).build(); + + let events = result.events; + assert!(!System::events().is_empty()); + assert!(events.is_none()); + }); + } + + #[test] + fn bare_call_returns_events() { + let (wasm, _code_hash) = compile_module("transfer_return_code").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let result = + builder::bare_call(addr).collect_events(CollectEvents::UnsafeCollect).build(); + + let events = result.events.unwrap(); + assert_return_code!(&result.result.unwrap(), RuntimeReturnCode::Success); + assert!(!events.is_empty()); + assert_eq!(events, System::events()); + }); + } + + #[test] + fn bare_call_does_not_return_events() { + let (wasm, _code_hash) = compile_module("transfer_return_code").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(min_balance * 100) + .build_and_unwrap_contract(); + + let result = builder::bare_call(addr).build(); + + let events = result.events; + assert_return_code!(&result.result.unwrap(), RuntimeReturnCode::Success); + assert!(!System::events().is_empty()); + assert!(events.is_none()); + }); + } + + #[test] + fn sr25519_verify() { + let (wasm, _code_hash) = compile_module("sr25519_verify").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the sr25519_verify contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm)) + .value(100_000) + .build_and_unwrap_contract(); + + let call_with = |message: &[u8; 11]| { + // Alice's signature for "hello world" + #[rustfmt::skip] + let signature: [u8; 64] = [ + 184, 49, 74, 238, 78, 165, 102, 252, 22, 92, 156, 176, 124, 118, 168, 116, 247, + 99, 0, 94, 2, 45, 9, 170, 73, 222, 182, 74, 60, 32, 75, 64, 98, 174, 69, 55, 83, + 85, 180, 98, 208, 75, 231, 57, 205, 62, 4, 105, 26, 136, 172, 17, 123, 99, 90, 255, + 228, 54, 115, 63, 30, 207, 205, 131, + ]; + + // Alice's public key + #[rustfmt::skip] + let public_key: [u8; 32] = [ + 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, + 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, + ]; + + let mut params = vec![]; + params.extend_from_slice(&signature); + params.extend_from_slice(&public_key); + params.extend_from_slice(message); + + builder::bare_call(addr).data(params).build_and_unwrap_result() + }; + + // verification should succeed for "hello world" + assert_return_code!(call_with(&b"hello world"), RuntimeReturnCode::Success); + + // verification should fail for other messages + assert_return_code!(call_with(&b"hello worlD"), RuntimeReturnCode::Sr25519VerifyFailed); + }); + } + + #[test] + fn failed_deposit_charge_should_roll_back_call() { + let (wasm_caller, _) = compile_module("call_runtime_and_call").unwrap(); + let (wasm_callee, _) = compile_module("store_call").unwrap(); + const ED: u64 = 200; + + let execute = || { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate both contracts. + let caller = builder::bare_instantiate(Code::Upload(wasm_caller.clone())) + .build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(wasm_callee.clone())) + .build_and_unwrap_contract(); + + // Give caller proxy access to Alice. + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(ALICE), + caller.account_id.clone(), + (), + 0 + )); + + // Create a Proxy call that will attempt to transfer away Alice's balance. + let transfer_call = + Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { + dest: CHARLIE, + value: pallet_balances::Pallet::::free_balance(&ALICE) - 2 * ED, + })); + + // Wrap the transfer call in a proxy call. + let transfer_proxy_call = RuntimeCall::Proxy(pallet_proxy::Call::proxy { + real: ALICE, + force_proxy_type: Some(()), + call: transfer_call, + }); + + let data = ( + (ED - DepositPerItem::get()) as u32, // storage length + addr_callee, + transfer_proxy_call, + ); + + builder::call(caller.addr).data(data.encode()).build() + }) + }; + + // With a low enough deposit per byte, the call should succeed. + let result = execute().unwrap(); + + // Bump the deposit per byte to a high value to trigger a FundsUnavailable error. + DEPOSIT_PER_BYTE.with(|c| *c.borrow_mut() = 20); + assert_err_with_weight!(execute(), TokenError::FundsUnavailable, result.actual_weight); + } + + #[test] + fn upload_code_works() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert!(!PristineCode::::contains_key(&code_hash)); + + assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, 1_000,)); + // Ensure the contract was stored and get expected deposit amount to be reserved. + let deposit_expected = expected_deposit(ensure_stored(code_hash)); + + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::CodeStored { + code_hash, + deposit_held: deposit_expected, + uploader: ALICE_ADDR + }), + topics: vec![], + },] + ); + }); + } + + #[test] + fn upload_code_limit_too_low() { + let (wasm, _code_hash) = compile_module("dummy").unwrap(); + let deposit_expected = expected_deposit(wasm.len()); + let deposit_insufficient = deposit_expected.saturating_sub(1); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, deposit_insufficient,), + >::StorageDepositLimitExhausted, + ); + + assert_eq!(System::events(), vec![]); + }); + } + + #[test] + fn upload_code_not_enough_balance() { + let (wasm, _code_hash) = compile_module("dummy").unwrap(); + let deposit_expected = expected_deposit(wasm.len()); + let deposit_insufficient = deposit_expected.saturating_sub(1); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, deposit_insufficient); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, 1_000,), + >::StorageDepositNotEnoughFunds, + ); + + assert_eq!(System::events(), vec![]); + }); + } + + #[test] + fn remove_code_works() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, 1_000,)); + // Ensure the contract was stored and get expected deposit amount to be reserved. + let deposit_expected = expected_deposit(ensure_stored(code_hash)); + + assert_ok!(Contracts::remove_code(RuntimeOrigin::signed(ALICE), code_hash)); + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::CodeStored { + code_hash, + deposit_held: deposit_expected, + uploader: ALICE_ADDR + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::CodeRemoved { + code_hash, + deposit_released: deposit_expected, + remover: ALICE_ADDR + }), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn remove_code_wrong_origin() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, 1_000,)); + // Ensure the contract was stored and get expected deposit amount to be reserved. + let deposit_expected = expected_deposit(ensure_stored(code_hash)); + + assert_noop!( + Contracts::remove_code(RuntimeOrigin::signed(BOB), code_hash), + sp_runtime::traits::BadOrigin, + ); + + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::CodeStored { + code_hash, + deposit_held: deposit_expected, + uploader: ALICE_ADDR + }), + topics: vec![], + },] + ); + }); + } + + #[test] + fn remove_code_in_use() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + assert_ok!(builder::instantiate_with_code(wasm).build()); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::remove_code(RuntimeOrigin::signed(ALICE), code_hash), + >::CodeInUse, + ); + + assert_eq!(System::events(), vec![]); + }); + } + + #[test] + fn remove_code_not_found() { + let (_wasm, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::remove_code(RuntimeOrigin::signed(ALICE), code_hash), + >::CodeNotFound, + ); + + assert_eq!(System::events(), vec![]); + }); + } + + #[test] + fn instantiate_with_zero_balance_works() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Drop previous events + initialize_block(2); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + // Ensure the contract was stored and get expected deposit amount to be reserved. + let deposit_expected = expected_deposit(ensure_stored(code_hash)); + + // Make sure the account exists even though no free balance was send + assert_eq!(::Currency::free_balance(&account_id), min_balance); + assert_eq!( + ::Currency::total_balance(&account_id), + min_balance + test_utils::contract_info_storage_deposit(&addr) + ); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::CodeStored { + code_hash, + deposit_held: deposit_expected, + uploader: ALICE_ADDR + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: account_id.clone(), + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: account_id.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id, + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Instantiated { + deployer: ALICE_ADDR, + contract: addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndHeld { + from: ALICE_ADDR, + to: addr, + amount: test_utils::contract_info_storage_deposit(&addr), + } + ), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn instantiate_with_below_existential_deposit_works() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let value = 50; + + // Drop previous events + initialize_block(2); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = builder::bare_instantiate(Code::Upload(wasm)) + .value(value) + .build_and_unwrap_contract(); + + // Ensure the contract was stored and get expected deposit amount to be reserved. + let deposit_expected = expected_deposit(ensure_stored(code_hash)); + // Make sure the account exists even though not enough free balance was send + assert_eq!(::Currency::free_balance(&account_id), min_balance + value); + assert_eq!( + ::Currency::total_balance(&account_id), + min_balance + value + test_utils::contract_info_storage_deposit(&addr) + ); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::CodeStored { + code_hash, + deposit_held: deposit_expected, + uploader: ALICE_ADDR + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: account_id.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: account_id.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: 50, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Instantiated { + deployer: ALICE_ADDR, + contract: addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndHeld { + from: ALICE_ADDR, + to: addr, + amount: test_utils::contract_info_storage_deposit(&addr), + } + ), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn storage_deposit_works() { + let (wasm, _code_hash) = compile_module("multi_store").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + let mut deposit = test_utils::contract_info_storage_deposit(&addr); + + // Drop previous events + initialize_block(2); + + // Create storage + assert_ok!(builder::call(addr).value(42).data((50u32, 20u32).encode()).build()); + // 4 is for creating 2 storage items + let charged0 = 4 + 50 + 20; + deposit += charged0; + assert_eq!(get_contract(&addr).total_deposit(), deposit); + + // Add more storage (but also remove some) + assert_ok!(builder::call(addr).data((100u32, 10u32).encode()).build()); + let charged1 = 50 - 10; + deposit += charged1; + assert_eq!(get_contract(&addr).total_deposit(), deposit); + + // Remove more storage (but also add some) + assert_ok!(builder::call(addr).data((10u32, 20u32).encode()).build()); + // -1 for numeric instability + let refunded0 = 90 - 10 - 1; + deposit -= refunded0; + assert_eq!(get_contract(&addr).total_deposit(), deposit); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: 42, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndHeld { + from: ALICE_ADDR, + to: addr, + amount: charged0, + } + ), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndHeld { + from: ALICE_ADDR, + to: addr, + amount: charged1, + } + ), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts( + pallet_revive::Event::StorageDepositTransferredAndReleased { + from: addr, + to: ALICE_ADDR, + amount: refunded0, + } + ), + topics: vec![], + }, + ] + ); + }); + } + + #[test] + fn storage_deposit_callee_works() { + let (wasm_caller, _code_hash_caller) = compile_module("call").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, account_id } = + builder::bare_instantiate(Code::Upload(wasm_callee)).build_and_unwrap_contract(); + + assert_ok!(builder::call(addr_caller).data((100u32, &addr_callee).encode()).build()); + + let callee = get_contract(&addr_callee); + let deposit = DepositPerByte::get() * 100 + DepositPerItem::get() * 1; + + assert_eq!(test_utils::get_balance(&account_id), min_balance); + assert_eq!( + callee.total_deposit(), + deposit + test_utils::contract_info_storage_deposit(&addr_callee) + ); + }); + } + + #[test] + fn set_code_extrinsic() { + let (wasm, code_hash) = compile_module("dummy").unwrap(); + let (new_wasm, new_code_hash) = compile_module("crypto_hashes").unwrap(); + + assert_ne!(code_hash, new_code_hash); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + new_wasm, + deposit_limit::(), + )); + + // Drop previous events + initialize_block(2); + + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + + // only root can execute this extrinsic + assert_noop!( + Contracts::set_code(RuntimeOrigin::signed(ALICE), addr, new_code_hash), + sp_runtime::traits::BadOrigin, + ); + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + assert_eq!(System::events(), vec![]); + + // contract must exist + assert_noop!( + Contracts::set_code(RuntimeOrigin::root(), BOB_ADDR, new_code_hash), + >::ContractNotFound, + ); + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + assert_eq!(System::events(), vec![]); + + // new code hash must exist + assert_noop!( + Contracts::set_code(RuntimeOrigin::root(), addr, Default::default()), + >::CodeNotFound, + ); + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + assert_eq!(System::events(), vec![]); + + // successful call + assert_ok!(Contracts::set_code(RuntimeOrigin::root(), addr, new_code_hash)); + assert_eq!(get_contract(&addr).code_hash, new_code_hash); + assert_refcount!(&code_hash, 0); + assert_refcount!(&new_code_hash, 1); + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(pallet_revive::Event::ContractCodeUpdated { + contract: addr, + new_code_hash, + old_code_hash: code_hash, + }), + topics: vec![], + },] + ); + }); + } + + #[test] + fn slash_cannot_kill_account() { + let (wasm, _code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let value = 700; + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + let Contract { addr, account_id } = builder::bare_instantiate(Code::Upload(wasm)) + .value(value) + .build_and_unwrap_contract(); + + // Drop previous events + initialize_block(2); + + let info_deposit = test_utils::contract_info_storage_deposit(&addr); + + assert_eq!( + test_utils::get_balance_on_hold( + &HoldReason::StorageDepositReserve.into(), + &account_id + ), + info_deposit + ); + + assert_eq!( + ::Currency::total_balance(&account_id), + info_deposit + value + min_balance + ); + + // Try to destroy the account of the contract by slashing the total balance. + // The account does not get destroyed because slashing only affects the balance held + // under certain `reason`. Slashing can for example happen if the contract takes part + // in staking. + let _ = ::Currency::slash( + &HoldReason::StorageDepositReserve.into(), + &account_id, + ::Currency::total_balance(&account_id), + ); + + // Slashing only removed the balance held. + assert_eq!(::Currency::total_balance(&account_id), value + min_balance); + }); + } + + #[test] + fn contract_reverted() { + let (wasm, code_hash) = compile_module("return_with_data").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let flags = ReturnFlags::REVERT; + let buffer = [4u8, 8, 15, 16, 23, 42]; + let input = (flags.bits(), buffer).encode(); + + // We just upload the code for later use + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + wasm.clone(), + deposit_limit::(), + )); + + // Calling extrinsic: revert leads to an error + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).data(input.clone()).build(), + >::ContractReverted, + ); + + // Calling extrinsic: revert leads to an error + assert_err_ignore_postinfo!( + builder::instantiate_with_code(wasm).data(input.clone()).build(), + >::ContractReverted, + ); + + // Calling directly: revert leads to success but the flags indicate the error + // This is just a different way of transporting the error that allows the read out + // the `data` which is only there on success. Obviously, the contract isn't + // instantiated. + let result = builder::bare_instantiate(Code::Existing(code_hash)) + .data(input.clone()) + .build_and_unwrap_result(); + assert_eq!(result.result.flags, flags); + assert_eq!(result.result.data, buffer); + assert!(!>::contains_key(result.addr)); + + // Pass empty flags and therefore successfully instantiate the contract for later use. + let Contract { addr, .. } = builder::bare_instantiate(Code::Existing(code_hash)) + .data(ReturnFlags::empty().bits().encode()) + .build_and_unwrap_contract(); + + // Calling extrinsic: revert leads to an error + assert_err_ignore_postinfo!( + builder::call(addr).data(input.clone()).build(), + >::ContractReverted, + ); + + // Calling directly: revert leads to success but the flags indicate the error + let result = builder::bare_call(addr).data(input).build_and_unwrap_result(); + assert_eq!(result.flags, flags); + assert_eq!(result.data, buffer); + }); + } + + #[test] + fn set_code_hash() { + let (wasm, code_hash) = compile_module("set_code_hash").unwrap(); + let (new_wasm, new_code_hash) = compile_module("new_set_code_hash_contract").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the 'caller' + let Contract { addr: contract_addr, .. } = + builder::bare_instantiate(Code::Upload(wasm)) + .value(300_000) + .build_and_unwrap_contract(); + // upload new code + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + new_wasm.clone(), + deposit_limit::(), + )); + + System::reset_events(); + + // First call sets new code_hash and returns 1 + let result = builder::bare_call(contract_addr) + .data(new_code_hash.as_ref().to_vec()) + .debug(DebugInfo::UnsafeDebug) + .build_and_unwrap_result(); + assert_return_code!(result, 1); + + // Second calls new contract code that returns 2 + let result = builder::bare_call(contract_addr) + .debug(DebugInfo::UnsafeDebug) + .build_and_unwrap_result(); + assert_return_code!(result, 2); + + // Checking for the last event only + assert_eq!( + &System::events(), + &[ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::ContractCodeUpdated { + contract: contract_addr, + new_code_hash, + old_code_hash: code_hash, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: contract_addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Called { + caller: Origin::from_account_id(ALICE), + contract: contract_addr, + }), + topics: vec![], + }, + ], + ); + }); + } + + #[test] + fn storage_deposit_limit_is_enforced() { + let (wasm, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Setting insufficient storage_deposit should fail. + assert_err!( + builder::bare_instantiate(Code::Upload(wasm.clone())) + // expected deposit is 2 * ed + 3 for the call + .storage_deposit_limit((2 * min_balance + 3 - 1).into()) + .build() + .result, + >::StorageDepositLimitExhausted, + ); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + let info_deposit = test_utils::contract_info_storage_deposit(&addr); + // Check that the BOB contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&addr).total_deposit(), info_deposit); + assert_eq!( + ::Currency::total_balance(&account_id), + info_deposit + min_balance + ); + + // Create 1 byte of storage with a price of per byte, + // setting insufficient deposit limit, as it requires 3 Balance: + // 2 for the item added + 1 for the new storage item. + assert_err_ignore_postinfo!( + builder::call(addr) + .storage_deposit_limit(2) + .data(1u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // Create 1 byte of storage, should cost 3 Balance: + // 2 for the item added + 1 for the new storage item. + // Should pass as it fallbacks to DefaultDepositLimit. + assert_ok!(builder::call(addr) + .storage_deposit_limit(3) + .data(1u32.to_le_bytes().to_vec()) + .build()); + + // Use 4 more bytes of the storage for the same item, which requires 4 Balance. + // Should fail as DefaultDepositLimit is 3 and hence isn't enough. + assert_err_ignore_postinfo!( + builder::call(addr) + .storage_deposit_limit(3) + .data(5u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositLimitExhausted, + ); + }); + } + + #[test] + fn deposit_limit_in_nested_calls() { + let (wasm_caller, _code_hash_caller) = compile_module("create_storage_and_call").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(wasm_callee)).build_and_unwrap_contract(); + + // Create 100 bytes of storage with a price of per byte + // This is 100 Balance + 2 Balance for the item + assert_ok!(builder::call(addr_callee) + .storage_deposit_limit(102) + .data(100u32.to_le_bytes().to_vec()) + .build()); + + // We do not remove any storage but add a storage item of 12 bytes in the caller + // contract. This would cost 12 + 2 = 14 Balance. + // The nested call doesn't get a special limit, which is set by passing 0 to it. + // This should fail as the specified parent's limit is less than the cost: 13 < + // 14. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(13) + .data((100u32, &addr_callee, U256::from(0u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // Now we specify the parent's limit high enough to cover the caller's storage + // additions. However, we use a single byte more in the callee, hence the storage + // deposit should be 15 Balance. + // The nested call doesn't get a special limit, which is set by passing 0 to it. + // This should fail as the specified parent's limit is less than the cost: 14 + // < 15. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(14) + .data((101u32, &addr_callee, U256::from(0u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // Now we specify the parent's limit high enough to cover both the caller's and callee's + // storage additions. However, we set a special deposit limit of 1 Balance for the + // nested call. This should fail as callee adds up 2 bytes to the storage, meaning + // that the nested call should have a deposit limit of at least 2 Balance. The + // sub-call should be rolled back, which is covered by the next test case. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(16) + .data((102u32, &addr_callee, U256::from(1u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // Refund in the callee contract but not enough to cover the 14 Balance required by the + // caller. Note that if previous sub-call wouldn't roll back, this call would pass + // making the test case fail. We don't set a special limit for the nested call here. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(0) + .data((87u32, &addr_callee, U256::from(0u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + let _ = ::Currency::set_balance(&ALICE, 511); + + // Require more than the sender's balance. + // We don't set a special limit for the nested call. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data((512u32, &addr_callee, U256::from(1u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // Same as above but allow for the additional deposit of 1 Balance in parent. + // We set the special deposit limit of 1 Balance for the nested call, which isn't + // enforced as callee frees up storage. This should pass. + assert_ok!(builder::call(addr_caller) + .storage_deposit_limit(1) + .data((87u32, &addr_callee, U256::from(1u64)).encode()) + .build()); + }); + } + + #[test] + fn deposit_limit_in_nested_instantiate() { + let (wasm_caller, _code_hash_caller) = + compile_module("create_storage_and_instantiate").unwrap(); + let (wasm_callee, code_hash_callee) = compile_module("store_deploy").unwrap(); + const ED: u64 = 5; + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, 1_000_000); + // Create caller contract + let Contract { addr: addr_caller, account_id: caller_id } = + builder::bare_instantiate(Code::Upload(wasm_caller)) + .value(10_000u64) // this balance is later passed to the deployed contract + .build_and_unwrap_contract(); + // Deploy a contract to get its occupied storage size + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(wasm_callee)) + .data(vec![0, 0, 0, 0]) + .build_and_unwrap_contract(); + + let callee_info_len = ContractInfoOf::::get(&addr).unwrap().encoded_size() as u64; + + // We don't set a special deposit limit for the nested instantiation. + // + // The deposit limit set for the parent is insufficient for the instantiation, which + // requires: + // - callee_info_len + 2 for storing the new contract info, + // - ED for deployed contract account, + // - 2 for the storage item of 0 bytes being created in the callee constructor + // or (callee_info_len + 2 + ED + 2) Balance in total. + // + // Provided the limit is set to be 1 Balance less, + // this call should fail on the return from the caller contract. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(callee_info_len + 2 + ED + 1) + .data((0u32, &code_hash_callee, U256::from(0u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + // The charges made on instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Now we give enough limit for the instantiation itself, but require for 1 more storage + // byte in the constructor. Hence +1 Balance to the limit is needed. This should fail on + // the return from constructor. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(callee_info_len + 2 + ED + 2) + .data((1u32, &code_hash_callee, U256::from(0u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Now we set enough limit in parent call, but an insufficient limit for child + // instantiate. This should fail during the charging for the instantiation in + // `RawMeter::charge_instantiate()` + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(callee_info_len + 2 + ED + 2) + .data( + (0u32, &code_hash_callee, U256::from(callee_info_len + 2 + ED + 1)) + .encode() + ) + .build(), + >::StorageDepositLimitExhausted, + ); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Same as above but requires for single added storage + // item of 1 byte to be covered by the limit, which implies 3 more Balance. + // Now we set enough limit for the parent call, but insufficient limit for child + // instantiate. This should fail right after the constructor execution. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(callee_info_len + 2 + ED + 3) // enough parent limit + .data( + (1u32, &code_hash_callee, U256::from(callee_info_len + 2 + ED + 2)) + .encode() + ) + .build(), + >::StorageDepositLimitExhausted, + ); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Set enough deposit limit for the child instantiate. This should succeed. + let result = builder::bare_call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(callee_info_len + 2 + ED + 4) + .data((1u32, &code_hash_callee, U256::from(callee_info_len + 2 + ED + 3)).encode()) + .build(); + + let returned = result.result.unwrap(); + // All balance of the caller except ED has been transferred to the callee. + // No deposit has been taken from it. + assert_eq!(::Currency::free_balance(&caller_id), ED); + // Get address of the deployed contract. + let addr_callee = H160::from_slice(&returned.data[0..20]); + let callee_account_id = ::AddressMapper::to_account_id(&addr_callee); + // 10_000 should be sent to callee from the caller contract, plus ED to be sent from the + // origin. + assert_eq!(::Currency::free_balance(&callee_account_id), 10_000 + ED); + // The origin should be charged with: + // - callee instantiation deposit = (callee_info_len + 2) + // - callee account ED + // - for writing an item of 1 byte to storage = 3 Balance + assert_eq!( + ::Currency::free_balance(&BOB), + 1_000_000 - (callee_info_len + 2 + ED + 3) + ); + // Check that deposit due to be charged still includes these 3 Balance + assert_eq!(result.storage_deposit.charge_or_zero(), (callee_info_len + 2 + ED + 3)) + }); + } + + #[test] + fn deposit_limit_honors_liquidity_restrictions() { + let (wasm, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let bobs_balance = 1_000; + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, bobs_balance); + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + let info_deposit = test_utils::contract_info_storage_deposit(&addr); + // Check that the contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&addr).total_deposit(), info_deposit); + assert_eq!( + ::Currency::total_balance(&account_id), + info_deposit + min_balance + ); + + // check that the hold is honored + ::Currency::hold( + &HoldReason::CodeUploadDepositReserve.into(), + &BOB, + bobs_balance - min_balance, + ) + .unwrap(); + assert_err_ignore_postinfo!( + builder::call(addr) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(10_000) + .data(100u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositLimitExhausted, + ); + assert_eq!(::Currency::free_balance(&BOB), min_balance); + }); + } + + #[test] + fn deposit_limit_honors_existential_deposit() { + let (wasm, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, 300); + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + let info_deposit = test_utils::contract_info_storage_deposit(&addr); + + // Check that the contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&addr).total_deposit(), info_deposit); + assert_eq!( + ::Currency::total_balance(&account_id), + min_balance + info_deposit + ); + + // check that the deposit can't bring the account below the existential deposit + assert_err_ignore_postinfo!( + builder::call(addr) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(10_000) + .data(100u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositLimitExhausted, + ); + assert_eq!(::Currency::free_balance(&BOB), 300); + }); + } + + #[test] + fn deposit_limit_honors_min_leftover() { + let (wasm, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, 1_000); + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + let info_deposit = test_utils::contract_info_storage_deposit(&addr); + + // Check that the contract has been instantiated and has the minimum balance and the + // storage deposit + assert_eq!(get_contract(&addr).total_deposit(), info_deposit); + assert_eq!( + ::Currency::total_balance(&account_id), + info_deposit + min_balance + ); + + // check that the minimum leftover (value send) is considered + // given the minimum deposit of 200 sending 750 will only leave + // 50 for the storage deposit. Which is not enough to store the 50 bytes + // as we also need 2 bytes for the item + assert_err_ignore_postinfo!( + builder::call(addr) + .origin(RuntimeOrigin::signed(BOB)) + .value(750) + .storage_deposit_limit(10_000) + .data(50u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositLimitExhausted, + ); + assert_eq!(::Currency::free_balance(&BOB), 1_000); + }); + } + + #[test] + fn locking_delegate_dependency_works() { + // set hash lock up deposit to 30%, to test deposit calculation. + CODE_HASH_LOCKUP_DEPOSIT_PERCENT.with(|c| *c.borrow_mut() = Perbill::from_percent(30)); + + let (wasm_caller, self_code_hash) = compile_module("locking_delegate_dependency").unwrap(); + let callee_codes: Vec<_> = + (0..limits::DELEGATE_DEPENDENCIES + 1).map(|idx| dummy_unique(idx)).collect(); + let callee_hashes: Vec<_> = callee_codes + .iter() + .map(|c| sp_core::H256(sp_io::hashing::keccak_256(c))) + .collect(); + + // Define inputs with various actions to test locking / unlocking delegate_dependencies. + // See the contract for more details. + let noop_input = (0u32, callee_hashes[0]); + let lock_delegate_dependency_input = (1u32, callee_hashes[0]); + let unlock_delegate_dependency_input = (2u32, callee_hashes[0]); + let terminate_input = (3u32, callee_hashes[0]); + + // Instantiate the caller contract with the given input. + let instantiate = |input: &(u32, H256)| { + builder::bare_instantiate(Code::Upload(wasm_caller.clone())) + .origin(RuntimeOrigin::signed(ETH_ALICE)) + .data(input.encode()) + .build() + }; + + // Call contract with the given input. + let call = |addr_caller: &H160, input: &(u32, H256)| { + builder::bare_call(*addr_caller) + .origin(RuntimeOrigin::signed(ETH_ALICE)) + .data(input.encode()) + .build() + }; + const ED: u64 = 2000; + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let _ = Balances::set_balance(Ð_ALICE, 1_000_000); + + // Instantiate with lock_delegate_dependency should fail since the code is not yet on + // chain. + assert_err!( + instantiate(&lock_delegate_dependency_input).result, + Error::::CodeNotFound + ); + + // Upload all the delegated codes (they all have the same size) + let mut deposit = Default::default(); + for code in callee_codes.iter() { + let CodeUploadReturnValue { deposit: deposit_per_code, .. } = + Contracts::bare_upload_code( + RuntimeOrigin::signed(ETH_ALICE), + code.clone(), + deposit_limit::(), + ) + .unwrap(); + deposit = deposit_per_code; + } + + // Instantiate should now work. + let addr_caller = instantiate(&lock_delegate_dependency_input).result.unwrap().addr; + let caller_account_id = ::AddressMapper::to_account_id(&addr_caller); + + // There should be a dependency and a deposit. + let contract = test_utils::get_contract(&addr_caller); + + let dependency_deposit = &CodeHashLockupDepositPercent::get().mul_ceil(deposit); + assert_eq!( + contract.delegate_dependencies().get(&callee_hashes[0]), + Some(dependency_deposit) + ); + assert_eq!( + test_utils::get_balance_on_hold( + &HoldReason::StorageDepositReserve.into(), + &caller_account_id + ), + dependency_deposit + contract.storage_base_deposit() + ); + + // Removing the code should fail, since we have added a dependency. + assert_err!( + Contracts::remove_code(RuntimeOrigin::signed(ETH_ALICE), callee_hashes[0]), + >::CodeInUse + ); + + // Locking an already existing dependency should fail. + assert_err!( + call(&addr_caller, &lock_delegate_dependency_input).result, + Error::::DelegateDependencyAlreadyExists + ); + + // Locking self should fail. + assert_err!( + call(&addr_caller, &(1u32, self_code_hash)).result, + Error::::CannotAddSelfAsDelegateDependency + ); + + // Locking more than the maximum allowed delegate_dependencies should fail. + for hash in &callee_hashes[1..callee_hashes.len() - 1] { + call(&addr_caller, &(1u32, *hash)).result.unwrap(); + } + assert_err!( + call(&addr_caller, &(1u32, *callee_hashes.last().unwrap())).result, + Error::::MaxDelegateDependenciesReached + ); + + // Unlocking all dependency should work. + for hash in &callee_hashes[..callee_hashes.len() - 1] { + call(&addr_caller, &(2u32, *hash)).result.unwrap(); + } + + // Dependency should be removed, and deposit should be returned. + let contract = test_utils::get_contract(&addr_caller); + assert!(contract.delegate_dependencies().is_empty()); + assert_eq!( + test_utils::get_balance_on_hold( + &HoldReason::StorageDepositReserve.into(), + &caller_account_id + ), + contract.storage_base_deposit() + ); + + // Removing a nonexistent dependency should fail. + assert_err!( + call(&addr_caller, &unlock_delegate_dependency_input).result, + Error::::DelegateDependencyNotFound + ); + + // Locking a dependency with a storage limit too low should fail. + assert_err!( + builder::bare_call(addr_caller) + .storage_deposit_limit(dependency_deposit - 1) + .data(lock_delegate_dependency_input.encode()) + .build() + .result, + Error::::StorageDepositLimitExhausted + ); + + // Since we unlocked the dependency we should now be able to remove the code. + assert_ok!(Contracts::remove_code(RuntimeOrigin::signed(ETH_ALICE), callee_hashes[0])); + + // Calling should fail since the delegated contract is not on chain anymore. + assert_err!(call(&addr_caller, &noop_input).result, Error::::ContractTrapped); + + // Add the dependency back. + Contracts::upload_code( + RuntimeOrigin::signed(ETH_ALICE), + callee_codes[0].clone(), + deposit_limit::(), + ) + .unwrap(); + call(&addr_caller, &lock_delegate_dependency_input).result.unwrap(); + + // Call terminate should work, and return the deposit. + let balance_before = test_utils::get_balance(Ð_ALICE); + assert_ok!(call(&addr_caller, &terminate_input).result); + assert_eq!( + test_utils::get_balance(Ð_ALICE), + ED + balance_before + contract.storage_base_deposit() + dependency_deposit + ); + + // Terminate should also remove the dependency, so we can remove the code. + assert_ok!(Contracts::remove_code(RuntimeOrigin::signed(ETH_ALICE), callee_hashes[0])); + }); + } + + #[test] + fn native_dependency_deposit_works() { + let (wasm, code_hash) = compile_module("set_code_hash").unwrap(); + let (dummy_wasm, dummy_code_hash) = compile_module("dummy").unwrap(); + + // Set hash lock up deposit to 30%, to test deposit calculation. + CODE_HASH_LOCKUP_DEPOSIT_PERCENT.with(|c| *c.borrow_mut() = Perbill::from_percent(30)); + + // Test with both existing and uploaded code + for code in [Code::Upload(wasm.clone()), Code::Existing(code_hash)] { + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let lockup_deposit_percent = CodeHashLockupDepositPercent::get(); + + // Upload the dummy contract, + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + dummy_wasm.clone(), + deposit_limit::(), + ) + .unwrap(); + + // Upload `set_code_hash` contracts if using Code::Existing. + let add_upload_deposit = match code { + Code::Existing(_) => { + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + wasm.clone(), + deposit_limit::(), + ) + .unwrap(); + false + }, + Code::Upload(_) => true, + }; + + // Instantiate the set_code_hash contract. + let res = builder::bare_instantiate(code).build(); + + let addr = res.result.unwrap().addr; + let account_id = ::AddressMapper::to_account_id(&addr); + let base_deposit = test_utils::contract_info_storage_deposit(&addr); + let upload_deposit = test_utils::get_code_deposit(&code_hash); + let extra_deposit = add_upload_deposit.then(|| upload_deposit).unwrap_or_default(); + + // Check initial storage_deposit + // The base deposit should be: contract_info_storage_deposit + 30% * deposit + let deposit = + extra_deposit + base_deposit + lockup_deposit_percent.mul_ceil(upload_deposit); + + assert_eq!( + res.storage_deposit.charge_or_zero(), + deposit + Contracts::min_balance() + ); + + // call set_code_hash + builder::bare_call(addr) + .data(dummy_code_hash.encode()) + .build_and_unwrap_result(); + + // Check updated storage_deposit + let code_deposit = test_utils::get_code_deposit(&dummy_code_hash); + let deposit = base_deposit + lockup_deposit_percent.mul_ceil(code_deposit); + assert_eq!(test_utils::get_contract(&addr).storage_base_deposit(), deposit); + + assert_eq!( + test_utils::get_balance_on_hold( + &HoldReason::StorageDepositReserve.into(), + &account_id + ), + deposit + ); + }); + } + } + + #[test] + fn root_cannot_upload_code() { + let (wasm, _) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Contracts::upload_code(RuntimeOrigin::root(), wasm, deposit_limit::()), + DispatchError::BadOrigin, + ); + }); + } + + #[test] + fn root_cannot_remove_code() { + let (_, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Contracts::remove_code(RuntimeOrigin::root(), code_hash), + DispatchError::BadOrigin, + ); + }); + } + + #[test] + fn signed_cannot_set_code() { + let (_, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Contracts::set_code(RuntimeOrigin::signed(ALICE), BOB_ADDR, code_hash), + DispatchError::BadOrigin, + ); + }); + } + + #[test] + fn none_cannot_call_code() { + ExtBuilder::default().build().execute_with(|| { + assert_err_ignore_postinfo!( + builder::call(BOB_ADDR).origin(RuntimeOrigin::none()).build(), + DispatchError::BadOrigin, + ); + }); + } + + #[test] + fn root_can_call() { + let (wasm, _) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(wasm)).build_and_unwrap_contract(); + + // Call the contract. + assert_ok!(builder::call(addr).origin(RuntimeOrigin::root()).build()); + }); + } + + #[test] + fn root_cannot_instantiate_with_code() { + let (wasm, _) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_err_ignore_postinfo!( + builder::instantiate_with_code(wasm).origin(RuntimeOrigin::root()).build(), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn root_cannot_instantiate() { + let (_, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).origin(RuntimeOrigin::root()).build(), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn only_upload_origin_can_upload() { + let (wasm, _) = compile_module("dummy").unwrap(); + UploadAccount::set(Some(ALICE)); + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let _ = Balances::set_balance(&BOB, 1_000_000); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::root(), + wasm.clone(), + deposit_limit::(), + ), + DispatchError::BadOrigin + ); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(BOB), + wasm.clone(), + deposit_limit::(), + ), + DispatchError::BadOrigin + ); + + // Only alice is allowed to upload contract code. + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + wasm.clone(), + deposit_limit::(), + )); + }); + } + + #[test] + fn only_instantiation_origin_can_instantiate() { + let (code, code_hash) = compile_module("dummy").unwrap(); + InstantiateAccount::set(Some(ALICE)); + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let _ = Balances::set_balance(&BOB, 1_000_000); + + assert_err_ignore_postinfo!( + builder::instantiate_with_code(code.clone()) + .origin(RuntimeOrigin::root()) + .build(), + DispatchError::BadOrigin + ); + + assert_err_ignore_postinfo!( + builder::instantiate_with_code(code.clone()) + .origin(RuntimeOrigin::signed(BOB)) + .build(), + DispatchError::BadOrigin + ); + + // Only Alice can instantiate + assert_ok!(builder::instantiate_with_code(code).build()); + + // Bob cannot instantiate with either `instantiate_with_code` or `instantiate`. + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).origin(RuntimeOrigin::signed(BOB)).build(), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn balance_of_api() { + let (wasm, _code_hash) = compile_module("balance_of").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let _ = Balances::set_balance(Ð_ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(wasm.to_vec())).build_and_unwrap_contract(); + + // The fixture asserts a non-zero returned free balance of the account; + // The ETH_ALICE account is endowed; + // Hence we should not revert + assert_ok!(builder::call(addr).data(ALICE_ADDR.0.to_vec()).build()); + + // The fixture asserts a non-zero returned free balance of the account; + // The ETH_BOB account is not endowed; + // Hence we should revert + assert_err_ignore_postinfo!( + builder::call(addr).data(BOB_ADDR.0.to_vec()).build(), + >::ContractTrapped + ); + }); + } + + #[test] + fn balance_api_returns_free_balance() { + let (wasm, _code_hash) = compile_module("balance").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the BOB contract without any extra balance. + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(wasm.to_vec())).build_and_unwrap_contract(); + + let value = 0; + // Call BOB which makes it call the balance runtime API. + // The contract code asserts that the returned balance is 0. + assert_ok!(builder::call(addr).value(value).build()); + + let value = 1; + // Calling with value will trap the contract. + assert_err_ignore_postinfo!( + builder::call(addr).value(value).build(), + >::ContractTrapped + ); + }); + } + + #[test] + fn gas_consumed_is_linear_for_nested_calls() { + let (code, _code_hash) = compile_module("recurse").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let [gas_0, gas_1, gas_2, gas_max] = { + [0u32, 1u32, 2u32, limits::CALL_STACK_DEPTH] + .iter() + .map(|i| { + let result = builder::bare_call(addr).data(i.encode()).build(); + assert_ok!(result.result); + result.gas_consumed + }) + .collect::>() + .try_into() + .unwrap() + }; + + let gas_per_recursion = gas_2.checked_sub(&gas_1).unwrap(); + assert_eq!(gas_max, gas_0 + gas_per_recursion * limits::CALL_STACK_DEPTH as u64); + }); + } + + #[test] + fn read_only_call_cannot_store() { + let (wasm_caller, _code_hash_caller) = compile_module("read_only_call").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(wasm_callee)).build_and_unwrap_contract(); + + // Read-only call fails when modifying storage. + assert_err_ignore_postinfo!( + builder::call(addr_caller).data((&addr_callee, 100u32).encode()).build(), + >::ContractTrapped + ); + }); + } + + #[test] + fn read_only_call_cannot_transfer() { + let (wasm_caller, _code_hash_caller) = compile_module("call_with_flags_and_value").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(wasm_callee)).build_and_unwrap_contract(); + + // Read-only call fails when a non-zero value is set. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data( + (addr_callee, pallet_revive_uapi::CallFlags::READ_ONLY.bits(), 100u64) + .encode() + ) + .build(), + >::StateChangeDenied + ); + }); + } + + #[test] + fn read_only_subsequent_call_cannot_store() { + let (wasm_read_only_caller, _code_hash_caller) = compile_module("read_only_call").unwrap(); + let (wasm_caller, _code_hash_caller) = compile_module("call_with_flags_and_value").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_read_only_caller)) + .build_and_unwrap_contract(); + let Contract { addr: addr_subsequent_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(wasm_callee)).build_and_unwrap_contract(); + + // Subsequent call input. + let input = (&addr_callee, pallet_revive_uapi::CallFlags::empty().bits(), 0u64, 100u32); + + // Read-only call fails when modifying storage. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data((&addr_subsequent_caller, input).encode()) + .build(), + >::ContractTrapped + ); + }); + } + + #[test] + fn read_only_call_works() { + let (wasm_caller, _code_hash_caller) = compile_module("read_only_call").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(wasm_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(wasm_callee)).build_and_unwrap_contract(); + + assert_ok!(builder::call(addr_caller).data(addr_callee.encode()).build()); + }); + } + + #[test] + fn create1_with_value_works() { + let (code, code_hash) = compile_module("create1_with_value").unwrap(); + let value = 42; + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing. + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: Deploys itself using create1 and the expected value + assert_ok!(builder::call(addr).value(value).data(code_hash.encode()).build()); + + // We should see the expected balance at the expected account + let address = crate::address::create1(&addr, 0); + let account_id = ::AddressMapper::to_account_id(&address); + let usable_balance = ::Currency::usable_balance(&account_id); + assert_eq!(usable_balance, value); + }); + } + + #[test] + fn static_data_limit_is_enforced() { + let (oom_rw_trailing, _) = compile_module("oom_rw_trailing").unwrap(); + let (oom_rw_included, _) = compile_module("oom_rw_included").unwrap(); + let (oom_ro, _) = compile_module("oom_ro").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + oom_rw_trailing, + deposit_limit::(), + ), + >::StaticMemoryTooLarge + ); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + oom_rw_included, + deposit_limit::(), + ), + >::BlobTooLarge + ); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + oom_ro, + deposit_limit::(), + ), + >::BlobTooLarge + ); + }); + } + + #[test] + fn call_diverging_out_len_works() { + let (code, _) = compile_module("call_diverging_out_len").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: It will issue calls and deploys, asserting on + // correct output if the supplied output length was smaller than + // than what the callee returned. + assert_ok!(builder::call(addr).build()); + }); + } + + #[test] + fn chain_id_works() { + let (code, _) = compile_module("chain_id").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let chain_id = U256::from(::ChainId::get()); + let received = builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_result(); + assert_eq!(received.result.data, chain_id.encode()); + }); + } + + #[test] + fn return_data_api_works() { + let (code_return_data_api, _) = compile_module("return_data_api").unwrap(); + let (code_return_with_data, hash_return_with_data) = + compile_module("return_with_data").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Upload the io echoing fixture for later use + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + code_return_with_data, + deposit_limit::(), + )); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code_return_data_api)) + .build_and_unwrap_contract(); + + // Call the contract: It will issue calls and deploys, asserting on + assert_ok!(builder::call(addr) + .value(10 * 1024) + .data(hash_return_with_data.encode()) + .build()); + }); + } +} diff --git a/pallets/revive/src/tests/pallet_dummy.rs b/pallets/revive/src/tests/pallet_dummy.rs new file mode 100644 index 00000000..2af8475d --- /dev/null +++ b/pallets/revive/src/tests/pallet_dummy.rs @@ -0,0 +1,53 @@ +// 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. + +pub use pallet::*; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use frame_support::{ + dispatch::{Pays, PostDispatchInfo}, + ensure, + pallet_prelude::DispatchResultWithPostInfo, + weights::Weight, + }; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::call] + impl Pallet { + /// Dummy function that overcharges the predispatch weight, allowing us to test the correct + /// values of [`ContractResult::gas_consumed`] and [`ContractResult::gas_required`] in + /// tests. + #[pallet::call_index(1)] + #[pallet::weight(*pre_charge)] + pub fn overestimate_pre_charge( + origin: OriginFor, + pre_charge: Weight, + actual_weight: Weight, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + ensure!(pre_charge.any_gt(actual_weight), "pre_charge must be > actual_weight"); + Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes }) + } + } +} diff --git a/pallets/revive/src/tests/test_debug.rs b/pallets/revive/src/tests/test_debug.rs new file mode 100644 index 00000000..cdf32e54 --- /dev/null +++ b/pallets/revive/src/tests/test_debug.rs @@ -0,0 +1,246 @@ +// 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 std::cell::RefCell; + +use frame_support::traits::Currency; +use sp_core::H160; + +use super::*; +use crate::{ + debug::{CallInterceptor, CallSpan, ExecResult, ExportedFunction, Tracing}, + primitives::ExecReturnValue, + test_utils::*, +}; + +#[derive(Clone, PartialEq, Eq, Debug)] +struct DebugFrame { + contract_address: sp_core::H160, + call: ExportedFunction, + input: Vec, + result: Option>, +} + +thread_local! { + static DEBUG_EXECUTION_TRACE: RefCell> = RefCell::new(Vec::new()); + static INTERCEPTED_ADDRESS: RefCell> = RefCell::new(None); +} + +pub struct TestDebug; +pub struct TestCallSpan { + contract_address: sp_core::H160, + call: ExportedFunction, + input: Vec, +} + +impl Tracing for TestDebug { + type CallSpan = TestCallSpan; + + fn new_call_span( + contract_address: &crate::H160, + entry_point: ExportedFunction, + input_data: &[u8], + ) -> TestCallSpan { + DEBUG_EXECUTION_TRACE.with(|d| { + d.borrow_mut().push(DebugFrame { + contract_address: *contract_address, + call: entry_point, + input: input_data.to_vec(), + result: None, + }) + }); + TestCallSpan { + contract_address: *contract_address, + call: entry_point, + input: input_data.to_vec(), + } + } +} + +impl CallInterceptor for TestDebug { + fn intercept_call( + contract_address: &sp_core::H160, + _entry_point: ExportedFunction, + _input_data: &[u8], + ) -> Option { + INTERCEPTED_ADDRESS.with(|i| { + if i.borrow().as_ref() == Some(contract_address) { + Some(Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![] })) + } else { + None + } + }) + } +} + +impl CallSpan for TestCallSpan { + fn after_call(self, output: &ExecReturnValue) { + DEBUG_EXECUTION_TRACE.with(|d| { + d.borrow_mut().push(DebugFrame { + contract_address: self.contract_address, + call: self.call, + input: self.input, + result: Some(output.data.clone()), + }) + }); + } +} + +/// We can only run the tests if we have a riscv toolchain installed +#[cfg(feature = "riscv")] +mod run_tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn debugging_works() { + let (wasm_caller, _) = compile_module("call").unwrap(); + let (wasm_callee, _) = compile_module("store_call").unwrap(); + + fn current_stack() -> Vec { + DEBUG_EXECUTION_TRACE.with(|stack| stack.borrow().clone()) + } + + fn deploy(wasm: Vec) -> H160 { + Contracts::bare_instantiate( + RuntimeOrigin::signed(ALICE), + 0, + GAS_LIMIT, + deposit_limit::(), + Code::Upload(wasm), + vec![], + Some([0u8; 32]), + DebugInfo::Skip, + CollectEvents::Skip, + ) + .result + .unwrap() + .addr + } + + fn constructor_frame(contract_address: &H160, after: bool) -> DebugFrame { + DebugFrame { + contract_address: *contract_address, + call: ExportedFunction::Constructor, + input: vec![], + result: if after { Some(vec![]) } else { None }, + } + } + + fn call_frame(contract_address: &H160, args: Vec, after: bool) -> DebugFrame { + DebugFrame { + contract_address: *contract_address, + call: ExportedFunction::Call, + input: args, + result: if after { Some(vec![]) } else { None }, + } + } + + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = Balances::deposit_creating(&ALICE, 1_000_000); + + assert_eq!(current_stack(), vec![]); + + let addr_caller = deploy(wasm_caller); + let addr_callee = deploy(wasm_callee); + + assert_eq!( + current_stack(), + vec![ + constructor_frame(&addr_caller, false), + constructor_frame(&addr_caller, true), + constructor_frame(&addr_callee, false), + constructor_frame(&addr_callee, true), + ] + ); + + let main_args = (100u32, &addr_callee.clone()).encode(); + let inner_args = (100u32).encode(); + + assert_ok!(Contracts::call( + RuntimeOrigin::signed(ALICE), + addr_caller, + 0, + GAS_LIMIT, + deposit_limit::(), + main_args.clone() + )); + + let stack_top = current_stack()[4..].to_vec(); + assert_eq!( + stack_top, + vec![ + call_frame(&addr_caller, main_args.clone(), false), + call_frame(&addr_callee, inner_args.clone(), false), + call_frame(&addr_callee, inner_args, true), + call_frame(&addr_caller, main_args, true), + ] + ); + }); + } + + #[test] + fn call_interception_works() { + let (wasm, _) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = Balances::deposit_creating(&ALICE, 1_000_000); + + let account_id = Contracts::bare_instantiate( + RuntimeOrigin::signed(ALICE), + 0, + GAS_LIMIT, + deposit_limit::(), + Code::Upload(wasm), + vec![], + // some salt to ensure that the address of this contract is unique among all tests + Some([0x41; 32]), + DebugInfo::Skip, + CollectEvents::Skip, + ) + .result + .unwrap() + .addr; + + // no interception yet + assert_ok!(Contracts::call( + RuntimeOrigin::signed(ALICE), + account_id, + 0, + GAS_LIMIT, + deposit_limit::(), + vec![], + )); + + // intercept calls to this contract + INTERCEPTED_ADDRESS.with(|i| *i.borrow_mut() = Some(account_id)); + + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(ALICE), + account_id, + 0, + GAS_LIMIT, + deposit_limit::(), + vec![], + ), + >::ContractReverted, + ); + }); + } +} diff --git a/pallets/revive/src/transient_storage.rs b/pallets/revive/src/transient_storage.rs new file mode 100644 index 00000000..772077b5 --- /dev/null +++ b/pallets/revive/src/transient_storage.rs @@ -0,0 +1,692 @@ +// 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. + +//! This module contains routines for accessing and altering a contract transient storage. + +use core::marker::PhantomData; + +use codec::Encode; +use frame_support::DefaultNoBound; +use sp_runtime::{DispatchError, DispatchResult, Saturating}; +use sp_std::{collections::btree_map::BTreeMap, mem, vec::Vec}; + +use crate::{ + exec::{AccountIdOf, Key}, + storage::WriteOutcome, + Config, Error, +}; + +/// Meter entry tracks transaction allocations. +#[derive(Default, Debug)] +pub struct MeterEntry { + /// Allocations made in the current transaction. + pub amount: u32, + /// Allocations limit in the current transaction. + pub limit: u32, +} + +impl MeterEntry { + /// Create a new entry. + fn new(limit: u32) -> Self { + Self { limit, amount: Default::default() } + } + + /// Check if the allocated amount exceeds the limit. + fn exceeds_limit(&self, amount: u32) -> bool { + self.amount.saturating_add(amount) > self.limit + } + + /// Absorb the allocation amount of the nested entry into the current entry. + fn absorb(&mut self, rhs: Self) { + self.amount.saturating_accrue(rhs.amount) + } +} + +// The storage meter enforces a limit for each transaction, +// which is calculated as free_storage * (1 - 1/16) for each subsequent frame. +#[derive(DefaultNoBound)] +pub struct StorageMeter { + nested_meters: Vec, + root_meter: MeterEntry, + _phantom: PhantomData, +} + +impl StorageMeter { + const STORAGE_FRACTION_DENOMINATOR: u32 = 16; + + /// Create a new storage allocation meter. + fn new(memory_limit: u32) -> Self { + Self { root_meter: MeterEntry::new(memory_limit), ..Default::default() } + } + + /// Charge the allocated amount of transaction storage from the meter. + fn charge(&mut self, amount: u32) -> DispatchResult { + let meter = self.current_mut(); + if meter.exceeds_limit(amount) { + return Err(Error::::OutOfTransientStorage.into()); + } + meter.amount.saturating_accrue(amount); + Ok(()) + } + + /// Revert a transaction meter. + fn revert(&mut self) { + self.nested_meters.pop().expect( + "A call to revert a meter must be preceded by a corresponding call to start a meter; + the code within this crate makes sure that this is always the case; qed", + ); + } + + /// Start a transaction meter. + fn start(&mut self) { + let meter = self.current(); + let mut transaction_limit = meter.limit.saturating_sub(meter.amount); + if !self.nested_meters.is_empty() { + // Allow use of (1 - 1/STORAGE_FRACTION_DENOMINATOR) of free storage for subsequent + // calls. + transaction_limit.saturating_reduce( + transaction_limit.saturating_div(Self::STORAGE_FRACTION_DENOMINATOR), + ); + } + + self.nested_meters.push(MeterEntry::new(transaction_limit)); + } + + /// Commit a transaction meter. + fn commit(&mut self) { + let transaction_meter = self.nested_meters.pop().expect( + "A call to commit a meter must be preceded by a corresponding call to start a meter; + the code within this crate makes sure that this is always the case; qed", + ); + self.current_mut().absorb(transaction_meter) + } + + /// The total allocated amount of memory. + #[cfg(test)] + fn total_amount(&self) -> u32 { + self.nested_meters + .iter() + .fold(self.root_meter.amount, |acc, e| acc.saturating_add(e.amount)) + } + + /// A mutable reference to the current meter entry. + pub fn current_mut(&mut self) -> &mut MeterEntry { + self.nested_meters.last_mut().unwrap_or(&mut self.root_meter) + } + + /// A reference to the current meter entry. + pub fn current(&self) -> &MeterEntry { + self.nested_meters.last().unwrap_or(&self.root_meter) + } +} + +/// An entry representing a journal change. +struct JournalEntry { + key: Vec, + prev_value: Option>, +} + +impl JournalEntry { + /// Create a new change. + fn new(key: Vec, prev_value: Option>) -> Self { + Self { key, prev_value } + } + + /// Revert the change. + fn revert(self, storage: &mut Storage) { + storage.write(&self.key, self.prev_value); + } +} + +/// A journal containing transient storage modifications. +struct Journal(Vec); + +impl Journal { + /// Create a new journal. + fn new() -> Self { + Self(Default::default()) + } + + /// Add a change to the journal. + fn push(&mut self, entry: JournalEntry) { + self.0.push(entry); + } + + /// Length of the journal. + fn len(&self) -> usize { + self.0.len() + } + + /// Roll back all journal changes until the chackpoint + fn rollback(&mut self, storage: &mut Storage, checkpoint: usize) { + self.0.drain(checkpoint..).rev().for_each(|entry| entry.revert(storage)); + } +} + +/// Storage for maintaining the current transaction state. +#[derive(Default)] +struct Storage(BTreeMap, Vec>); + +impl Storage { + /// Read the storage entry. + fn read(&self, key: &Vec) -> Option> { + self.0.get(key).cloned() + } + + /// Write the storage entry. + fn write(&mut self, key: &Vec, value: Option>) -> Option> { + if let Some(value) = value { + // Insert storage entry. + self.0.insert(key.clone(), value) + } else { + // Remove storage entry. + self.0.remove(key) + } + } +} + +/// Transient storage behaves almost identically to regular storage but is discarded after each +/// transaction. It consists of a `BTreeMap` for the current state, a journal of all changes, and a +/// list of checkpoints. On entry to the `start_transaction` function, a marker (checkpoint) is +/// added to the list. New values are written to the current state, and the previous value is +/// recorded in the journal (`write`). When the `commit_transaction` function is called, the marker +/// to the journal index (checkpoint) of when that call was entered is discarded. +/// On `rollback_transaction`, all entries are reverted up to the last checkpoint. +pub struct TransientStorage { + // The storage and journal size is limited by the storage meter. + storage: Storage, + journal: Journal, + // The size of the StorageMeter is limited by the stack depth. + meter: StorageMeter, + // The size of the checkpoints is limited by the stack depth. + checkpoints: Vec, +} + +impl TransientStorage { + /// Create new transient storage with the supplied memory limit. + pub fn new(memory_limit: u32) -> Self { + TransientStorage { + storage: Default::default(), + journal: Journal::new(), + checkpoints: Default::default(), + meter: StorageMeter::new(memory_limit), + } + } + + /// Read the storage value. If the entry does not exist, `None` is returned. + pub fn read(&self, account: &AccountIdOf, key: &Key) -> Option> { + self.storage.read(&Self::storage_key(&account.encode(), &key.hash())) + } + + /// Write a value to storage. + /// + /// If the `value` is `None`, then the entry is removed. If `take` is true, + /// a [`WriteOutcome::Taken`] is returned instead of a [`WriteOutcome::Overwritten`]. + /// If the entry did not exist, [`WriteOutcome::New`] is returned. + pub fn write( + &mut self, + account: &AccountIdOf, + key: &Key, + value: Option>, + take: bool, + ) -> Result { + let key = Self::storage_key(&account.encode(), &key.hash()); + let prev_value = self.storage.read(&key); + // Skip if the same value is being set. + if prev_value != value { + // Calculate the allocation size. + if let Some(value) = &value { + // Charge the key, value and journal entry. + // If a new value is written, a new journal entry is created. The previous value is + // moved to the journal along with its key, and the new value is written to + // storage. + let key_len = key.capacity(); + let mut amount = value + .capacity() + .saturating_add(key_len) + .saturating_add(mem::size_of::()); + if prev_value.is_none() { + // Charge a new storage entry. + // If there was no previous value, a new entry is added to storage (BTreeMap) + // containing a Vec for the key and a Vec for the value. The value was already + // included in the amount. + amount.saturating_accrue(key_len.saturating_add(mem::size_of::>())); + } + self.meter.charge(amount as _)?; + } + self.storage.write(&key, value); + // Update the journal. + self.journal.push(JournalEntry::new(key, prev_value.clone())); + } + + Ok(match (take, prev_value) { + (_, None) => WriteOutcome::New, + (false, Some(prev_value)) => WriteOutcome::Overwritten(prev_value.len() as _), + (true, Some(prev_value)) => WriteOutcome::Taken(prev_value), + }) + } + + /// Start a new nested transaction. + /// + /// This allows to either commit or roll back all changes that are made after this call. + /// For every transaction there must be a matching call to either `rollback_transaction` + /// or `commit_transaction`. + pub fn start_transaction(&mut self) { + self.meter.start(); + self.checkpoints.push(self.journal.len()); + } + + /// Rollback the last transaction started by `start_transaction`. + /// + /// Any changes made during that transaction are discarded. + /// + /// # Panics + /// + /// Will panic if there is no open transaction. + pub fn rollback_transaction(&mut self) { + let checkpoint = self.checkpoints.pop().expect( + "A call to rollback_transaction must be preceded by a corresponding call to \ + start_transaction; + the code within this crate makes sure that this is always the case; qed", + ); + self.meter.revert(); + self.journal.rollback(&mut self.storage, checkpoint); + } + + /// Commit the last transaction started by `start_transaction`. + /// + /// Any changes made during that transaction are committed. + /// + /// # Panics + /// + /// Will panic if there is no open transaction. + pub fn commit_transaction(&mut self) { + self.checkpoints.pop().expect( + "A call to commit_transaction must be preceded by a corresponding call to \ + start_transaction; + the code within this crate makes sure that this is always the case; qed", + ); + self.meter.commit(); + } + + /// The storage allocation meter used for transaction metering. + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn meter(&mut self) -> &mut StorageMeter { + return &mut self.meter + } + + fn storage_key(account: &[u8], key: &[u8]) -> Vec { + let mut storage_key = Vec::with_capacity(account.len() + key.len()); + storage_key.extend_from_slice(&account); + storage_key.extend_from_slice(&key); + storage_key + } +} + +#[cfg(test)] +mod tests { + use core::u32::MAX; + + use super::*; + use crate::{test_utils::*, tests::Test, Error}; + + // Calculate the allocation size for the given entry. + fn allocation_size(account: &AccountIdOf, key: &Key, value: Option>) -> u32 { + let mut storage: TransientStorage = TransientStorage::::new(MAX); + storage + .write(account, key, value, false) + .expect("Could not write to transient storage."); + storage.meter().current().amount + } + + #[test] + fn read_write_works() { + let mut storage: TransientStorage = TransientStorage::::new(2048); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![2]), true), + Ok(WriteOutcome::New) + ); + assert_eq!( + storage.write(&BOB, &Key::Fix([3; 32]), Some(vec![3]), false), + Ok(WriteOutcome::New) + ); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1])); + assert_eq!(storage.read(&ALICE, &Key::Fix([2; 32])), Some(vec![2])); + assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), Some(vec![3])); + // Overwrite values. + assert_eq!( + storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![4, 5]), false), + Ok(WriteOutcome::Overwritten(1)) + ); + assert_eq!( + storage.write(&BOB, &Key::Fix([3; 32]), Some(vec![6, 7]), true), + Ok(WriteOutcome::Taken(vec![3])) + ); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1])); + assert_eq!(storage.read(&ALICE, &Key::Fix([2; 32])), Some(vec![4, 5])); + assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), Some(vec![6, 7])); + + // Check for an empty value. + assert_eq!( + storage.write(&BOB, &Key::Fix([3; 32]), Some(vec![]), true), + Ok(WriteOutcome::Taken(vec![6, 7])) + ); + assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), Some(vec![])); + + assert_eq!( + storage.write(&BOB, &Key::Fix([3; 32]), None, true), + Ok(WriteOutcome::Taken(vec![])) + ); + assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), None); + } + + #[test] + fn read_write_with_var_sized_keys_works() { + let mut storage = TransientStorage::::new(2048); + assert_eq!( + storage.write( + &ALICE, + &Key::try_from_var([1; 64].to_vec()).unwrap(), + Some(vec![1]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + storage.write( + &BOB, + &Key::try_from_var([2; 64].to_vec()).unwrap(), + Some(vec![2, 3]), + false + ), + Ok(WriteOutcome::New) + ); + assert_eq!( + storage.read(&ALICE, &Key::try_from_var([1; 64].to_vec()).unwrap()), + Some(vec![1]) + ); + assert_eq!( + storage.read(&BOB, &Key::try_from_var([2; 64].to_vec()).unwrap()), + Some(vec![2, 3]) + ); + // Overwrite values. + assert_eq!( + storage.write( + &ALICE, + &Key::try_from_var([1; 64].to_vec()).unwrap(), + Some(vec![4, 5]), + false + ), + Ok(WriteOutcome::Overwritten(1)) + ); + assert_eq!( + storage.read(&ALICE, &Key::try_from_var([1; 64].to_vec()).unwrap()), + Some(vec![4, 5]) + ); + } + + #[test] + fn rollback_transaction_works() { + let mut storage = TransientStorage::::new(1024); + + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + storage.rollback_transaction(); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), None) + } + + #[test] + fn commit_transaction_works() { + let mut storage = TransientStorage::::new(1024); + + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + storage.commit_transaction(); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1])) + } + + #[test] + fn overwrite_and_commmit_transaction_works() { + let mut storage = TransientStorage::::new(1024); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1, 2]), false), + Ok(WriteOutcome::Overwritten(1)) + ); + storage.commit_transaction(); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1, 2])) + } + + #[test] + fn rollback_in_nested_transaction_works() { + let mut storage = TransientStorage::::new(1024); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!( + storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + storage.rollback_transaction(); + storage.commit_transaction(); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1])); + assert_eq!(storage.read(&BOB, &Key::Fix([1; 32])), None) + } + + #[test] + fn commit_in_nested_transaction_works() { + let mut storage = TransientStorage::::new(1024); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!( + storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![2]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!( + storage.write(&CHARLIE, &Key::Fix([1; 32]), Some(vec![3]), false), + Ok(WriteOutcome::New) + ); + storage.commit_transaction(); + storage.commit_transaction(); + storage.commit_transaction(); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1])); + assert_eq!(storage.read(&BOB, &Key::Fix([1; 32])), Some(vec![2])); + assert_eq!(storage.read(&CHARLIE, &Key::Fix([1; 32])), Some(vec![3])); + } + + #[test] + fn rollback_all_transactions_works() { + let mut storage = TransientStorage::::new(1024); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!( + storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![2]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!( + storage.write(&CHARLIE, &Key::Fix([1; 32]), Some(vec![3]), false), + Ok(WriteOutcome::New) + ); + storage.commit_transaction(); + storage.commit_transaction(); + storage.rollback_transaction(); + assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), None); + assert_eq!(storage.read(&BOB, &Key::Fix([1; 32])), None); + assert_eq!(storage.read(&CHARLIE, &Key::Fix([1; 32])), None); + } + + #[test] + fn metering_transactions_works() { + let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096])); + let mut storage = TransientStorage::::new(size * 2); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + let limit = storage.meter().current().limit; + storage.commit_transaction(); + + storage.start_transaction(); + assert_eq!(storage.meter().current().limit, limit - size); + assert_eq!(storage.meter().current().limit - storage.meter().current().amount, size); + assert_eq!( + storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + assert_eq!(storage.meter().current().amount, size); + storage.commit_transaction(); + assert_eq!(storage.meter().total_amount(), size * 2); + } + + #[test] + fn metering_nested_transactions_works() { + let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096])); + let mut storage = TransientStorage::::new(size * 3); + + storage.start_transaction(); + let limit = storage.meter().current().limit; + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!(storage.meter().total_amount(), size); + assert!(storage.meter().current().limit < limit - size); + assert_eq!( + storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + storage.commit_transaction(); + assert_eq!(storage.meter().current().limit, limit); + assert_eq!(storage.meter().total_amount(), storage.meter().current().amount); + storage.commit_transaction(); + } + + #[test] + fn metering_transaction_fails() { + let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096])); + let mut storage = TransientStorage::::new(size - 1); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false), + Err(Error::::OutOfTransientStorage.into()) + ); + assert_eq!(storage.meter.current().amount, 0); + storage.commit_transaction(); + assert_eq!(storage.meter.total_amount(), 0); + } + + #[test] + fn metering_nested_transactions_fails() { + let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096])); + let mut storage = TransientStorage::::new(size * 2); + + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false), + Err(Error::::OutOfTransientStorage.into()) + ); + storage.commit_transaction(); + storage.commit_transaction(); + assert_eq!(storage.meter.total_amount(), size); + } + + #[test] + fn metering_nested_transaction_with_rollback_works() { + let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096])); + let mut storage = TransientStorage::::new(size * 2); + + storage.start_transaction(); + let limit = storage.meter.current().limit; + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + storage.rollback_transaction(); + + assert_eq!(storage.meter.total_amount(), 0); + assert_eq!(storage.meter.current().limit, limit); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + let amount = storage.meter().current().amount; + assert_eq!(storage.meter().total_amount(), amount); + storage.commit_transaction(); + } + + #[test] + fn metering_with_rollback_works() { + let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096])); + let mut storage = TransientStorage::::new(size * 5); + + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + let amount = storage.meter.total_amount(); + storage.start_transaction(); + assert_eq!( + storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + storage.start_transaction(); + assert_eq!( + storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false), + Ok(WriteOutcome::New) + ); + storage.commit_transaction(); + storage.rollback_transaction(); + assert_eq!(storage.meter.total_amount(), amount); + storage.commit_transaction(); + } +} diff --git a/pallets/revive/src/wasm/mod.rs b/pallets/revive/src/wasm/mod.rs new file mode 100644 index 00000000..34066674 --- /dev/null +++ b/pallets/revive/src/wasm/mod.rs @@ -0,0 +1,364 @@ +// 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. + +//! This module provides a means for executing contracts +//! represented in wasm. + +mod runtime; + +use alloc::vec::Vec; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::DispatchResult, + ensure, + traits::{fungible::MutateHold, tokens::Precision::BestEffort}, +}; +#[cfg(test)] +pub use runtime::HIGHEST_API_VERSION; +use sp_core::{Get, U256}; +use sp_runtime::DispatchError; + +#[cfg(doc)] +pub use crate::wasm::runtime::SyscallDoc; +pub use crate::wasm::runtime::{ApiVersion, Memory, Runtime, RuntimeCosts}; +#[cfg(all(feature = "runtime-benchmarks", feature = "riscv"))] +pub use crate::wasm::runtime::{ReturnData, TrapReason}; +use crate::{ + address::AddressMapper, + exec::{ExecResult, Executable, ExportedFunction, Ext}, + gas::{GasMeter, Token}, + limits, + storage::meter::Diff, + weights::WeightInfo, + AccountIdOf, BadOrigin, BalanceOf, CodeInfoOf, CodeVec, Config, Error, Event, ExecError, + HoldReason, Pallet, PristineCode, Weight, API_VERSION, LOG_TARGET, +}; + +/// Validated Wasm module ready for execution. +/// This data structure is immutable once created and stored. +#[derive(Encode, Decode, scale_info::TypeInfo)] +#[codec(mel_bound())] +#[scale_info(skip_type_params(T))] +pub struct WasmBlob { + code: CodeVec, + // This isn't needed for contract execution and is not stored alongside it. + #[codec(skip)] + code_info: CodeInfo, + // This is for not calculating the hash every time we need it. + #[codec(skip)] + code_hash: sp_core::H256, +} + +/// Contract code related data, such as: +/// +/// - owner of the contract, i.e. account uploaded its code, +/// - storage deposit amount, +/// - reference count, +/// +/// It is stored in a separate storage entry to avoid loading the code when not necessary. +#[derive(Clone, Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)] +#[codec(mel_bound())] +#[scale_info(skip_type_params(T))] +pub struct CodeInfo { + /// The account that has uploaded the contract code and hence is allowed to remove it. + owner: AccountIdOf, + /// The amount of balance that was deposited by the owner in order to store it on-chain. + #[codec(compact)] + deposit: BalanceOf, + /// The number of instantiated contracts that use this as their code. + #[codec(compact)] + refcount: u64, + /// Length of the code in bytes. + code_len: u32, + /// The API version that this contract operates under. + /// + /// This determines which host functions are available to the contract. This + /// prevents that new host functions become available to already deployed contracts. + api_version: u16, + /// The behaviour version that this contract operates under. + /// + /// Whenever any observeable change (with the exception of weights) are made we need + /// to make sure that already deployed contracts will not be affected. We do this by + /// exposing the old behaviour depending on the set behaviour version of the contract. + /// + /// As of right now this is a reserved field that is always set to 0. + behaviour_version: u16, +} + +impl ExportedFunction { + /// The wasm export name for the function. + fn identifier(&self) -> &str { + match self { + Self::Constructor => "deploy", + Self::Call => "call", + } + } +} + +/// Cost of code loading from storage. +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +#[derive(Clone, Copy)] +struct CodeLoadToken(u32); + +impl Token for CodeLoadToken { + fn weight(&self) -> Weight { + T::WeightInfo::call_with_code_per_byte(self.0) + .saturating_sub(T::WeightInfo::call_with_code_per_byte(0)) + } +} + +impl WasmBlob +where + BalanceOf: Into + TryFrom, +{ + /// We only check for size and nothing else when the code is uploaded. + pub fn from_code(code: Vec, owner: AccountIdOf) -> Result { + // We do size checks when new code is deployed. This allows us to increase + // the limits later without affecting already deployed code. + let code = limits::code::enforce::(code)?; + + let code_len = code.len() as u32; + let bytes_added = code_len.saturating_add(>::max_encoded_len() as u32); + let deposit = Diff { bytes_added, items_added: 2, ..Default::default() } + .update_contract::(None) + .charge_or_zero(); + let code_info = CodeInfo { + owner, + deposit, + refcount: 0, + code_len, + api_version: API_VERSION, + behaviour_version: Default::default(), + }; + let code_hash = sp_core::H256(sp_io::hashing::keccak_256(&code)); + Ok(WasmBlob { code, code_info, code_hash }) + } + + /// Remove the code from storage and refund the deposit to its owner. + /// + /// Applies all necessary checks before removing the code. + pub fn remove(origin: &T::AccountId, code_hash: sp_core::H256) -> DispatchResult { + >::try_mutate_exists(&code_hash, |existing| { + if let Some(code_info) = existing { + ensure!(code_info.refcount == 0, >::CodeInUse); + ensure!(&code_info.owner == origin, BadOrigin); + let _ = T::Currency::release( + &HoldReason::CodeUploadDepositReserve.into(), + &code_info.owner, + code_info.deposit, + BestEffort, + ); + let deposit_released = code_info.deposit; + let remover = T::AddressMapper::to_address(&code_info.owner); + + *existing = None; + >::remove(&code_hash); + >::deposit_event(Event::CodeRemoved { + code_hash, + deposit_released, + remover, + }); + Ok(()) + } else { + Err(>::CodeNotFound.into()) + } + }) + } + + /// Puts the module blob into storage, and returns the deposit collected for the storage. + pub fn store_code(&mut self) -> Result, Error> { + let code_hash = *self.code_hash(); + >::mutate(code_hash, |stored_code_info| { + match stored_code_info { + // Contract code is already stored in storage. Nothing to be done here. + Some(_) => Ok(Default::default()), + // Upload a new contract code. + // We need to store the code and its code_info, and collect the deposit. + // This `None` case happens only with freshly uploaded modules. This means that + // the `owner` is always the origin of the current transaction. + None => { + let deposit = self.code_info.deposit; + T::Currency::hold( + &HoldReason::CodeUploadDepositReserve.into(), + &self.code_info.owner, + deposit, + ) + .map_err(|_| >::StorageDepositNotEnoughFunds)?; + + self.code_info.refcount = 0; + >::insert(code_hash, &self.code); + *stored_code_info = Some(self.code_info.clone()); + let uploader = T::AddressMapper::to_address(&self.code_info.owner); + >::deposit_event(Event::CodeStored { + code_hash, + deposit_held: deposit, + uploader, + }); + Ok(deposit) + }, + } + }) + } +} + +impl CodeInfo { + #[cfg(test)] + pub fn new(owner: T::AccountId) -> Self { + CodeInfo { + owner, + deposit: Default::default(), + refcount: 0, + code_len: 0, + api_version: API_VERSION, + behaviour_version: Default::default(), + } + } + + /// Returns reference count of the module. + pub fn refcount(&self) -> u64 { + self.refcount + } + + /// Return mutable reference to the refcount of the module. + pub fn refcount_mut(&mut self) -> &mut u64 { + &mut self.refcount + } + + /// Returns the deposit of the module. + pub fn deposit(&self) -> BalanceOf { + self.deposit + } +} + +pub struct PreparedCall<'a, E: Ext> { + module: polkavm::Module, + instance: polkavm::RawInstance, + runtime: Runtime<'a, E, polkavm::RawInstance>, + api_version: ApiVersion, +} + +impl<'a, E: Ext> PreparedCall<'a, E> +where + BalanceOf: Into, + BalanceOf: TryFrom, +{ + pub fn call(mut self) -> ExecResult { + let exec_result = loop { + let interrupt = self.instance.run(); + if let Some(exec_result) = self.runtime.handle_interrupt( + interrupt, + &self.module, + &mut self.instance, + self.api_version, + ) { + break exec_result + } + }; + let _ = self.runtime.ext().gas_meter_mut().sync_from_executor(self.instance.gas())?; + exec_result + } +} + +impl WasmBlob { + pub fn prepare_call>( + self, + mut runtime: Runtime, + entry_point: ExportedFunction, + api_version: ApiVersion, + ) -> Result, ExecError> { + let mut config = polkavm::Config::default(); + config.set_backend(Some(polkavm::BackendKind::Interpreter)); + let engine = + polkavm::Engine::new(&config).expect("interpreter is available on all plattforms; qed"); + + let mut module_config = polkavm::ModuleConfig::new(); + module_config.set_page_size(limits::PAGE_SIZE); + module_config.set_gas_metering(Some(polkavm::GasMeteringKind::Sync)); + let module = polkavm::Module::new(&engine, &module_config, self.code.into_inner().into()) + .map_err(|err| { + log::debug!(target: LOG_TARGET, "failed to create polkavm module: {err:?}"); + Error::::CodeRejected + })?; + + let entry_program_counter = module + .exports() + .find(|export| export.symbol().as_bytes() == entry_point.identifier().as_bytes()) + .ok_or_else(|| >::CodeRejected)? + .program_counter(); + + let gas_limit_polkavm: polkavm::Gas = runtime.ext().gas_meter_mut().engine_fuel_left()?; + + let mut instance = module.instantiate().map_err(|err| { + log::debug!(target: LOG_TARGET, "failed to instantiate polkavm module: {err:?}"); + Error::::CodeRejected + })?; + + // Increment before execution so that the constructor sees the correct refcount + if let ExportedFunction::Constructor = entry_point { + E::increment_refcount(self.code_hash)?; + } + + instance.set_gas(gas_limit_polkavm); + instance.prepare_call_untyped(entry_program_counter, &[]); + + Ok(PreparedCall { module, instance, runtime, api_version }) + } +} + +impl Executable for WasmBlob +where + BalanceOf: Into + TryFrom, +{ + fn from_storage( + code_hash: sp_core::H256, + gas_meter: &mut GasMeter, + ) -> Result { + let code_info = >::get(code_hash).ok_or(Error::::CodeNotFound)?; + gas_meter.charge(CodeLoadToken(code_info.code_len))?; + let code = >::get(code_hash).ok_or(Error::::CodeNotFound)?; + Ok(Self { code, code_info, code_hash }) + } + + fn execute>( + self, + ext: &mut E, + function: ExportedFunction, + input_data: Vec, + ) -> ExecResult { + let api_version = if ::UnsafeUnstableInterface::get() { + ApiVersion::UnsafeNewest + } else { + ApiVersion::Versioned(self.code_info.api_version) + }; + let prepared_call = + self.prepare_call(Runtime::new(ext, input_data), function, api_version)?; + prepared_call.call() + } + + fn code(&self) -> &[u8] { + self.code.as_ref() + } + + fn code_hash(&self) -> &sp_core::H256 { + &self.code_hash + } + + fn code_info(&self) -> &CodeInfo { + &self.code_info + } +} diff --git a/pallets/revive/src/wasm/runtime.rs b/pallets/revive/src/wasm/runtime.rs new file mode 100644 index 00000000..6d6fef37 --- /dev/null +++ b/pallets/revive/src/wasm/runtime.rs @@ -0,0 +1,2044 @@ +// 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. + +//! Environment definition of the wasm smart-contract runtime. + +use alloc::{boxed::Box, vec, vec::Vec}; +use core::{fmt, marker::PhantomData, mem}; + +use codec::{Decode, DecodeLimit, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::DispatchInfo, ensure, pallet_prelude::DispatchResultWithPostInfo, parameter_types, + traits::Get, weights::Weight, +}; +use pallet_revive_proc_macro::define_env; +use pallet_revive_uapi::{CallFlags, ReturnErrorCode, ReturnFlags, StorageFlags}; +use sp_core::{H160, H256, U256}; +use sp_io::hashing::{blake2_128, blake2_256, keccak_256, sha2_256}; +use sp_runtime::{DispatchError, RuntimeDebug}; + +use crate::{ + address::AddressMapper, + exec::{ExecError, ExecResult, Ext, Key}, + gas::{ChargedAmount, Token}, + limits, + primitives::ExecReturnValue, + weights::WeightInfo, + Config, Error, LOG_TARGET, SENTINEL, +}; + +type CallOf = ::RuntimeCall; + +/// The maximum nesting depth a contract can use when encoding types. +const MAX_DECODE_NESTING: u32 = 256; + +/// Encode a `U256` into a 32 byte buffer. +fn as_bytes(u: U256) -> [u8; 32] { + let mut bytes = [0u8; 32]; + u.to_little_endian(&mut bytes); + bytes +} + +#[derive(Clone, Copy)] +pub enum ApiVersion { + /// Expose all APIs even unversioned ones. Only used for testing and benchmarking. + UnsafeNewest, + /// Only expose API's up to and including the specified version. + Versioned(u16), +} + +/// Abstraction over the memory access within syscalls. +/// +/// The reason for this abstraction is that we run syscalls on the host machine when +/// benchmarking them. In that case we have direct access to the contract's memory. However, when +/// running within PolkaVM we need to resort to copying as we can't map the contracts memory into +/// the host (as of now). +pub trait Memory { + /// Read designated chunk from the sandbox memory into the supplied buffer. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - requested buffer is not within the bounds of the sandbox memory. + fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), DispatchError>; + + /// Write the given buffer to the designated location in the sandbox memory. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - designated area is not within the bounds of the sandbox memory. + fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError>; + + /// Read designated chunk from the sandbox memory. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - requested buffer is not within the bounds of the sandbox memory. + fn read(&self, ptr: u32, len: u32) -> Result, DispatchError> { + let mut buf = vec![0u8; len as usize]; + self.read_into_buf(ptr, buf.as_mut_slice())?; + Ok(buf) + } + + /// Same as `read` but reads into a fixed size buffer. + fn read_array(&self, ptr: u32) -> Result<[u8; N], DispatchError> { + let mut buf = [0u8; N]; + self.read_into_buf(ptr, &mut buf)?; + Ok(buf) + } + + /// Read a `u32` from the sandbox memory. + fn read_u32(&self, ptr: u32) -> Result { + let buf: [u8; 4] = self.read_array(ptr)?; + Ok(u32::from_le_bytes(buf)) + } + + /// Read a `U256` from the sandbox memory. + fn read_u256(&self, ptr: u32) -> Result { + let buf: [u8; 32] = self.read_array(ptr)?; + Ok(U256::from_little_endian(&buf)) + } + + /// Read a `H256` from the sandbox memory. + fn read_h256(&self, ptr: u32) -> Result { + let mut code_hash = H256::default(); + self.read_into_buf(ptr, code_hash.as_bytes_mut())?; + Ok(code_hash) + } + + /// Read designated chunk from the sandbox memory and attempt to decode into the specified type. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - requested buffer is not within the bounds of the sandbox memory. + /// - the buffer contents cannot be decoded as the required type. + /// + /// # Note + /// + /// There must be an extra benchmark for determining the influence of `len` with + /// regard to the overall weight. + fn read_as_unbounded(&self, ptr: u32, len: u32) -> Result { + let buf = self.read(ptr, len)?; + let decoded = D::decode_all_with_depth_limit(MAX_DECODE_NESTING, &mut buf.as_ref()) + .map_err(|_| DispatchError::from(Error::::DecodingFailed))?; + Ok(decoded) + } + + /// Reads and decodes a type with a size fixed at compile time from contract memory. + /// + /// # Only use on fixed size types + /// + /// Don't use this for types where the encoded size is not fixed but merely bounded. Otherwise + /// this implementation will out of bound access the buffer declared by the guest. Some examples + /// of those bounded but not fixed types: Enums with data, `BoundedVec` or any compact encoded + /// integer. + /// + /// # Note + /// + /// The weight of reading a fixed value is included in the overall weight of any + /// contract callable function. + fn read_as(&self, ptr: u32) -> Result { + let buf = self.read(ptr, D::max_encoded_len() as u32)?; + let decoded = D::decode_with_depth_limit(MAX_DECODE_NESTING, &mut buf.as_ref()) + .map_err(|_| DispatchError::from(Error::::DecodingFailed))?; + Ok(decoded) + } +} + +/// Allows syscalls access to the PolkaVM instance they are executing in. +/// +/// In case a contract is executing within PolkaVM its `memory` argument will also implement +/// this trait. The benchmarking implementation of syscalls will only require `Memory` +/// to be implemented. +pub trait PolkaVmInstance: Memory { + fn gas(&self) -> polkavm::Gas; + fn set_gas(&mut self, gas: polkavm::Gas); + fn read_input_regs(&self) -> (u32, u32, u32, u32, u32, u32); + fn write_output(&mut self, output: u32); +} + +// Memory implementation used in benchmarking where guest memory is mapped into the host. +// +// Please note that we could optimize the `read_as_*` functions by decoding directly from +// memory without a copy. However, we don't do that because as it would change the behaviour +// of those functions: A `read_as` with a `len` larger than the actual type can succeed +// in the streaming implementation while it could fail with a segfault in the copy implementation. +#[cfg(feature = "runtime-benchmarks")] +impl Memory for [u8] { + fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), DispatchError> { + let ptr = ptr as usize; + let bound_checked = + self.get(ptr..ptr + buf.len()).ok_or_else(|| Error::::OutOfBounds)?; + buf.copy_from_slice(bound_checked); + Ok(()) + } + + fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError> { + let ptr = ptr as usize; + let bound_checked = + self.get_mut(ptr..ptr + buf.len()).ok_or_else(|| Error::::OutOfBounds)?; + bound_checked.copy_from_slice(buf); + Ok(()) + } +} + +impl Memory for polkavm::RawInstance { + fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), DispatchError> { + self.read_memory_into(ptr, buf) + .map(|_| ()) + .map_err(|_| Error::::OutOfBounds.into()) + } + + fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError> { + self.write_memory(ptr, buf).map_err(|_| Error::::OutOfBounds.into()) + } +} + +impl PolkaVmInstance for polkavm::RawInstance { + fn gas(&self) -> polkavm::Gas { + self.gas() + } + + fn set_gas(&mut self, gas: polkavm::Gas) { + self.set_gas(gas) + } + + fn read_input_regs(&self) -> (u32, u32, u32, u32, u32, u32) { + ( + self.reg(polkavm::Reg::A0), + self.reg(polkavm::Reg::A1), + self.reg(polkavm::Reg::A2), + self.reg(polkavm::Reg::A3), + self.reg(polkavm::Reg::A4), + self.reg(polkavm::Reg::A5), + ) + } + + fn write_output(&mut self, output: u32) { + self.set_reg(polkavm::Reg::A0, output); + } +} + +parameter_types! { + /// Getter types used by [`crate::SyscallDoc:call_runtime`] + const CallRuntimeFailed: ReturnErrorCode = ReturnErrorCode::CallRuntimeFailed; + /// Getter types used by [`crate::SyscallDoc::xcm_execute`] + const XcmExecutionFailed: ReturnErrorCode = ReturnErrorCode::XcmExecutionFailed; +} + +impl From<&ExecReturnValue> for ReturnErrorCode { + fn from(from: &ExecReturnValue) -> Self { + if from.flags.contains(ReturnFlags::REVERT) { + Self::CalleeReverted + } else { + Self::Success + } + } +} + +/// The data passed through when a contract uses `seal_return`. +#[derive(RuntimeDebug)] +pub struct ReturnData { + /// The flags as passed through by the contract. They are still unchecked and + /// will later be parsed into a `ReturnFlags` bitflags struct. + flags: u32, + /// The output buffer passed by the contract as return data. + data: Vec, +} + +/// Enumerates all possible reasons why a trap was generated. +/// +/// This is either used to supply the caller with more information about why an error +/// occurred (the SupervisorError variant). +/// The other case is where the trap does not constitute an error but rather was invoked +/// as a quick way to terminate the application (all other variants). +#[derive(RuntimeDebug)] +pub enum TrapReason { + /// The supervisor trapped the contract because of an error condition occurred during + /// execution in privileged code. + SupervisorError(DispatchError), + /// Signals that trap was generated in response to call `seal_return` host function. + Return(ReturnData), + /// Signals that a trap was generated in response to a successful call to the + /// `seal_terminate` host function. + Termination, +} + +impl> From for TrapReason { + fn from(from: T) -> Self { + Self::SupervisorError(from.into()) + } +} + +impl fmt::Display for TrapReason { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + Ok(()) + } +} + +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +#[derive(Copy, Clone)] +pub enum RuntimeCosts { + /// Base Weight of calling a host function. + HostFn, + /// Weight charged for copying data from the sandbox. + CopyFromContract(u32), + /// Weight charged for copying data to the sandbox. + CopyToContract(u32), + /// Weight of calling `seal_caller`. + Caller, + /// Weight of calling `seal_is_contract`. + IsContract, + /// Weight of calling `seal_code_hash`. + CodeHash, + /// Weight of calling `seal_own_code_hash`. + OwnCodeHash, + /// Weight of calling `seal_caller_is_origin`. + CallerIsOrigin, + /// Weight of calling `caller_is_root`. + CallerIsRoot, + /// Weight of calling `seal_address`. + Address, + /// Weight of calling `seal_gas_left`. + GasLeft, + /// Weight of calling `seal_balance`. + Balance, + /// Weight of calling `seal_balance_of`. + BalanceOf, + /// Weight of calling `seal_value_transferred`. + ValueTransferred, + /// Weight of calling `seal_minimum_balance`. + MinimumBalance, + /// Weight of calling `seal_block_number`. + BlockNumber, + /// Weight of calling `seal_now`. + Now, + /// Weight of calling `seal_weight_to_fee`. + WeightToFee, + /// Weight of calling `seal_terminate`, passing the number of locked dependencies. + Terminate(u32), + /// Weight of calling `seal_deposit_event` with the given number of topics and event size. + DepositEvent { num_topic: u32, len: u32 }, + /// Weight of calling `seal_debug_message` per byte of passed message. + DebugMessage(u32), + /// Weight of calling `seal_set_storage` for the given storage item sizes. + SetStorage { old_bytes: u32, new_bytes: u32 }, + /// Weight of calling `seal_clear_storage` per cleared byte. + ClearStorage(u32), + /// Weight of calling `seal_contains_storage` per byte of the checked item. + ContainsStorage(u32), + /// Weight of calling `seal_get_storage` with the specified size in storage. + GetStorage(u32), + /// Weight of calling `seal_take_storage` for the given size. + TakeStorage(u32), + /// Weight of calling `seal_set_transient_storage` for the given storage item sizes. + SetTransientStorage { old_bytes: u32, new_bytes: u32 }, + /// Weight of calling `seal_clear_transient_storage` per cleared byte. + ClearTransientStorage(u32), + /// Weight of calling `seal_contains_transient_storage` per byte of the checked item. + ContainsTransientStorage(u32), + /// Weight of calling `seal_get_transient_storage` with the specified size in storage. + GetTransientStorage(u32), + /// Weight of calling `seal_take_transient_storage` for the given size. + TakeTransientStorage(u32), + /// Weight of calling `seal_transfer`. + Transfer, + /// Base weight of calling `seal_call`. + CallBase, + /// Weight of calling `seal_delegate_call` for the given input size. + DelegateCallBase, + /// Weight of the transfer performed during a call. + CallTransferSurcharge, + /// Weight per byte that is cloned by supplying the `CLONE_INPUT` flag. + CallInputCloned(u32), + /// Weight of calling `seal_instantiate` for the given input lenth. + Instantiate { input_data_len: u32 }, + /// Weight of calling `seal_hash_sha_256` for the given input size. + HashSha256(u32), + /// Weight of calling `seal_hash_keccak_256` for the given input size. + HashKeccak256(u32), + /// Weight of calling `seal_hash_blake2_256` for the given input size. + HashBlake256(u32), + /// Weight of calling `seal_hash_blake2_128` for the given input size. + HashBlake128(u32), + /// Weight of calling `seal_ecdsa_recover`. + EcdsaRecovery, + /// Weight of calling `seal_sr25519_verify` for the given input size. + Sr25519Verify(u32), + /// Weight charged by a chain extension through `seal_call_chain_extension`. + ChainExtension(Weight), + /// Weight charged for calling into the runtime. + CallRuntime(Weight), + /// Weight charged for calling xcm_execute. + CallXcmExecute(Weight), + /// Weight of calling `seal_set_code_hash` + SetCodeHash, + /// Weight of calling `ecdsa_to_eth_address` + EcdsaToEthAddress, + /// Weight of calling `lock_delegate_dependency` + LockDelegateDependency, + /// Weight of calling `unlock_delegate_dependency` + UnlockDelegateDependency, +} + +/// For functions that modify storage, benchmarks are performed with one item in the +/// storage. To account for the worst-case scenario, the weight of the overhead of +/// writing to or reading from full storage is included. For transient storage writes, +/// the rollback weight is added to reflect the worst-case scenario for this operation. +macro_rules! cost_storage { + (write_transient, $name:ident $(, $arg:expr )*) => { + T::WeightInfo::$name($( $arg ),*) + .saturating_add(T::WeightInfo::rollback_transient_storage()) + .saturating_add(T::WeightInfo::set_transient_storage_full() + .saturating_sub(T::WeightInfo::set_transient_storage_empty())) + }; + + (read_transient, $name:ident $(, $arg:expr )*) => { + T::WeightInfo::$name($( $arg ),*) + .saturating_add(T::WeightInfo::get_transient_storage_full() + .saturating_sub(T::WeightInfo::get_transient_storage_empty())) + }; + + (write, $name:ident $(, $arg:expr )*) => { + T::WeightInfo::$name($( $arg ),*) + .saturating_add(T::WeightInfo::set_storage_full() + .saturating_sub(T::WeightInfo::set_storage_empty())) + }; + + (read, $name:ident $(, $arg:expr )*) => { + T::WeightInfo::$name($( $arg ),*) + .saturating_add(T::WeightInfo::get_storage_full() + .saturating_sub(T::WeightInfo::get_storage_empty())) + }; +} + +macro_rules! cost_args { + // cost_args!(name, a, b, c) -> T::WeightInfo::name(a, b, c).saturating_sub(T::WeightInfo::name(0, 0, 0)) + ($name:ident, $( $arg: expr ),+) => { + (T::WeightInfo::$name($( $arg ),+).saturating_sub(cost_args!(@call_zero $name, $( $arg ),+))) + }; + // Transform T::WeightInfo::name(a, b, c) into T::WeightInfo::name(0, 0, 0) + (@call_zero $name:ident, $( $arg:expr ),*) => { + T::WeightInfo::$name($( cost_args!(@replace_token $arg) ),*) + }; + // Replace the token with 0. + (@replace_token $_in:tt) => { 0 }; +} + +impl Token for RuntimeCosts { + fn influence_lowest_gas_limit(&self) -> bool { + match self { + &Self::CallXcmExecute(_) => false, + _ => true, + } + } + + fn weight(&self) -> Weight { + use self::RuntimeCosts::*; + match *self { + HostFn => cost_args!(noop_host_fn, 1), + CopyToContract(len) => T::WeightInfo::seal_input(len), + CopyFromContract(len) => T::WeightInfo::seal_return(len), + Caller => T::WeightInfo::seal_caller(), + IsContract => T::WeightInfo::seal_is_contract(), + CodeHash => T::WeightInfo::seal_code_hash(), + OwnCodeHash => T::WeightInfo::seal_own_code_hash(), + CallerIsOrigin => T::WeightInfo::seal_caller_is_origin(), + CallerIsRoot => T::WeightInfo::seal_caller_is_root(), + Address => T::WeightInfo::seal_address(), + GasLeft => T::WeightInfo::seal_gas_left(), + Balance => T::WeightInfo::seal_balance(), + BalanceOf => T::WeightInfo::seal_balance_of(), + ValueTransferred => T::WeightInfo::seal_value_transferred(), + MinimumBalance => T::WeightInfo::seal_minimum_balance(), + BlockNumber => T::WeightInfo::seal_block_number(), + Now => T::WeightInfo::seal_now(), + WeightToFee => T::WeightInfo::seal_weight_to_fee(), + Terminate(locked_dependencies) => T::WeightInfo::seal_terminate(locked_dependencies), + DepositEvent { num_topic, len } => T::WeightInfo::seal_deposit_event(num_topic, len), + DebugMessage(len) => T::WeightInfo::seal_debug_message(len), + SetStorage { new_bytes, old_bytes } => { + cost_storage!(write, seal_set_storage, new_bytes, old_bytes) + }, + ClearStorage(len) => cost_storage!(write, seal_clear_storage, len), + ContainsStorage(len) => cost_storage!(read, seal_contains_storage, len), + GetStorage(len) => cost_storage!(read, seal_get_storage, len), + TakeStorage(len) => cost_storage!(write, seal_take_storage, len), + SetTransientStorage { new_bytes, old_bytes } => { + cost_storage!(write_transient, seal_set_transient_storage, new_bytes, old_bytes) + }, + ClearTransientStorage(len) => { + cost_storage!(write_transient, seal_clear_transient_storage, len) + }, + ContainsTransientStorage(len) => { + cost_storage!(read_transient, seal_contains_transient_storage, len) + }, + GetTransientStorage(len) => { + cost_storage!(read_transient, seal_get_transient_storage, len) + }, + TakeTransientStorage(len) => { + cost_storage!(write_transient, seal_take_transient_storage, len) + }, + Transfer => T::WeightInfo::seal_transfer(), + CallBase => T::WeightInfo::seal_call(0, 0), + DelegateCallBase => T::WeightInfo::seal_delegate_call(), + CallTransferSurcharge => cost_args!(seal_call, 1, 0), + CallInputCloned(len) => cost_args!(seal_call, 0, len), + Instantiate { input_data_len } => T::WeightInfo::seal_instantiate(input_data_len), + HashSha256(len) => T::WeightInfo::seal_hash_sha2_256(len), + HashKeccak256(len) => T::WeightInfo::seal_hash_keccak_256(len), + HashBlake256(len) => T::WeightInfo::seal_hash_blake2_256(len), + HashBlake128(len) => T::WeightInfo::seal_hash_blake2_128(len), + EcdsaRecovery => T::WeightInfo::seal_ecdsa_recover(), + Sr25519Verify(len) => T::WeightInfo::seal_sr25519_verify(len), + ChainExtension(weight) | CallRuntime(weight) | CallXcmExecute(weight) => weight, + SetCodeHash => T::WeightInfo::seal_set_code_hash(), + EcdsaToEthAddress => T::WeightInfo::seal_ecdsa_to_eth_address(), + LockDelegateDependency => T::WeightInfo::lock_delegate_dependency(), + UnlockDelegateDependency => T::WeightInfo::unlock_delegate_dependency(), + } + } +} + +/// Same as [`Runtime::charge_gas`]. +/// +/// We need this access as a macro because sometimes hiding the lifetimes behind +/// a function won't work out. +macro_rules! charge_gas { + ($runtime:expr, $costs:expr) => {{ + $runtime.ext.gas_meter_mut().charge($costs) + }}; +} + +/// The kind of call that should be performed. +enum CallType { + /// Execute another instantiated contract + Call { callee_ptr: u32, value_ptr: u32, deposit_ptr: u32, weight: Weight }, + /// Execute deployed code in the context (storage, account ID, value) of the caller contract + DelegateCall { code_hash_ptr: u32 }, +} + +impl CallType { + fn cost(&self) -> RuntimeCosts { + match self { + CallType::Call { .. } => RuntimeCosts::CallBase, + CallType::DelegateCall { .. } => RuntimeCosts::DelegateCallBase, + } + } +} + +/// This is only appropriate when writing out data of constant size that does not depend on user +/// input. In this case the costs for this copy was already charged as part of the token at +/// the beginning of the API entry point. +fn already_charged(_: u32) -> Option { + None +} + +/// Can only be used for one call. +pub struct Runtime<'a, E: Ext, M: ?Sized> { + ext: &'a mut E, + input_data: Option>, + chain_extension: Option::ChainExtension>>, + _phantom_data: PhantomData, +} + +impl<'a, E: Ext, M: PolkaVmInstance> Runtime<'a, E, M> { + pub fn handle_interrupt( + &mut self, + interrupt: Result, + module: &polkavm::Module, + instance: &mut M, + api_version: ApiVersion, + ) -> Option { + use polkavm::InterruptKind::*; + + match interrupt { + Err(error) => { + // in contrast to the other returns this "should" not happen: log level error + log::error!(target: LOG_TARGET, "polkavm execution error: {error}"); + Some(Err(Error::::ExecutionFailed.into())) + }, + Ok(Finished) => + Some(Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() })), + Ok(Trap) => Some(Err(Error::::ContractTrapped.into())), + Ok(Segfault(_)) => Some(Err(Error::::ExecutionFailed.into())), + Ok(NotEnoughGas) => Some(Err(Error::::OutOfGas.into())), + Ok(Step) => None, + Ok(Ecalli(idx)) => { + let Some(syscall_symbol) = module.imports().get(idx) else { + return Some(Err(>::InvalidSyscall.into())); + }; + match self.handle_ecall(instance, syscall_symbol.as_bytes(), api_version) { + Ok(None) => None, + Ok(Some(return_value)) => { + instance.write_output(return_value); + None + }, + Err(TrapReason::Return(ReturnData { flags, data })) => + match ReturnFlags::from_bits(flags) { + None => Some(Err(Error::::InvalidCallFlags.into())), + Some(flags) => Some(Ok(ExecReturnValue { flags, data })), + }, + Err(TrapReason::Termination) => Some(Ok(Default::default())), + Err(TrapReason::SupervisorError(error)) => Some(Err(error.into())), + } + }, + } + } +} + +impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { + pub fn new(ext: &'a mut E, input_data: Vec) -> Self { + Self { + ext, + input_data: Some(input_data), + chain_extension: Some(Box::new(Default::default())), + _phantom_data: Default::default(), + } + } + + /// Get a mutable reference to the inner `Ext`. + /// + /// This is mainly for the chain extension to have access to the environment the + /// contract is executing in. + pub fn ext(&mut self) -> &mut E { + self.ext + } + + /// Charge the gas meter with the specified token. + /// + /// Returns `Err(HostError)` if there is not enough gas. + pub fn charge_gas(&mut self, costs: RuntimeCosts) -> Result { + charge_gas!(self, costs) + } + + /// Adjust a previously charged amount down to its actual amount. + /// + /// This is when a maximum a priori amount was charged and then should be partially + /// refunded to match the actual amount. + pub fn adjust_gas(&mut self, charged: ChargedAmount, actual_costs: RuntimeCosts) { + self.ext.gas_meter_mut().adjust_gas(charged, actual_costs); + } + + /// Charge, Run and adjust gas, for executing the given dispatchable. + fn call_dispatchable>( + &mut self, + dispatch_info: DispatchInfo, + runtime_cost: impl Fn(Weight) -> RuntimeCosts, + run: impl FnOnce(&mut Self) -> DispatchResultWithPostInfo, + ) -> Result { + use frame_support::dispatch::extract_actual_weight; + let charged = self.charge_gas(runtime_cost(dispatch_info.weight))?; + let result = run(self); + let actual_weight = extract_actual_weight(&result, &dispatch_info); + self.adjust_gas(charged, runtime_cost(actual_weight)); + match result { + Ok(_) => Ok(ReturnErrorCode::Success), + Err(e) => { + if self.ext.debug_buffer_enabled() { + self.ext.append_debug_buffer("call failed with: "); + self.ext.append_debug_buffer(e.into()); + }; + Ok(ErrorReturnCode::get()) + }, + } + } + + /// Write the given buffer and its length to the designated locations in sandbox memory and + /// charge gas according to the token returned by `create_token`. + /// + /// `out_ptr` is the location in sandbox memory where `buf` should be written to. + /// `out_len_ptr` is an in-out location in sandbox memory. It is read to determine the + /// length of the buffer located at `out_ptr`. If that buffer is smaller than the actual + /// `buf.len()`, only what fits into that buffer is written to `out_ptr`. + /// The actual amount of bytes copied to `out_ptr` is written to `out_len_ptr`. + /// + /// If `out_ptr` is set to the sentinel value of `SENTINEL` and `allow_skip` is true the + /// operation is skipped and `Ok` is returned. This is supposed to help callers to make copying + /// output optional. For example to skip copying back the output buffer of an `seal_call` + /// when the caller is not interested in the result. + /// + /// `create_token` can optionally instruct this function to charge the gas meter with the token + /// it returns. `create_token` receives the variable amount of bytes that are about to be copied + /// by this function. + /// + /// In addition to the error conditions of `Memory::write` this functions returns + /// `Err` if the size of the buffer located at `out_ptr` is too small to fit `buf`. + pub fn write_sandbox_output( + &mut self, + memory: &mut M, + out_ptr: u32, + out_len_ptr: u32, + buf: &[u8], + allow_skip: bool, + create_token: impl FnOnce(u32) -> Option, + ) -> Result<(), DispatchError> { + if allow_skip && out_ptr == SENTINEL { + return Ok(()); + } + + let len = memory.read_u32(out_len_ptr)?; + let buf_len = len.min(buf.len() as u32); + + if let Some(costs) = create_token(buf_len) { + self.charge_gas(costs)?; + } + + memory.write(out_ptr, &buf[..buf_len as usize])?; + memory.write(out_len_ptr, &buf_len.encode()) + } + + /// Same as `write_sandbox_output` but for static size output. + pub fn write_fixed_sandbox_output( + &mut self, + memory: &mut M, + out_ptr: u32, + buf: &[u8], + allow_skip: bool, + create_token: impl FnOnce(u32) -> Option, + ) -> Result<(), DispatchError> { + if allow_skip && out_ptr == SENTINEL { + return Ok(()); + } + + let buf_len = buf.len() as u32; + if let Some(costs) = create_token(buf_len) { + self.charge_gas(costs)?; + } + + memory.write(out_ptr, buf) + } + + /// Computes the given hash function on the supplied input. + /// + /// Reads from the sandboxed input buffer into an intermediate buffer. + /// Returns the result directly to the output buffer of the sandboxed memory. + /// + /// It is the callers responsibility to provide an output buffer that + /// is large enough to hold the expected amount of bytes returned by the + /// chosen hash function. + /// + /// # Note + /// + /// The `input` and `output` buffers may overlap. + fn compute_hash_on_intermediate_buffer( + &self, + memory: &mut M, + hash_fn: F, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + ) -> Result<(), DispatchError> + where + F: FnOnce(&[u8]) -> R, + R: AsRef<[u8]>, + { + // Copy input into supervisor memory. + let input = memory.read(input_ptr, input_len)?; + // Compute the hash on the input buffer using the given hash function. + let hash = hash_fn(&input); + // Write the resulting hash back into the sandboxed output buffer. + memory.write(output_ptr, hash.as_ref())?; + Ok(()) + } + + /// Fallible conversion of `DispatchError` to `ReturnErrorCode`. + fn err_into_return_code(from: DispatchError) -> Result { + use ReturnErrorCode::*; + + let transfer_failed = Error::::TransferFailed.into(); + let no_code = Error::::CodeNotFound.into(); + let not_found = Error::::ContractNotFound.into(); + + match from { + x if x == transfer_failed => Ok(TransferFailed), + x if x == no_code => Ok(CodeNotFound), + x if x == not_found => Ok(NotCallable), + err => Err(err), + } + } + + /// Fallible conversion of a `ExecError` to `ReturnErrorCode`. + fn exec_error_into_return_code(from: ExecError) -> Result { + use crate::exec::ErrorOrigin::Callee; + + match (from.error, from.origin) { + (_, Callee) => Ok(ReturnErrorCode::CalleeTrapped), + (err, _) => Self::err_into_return_code(err), + } + } + + fn decode_key(&self, memory: &M, key_ptr: u32, key_len: u32) -> Result { + let res = match key_len { + SENTINEL => { + let mut buffer = [0u8; 32]; + memory.read_into_buf(key_ptr, buffer.as_mut())?; + Ok(Key::from_fixed(buffer)) + }, + len => { + ensure!(len <= limits::STORAGE_KEY_BYTES, Error::::DecodingFailed); + let key = memory.read(key_ptr, len)?; + Key::try_from_var(key) + }, + }; + + res.map_err(|_| Error::::DecodingFailed.into()) + } + + fn is_transient(flags: u32) -> Result { + StorageFlags::from_bits(flags) + .ok_or_else(|| >::InvalidStorageFlags.into()) + .map(|flags| flags.contains(StorageFlags::TRANSIENT)) + } + + fn set_storage( + &mut self, + memory: &M, + flags: u32, + key_ptr: u32, + key_len: u32, + value_ptr: u32, + value_len: u32, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |new_bytes: u32, old_bytes: u32| { + if transient { + RuntimeCosts::SetTransientStorage { new_bytes, old_bytes } + } else { + RuntimeCosts::SetStorage { new_bytes, old_bytes } + } + }; + let max_size = self.ext.max_value_size(); + let charged = self.charge_gas(costs(value_len, self.ext.max_value_size()))?; + if value_len > max_size { + return Err(Error::::ValueTooLarge.into()); + } + let key = self.decode_key(memory, key_ptr, key_len)?; + let value = Some(memory.read(value_ptr, value_len)?); + let write_outcome = if transient { + self.ext.set_transient_storage(&key, value, false)? + } else { + self.ext.set_storage(&key, value, false)? + }; + self.adjust_gas(charged, costs(value_len, write_outcome.old_len())); + Ok(write_outcome.old_len_with_sentinel()) + } + + fn clear_storage( + &mut self, + memory: &M, + flags: u32, + key_ptr: u32, + key_len: u32, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |len| { + if transient { + RuntimeCosts::ClearTransientStorage(len) + } else { + RuntimeCosts::ClearStorage(len) + } + }; + let charged = self.charge_gas(costs(self.ext.max_value_size()))?; + let key = self.decode_key(memory, key_ptr, key_len)?; + let outcome = if transient { + self.ext.set_transient_storage(&key, None, false)? + } else { + self.ext.set_storage(&key, None, false)? + }; + self.adjust_gas(charged, costs(outcome.old_len())); + Ok(outcome.old_len_with_sentinel()) + } + + fn get_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |len| { + if transient { + RuntimeCosts::GetTransientStorage(len) + } else { + RuntimeCosts::GetStorage(len) + } + }; + let charged = self.charge_gas(costs(self.ext.max_value_size()))?; + let key = self.decode_key(memory, key_ptr, key_len)?; + let outcome = if transient { + self.ext.get_transient_storage(&key) + } else { + self.ext.get_storage(&key) + }; + if let Some(value) = outcome { + self.adjust_gas(charged, costs(value.len() as u32)); + self.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + &value, + false, + already_charged, + )?; + Ok(ReturnErrorCode::Success) + } else { + self.adjust_gas(charged, costs(0)); + Ok(ReturnErrorCode::KeyNotFound) + } + } + + fn contains_storage( + &mut self, + memory: &M, + flags: u32, + key_ptr: u32, + key_len: u32, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |len| { + if transient { + RuntimeCosts::ContainsTransientStorage(len) + } else { + RuntimeCosts::ContainsStorage(len) + } + }; + let charged = self.charge_gas(costs(self.ext.max_value_size()))?; + let key = self.decode_key(memory, key_ptr, key_len)?; + let outcome = if transient { + self.ext.get_transient_storage_size(&key) + } else { + self.ext.get_storage_size(&key) + }; + self.adjust_gas(charged, costs(outcome.unwrap_or(0))); + Ok(outcome.unwrap_or(SENTINEL)) + } + + fn take_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |len| { + if transient { + RuntimeCosts::TakeTransientStorage(len) + } else { + RuntimeCosts::TakeStorage(len) + } + }; + let charged = self.charge_gas(costs(self.ext.max_value_size()))?; + let key = self.decode_key(memory, key_ptr, key_len)?; + let outcome = if transient { + self.ext.set_transient_storage(&key, None, true)? + } else { + self.ext.set_storage(&key, None, true)? + }; + + if let crate::storage::WriteOutcome::Taken(value) = outcome { + self.adjust_gas(charged, costs(value.len() as u32)); + self.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + &value, + false, + already_charged, + )?; + Ok(ReturnErrorCode::Success) + } else { + self.adjust_gas(charged, costs(0)); + Ok(ReturnErrorCode::KeyNotFound) + } + } + + fn call( + &mut self, + memory: &mut M, + flags: CallFlags, + call_type: CallType, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32, + ) -> Result { + self.charge_gas(call_type.cost())?; + + let input_data = if flags.contains(CallFlags::CLONE_INPUT) { + let input = self.input_data.as_ref().ok_or(Error::::InputForwarded)?; + charge_gas!(self, RuntimeCosts::CallInputCloned(input.len() as u32))?; + input.clone() + } else if flags.contains(CallFlags::FORWARD_INPUT) { + self.input_data.take().ok_or(Error::::InputForwarded)? + } else { + self.charge_gas(RuntimeCosts::CopyFromContract(input_data_len))?; + memory.read(input_data_ptr, input_data_len)? + }; + + let call_outcome = match call_type { + CallType::Call { callee_ptr, value_ptr, deposit_ptr, weight } => { + let mut callee = H160::zero(); + memory.read_into_buf(callee_ptr, callee.as_bytes_mut())?; + let deposit_limit = if deposit_ptr == SENTINEL { + U256::zero() + } else { + memory.read_u256(deposit_ptr)? + }; + let read_only = flags.contains(CallFlags::READ_ONLY); + let value = memory.read_u256(value_ptr)?; + if value > 0u32.into() { + // If the call value is non-zero and state change is not allowed, issue an + // error. + if read_only || self.ext.is_read_only() { + return Err(Error::::StateChangeDenied.into()); + } + self.charge_gas(RuntimeCosts::CallTransferSurcharge)?; + } + self.ext.call( + weight, + deposit_limit, + &callee, + value, + input_data, + flags.contains(CallFlags::ALLOW_REENTRY), + read_only, + ) + }, + CallType::DelegateCall { code_hash_ptr } => { + if flags.intersects(CallFlags::ALLOW_REENTRY | CallFlags::READ_ONLY) { + return Err(Error::::InvalidCallFlags.into()); + } + + let code_hash = memory.read_h256(code_hash_ptr)?; + self.ext.delegate_call(code_hash, input_data) + }, + }; + + match call_outcome { + // `TAIL_CALL` only matters on an `OK` result. Otherwise the call stack comes to + // a halt anyways without anymore code being executed. + Ok(_) if flags.contains(CallFlags::TAIL_CALL) => { + let output = mem::take(self.ext.last_frame_output_mut()); + return Err(TrapReason::Return(ReturnData { + flags: output.flags.bits(), + data: output.data, + })); + }, + Ok(_) => { + let output = mem::take(self.ext.last_frame_output_mut()); + let write_result = self.write_sandbox_output( + memory, + output_ptr, + output_len_ptr, + &output.data, + true, + |len| Some(RuntimeCosts::CopyToContract(len)), + ); + *self.ext.last_frame_output_mut() = output; + write_result?; + Ok(self.ext.last_frame_output().into()) + }, + Err(err) => Ok(Self::exec_error_into_return_code(err)?), + } + } + + fn instantiate( + &mut self, + memory: &mut M, + code_hash_ptr: u32, + weight: Weight, + deposit_ptr: u32, + value_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + address_ptr: u32, + output_ptr: u32, + output_len_ptr: u32, + salt_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::Instantiate { input_data_len })?; + let deposit_limit: U256 = + if deposit_ptr == SENTINEL { U256::zero() } else { memory.read_u256(deposit_ptr)? }; + let value = memory.read_u256(value_ptr)?; + let code_hash = memory.read_h256(code_hash_ptr)?; + let input_data = memory.read(input_data_ptr, input_data_len)?; + let salt = if salt_ptr == SENTINEL { + None + } else { + let salt: [u8; 32] = memory.read_array(salt_ptr)?; + Some(salt) + }; + + match self.ext.instantiate( + weight, + deposit_limit, + code_hash, + value, + input_data, + salt.as_ref(), + ) { + Ok(address) => { + if !self.ext.last_frame_output().flags.contains(ReturnFlags::REVERT) { + self.write_fixed_sandbox_output( + memory, + address_ptr, + &address.as_bytes(), + true, + already_charged, + )?; + } + let output = mem::take(self.ext.last_frame_output_mut()); + let write_result = self.write_sandbox_output( + memory, + output_ptr, + output_len_ptr, + &output.data, + true, + |len| Some(RuntimeCosts::CopyToContract(len)), + ); + *self.ext.last_frame_output_mut() = output; + write_result?; + Ok(self.ext.last_frame_output().into()) + }, + Err(err) => Ok(Self::exec_error_into_return_code(err)?), + } + } + + fn terminate(&mut self, memory: &M, beneficiary_ptr: u32) -> Result<(), TrapReason> { + let count = self.ext.locked_delegate_dependencies_count() as _; + self.charge_gas(RuntimeCosts::Terminate(count))?; + + let mut beneficiary = H160::zero(); + memory.read_into_buf(beneficiary_ptr, beneficiary.as_bytes_mut())?; + self.ext.terminate(&beneficiary)?; + Err(TrapReason::Termination) + } +} + +// This is the API exposed to contracts. +// +// # Note +// +// Any input that leads to a out of bound error (reading or writing) or failing to decode +// data passed to the supervisor will lead to a trap. This is not documented explicitly +// for every function. +#[define_env] +pub mod env { + /// Noop function used to benchmark the time it takes to execute an empty function. + #[cfg(feature = "runtime-benchmarks")] + fn noop(&mut self, memory: &mut M) -> Result<(), TrapReason> { + Ok(()) + } + + /// Set the value at the given key in the contract storage. + /// See [`pallet_revive_uapi::HostFn::set_storage_v2`] + #[api_version(0)] + #[mutating] + fn set_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + value_ptr: u32, + value_len: u32, + ) -> Result { + self.set_storage(memory, flags, key_ptr, key_len, value_ptr, value_len) + } + + /// Clear the value at the given key in the contract storage. + /// See [`pallet_revive_uapi::HostFn::clear_storage`] + #[api_version(0)] + #[mutating] + fn clear_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + ) -> Result { + self.clear_storage(memory, flags, key_ptr, key_len) + } + + /// Retrieve the value under the given key from storage. + /// See [`pallet_revive_uapi::HostFn::get_storage`] + #[api_version(0)] + fn get_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result { + self.get_storage(memory, flags, key_ptr, key_len, out_ptr, out_len_ptr) + } + + /// Checks whether there is a value stored under the given key. + /// See [`pallet_revive_uapi::HostFn::contains_storage`] + #[api_version(0)] + fn contains_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + ) -> Result { + self.contains_storage(memory, flags, key_ptr, key_len) + } + + /// Retrieve and remove the value under the given key from storage. + /// See [`pallet_revive_uapi::HostFn::take_storage`] + #[api_version(0)] + #[mutating] + fn take_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result { + self.take_storage(memory, flags, key_ptr, key_len, out_ptr, out_len_ptr) + } + + /// Transfer some value to another account. + /// See [`pallet_revive_uapi::HostFn::transfer`]. + #[api_version(0)] + #[mutating] + fn transfer( + &mut self, + memory: &mut M, + address_ptr: u32, + value_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::Transfer)?; + let mut callee = H160::zero(); + memory.read_into_buf(address_ptr, callee.as_bytes_mut())?; + let value: U256 = memory.read_u256(value_ptr)?; + let result = self.ext.transfer(&callee, value); + match result { + Ok(()) => Ok(ReturnErrorCode::Success), + Err(err) => { + let code = Self::err_into_return_code(err)?; + Ok(code) + }, + } + } + + /// Make a call to another contract. + /// See [`pallet_revive_uapi::HostFn::call`]. + #[api_version(0)] + fn call( + &mut self, + memory: &mut M, + flags: u32, + callee_ptr: u32, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_ptr: u32, + value_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32, + ) -> Result { + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::Call { + callee_ptr, + value_ptr, + deposit_ptr, + weight: Weight::from_parts(ref_time_limit, proof_size_limit), + }, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Execute code in the context (storage, caller, value) of the current contract. + /// See [`pallet_revive_uapi::HostFn::delegate_call`]. + #[api_version(0)] + fn delegate_call( + &mut self, + memory: &mut M, + flags: u32, + code_hash_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32, + ) -> Result { + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::DelegateCall { code_hash_ptr }, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Instantiate a contract with the specified code hash. + /// See [`pallet_revive_uapi::HostFn::instantiate`]. + #[api_version(0)] + #[mutating] + fn instantiate( + &mut self, + memory: &mut M, + code_hash_ptr: u32, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_ptr: u32, + value_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + address_ptr: u32, + output_ptr: u32, + output_len_ptr: u32, + salt_ptr: u32, + ) -> Result { + self.instantiate( + memory, + code_hash_ptr, + Weight::from_parts(ref_time_limit, proof_size_limit), + deposit_ptr, + value_ptr, + input_data_ptr, + input_data_len, + address_ptr, + output_ptr, + output_len_ptr, + salt_ptr, + ) + } + + /// Remove the calling account and transfer remaining **free** balance. + /// See [`pallet_revive_uapi::HostFn::terminate`]. + #[api_version(0)] + #[mutating] + fn terminate(&mut self, memory: &mut M, beneficiary_ptr: u32) -> Result<(), TrapReason> { + self.terminate(memory, beneficiary_ptr) + } + + /// Stores the input passed by the caller into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::input`]. + #[api_version(0)] + fn input(&mut self, memory: &mut M, out_ptr: u32, out_len_ptr: u32) -> Result<(), TrapReason> { + if let Some(input) = self.input_data.take() { + self.write_sandbox_output(memory, out_ptr, out_len_ptr, &input, false, |len| { + Some(RuntimeCosts::CopyToContract(len)) + })?; + self.input_data = Some(input); + Ok(()) + } else { + Err(Error::::InputForwarded.into()) + } + } + + /// Cease contract execution and save a data buffer as a result of the execution. + /// See [`pallet_revive_uapi::HostFn::return_value`]. + #[api_version(0)] + fn seal_return( + &mut self, + memory: &mut M, + flags: u32, + data_ptr: u32, + data_len: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::CopyFromContract(data_len))?; + Err(TrapReason::Return(ReturnData { flags, data: memory.read(data_ptr, data_len)? })) + } + + /// Stores the address of the caller into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::caller`]. + #[api_version(0)] + fn caller(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Caller)?; + let caller = ::AddressMapper::to_address(self.ext.caller().account_id()?); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + caller.as_bytes(), + false, + already_charged, + )?) + } + + /// Checks whether a specified address belongs to a contract. + /// See [`pallet_revive_uapi::HostFn::is_contract`]. + #[api_version(0)] + fn is_contract(&mut self, memory: &mut M, account_ptr: u32) -> Result { + self.charge_gas(RuntimeCosts::IsContract)?; + let mut address = H160::zero(); + memory.read_into_buf(account_ptr, address.as_bytes_mut())?; + Ok(self.ext.is_contract(&address) as u32) + } + + /// Retrieve the code hash for a specified contract address. + /// See [`pallet_revive_uapi::HostFn::code_hash`]. + #[api_version(0)] + fn code_hash( + &mut self, + memory: &mut M, + addr_ptr: u32, + out_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::CodeHash)?; + let mut address = H160::zero(); + memory.read_into_buf(addr_ptr, address.as_bytes_mut())?; + if let Some(value) = self.ext.code_hash(&address) { + self.write_fixed_sandbox_output( + memory, + out_ptr, + &value.as_bytes(), + false, + already_charged, + )?; + Ok(ReturnErrorCode::Success) + } else { + Ok(ReturnErrorCode::KeyNotFound) + } + } + + /// Retrieve the code hash of the currently executing contract. + /// See [`pallet_revive_uapi::HostFn::own_code_hash`]. + #[api_version(0)] + fn own_code_hash(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::OwnCodeHash)?; + let code_hash = *self.ext.own_code_hash(); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + code_hash.as_bytes(), + false, + already_charged, + )?) + } + + /// Checks whether the caller of the current contract is the origin of the whole call stack. + /// See [`pallet_revive_uapi::HostFn::caller_is_origin`]. + #[api_version(0)] + fn caller_is_origin(&mut self, _memory: &mut M) -> Result { + self.charge_gas(RuntimeCosts::CallerIsOrigin)?; + Ok(self.ext.caller_is_origin() as u32) + } + + /// Checks whether the caller of the current contract is root. + /// See [`pallet_revive_uapi::HostFn::caller_is_root`]. + #[api_version(0)] + fn caller_is_root(&mut self, _memory: &mut M) -> Result { + self.charge_gas(RuntimeCosts::CallerIsRoot)?; + Ok(self.ext.caller_is_root() as u32) + } + + /// Stores the address of the current contract into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::address`]. + #[api_version(0)] + fn address(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Address)?; + let address = self.ext.address(); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + address.as_bytes(), + false, + already_charged, + )?) + } + + /// Stores the price for the specified amount of weight into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::weight_to_fee`]. + #[api_version(0)] + fn weight_to_fee( + &mut self, + memory: &mut M, + ref_time_limit: u64, + proof_size_limit: u64, + out_ptr: u32, + ) -> Result<(), TrapReason> { + let weight = Weight::from_parts(ref_time_limit, proof_size_limit); + self.charge_gas(RuntimeCosts::WeightToFee)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &self.ext.get_weight_price(weight).encode(), + false, + already_charged, + )?) + } + + /// Stores the amount of weight left into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::weight_left`]. + #[api_version(0)] + fn weight_left( + &mut self, + memory: &mut M, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::GasLeft)?; + let gas_left = &self.ext.gas_meter().gas_left().encode(); + Ok(self.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + gas_left, + false, + already_charged, + )?) + } + + /// Stores the *free* balance of the current account into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::balance`]. + #[api_version(0)] + fn balance(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Balance)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(self.ext.balance()), + false, + already_charged, + )?) + } + + /// Stores the *free* balance of the supplied address into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::balance`]. + #[api_version(0)] + fn balance_of( + &mut self, + memory: &mut M, + addr_ptr: u32, + out_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::BalanceOf)?; + let mut address = H160::zero(); + memory.read_into_buf(addr_ptr, address.as_bytes_mut())?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(self.ext.balance_of(&address)), + false, + already_charged, + )?) + } + + /// Returns the chain ID. + /// See [`pallet_revive_uapi::HostFn::chain_id`]. + #[api_version(0)] + fn chain_id(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(U256::from(::ChainId::get())), + false, + |_| Some(RuntimeCosts::CopyToContract(32)), + )?) + } + + /// Stores the value transferred along with this call/instantiate into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::value_transferred`]. + #[api_version(0)] + fn value_transferred(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::ValueTransferred)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(self.ext.value_transferred()), + false, + already_charged, + )?) + } + + /// Load the latest block timestamp into the supplied buffer + /// See [`pallet_revive_uapi::HostFn::now`]. + #[api_version(0)] + fn now(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Now)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(self.ext.now()), + false, + already_charged, + )?) + } + + /// Stores the minimum balance (a.k.a. existential deposit) into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::minimum_balance`]. + #[api_version(0)] + fn minimum_balance(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::MinimumBalance)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(self.ext.minimum_balance()), + false, + already_charged, + )?) + } + + /// Deposit a contract event with the data buffer and optional list of topics. + /// See [pallet_revive_uapi::HostFn::deposit_event] + #[api_version(0)] + #[mutating] + fn deposit_event( + &mut self, + memory: &mut M, + topics_ptr: u32, + num_topic: u32, + data_ptr: u32, + data_len: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::DepositEvent { num_topic, len: data_len })?; + + if num_topic > limits::NUM_EVENT_TOPICS { + return Err(Error::::TooManyTopics.into()); + } + + if data_len > self.ext.max_value_size() { + return Err(Error::::ValueTooLarge.into()); + } + + let topics: Vec = match num_topic { + 0 => Vec::new(), + _ => { + let mut v = Vec::with_capacity(num_topic as usize); + let topics_len = num_topic * H256::len_bytes() as u32; + let buf = memory.read(topics_ptr, topics_len)?; + for chunk in buf.chunks_exact(H256::len_bytes()) { + v.push(H256::from_slice(chunk)); + } + v + }, + }; + + let event_data = memory.read(data_ptr, data_len)?; + self.ext.deposit_event(topics, event_data); + Ok(()) + } + + /// Stores the current block number of the current contract into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::block_number`]. + #[api_version(0)] + fn block_number(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::BlockNumber)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(self.ext.block_number()), + false, + already_charged, + )?) + } + + /// Computes the SHA2 256-bit hash on the given input buffer. + /// See [`pallet_revive_uapi::HostFn::hash_sha2_256`]. + #[api_version(0)] + fn hash_sha2_256( + &mut self, + memory: &mut M, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::HashSha256(input_len))?; + Ok(self.compute_hash_on_intermediate_buffer( + memory, sha2_256, input_ptr, input_len, output_ptr, + )?) + } + + /// Computes the KECCAK 256-bit hash on the given input buffer. + /// See [`pallet_revive_uapi::HostFn::hash_keccak_256`]. + #[api_version(0)] + fn hash_keccak_256( + &mut self, + memory: &mut M, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::HashKeccak256(input_len))?; + Ok(self.compute_hash_on_intermediate_buffer( + memory, keccak_256, input_ptr, input_len, output_ptr, + )?) + } + + /// Computes the BLAKE2 256-bit hash on the given input buffer. + /// See [`pallet_revive_uapi::HostFn::hash_blake2_256`]. + #[api_version(0)] + fn hash_blake2_256( + &mut self, + memory: &mut M, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::HashBlake256(input_len))?; + Ok(self.compute_hash_on_intermediate_buffer( + memory, blake2_256, input_ptr, input_len, output_ptr, + )?) + } + + /// Computes the BLAKE2 128-bit hash on the given input buffer. + /// See [`pallet_revive_uapi::HostFn::hash_blake2_128`]. + #[api_version(0)] + fn hash_blake2_128( + &mut self, + memory: &mut M, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::HashBlake128(input_len))?; + Ok(self.compute_hash_on_intermediate_buffer( + memory, blake2_128, input_ptr, input_len, output_ptr, + )?) + } + + /// Call into the chain extension provided by the chain if any. + /// See [`pallet_revive_uapi::HostFn::call_chain_extension`]. + fn call_chain_extension( + &mut self, + memory: &mut M, + id: u32, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + output_len_ptr: u32, + ) -> Result { + use crate::chain_extension::{ChainExtension, Environment, RetVal}; + if !::ChainExtension::enabled() { + return Err(Error::::NoChainExtension.into()); + } + let mut chain_extension = self.chain_extension.take().expect( + "Constructor initializes with `Some`. This is the only place where it is set to \ + `None`.It is always reset to `Some` afterwards. qed", + ); + let env = + Environment::new(self, memory, id, input_ptr, input_len, output_ptr, output_len_ptr); + let ret = match chain_extension.call(env)? { + RetVal::Converging(val) => Ok(val), + RetVal::Diverging { flags, data } => + Err(TrapReason::Return(ReturnData { flags: flags.bits(), data })), + }; + self.chain_extension = Some(chain_extension); + ret + } + + /// Emit a custom debug message. + /// See [`pallet_revive_uapi::HostFn::debug_message`]. + #[api_version(0)] + fn debug_message( + &mut self, + memory: &mut M, + str_ptr: u32, + str_len: u32, + ) -> Result { + let str_len = str_len.min(limits::DEBUG_BUFFER_BYTES); + self.charge_gas(RuntimeCosts::DebugMessage(str_len))?; + if self.ext.append_debug_buffer("") { + let data = memory.read(str_ptr, str_len)?; + if let Some(msg) = core::str::from_utf8(&data).ok() { + self.ext.append_debug_buffer(msg); + } + Ok(ReturnErrorCode::Success) + } else { + Ok(ReturnErrorCode::LoggingDisabled) + } + } + + /// Call some dispatchable of the runtime. + /// See [`frame_support::traits::call_runtime`]. + #[mutating] + fn call_runtime( + &mut self, + memory: &mut M, + call_ptr: u32, + call_len: u32, + ) -> Result { + use frame_support::dispatch::GetDispatchInfo; + self.charge_gas(RuntimeCosts::CopyFromContract(call_len))?; + let call: ::RuntimeCall = memory.read_as_unbounded(call_ptr, call_len)?; + self.call_dispatchable::( + call.get_dispatch_info(), + RuntimeCosts::CallRuntime, + |runtime| runtime.ext.call_runtime(call), + ) + } + + /// Execute an XCM program locally, using the contract's address as the origin. + /// See [`pallet_revive_uapi::HostFn::execute_xcm`]. + #[mutating] + fn xcm_execute( + &mut self, + memory: &mut M, + msg_ptr: u32, + msg_len: u32, + ) -> Result { + use frame_support::dispatch::DispatchInfo; + use xcm::VersionedXcm; + use xcm_builder::{ExecuteController, ExecuteControllerWeightInfo}; + + self.charge_gas(RuntimeCosts::CopyFromContract(msg_len))?; + let message: VersionedXcm> = memory.read_as_unbounded(msg_ptr, msg_len)?; + + let execute_weight = + <::Xcm as ExecuteController<_, _>>::WeightInfo::execute(); + let weight = self.ext.gas_meter().gas_left().max(execute_weight); + let dispatch_info = DispatchInfo { weight, ..Default::default() }; + + self.call_dispatchable::( + dispatch_info, + RuntimeCosts::CallXcmExecute, + |runtime| { + let origin = crate::RawOrigin::Signed(runtime.ext.account_id().clone()).into(); + let weight_used = <::Xcm>::execute( + origin, + Box::new(message), + weight.saturating_sub(execute_weight), + )?; + + Ok(Some(weight_used.saturating_add(execute_weight)).into()) + }, + ) + } + + /// Send an XCM program from the contract to the specified destination. + /// See [`pallet_revive_uapi::HostFn::send_xcm`]. + #[mutating] + fn xcm_send( + &mut self, + memory: &mut M, + dest_ptr: u32, + dest_len: u32, + msg_ptr: u32, + msg_len: u32, + output_ptr: u32, + ) -> Result { + use xcm::{VersionedLocation, VersionedXcm}; + use xcm_builder::{SendController, SendControllerWeightInfo}; + + self.charge_gas(RuntimeCosts::CopyFromContract(dest_len))?; + let dest: VersionedLocation = memory.read_as_unbounded(dest_ptr, dest_len)?; + + self.charge_gas(RuntimeCosts::CopyFromContract(msg_len))?; + let message: VersionedXcm<()> = memory.read_as_unbounded(msg_ptr, msg_len)?; + + let weight = <::Xcm as SendController<_>>::WeightInfo::send(); + self.charge_gas(RuntimeCosts::CallRuntime(weight))?; + let origin = crate::RawOrigin::Signed(self.ext.account_id().clone()).into(); + + match <::Xcm>::send(origin, dest.into(), message.into()) { + Ok(message_id) => { + memory.write(output_ptr, &message_id.encode())?; + Ok(ReturnErrorCode::Success) + }, + Err(e) => { + if self.ext.append_debug_buffer("") { + self.ext.append_debug_buffer("seal0::xcm_send failed with: "); + self.ext.append_debug_buffer(e.into()); + }; + Ok(ReturnErrorCode::XcmSendFailed) + }, + } + } + + /// Recovers the ECDSA public key from the given message hash and signature. + /// See [`pallet_revive_uapi::HostFn::ecdsa_recover`]. + #[api_version(0)] + fn ecdsa_recover( + &mut self, + memory: &mut M, + signature_ptr: u32, + message_hash_ptr: u32, + output_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::EcdsaRecovery)?; + + let mut signature: [u8; 65] = [0; 65]; + memory.read_into_buf(signature_ptr, &mut signature)?; + let mut message_hash: [u8; 32] = [0; 32]; + memory.read_into_buf(message_hash_ptr, &mut message_hash)?; + + let result = self.ext.ecdsa_recover(&signature, &message_hash); + + match result { + Ok(pub_key) => { + // Write the recovered compressed ecdsa public key back into the sandboxed output + // buffer. + memory.write(output_ptr, pub_key.as_ref())?; + + Ok(ReturnErrorCode::Success) + }, + Err(_) => Ok(ReturnErrorCode::EcdsaRecoveryFailed), + } + } + + /// Verify a sr25519 signature + /// See [`pallet_revive_uapi::HostFn::sr25519_verify`]. + #[api_version(0)] + fn sr25519_verify( + &mut self, + memory: &mut M, + signature_ptr: u32, + pub_key_ptr: u32, + message_len: u32, + message_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::Sr25519Verify(message_len))?; + + let mut signature: [u8; 64] = [0; 64]; + memory.read_into_buf(signature_ptr, &mut signature)?; + + let mut pub_key: [u8; 32] = [0; 32]; + memory.read_into_buf(pub_key_ptr, &mut pub_key)?; + + let message: Vec = memory.read(message_ptr, message_len)?; + + if self.ext.sr25519_verify(&signature, &message, &pub_key) { + Ok(ReturnErrorCode::Success) + } else { + Ok(ReturnErrorCode::Sr25519VerifyFailed) + } + } + + /// Replace the contract code at the specified address with new code. + /// See [`pallet_revive_uapi::HostFn::set_code_hash`]. + #[api_version(0)] + #[mutating] + fn set_code_hash( + &mut self, + memory: &mut M, + code_hash_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::SetCodeHash)?; + let code_hash: H256 = memory.read_h256(code_hash_ptr)?; + match self.ext.set_code_hash(code_hash) { + Err(err) => { + let code = Self::err_into_return_code(err)?; + Ok(code) + }, + Ok(()) => Ok(ReturnErrorCode::Success), + } + } + + /// Calculates Ethereum address from the ECDSA compressed public key and stores + /// See [`pallet_revive_uapi::HostFn::ecdsa_to_eth_address`]. + #[api_version(0)] + fn ecdsa_to_eth_address( + &mut self, + memory: &mut M, + key_ptr: u32, + out_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::EcdsaToEthAddress)?; + let mut compressed_key: [u8; 33] = [0; 33]; + memory.read_into_buf(key_ptr, &mut compressed_key)?; + let result = self.ext.ecdsa_to_eth_address(&compressed_key); + match result { + Ok(eth_address) => { + memory.write(out_ptr, eth_address.as_ref())?; + Ok(ReturnErrorCode::Success) + }, + Err(_) => Ok(ReturnErrorCode::EcdsaRecoveryFailed), + } + } + + /// Adds a new delegate dependency to the contract. + /// See [`pallet_revive_uapi::HostFn::lock_delegate_dependency`]. + #[api_version(0)] + #[mutating] + fn lock_delegate_dependency( + &mut self, + memory: &mut M, + code_hash_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::LockDelegateDependency)?; + let code_hash = memory.read_h256(code_hash_ptr)?; + self.ext.lock_delegate_dependency(code_hash)?; + Ok(()) + } + + /// Removes the delegate dependency from the contract. + /// see [`pallet_revive_uapi::HostFn::unlock_delegate_dependency`]. + #[api_version(0)] + #[mutating] + fn unlock_delegate_dependency( + &mut self, + memory: &mut M, + code_hash_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::UnlockDelegateDependency)?; + let code_hash = memory.read_h256(code_hash_ptr)?; + self.ext.unlock_delegate_dependency(&code_hash)?; + Ok(()) + } + + /// Stores the length of the data returned by the last call into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::return_data_size`]. + #[api_version(0)] + fn return_data_size(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &as_bytes(U256::from(self.ext.last_frame_output().data.len())), + false, + |len| Some(RuntimeCosts::CopyToContract(len)), + )?) + } + + /// Stores data returned by the last call, starting from `offset`, into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::return_data`]. + #[api_version(0)] + fn return_data_copy( + &mut self, + memory: &mut M, + out_ptr: u32, + out_len_ptr: u32, + offset: u32, + ) -> Result<(), TrapReason> { + let output = mem::take(self.ext.last_frame_output_mut()); + let result = if offset as usize > output.data.len() { + Err(Error::::OutOfBounds.into()) + } else { + self.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + &output.data[offset as usize..], + false, + |len| Some(RuntimeCosts::CopyToContract(len)), + ) + }; + *self.ext.last_frame_output_mut() = output; + Ok(result?) + } +} diff --git a/pallets/revive/src/weights.rs b/pallets/revive/src/weights.rs new file mode 100644 index 00000000..b66c28bd --- /dev/null +++ b/pallets/revive/src/weights.rs @@ -0,0 +1,1770 @@ +// 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_revive` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-07-17, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-yaoqqom-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/production/substrate-node +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_revive +// --chain=dev +// --header=./substrate/HEADER-APACHE2 +// --output=./substrate/frame/contracts/src/weights.rs +// --template=./substrate/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_revive`. +pub trait WeightInfo { + fn on_process_deletion_queue_batch() -> Weight; + fn on_initialize_per_trie_key(k: u32, ) -> Weight; + fn call_with_code_per_byte(c: u32, ) -> Weight; + fn instantiate_with_code(c: u32, i: u32) -> Weight; + fn instantiate(i: u32) -> Weight; + fn call() -> Weight; + fn upload_code_determinism_enforced(c: u32, ) -> Weight; + fn upload_code_determinism_relaxed(c: u32, ) -> Weight; + fn remove_code() -> Weight; + fn set_code() -> Weight; + fn noop_host_fn(r: u32, ) -> Weight; + fn seal_caller() -> Weight; + fn seal_is_contract() -> Weight; + fn seal_code_hash() -> Weight; + fn seal_own_code_hash() -> Weight; + fn seal_caller_is_origin() -> Weight; + fn seal_caller_is_root() -> Weight; + fn seal_address() -> Weight; + fn seal_gas_left() -> Weight; + fn seal_balance() -> Weight; + fn seal_balance_of() -> Weight; + fn seal_value_transferred() -> Weight; + fn seal_minimum_balance() -> Weight; + fn seal_block_number() -> Weight; + fn seal_now() -> Weight; + fn seal_weight_to_fee() -> Weight; + fn seal_input(n: u32, ) -> Weight; + fn seal_return(n: u32, ) -> Weight; + fn seal_terminate(n: u32, ) -> Weight; + fn seal_random() -> Weight; + fn seal_deposit_event(t: u32, n: u32, ) -> Weight; + fn seal_debug_message(i: u32, ) -> Weight; + fn get_storage_empty() -> Weight; + fn get_storage_full() -> Weight; + fn set_storage_empty() -> Weight; + fn set_storage_full() -> Weight; + fn seal_set_storage(n: u32, o: u32, ) -> Weight; + fn seal_clear_storage(n: u32, ) -> Weight; + fn seal_get_storage(n: u32, ) -> Weight; + fn seal_contains_storage(n: u32, ) -> Weight; + fn seal_take_storage(n: u32, ) -> Weight; + fn set_transient_storage_empty() -> Weight; + fn set_transient_storage_full() -> Weight; + fn get_transient_storage_empty() -> Weight; + fn get_transient_storage_full() -> Weight; + fn rollback_transient_storage() -> Weight; + fn seal_set_transient_storage(n: u32, o: u32, ) -> Weight; + fn seal_clear_transient_storage(n: u32, ) -> Weight; + fn seal_get_transient_storage(n: u32, ) -> Weight; + fn seal_contains_transient_storage(n: u32, ) -> Weight; + fn seal_take_transient_storage(n: u32, ) -> Weight; + fn seal_transfer() -> Weight; + fn seal_call(t: u32, i: u32, ) -> Weight; + fn seal_delegate_call() -> Weight; + fn seal_instantiate(i: u32) -> Weight; + fn seal_hash_sha2_256(n: u32, ) -> Weight; + fn seal_hash_keccak_256(n: u32, ) -> Weight; + fn seal_hash_blake2_256(n: u32, ) -> Weight; + fn seal_hash_blake2_128(n: u32, ) -> Weight; + fn seal_sr25519_verify(n: u32, ) -> Weight; + fn seal_ecdsa_recover() -> Weight; + fn seal_ecdsa_to_eth_address() -> Weight; + fn seal_set_code_hash() -> Weight; + fn lock_delegate_dependency() -> Weight; + fn unlock_delegate_dependency() -> Weight; + fn seal_reentrance_count() -> Weight; + fn seal_account_reentrance_count() -> Weight; + fn seal_instantiation_nonce() -> Weight; + fn instr_i64_load_store(r: u32, ) -> Weight; +} + +/// Weights for `pallet_revive` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Contracts::DeletionQueueCounter` (r:1 w:0) + /// Proof: `Contracts::DeletionQueueCounter` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + fn on_process_deletion_queue_batch() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `1627` + // Minimum execution time: 1_915_000 picoseconds. + Weight::from_parts(1_986_000, 1627) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `k` is `[0, 1024]`. + fn on_initialize_per_trie_key(k: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `452 + k * (69 ±0)` + // Estimated: `442 + k * (70 ±0)` + // Minimum execution time: 11_103_000 picoseconds. + Weight::from_parts(11_326_000, 442) + // Standard Error: 2_291 + .saturating_add(Weight::from_parts(1_196_329, 0).saturating_mul(k.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(k.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(k.into()))) + .saturating_add(Weight::from_parts(0, 70).saturating_mul(k.into())) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + fn call_with_code_per_byte(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `800 + c * (1 ±0)` + // Estimated: `4266 + c * (1 ±0)` + // Minimum execution time: 247_545_000 picoseconds. + Weight::from_parts(268_016_699, 4266) + // Standard Error: 4 + .saturating_add(Weight::from_parts(700, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(c.into())) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:2 w:2) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::Nonce` (r:1 w:1) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + /// The range of component `i` is `[0, 1048576]`. + /// The range of component `s` is `[0, 1048576]`. + fn instantiate_with_code(c: u32, i: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `323` + // Estimated: `6262` + // Minimum execution time: 4_396_772_000 picoseconds. + Weight::from_parts(235_107_907, 6262) + // Standard Error: 185 + .saturating_add(Weight::from_parts(53_843, 0).saturating_mul(c.into())) + // Standard Error: 22 + .saturating_add(Weight::from_parts(2_143, 0).saturating_mul(i.into())) + // Standard Error: 22 + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Contracts::Nonce` (r:1 w:1) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// The range of component `i` is `[0, 1048576]`. + /// The range of component `s` is `[0, 1048576]`. + fn instantiate(i: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `560` + // Estimated: `4017` + // Minimum execution time: 2_240_868_000 picoseconds. + Weight::from_parts(2_273_668_000, 4017) + // Standard Error: 32 + .saturating_add(Weight::from_parts(934, 0).saturating_mul(i.into())) + // Standard Error: 32 + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + fn call() -> Weight { + // Proof Size summary in bytes: + // Measured: `826` + // Estimated: `4291` + // Minimum execution time: 165_067_000 picoseconds. + Weight::from_parts(168_582_000, 4291) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + fn upload_code_determinism_enforced(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `3607` + // Minimum execution time: 229_454_000 picoseconds. + Weight::from_parts(251_495_551, 3607) + // Standard Error: 71 + .saturating_add(Weight::from_parts(51_428, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + fn upload_code_determinism_relaxed(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `3607` + // Minimum execution time: 240_390_000 picoseconds. + Weight::from_parts(273_854_266, 3607) + // Standard Error: 243 + .saturating_add(Weight::from_parts(51_836, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + fn remove_code() -> Weight { + // Proof Size summary in bytes: + // Measured: `315` + // Estimated: `3780` + // Minimum execution time: 39_374_000 picoseconds. + Weight::from_parts(40_247_000, 3780) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:2 w:2) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + fn set_code() -> Weight { + // Proof Size summary in bytes: + // Measured: `552` + // Estimated: `6492` + // Minimum execution time: 24_473_000 picoseconds. + Weight::from_parts(25_890_000, 6492) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// The range of component `r` is `[0, 1600]`. + fn noop_host_fn(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_528_000 picoseconds. + Weight::from_parts(9_301_010, 0) + // Standard Error: 98 + .saturating_add(Weight::from_parts(53_173, 0).saturating_mul(r.into())) + } + fn seal_caller() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 643_000 picoseconds. + Weight::from_parts(678_000, 0) + } + /// Storage: `Contracts::ContractInfoOf` (r:1 w:0) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + fn seal_is_contract() -> Weight { + // Proof Size summary in bytes: + // Measured: `354` + // Estimated: `3819` + // Minimum execution time: 6_107_000 picoseconds. + Weight::from_parts(6_235_000, 3819) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Contracts::ContractInfoOf` (r:1 w:0) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + fn seal_code_hash() -> Weight { + // Proof Size summary in bytes: + // Measured: `447` + // Estimated: `3912` + // Minimum execution time: 7_316_000 picoseconds. + Weight::from_parts(7_653_000, 3912) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + fn seal_own_code_hash() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 721_000 picoseconds. + Weight::from_parts(764_000, 0) + } + fn seal_caller_is_origin() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 369_000 picoseconds. + Weight::from_parts(417_000, 0) + } + fn seal_caller_is_root() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 318_000 picoseconds. + Weight::from_parts(349_000, 0) + } + fn seal_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 590_000 picoseconds. + Weight::from_parts(628_000, 0) + } + fn seal_gas_left() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 660_000 picoseconds. + Weight::from_parts(730_000, 0) + } + fn seal_balance() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `0` + // Minimum execution time: 4_361_000 picoseconds. + Weight::from_parts(4_577_000, 0) + } + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + fn seal_balance_of() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3517` + // Minimum execution time: 3_751_000 picoseconds. + Weight::from_parts(3_874_000, 3517) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + fn seal_value_transferred() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 560_000 picoseconds. + Weight::from_parts(603_000, 0) + } + fn seal_minimum_balance() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 561_000 picoseconds. + Weight::from_parts(610_000, 0) + } + fn seal_block_number() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 557_000 picoseconds. + Weight::from_parts(583_000, 0) + } + fn seal_now() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 550_000 picoseconds. + Weight::from_parts(602_000, 0) + } + /// Storage: `TransactionPayment::NextFeeMultiplier` (r:1 w:0) + /// Proof: `TransactionPayment::NextFeeMultiplier` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `Measured`) + fn seal_weight_to_fee() -> Weight { + // Proof Size summary in bytes: + // Measured: `67` + // Estimated: `1552` + // Minimum execution time: 4_065_000 picoseconds. + Weight::from_parts(4_291_000, 1552) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// The range of component `n` is `[0, 1048572]`. + fn seal_input(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 487_000 picoseconds. + Weight::from_parts(517_000, 0) + // Standard Error: 3 + .saturating_add(Weight::from_parts(301, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048572]`. + fn seal_return(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 318_000 picoseconds. + Weight::from_parts(372_000, 0) + // Standard Error: 10 + .saturating_add(Weight::from_parts(411, 0).saturating_mul(n.into())) + } + /// Storage: `Contracts::DeletionQueueCounter` (r:1 w:1) + /// Proof: `Contracts::DeletionQueueCounter` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:33 w:33) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::DeletionQueue` (r:0 w:1) + /// Proof: `Contracts::DeletionQueue` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `Measured`) + /// The range of component `n` is `[0, 32]`. + fn seal_terminate(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `319 + n * (78 ±0)` + // Estimated: `3784 + n * (2553 ±0)` + // Minimum execution time: 13_251_000 picoseconds. + Weight::from_parts(15_257_892, 3784) + // Standard Error: 7_089 + .saturating_add(Weight::from_parts(3_443_907, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2553).saturating_mul(n.into())) + } + /// Storage: `RandomnessCollectiveFlip::RandomMaterial` (r:1 w:0) + /// Proof: `RandomnessCollectiveFlip::RandomMaterial` (`max_values`: Some(1), `max_size`: Some(2594), added: 3089, mode: `Measured`) + fn seal_random() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1561` + // Minimum execution time: 3_434_000 picoseconds. + Weight::from_parts(3_605_000, 1561) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `System::EventTopics` (r:4 w:4) + /// Proof: `System::EventTopics` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `t` is `[0, 4]`. + /// The range of component `n` is `[0, 16384]`. + fn seal_deposit_event(t: u32, n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `990 + t * (2475 ±0)` + // Minimum execution time: 3_668_000 picoseconds. + Weight::from_parts(3_999_591, 990) + // Standard Error: 5_767 + .saturating_add(Weight::from_parts(2_011_090, 0).saturating_mul(t.into())) + // Standard Error: 1 + .saturating_add(Weight::from_parts(12, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(t.into()))) + .saturating_add(Weight::from_parts(0, 2475).saturating_mul(t.into())) + } + /// The range of component `i` is `[0, 1048576]`. + fn seal_debug_message(i: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 443_000 picoseconds. + Weight::from_parts(472_000, 0) + // Standard Error: 10 + .saturating_add(Weight::from_parts(1_207, 0).saturating_mul(i.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn get_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `16618` + // Estimated: `16618` + // Minimum execution time: 13_752_000 picoseconds. + Weight::from_parts(14_356_000, 16618) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn get_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `26628` + // Estimated: `26628` + // Minimum execution time: 43_444_000 picoseconds. + Weight::from_parts(45_087_000, 26628) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `16618` + // Estimated: `16618` + // Minimum execution time: 15_616_000 picoseconds. + Weight::from_parts(16_010_000, 16618) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `26628` + // Estimated: `26628` + // Minimum execution time: 47_020_000 picoseconds. + Weight::from_parts(50_152_000, 26628) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + /// The range of component `o` is `[0, 16384]`. + fn seal_set_storage(n: u32, o: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `250 + o * (1 ±0)` + // Estimated: `249 + o * (1 ±0)` + // Minimum execution time: 8_824_000 picoseconds. + Weight::from_parts(8_915_233, 249) + // Standard Error: 1 + .saturating_add(Weight::from_parts(255, 0).saturating_mul(n.into())) + // Standard Error: 1 + .saturating_add(Weight::from_parts(39, 0).saturating_mul(o.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(o.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_clear_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 7_133_000 picoseconds. + Weight::from_parts(7_912_778, 248) + // Standard Error: 1 + .saturating_add(Weight::from_parts(88, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_get_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 6_746_000 picoseconds. + Weight::from_parts(7_647_236, 248) + // Standard Error: 2 + .saturating_add(Weight::from_parts(603, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_contains_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 6_247_000 picoseconds. + Weight::from_parts(6_952_661, 248) + // Standard Error: 1 + .saturating_add(Weight::from_parts(77, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_take_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 7_428_000 picoseconds. + Weight::from_parts(8_384_015, 248) + // Standard Error: 2 + .saturating_add(Weight::from_parts(625, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + fn set_transient_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_478_000 picoseconds. + Weight::from_parts(1_533_000, 0) + } + fn set_transient_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_485_000 picoseconds. + Weight::from_parts(2_728_000, 0) + } + fn get_transient_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_195_000 picoseconds. + Weight::from_parts(3_811_000, 0) + } + fn get_transient_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_902_000 picoseconds. + Weight::from_parts(4_118_000, 0) + } + fn rollback_transient_storage() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_571_000 picoseconds. + Weight::from_parts(1_662_000, 0) + } + /// The range of component `n` is `[0, 16384]`. + /// The range of component `o` is `[0, 16384]`. + fn seal_set_transient_storage(n: u32, o: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 5_250_000 picoseconds. + Weight::from_parts(2_465_568, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(201, 0).saturating_mul(n.into())) + // Standard Error: 0 + .saturating_add(Weight::from_parts(223, 0).saturating_mul(o.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_clear_transient_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_012_000 picoseconds. + Weight::from_parts(2_288_004, 0) + // Standard Error: 3 + .saturating_add(Weight::from_parts(239, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_get_transient_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_906_000 picoseconds. + Weight::from_parts(2_121_040, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(225, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_contains_transient_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_736_000 picoseconds. + Weight::from_parts(1_954_728, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(111, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_take_transient_storage(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_872_000 picoseconds. + Weight::from_parts(8_125_644, 0) + } + fn seal_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `0` + // Minimum execution time: 8_489_000 picoseconds. + Weight::from_parts(8_791_000, 0) + } + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// The range of component `t` is `[0, 1]`. + /// The range of component `i` is `[0, 1048576]`. + fn seal_call(t: u32, i: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `620 + t * (280 ±0)` + // Estimated: `4085 + t * (2182 ±0)` + // Minimum execution time: 122_759_000 picoseconds. + Weight::from_parts(120_016_020, 4085) + // Standard Error: 173_118 + .saturating_add(Weight::from_parts(42_848_338, 0).saturating_mul(t.into())) + // Standard Error: 0 + .saturating_add(Weight::from_parts(6, 0).saturating_mul(i.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(t.into()))) + .saturating_add(Weight::from_parts(0, 2182).saturating_mul(t.into())) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + fn seal_delegate_call() -> Weight { + // Proof Size summary in bytes: + // Measured: `430` + // Estimated: `3895` + // Minimum execution time: 111_566_000 picoseconds. + Weight::from_parts(115_083_000, 3895) + .saturating_add(T::DbWeight::get().reads(2_u64)) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Contracts::Nonce` (r:1 w:0) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// The range of component `i` is `[0, 983040]`. + /// The range of component `s` is `[0, 983040]`. + fn seal_instantiate(i: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `676` + // Estimated: `4132` + // Minimum execution time: 1_871_402_000 picoseconds. + Weight::from_parts(1_890_038_000, 4132) + // Standard Error: 24 + .saturating_add(Weight::from_parts(581, 0).saturating_mul(i.into())) + // Standard Error: 24 + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_sha2_256(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 966_000 picoseconds. + Weight::from_parts(9_599_151, 0) + // Standard Error: 1 + .saturating_add(Weight::from_parts(1_336, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_keccak_256(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_416_000 picoseconds. + Weight::from_parts(10_964_255, 0) + // Standard Error: 1 + .saturating_add(Weight::from_parts(3_593, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_blake2_256(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 821_000 picoseconds. + Weight::from_parts(6_579_283, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(1_466, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_blake2_128(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 773_000 picoseconds. + Weight::from_parts(10_990_209, 0) + // Standard Error: 1 + .saturating_add(Weight::from_parts(1_457, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 125697]`. + fn seal_sr25519_verify(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 43_195_000 picoseconds. + Weight::from_parts(41_864_855, 0) + // Standard Error: 9 + .saturating_add(Weight::from_parts(5_154, 0).saturating_mul(n.into())) + } + fn seal_ecdsa_recover() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 47_747_000 picoseconds. + Weight::from_parts(49_219_000, 0) + } + fn seal_ecdsa_to_eth_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 12_854_000 picoseconds. + Weight::from_parts(12_962_000, 0) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + fn seal_set_code_hash() -> Weight { + // Proof Size summary in bytes: + // Measured: `430` + // Estimated: `3895` + // Minimum execution time: 17_868_000 picoseconds. + Weight::from_parts(18_486_000, 3895) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + fn lock_delegate_dependency() -> Weight { + // Proof Size summary in bytes: + // Measured: `355` + // Estimated: `3820` + // Minimum execution time: 8_393_000 picoseconds. + Weight::from_parts(8_640_000, 3820) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `MaxEncodedLen`) + fn unlock_delegate_dependency() -> Weight { + // Proof Size summary in bytes: + // Measured: `355` + // Estimated: `3558` + // Minimum execution time: 7_489_000 picoseconds. + Weight::from_parts(7_815_000, 3558) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn seal_reentrance_count() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 299_000 picoseconds. + Weight::from_parts(339_000, 0) + } + fn seal_account_reentrance_count() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 324_000 picoseconds. + Weight::from_parts(380_000, 0) + } + /// Storage: `Contracts::Nonce` (r:1 w:0) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + fn seal_instantiation_nonce() -> Weight { + // Proof Size summary in bytes: + // Measured: `219` + // Estimated: `1704` + // Minimum execution time: 2_768_000 picoseconds. + Weight::from_parts(3_025_000, 1704) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// The range of component `r` is `[0, 5000]`. + fn instr_i64_load_store(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 766_000 picoseconds. + Weight::from_parts(722_169, 0) + // Standard Error: 10 + .saturating_add(Weight::from_parts(7_191, 0).saturating_mul(r.into())) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Contracts::DeletionQueueCounter` (r:1 w:0) + /// Proof: `Contracts::DeletionQueueCounter` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + fn on_process_deletion_queue_batch() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `1627` + // Minimum execution time: 1_915_000 picoseconds. + Weight::from_parts(1_986_000, 1627) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `k` is `[0, 1024]`. + fn on_initialize_per_trie_key(k: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `452 + k * (69 ±0)` + // Estimated: `442 + k * (70 ±0)` + // Minimum execution time: 11_103_000 picoseconds. + Weight::from_parts(11_326_000, 442) + // Standard Error: 2_291 + .saturating_add(Weight::from_parts(1_196_329, 0).saturating_mul(k.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(k.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(k.into()))) + .saturating_add(Weight::from_parts(0, 70).saturating_mul(k.into())) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + fn call_with_code_per_byte(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `800 + c * (1 ±0)` + // Estimated: `4266 + c * (1 ±0)` + // Minimum execution time: 247_545_000 picoseconds. + Weight::from_parts(268_016_699, 4266) + // Standard Error: 4 + .saturating_add(Weight::from_parts(700, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(c.into())) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:2 w:2) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::Nonce` (r:1 w:1) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + /// The range of component `i` is `[0, 1048576]`. + /// The range of component `s` is `[0, 1048576]`. + fn instantiate_with_code(c: u32, i: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `323` + // Estimated: `6262` + // Minimum execution time: 4_396_772_000 picoseconds. + Weight::from_parts(235_107_907, 6262) + // Standard Error: 185 + .saturating_add(Weight::from_parts(53_843, 0).saturating_mul(c.into())) + // Standard Error: 22 + .saturating_add(Weight::from_parts(2_143, 0).saturating_mul(i.into())) + // Standard Error: 22 + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Contracts::Nonce` (r:1 w:1) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// The range of component `i` is `[0, 1048576]`. + /// The range of component `s` is `[0, 1048576]`. + fn instantiate(i: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `560` + // Estimated: `4017` + // Minimum execution time: 2_240_868_000 picoseconds. + Weight::from_parts(2_273_668_000, 4017) + // Standard Error: 32 + .saturating_add(Weight::from_parts(934, 0).saturating_mul(i.into())) + // Standard Error: 32 + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + fn call() -> Weight { + // Proof Size summary in bytes: + // Measured: `826` + // Estimated: `4291` + // Minimum execution time: 165_067_000 picoseconds. + Weight::from_parts(168_582_000, 4291) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + fn upload_code_determinism_enforced(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `3607` + // Minimum execution time: 229_454_000 picoseconds. + Weight::from_parts(251_495_551, 3607) + // Standard Error: 71 + .saturating_add(Weight::from_parts(51_428, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// The range of component `c` is `[0, 125952]`. + fn upload_code_determinism_relaxed(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `3607` + // Minimum execution time: 240_390_000 picoseconds. + Weight::from_parts(273_854_266, 3607) + // Standard Error: 243 + .saturating_add(Weight::from_parts(51_836, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(193), added: 2668, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:0 w:1) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + fn remove_code() -> Weight { + // Proof Size summary in bytes: + // Measured: `315` + // Estimated: `3780` + // Minimum execution time: 39_374_000 picoseconds. + Weight::from_parts(40_247_000, 3780) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `Contracts::MigrationInProgress` (r:1 w:0) + /// Proof: `Contracts::MigrationInProgress` (`max_values`: Some(1), `max_size`: Some(1026), added: 1521, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:2 w:2) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + fn set_code() -> Weight { + // Proof Size summary in bytes: + // Measured: `552` + // Estimated: `6492` + // Minimum execution time: 24_473_000 picoseconds. + Weight::from_parts(25_890_000, 6492) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// The range of component `r` is `[0, 1600]`. + fn noop_host_fn(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_528_000 picoseconds. + Weight::from_parts(9_301_010, 0) + // Standard Error: 98 + .saturating_add(Weight::from_parts(53_173, 0).saturating_mul(r.into())) + } + fn seal_caller() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 643_000 picoseconds. + Weight::from_parts(678_000, 0) + } + /// Storage: `Contracts::ContractInfoOf` (r:1 w:0) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + fn seal_is_contract() -> Weight { + // Proof Size summary in bytes: + // Measured: `354` + // Estimated: `3819` + // Minimum execution time: 6_107_000 picoseconds. + Weight::from_parts(6_235_000, 3819) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Contracts::ContractInfoOf` (r:1 w:0) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + fn seal_code_hash() -> Weight { + // Proof Size summary in bytes: + // Measured: `447` + // Estimated: `3912` + // Minimum execution time: 7_316_000 picoseconds. + Weight::from_parts(7_653_000, 3912) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + fn seal_own_code_hash() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 721_000 picoseconds. + Weight::from_parts(764_000, 0) + } + fn seal_caller_is_origin() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 369_000 picoseconds. + Weight::from_parts(417_000, 0) + } + fn seal_caller_is_root() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 318_000 picoseconds. + Weight::from_parts(349_000, 0) + } + fn seal_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 590_000 picoseconds. + Weight::from_parts(628_000, 0) + } + fn seal_gas_left() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 660_000 picoseconds. + Weight::from_parts(730_000, 0) + } + fn seal_balance() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `0` + // Minimum execution time: 4_361_000 picoseconds. + Weight::from_parts(4_577_000, 0) + } + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + fn seal_balance_of() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3517` + // Minimum execution time: 3_751_000 picoseconds. + Weight::from_parts(3_874_000, 3517) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + fn seal_value_transferred() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 560_000 picoseconds. + Weight::from_parts(603_000, 0) + } + fn seal_minimum_balance() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 561_000 picoseconds. + Weight::from_parts(610_000, 0) + } + fn seal_block_number() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 557_000 picoseconds. + Weight::from_parts(583_000, 0) + } + fn seal_now() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 550_000 picoseconds. + Weight::from_parts(602_000, 0) + } + /// Storage: `TransactionPayment::NextFeeMultiplier` (r:1 w:0) + /// Proof: `TransactionPayment::NextFeeMultiplier` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `Measured`) + fn seal_weight_to_fee() -> Weight { + // Proof Size summary in bytes: + // Measured: `67` + // Estimated: `1552` + // Minimum execution time: 4_065_000 picoseconds. + Weight::from_parts(4_291_000, 1552) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// The range of component `n` is `[0, 1048572]`. + fn seal_input(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 487_000 picoseconds. + Weight::from_parts(517_000, 0) + // Standard Error: 3 + .saturating_add(Weight::from_parts(301, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048572]`. + fn seal_return(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 318_000 picoseconds. + Weight::from_parts(372_000, 0) + // Standard Error: 10 + .saturating_add(Weight::from_parts(411, 0).saturating_mul(n.into())) + } + /// Storage: `Contracts::DeletionQueueCounter` (r:1 w:1) + /// Proof: `Contracts::DeletionQueueCounter` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:33 w:33) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::DeletionQueue` (r:0 w:1) + /// Proof: `Contracts::DeletionQueue` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `Measured`) + /// The range of component `n` is `[0, 32]`. + fn seal_terminate(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `319 + n * (78 ±0)` + // Estimated: `3784 + n * (2553 ±0)` + // Minimum execution time: 13_251_000 picoseconds. + Weight::from_parts(15_257_892, 3784) + // Standard Error: 7_089 + .saturating_add(Weight::from_parts(3_443_907, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2553).saturating_mul(n.into())) + } + /// Storage: `RandomnessCollectiveFlip::RandomMaterial` (r:1 w:0) + /// Proof: `RandomnessCollectiveFlip::RandomMaterial` (`max_values`: Some(1), `max_size`: Some(2594), added: 3089, mode: `Measured`) + fn seal_random() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1561` + // Minimum execution time: 3_434_000 picoseconds. + Weight::from_parts(3_605_000, 1561) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `System::EventTopics` (r:4 w:4) + /// Proof: `System::EventTopics` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `t` is `[0, 4]`. + /// The range of component `n` is `[0, 16384]`. + fn seal_deposit_event(t: u32, n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `990 + t * (2475 ±0)` + // Minimum execution time: 3_668_000 picoseconds. + Weight::from_parts(3_999_591, 990) + // Standard Error: 5_767 + .saturating_add(Weight::from_parts(2_011_090, 0).saturating_mul(t.into())) + // Standard Error: 1 + .saturating_add(Weight::from_parts(12, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(t.into()))) + .saturating_add(Weight::from_parts(0, 2475).saturating_mul(t.into())) + } + /// The range of component `i` is `[0, 1048576]`. + fn seal_debug_message(i: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 443_000 picoseconds. + Weight::from_parts(472_000, 0) + // Standard Error: 10 + .saturating_add(Weight::from_parts(1_207, 0).saturating_mul(i.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn get_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `16618` + // Estimated: `16618` + // Minimum execution time: 13_752_000 picoseconds. + Weight::from_parts(14_356_000, 16618) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn get_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `26628` + // Estimated: `26628` + // Minimum execution time: 43_444_000 picoseconds. + Weight::from_parts(45_087_000, 26628) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `16618` + // Estimated: `16618` + // Minimum execution time: 15_616_000 picoseconds. + Weight::from_parts(16_010_000, 16618) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `26628` + // Estimated: `26628` + // Minimum execution time: 47_020_000 picoseconds. + Weight::from_parts(50_152_000, 26628) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + /// The range of component `o` is `[0, 16384]`. + fn seal_set_storage(n: u32, o: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `250 + o * (1 ±0)` + // Estimated: `249 + o * (1 ±0)` + // Minimum execution time: 8_824_000 picoseconds. + Weight::from_parts(8_915_233, 249) + // Standard Error: 1 + .saturating_add(Weight::from_parts(255, 0).saturating_mul(n.into())) + // Standard Error: 1 + .saturating_add(Weight::from_parts(39, 0).saturating_mul(o.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(o.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_clear_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 7_133_000 picoseconds. + Weight::from_parts(7_912_778, 248) + // Standard Error: 1 + .saturating_add(Weight::from_parts(88, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_get_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 6_746_000 picoseconds. + Weight::from_parts(7_647_236, 248) + // Standard Error: 2 + .saturating_add(Weight::from_parts(603, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_contains_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 6_247_000 picoseconds. + Weight::from_parts(6_952_661, 248) + // Standard Error: 1 + .saturating_add(Weight::from_parts(77, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + /// Storage: `Skipped::Metadata` (r:0 w:0) + /// Proof: `Skipped::Metadata` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[0, 16384]`. + fn seal_take_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `248 + n * (1 ±0)` + // Estimated: `248 + n * (1 ±0)` + // Minimum execution time: 7_428_000 picoseconds. + Weight::from_parts(8_384_015, 248) + // Standard Error: 2 + .saturating_add(Weight::from_parts(625, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 1).saturating_mul(n.into())) + } + fn set_transient_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_478_000 picoseconds. + Weight::from_parts(1_533_000, 0) + } + fn set_transient_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_485_000 picoseconds. + Weight::from_parts(2_728_000, 0) + } + fn get_transient_storage_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_195_000 picoseconds. + Weight::from_parts(3_811_000, 0) + } + fn get_transient_storage_full() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_902_000 picoseconds. + Weight::from_parts(4_118_000, 0) + } + fn rollback_transient_storage() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_571_000 picoseconds. + Weight::from_parts(1_662_000, 0) + } + /// The range of component `n` is `[0, 16384]`. + /// The range of component `o` is `[0, 16384]`. + fn seal_set_transient_storage(n: u32, o: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 5_250_000 picoseconds. + Weight::from_parts(2_465_568, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(201, 0).saturating_mul(n.into())) + // Standard Error: 0 + .saturating_add(Weight::from_parts(223, 0).saturating_mul(o.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_clear_transient_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_012_000 picoseconds. + Weight::from_parts(2_288_004, 0) + // Standard Error: 3 + .saturating_add(Weight::from_parts(239, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_get_transient_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_906_000 picoseconds. + Weight::from_parts(2_121_040, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(225, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_contains_transient_storage(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_736_000 picoseconds. + Weight::from_parts(1_954_728, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(111, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 16384]`. + fn seal_take_transient_storage(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_872_000 picoseconds. + Weight::from_parts(8_125_644, 0) + } + fn seal_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `0` + // Minimum execution time: 8_489_000 picoseconds. + Weight::from_parts(8_791_000, 0) + } + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// The range of component `t` is `[0, 1]`. + /// The range of component `i` is `[0, 1048576]`. + fn seal_call(t: u32, i: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `620 + t * (280 ±0)` + // Estimated: `4085 + t * (2182 ±0)` + // Minimum execution time: 122_759_000 picoseconds. + Weight::from_parts(120_016_020, 4085) + // Standard Error: 173_118 + .saturating_add(Weight::from_parts(42_848_338, 0).saturating_mul(t.into())) + // Standard Error: 0 + .saturating_add(Weight::from_parts(6, 0).saturating_mul(i.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(t.into()))) + .saturating_add(Weight::from_parts(0, 2182).saturating_mul(t.into())) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:0) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + fn seal_delegate_call() -> Weight { + // Proof Size summary in bytes: + // Measured: `430` + // Estimated: `3895` + // Minimum execution time: 111_566_000 picoseconds. + Weight::from_parts(115_083_000, 3895) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + /// Storage: `Contracts::Nonce` (r:1 w:0) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Contracts::ContractInfoOf` (r:1 w:1) + /// Proof: `Contracts::ContractInfoOf` (`max_values`: None, `max_size`: Some(1795), added: 4270, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// The range of component `i` is `[0, 983040]`. + /// The range of component `s` is `[0, 983040]`. + fn seal_instantiate(i: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `676` + // Estimated: `4132` + // Minimum execution time: 1_871_402_000 picoseconds. + Weight::from_parts(1_890_038_000, 4132) + // Standard Error: 24 + .saturating_add(Weight::from_parts(581, 0).saturating_mul(i.into())) + // Standard Error: 24 + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_sha2_256(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 966_000 picoseconds. + Weight::from_parts(9_599_151, 0) + // Standard Error: 1 + .saturating_add(Weight::from_parts(1_336, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_keccak_256(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_416_000 picoseconds. + Weight::from_parts(10_964_255, 0) + // Standard Error: 1 + .saturating_add(Weight::from_parts(3_593, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_blake2_256(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 821_000 picoseconds. + Weight::from_parts(6_579_283, 0) + // Standard Error: 0 + .saturating_add(Weight::from_parts(1_466, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 1048576]`. + fn seal_hash_blake2_128(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 773_000 picoseconds. + Weight::from_parts(10_990_209, 0) + // Standard Error: 1 + .saturating_add(Weight::from_parts(1_457, 0).saturating_mul(n.into())) + } + /// The range of component `n` is `[0, 125697]`. + fn seal_sr25519_verify(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 43_195_000 picoseconds. + Weight::from_parts(41_864_855, 0) + // Standard Error: 9 + .saturating_add(Weight::from_parts(5_154, 0).saturating_mul(n.into())) + } + fn seal_ecdsa_recover() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 47_747_000 picoseconds. + Weight::from_parts(49_219_000, 0) + } + fn seal_ecdsa_to_eth_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 12_854_000 picoseconds. + Weight::from_parts(12_962_000, 0) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + /// Storage: `Contracts::PristineCode` (r:1 w:0) + /// Proof: `Contracts::PristineCode` (`max_values`: None, `max_size`: Some(125988), added: 128463, mode: `Measured`) + fn seal_set_code_hash() -> Weight { + // Proof Size summary in bytes: + // Measured: `430` + // Estimated: `3895` + // Minimum execution time: 17_868_000 picoseconds. + Weight::from_parts(18_486_000, 3895) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `Measured`) + fn lock_delegate_dependency() -> Weight { + // Proof Size summary in bytes: + // Measured: `355` + // Estimated: `3820` + // Minimum execution time: 8_393_000 picoseconds. + Weight::from_parts(8_640_000, 3820) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Contracts::CodeInfoOf` (r:1 w:1) + /// Proof: `Contracts::CodeInfoOf` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `MaxEncodedLen`) + fn unlock_delegate_dependency() -> Weight { + // Proof Size summary in bytes: + // Measured: `355` + // Estimated: `3558` + // Minimum execution time: 7_489_000 picoseconds. + Weight::from_parts(7_815_000, 3558) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + fn seal_reentrance_count() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 299_000 picoseconds. + Weight::from_parts(339_000, 0) + } + fn seal_account_reentrance_count() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 324_000 picoseconds. + Weight::from_parts(380_000, 0) + } + /// Storage: `Contracts::Nonce` (r:1 w:0) + /// Proof: `Contracts::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + fn seal_instantiation_nonce() -> Weight { + // Proof Size summary in bytes: + // Measured: `219` + // Estimated: `1704` + // Minimum execution time: 2_768_000 picoseconds. + Weight::from_parts(3_025_000, 1704) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// The range of component `r` is `[0, 5000]`. + fn instr_i64_load_store(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 766_000 picoseconds. + Weight::from_parts(722_169, 0) + // Standard Error: 10 + .saturating_add(Weight::from_parts(7_191, 0).saturating_mul(r.into())) + } +} diff --git a/pallets/revive/uapi/Cargo.toml b/pallets/revive/uapi/Cargo.toml new file mode 100644 index 00000000..651dfcd8 --- /dev/null +++ b/pallets/revive/uapi/Cargo.toml @@ -0,0 +1,31 @@ +[package] +authors.workspace = true +description = "Exposes all the host functions that a contract can import." +edition.workspace = true +homepage.workspace = true +license = "Apache-2.0" +name = "pallet-revive-uapi" +repository.workspace = true +version = "0.1.0" + +[lints] +workspace = true + +[dependencies] +bitflags = { workspace = true } +codec = { features = [ + "derive", + "max-encoded-len", +], optional = true, workspace = true } +paste = { workspace = true } +scale-info = { features = [ "derive" ], optional = true, workspace = true } + +[target.'cfg(target_arch = "riscv32")'.dependencies] +polkavm-derive = { version = "0.11.0" } + +[package.metadata.docs.rs] +default-target = [ "wasm32-unknown-unknown" ] + +[features] +default = [ "scale" ] +scale = [ "dep:codec", "scale-info" ] diff --git a/pallets/revive/uapi/src/flags.rs b/pallets/revive/uapi/src/flags.rs new file mode 100644 index 00000000..763a89d6 --- /dev/null +++ b/pallets/revive/uapi/src/flags.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. + +use bitflags::bitflags; + +bitflags! { + /// Flags used by a contract to customize exit behaviour. + #[cfg_attr(feature = "scale", derive(codec::Encode, codec::Decode, scale_info::TypeInfo))] + #[derive(Default)] + pub struct ReturnFlags: u32 { + /// If this bit is set all changes made by the contract execution are rolled back. + const REVERT = 0x0000_0001; + } +} + +bitflags! { + /// Flags used to change the behaviour of `seal_call` and `seal_delegate_call`. + pub struct CallFlags: u32 { + /// Forward the input of current function to the callee. + /// + /// Supplied input pointers are ignored when set. + /// + /// # Note + /// + /// A forwarding call will consume the current contracts input. Any attempt to + /// access the input after this call returns will lead to [`Error::InputForwarded`]. + /// It does not matter if this is due to calling `seal_input` or trying another + /// forwarding call. Consider using [`Self::CLONE_INPUT`] in order to preserve + /// the input. + const FORWARD_INPUT = 0b0000_0001; + /// Identical to [`Self::FORWARD_INPUT`] but without consuming the input. + /// + /// This adds some additional weight costs to the call. + /// + /// # Note + /// + /// This implies [`Self::FORWARD_INPUT`] and takes precedence when both are set. + const CLONE_INPUT = 0b0000_0010; + /// Do not return from the call but rather return the result of the callee to the + /// callers caller. + /// + /// # Note + /// + /// This makes the current contract completely transparent to its caller by replacing + /// this contracts potential output by the callee ones. Any code after `seal_call` + /// can be safely considered unreachable. + const TAIL_CALL = 0b0000_0100; + /// Allow the callee to reenter into the current contract. + /// + /// Without this flag any reentrancy into the current contract that originates from + /// the callee (or any of its callees) is denied. This includes the first callee: + /// You cannot call into yourself with this flag set. + /// + /// # Note + /// + /// For `seal_delegate_call` should be always unset, otherwise + /// [`Error::InvalidCallFlags`] is returned. + const ALLOW_REENTRY = 0b0000_1000; + /// Indicates that the callee is restricted from modifying the state during call execution, + /// equivalent to Ethereum's STATICCALL. + /// + /// # Note + /// + /// For `seal_delegate_call` should be always unset, otherwise + /// [`Error::InvalidCallFlags`] is returned. + const READ_ONLY = 0b0001_0000; + } +} + +bitflags! { + /// Flags used by a contract to customize storage behaviour. + pub struct StorageFlags: u32 { + /// Access the transient storage instead of the persistent one. + const TRANSIENT = 0x0000_0001; + } +} diff --git a/pallets/revive/uapi/src/host.rs b/pallets/revive/uapi/src/host.rs new file mode 100644 index 00000000..eca006c3 --- /dev/null +++ b/pallets/revive/uapi/src/host.rs @@ -0,0 +1,643 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// +// 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 paste::paste; + +use crate::{CallFlags, Result, ReturnFlags, StorageFlags}; + +#[cfg(target_arch = "riscv32")] +mod riscv32; + +macro_rules! hash_fn { + ( $name:ident, $bytes:literal ) => { + paste! { + #[doc = "Computes the " $name " " $bytes "-bit hash on the given input buffer."] + #[doc = "\n# Notes\n"] + #[doc = "- The `input` and `output` buffer may overlap."] + #[doc = "- The output buffer is expected to hold at least " $bytes " bits."] + #[doc = "- It is the callers responsibility to provide an output buffer that is large enough to hold the expected amount of bytes returned by the hash function."] + #[doc = "\n# Parameters\n"] + #[doc = "- `input`: The input data buffer."] + #[doc = "- `output`: The output buffer to write the hash result to."] + fn [](input: &[u8], output: &mut [u8; $bytes]); + } + }; +} + +/// Implements [`HostFn`] when compiled on supported architectures (RISC-V). +pub enum HostFnImpl {} + +/// Defines all the host apis available to contracts. +pub trait HostFn: private::Sealed { + /// Stores the address of the current contract into the supplied buffer. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the address. + fn address(output: &mut [u8; 20]); + + /// Lock a new delegate dependency to the contract. + /// + /// Traps if the maximum number of delegate_dependencies is reached or if + /// the delegate dependency already exists. + /// + /// # Parameters + /// + /// - `code_hash`: The code hash of the dependency. Should be decodable as an `T::Hash`. Traps + /// otherwise. + fn lock_delegate_dependency(code_hash: &[u8; 32]); + + /// Stores the **reducible** balance of the current account into the supplied buffer. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the balance. + fn balance(output: &mut [u8; 32]); + + /// Stores the **reducible** balance of the supplied address into the supplied buffer. + /// + /// # Parameters + /// + /// - `addr`: The target address of which to retreive the free balance. + /// - `output`: A reference to the output data buffer to write the balance. + fn balance_of(addr: &[u8; 20], output: &mut [u8; 32]); + + /// Returns the [EIP-155](https://eips.ethereum.org/EIPS/eip-155) chain ID. + fn chain_id(output: &mut [u8; 32]); + + /// Stores the current block number of the current contract into the supplied buffer. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the block number. + fn block_number(output: &mut [u8; 32]); + + /// Call (possibly transferring some amount of funds) into the specified account. + /// + /// # Parameters + /// + /// - `flags`: See [`CallFlags`] for a documentation of the supported flags. + /// - `callee`: The address of the callee. Should be decodable as an `T::AccountId`. Traps + /// otherwise. + /// - `ref_time_limit`: how much *ref_time* Weight to devote to the execution. + /// - `proof_size_limit`: how much *proof_size* Weight to devote to the execution. + /// - `deposit`: The storage deposit limit for instantiation. Passing `None` means setting no + /// specific limit for the call, which implies storage usage up to the limit of the parent + /// call. + /// - `value`: The value to transfer into the contract. + /// - `input`: The input data buffer used to call the contract. + /// - `output`: A reference to the output data buffer to write the call output buffer. If `None` + /// is provided then the output buffer is not copied. + /// + /// # Errors + /// + /// An error means that the call wasn't successful output buffer is returned unless + /// stated otherwise. + /// + /// - [CalleeReverted][`crate::ReturnErrorCode::CalleeReverted]: Output buffer is returned. + /// - [CalleeTrapped][`crate::ReturnErrorCode::CalleeTrapped] + /// - [TransferFailed][`crate::ReturnErrorCode::TransferFailed] + /// - [NotCallable][`crate::ReturnErrorCode::NotCallable] + fn call( + flags: CallFlags, + callee: &[u8; 20], + ref_time_limit: u64, + proof_size_limit: u64, + deposit: Option<&[u8; 32]>, + value: &[u8; 32], + input_data: &[u8], + output: Option<&mut &mut [u8]>, + ) -> Result; + + /// Call into the chain extension provided by the chain if any. + /// + /// Handling of the input values is up to the specific chain extension and so is the + /// return value. The extension can decide to use the inputs as primitive inputs or as + /// in/out arguments by interpreting them as pointers. Any caller of this function + /// must therefore coordinate with the chain that it targets. + /// + /// # Note + /// + /// If no chain extension exists the contract will trap with the `NoChainExtension` + /// module error. + /// + /// # Parameters + /// + /// - `func_id`: The function id of the chain extension. + /// - `input`: The input data buffer. + /// - `output`: A reference to the output data buffer to write the call output buffer. If `None` + /// is provided then the output buffer is not copied. + /// + /// # Return + /// + /// The chain extension returned value, if executed successfully. + fn call_chain_extension(func_id: u32, input: &[u8], output: Option<&mut &mut [u8]>) -> u32; + + /// Call some dispatchable of the runtime. + /// + /// # Parameters + /// + /// - `call`: The call data. + /// + /// # Return + /// + /// Returns `Error::Success` when the dispatchable was successfully executed and + /// returned `Ok`. When the dispatchable was executed but returned an error + /// `Error::CallRuntimeFailed` is returned. The full error is not + /// provided because it is not guaranteed to be stable. + /// + /// # Comparison with `ChainExtension` + /// + /// Just as a chain extension this API allows the runtime to extend the functionality + /// of contracts. While making use of this function is generally easier it cannot be + /// used in all cases. Consider writing a chain extension if you need to do perform + /// one of the following tasks: + /// + /// - Return data. + /// - Provide functionality **exclusively** to contracts. + /// - Provide custom weights. + /// - Avoid the need to keep the `Call` data structure stable. + fn call_runtime(call: &[u8]) -> Result; + + /// Stores the address of the caller into the supplied buffer. + /// + /// If this is a top-level call (i.e. initiated by an extrinsic) the origin address of the + /// extrinsic will be returned. Otherwise, if this call is initiated by another contract then + /// the address of the contract will be returned. + /// + /// If there is no address associated with the caller (e.g. because the caller is root) then + /// it traps with `BadOrigin`. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the caller address. + fn caller(output: &mut [u8; 20]); + + /// Checks whether the caller of the current contract is the origin of the whole call stack. + /// + /// Prefer this over [`is_contract()`][`Self::is_contract`] when checking whether your contract + /// is being called by a contract or a plain account. The reason is that it performs better + /// since it does not need to do any storage lookups. + /// + /// # Return + /// + /// A return value of `true` indicates that this contract is being called by a plain account + /// and `false` indicates that the caller is another contract. + fn caller_is_origin() -> bool; + + /// Checks whether the caller of the current contract is root. + /// + /// Note that only the origin of the call stack can be root. Hence this function returning + /// `true` implies that the contract is being called by the origin. + /// + /// A return value of `true` indicates that this contract is being called by a root origin, + /// and `false` indicates that the caller is a signed origin. + fn caller_is_root() -> u32; + + /// Clear the value at the given key in the contract storage. + /// + /// # Parameters + /// + /// - `key`: The storage key. + /// + /// # Return + /// + /// Returns the size of the pre-existing value at the specified key if any. + fn clear_storage(flags: StorageFlags, key: &[u8]) -> Option; + + /// Retrieve the code hash for a specified contract address. + /// + /// # Parameters + /// + /// - `addr`: The address of the contract. + /// - `output`: A reference to the output data buffer to write the code hash. + /// + /// # Errors + /// + /// - [CodeNotFound][`crate::ReturnErrorCode::CodeNotFound] + fn code_hash(addr: &[u8; 20], output: &mut [u8; 32]) -> Result; + + /// Checks whether there is a value stored under the given key. + /// + /// The key length must not exceed the maximum defined by the contracts module parameter. + /// + /// # Parameters + /// - `key`: The storage key. + /// + /// # Return + /// + /// Returns the size of the pre-existing value at the specified key if any. + fn contains_storage(flags: StorageFlags, key: &[u8]) -> Option; + + /// Emit a custom debug message. + /// + /// No newlines are added to the supplied message. + /// Specifying invalid UTF-8 just drops the message with no trap. + /// + /// This is a no-op if debug message recording is disabled which is always the case + /// when the code is executing on-chain. The message is interpreted as UTF-8 and + /// appended to the debug buffer which is then supplied to the calling RPC client. + /// + /// # Note + /// + /// Even though no action is taken when debug message recording is disabled there is still + /// a non trivial overhead (and weight cost) associated with calling this function. Contract + /// languages should remove calls to this function (either at runtime or compile time) when + /// not being executed as an RPC. For example, they could allow users to disable logging + /// through compile time flags (cargo features) for on-chain deployment. Additionally, the + /// return value of this function can be cached in order to prevent further calls at runtime. + fn debug_message(str: &[u8]) -> Result; + + /// Execute code in the context (storage, caller, value) of the current contract. + /// + /// Reentrancy protection is always disabled since the callee is allowed + /// to modify the callers storage. This makes going through a reentrancy attack + /// unnecessary for the callee when it wants to exploit the caller. + /// + /// # Parameters + /// + /// - `flags`: See [`CallFlags`] for a documentation of the supported flags. + /// - `code_hash`: The hash of the code to be executed. + /// - `input`: The input data buffer used to call the contract. + /// - `output`: A reference to the output data buffer to write the call output buffer. If `None` + /// is provided then the output buffer is not copied. + /// + /// # Errors + /// + /// An error means that the call wasn't successful and no output buffer is returned unless + /// stated otherwise. + /// + /// - [CalleeReverted][`crate::ReturnErrorCode::CalleeReverted]: Output buffer is returned. + /// - [CalleeTrapped][`crate::ReturnErrorCode::CalleeTrapped] + /// - [CodeNotFound][`crate::ReturnErrorCode::CodeNotFound] + fn delegate_call( + flags: CallFlags, + code_hash: &[u8; 32], + input_data: &[u8], + output: Option<&mut &mut [u8]>, + ) -> Result; + + /// Deposit a contract event with the data buffer and optional list of topics. There is a limit + /// on the maximum number of topics specified by `event_topics`. + /// + /// There should not be any duplicates in `topics`. + /// + /// # Parameters + /// + /// - `topics`: The topics list. It can't contain duplicates. + fn deposit_event(topics: &[[u8; 32]], data: &[u8]); + + /// Recovers the ECDSA public key from the given message hash and signature. + /// + /// Writes the public key into the given output buffer. + /// Assumes the secp256k1 curve. + /// + /// # Parameters + /// + /// - `signature`: The signature bytes. + /// - `message_hash`: The message hash bytes. + /// - `output`: A reference to the output data buffer to write the public key. + /// + /// # Errors + /// + /// - [EcdsaRecoveryFailed][`crate::ReturnErrorCode::EcdsaRecoveryFailed] + fn ecdsa_recover( + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], + ) -> Result; + + /// Calculates Ethereum address from the ECDSA compressed public key and stores + /// it into the supplied buffer. + /// + /// # Parameters + /// + /// - `pubkey`: The public key bytes. + /// - `output`: A reference to the output data buffer to write the address. + /// + /// # Errors + /// + /// - [EcdsaRecoveryFailed][`crate::ReturnErrorCode::EcdsaRecoveryFailed] + fn ecdsa_to_eth_address(pubkey: &[u8; 33], output: &mut [u8; 20]) -> Result; + + /// Stores the amount of weight left into the supplied buffer. + /// The data is encoded as Weight. + /// + /// If the available space in `output` is less than the size of the value a trap is triggered. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the weight left. + fn weight_left(output: &mut &mut [u8]); + + /// Retrieve the value under the given key from storage. + /// + /// The key length must not exceed the maximum defined by the contracts module parameter. + /// + /// # Parameters + /// - `key`: The storage key. + /// - `output`: A reference to the output data buffer to write the storage entry. + /// + /// # Errors + /// + /// [KeyNotFound][`crate::ReturnErrorCode::KeyNotFound] + fn get_storage(flags: StorageFlags, key: &[u8], output: &mut &mut [u8]) -> Result; + + hash_fn!(sha2_256, 32); + hash_fn!(keccak_256, 32); + hash_fn!(blake2_256, 32); + hash_fn!(blake2_128, 16); + + /// Stores the input passed by the caller into the supplied buffer. + /// + /// # Note + /// + /// This function traps if: + /// - the input is larger than the available space. + /// - the input was previously forwarded by a [`call()`][`Self::call()`]. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the input data. + fn input(output: &mut &mut [u8]); + + /// Instantiate a contract with the specified code hash. + /// + /// This function creates an account and executes the constructor defined in the code specified + /// by the code hash. + /// + /// # Parameters + /// + /// - `code_hash`: The hash of the code to be instantiated. + /// - `ref_time_limit`: how much *ref_time* Weight to devote to the execution. + /// - `proof_size_limit`: how much *proof_size* Weight to devote to the execution. + /// - `deposit`: The storage deposit limit for instantiation. Passing `None` means setting no + /// specific limit for the call, which implies storage usage up to the limit of the parent + /// call. + /// - `value`: The value to transfer into the contract. + /// - `input`: The input data buffer. + /// - `address`: A reference to the address buffer to write the address of the contract. If + /// `None` is provided then the output buffer is not copied. + /// - `output`: A reference to the return value buffer to write the constructor output buffer. + /// If `None` is provided then the output buffer is not copied. + /// - `salt`: The salt bytes to use for this instantiation. + /// + /// # Errors + /// + /// Please consult the [ReturnErrorCode][`crate::ReturnErrorCode`] enum declaration for more + /// information on those errors. Here we only note things specific to this function. + /// + /// An error means that the account wasn't created and no address or output buffer + /// is returned unless stated otherwise. + /// + /// - [CalleeReverted][`crate::ReturnErrorCode::CalleeReverted]: Output buffer is returned. + /// - [CalleeTrapped][`crate::ReturnErrorCode::CalleeTrapped] + /// - [TransferFailed][`crate::ReturnErrorCode::TransferFailed] + /// - [CodeNotFound][`crate::ReturnErrorCode::CodeNotFound] + fn instantiate( + code_hash: &[u8; 32], + ref_time_limit: u64, + proof_size_limit: u64, + deposit: Option<&[u8; 32]>, + value: &[u8; 32], + input: &[u8], + address: Option<&mut [u8; 20]>, + output: Option<&mut &mut [u8]>, + salt: Option<&[u8; 32]>, + ) -> Result; + + /// Checks whether a specified address belongs to a contract. + /// + /// # Parameters + /// + /// - `address`: The address to check + /// + /// # Return + /// + /// Returns `true` if the address belongs to a contract. + fn is_contract(address: &[u8; 20]) -> bool; + + /// Stores the minimum balance (a.k.a. existential deposit) into the supplied buffer. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the minimum balance. + fn minimum_balance(output: &mut [u8; 32]); + + /// Retrieve the code hash of the currently executing contract. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the code hash. + fn own_code_hash(output: &mut [u8; 32]); + + /// Load the latest block timestamp into the supplied buffer + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the timestamp. + fn now(output: &mut [u8; 32]); + + /// Removes the delegate dependency from the contract. + /// + /// Traps if the delegate dependency does not exist. + /// + /// # Parameters + /// + /// - `code_hash`: The code hash of the dependency. Should be decodable as an `T::Hash`. Traps + /// otherwise. + fn unlock_delegate_dependency(code_hash: &[u8; 32]); + + /// Cease contract execution and save a data buffer as a result of the execution. + /// + /// This function never returns as it stops execution of the caller. + /// This is the only way to return a data buffer to the caller. Returning from + /// execution without calling this function is equivalent to calling: + /// ```nocompile + /// return_value(ReturnFlags::empty(), &[]) + /// ``` + /// + /// Using an unnamed non empty `ReturnFlags` triggers a trap. + /// + /// # Parameters + /// + /// - `flags`: Flag used to signal special return conditions to the supervisor. See + /// [`ReturnFlags`] for a documentation of the supported flags. + /// - `return_value`: The return value buffer. + fn return_value(flags: ReturnFlags, return_value: &[u8]) -> !; + + /// Replace the contract code at the specified address with new code. + /// + /// # Note + /// + /// There are a couple of important considerations which must be taken into account when + /// using this API: + /// + /// 1. The storage at the code address will remain untouched. This means that contract + /// developers must ensure that the storage layout of the new code is compatible with that of + /// the old code. + /// + /// 2. Contracts using this API can't be assumed as having deterministic addresses. Said another + /// way, when using this API you lose the guarantee that an address always identifies a specific + /// code hash. + /// + /// 3. If a contract calls into itself after changing its code the new call would use + /// the new code. However, if the original caller panics after returning from the sub call it + /// would revert the changes made by [`set_code_hash()`][`Self::set_code_hash`] and the next + /// caller would use the old code. + /// + /// # Parameters + /// + /// - `code_hash`: The hash of the new code. Should be decodable as an `T::Hash`. Traps + /// otherwise. + /// + /// # Errors + /// + /// - [CodeNotFound][`crate::ReturnErrorCode::CodeNotFound] + fn set_code_hash(code_hash: &[u8; 32]) -> Result; + + /// Set the value at the given key in the contract storage. + /// + /// The key and value lengths must not exceed the maximums defined by the contracts module + /// parameters. + /// + /// # Parameters + /// + /// - `key`: The storage key. + /// - `encoded_value`: The storage value. + /// + /// # Return + /// + /// Returns the size of the pre-existing value at the specified key if any. + fn set_storage(flags: StorageFlags, key: &[u8], value: &[u8]) -> Option; + + /// Verify a sr25519 signature + /// + /// # Parameters + /// + /// - `signature`: The signature bytes. + /// - `message`: The message bytes. + /// + /// # Errors + /// + /// - [Sr25519VerifyFailed][`crate::ReturnErrorCode::Sr25519VerifyFailed] + fn sr25519_verify(signature: &[u8; 64], message: &[u8], pub_key: &[u8; 32]) -> Result; + + /// Retrieve and remove the value under the given key from storage. + /// + /// # Parameters + /// - `key`: The storage key. + /// - `output`: A reference to the output data buffer to write the storage entry. + /// + /// # Errors + /// + /// [KeyNotFound][`crate::ReturnErrorCode::KeyNotFound] + fn take_storage(flags: StorageFlags, key: &[u8], output: &mut &mut [u8]) -> Result; + + /// Transfer some amount of funds into the specified account. + /// + /// # Parameters + /// + /// - `address`: The address of the account to transfer funds to. + /// - `value`: The U256 value to transfer. + /// + /// # Errors + /// + /// - [TransferFailed][`crate::ReturnErrorCode::TransferFailed] + fn transfer(address: &[u8; 20], value: &[u8; 32]) -> Result; + + /// Remove the calling account and transfer remaining **free** balance. + /// + /// This function never returns. Either the termination was successful and the + /// execution of the destroyed contract is halted. Or it failed during the termination + /// which is considered fatal and results in a trap + rollback. + /// + /// # Parameters + /// + /// - `beneficiary`: The address of the beneficiary account + /// + /// # Traps + /// + /// - The contract is live i.e is already on the call stack. + /// - Failed to send the balance to the beneficiary. + /// - The deletion queue is full. + fn terminate(beneficiary: &[u8; 20]) -> !; + + /// Stores the value transferred along with this call/instantiate into the supplied buffer. + /// + /// # Parameters + /// + /// - `output`: A reference to the output data buffer to write the transferred value. + fn value_transferred(output: &mut [u8; 32]); + + /// Stores the price for the specified amount of gas into the supplied buffer. + /// + /// # Parameters + /// + /// - `ref_time_limit`: The *ref_time* Weight limit to query the price for. + /// - `proof_size_limit`: The *proof_size* Weight limit to query the price for. + /// - `output`: A reference to the output data buffer to write the price. + fn weight_to_fee(ref_time_limit: u64, proof_size_limit: u64, output: &mut [u8; 32]); + + /// Execute an XCM program locally, using the contract's address as the origin. + /// This is equivalent to dispatching `pallet_xcm::execute` through call_runtime, except that + /// the function is called directly instead of being dispatched. + /// + /// # Parameters + /// + /// - `msg`: The message, should be decodable as a [VersionedXcm](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/enum.VersionedXcm.html), + /// traps otherwise. + /// - `output`: A reference to the output data buffer to write the [Outcome](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/v3/enum.Outcome.html) + /// + /// # Return + /// + /// Returns `Error::Success` when the XCM execution attempt is successful. When the XCM + /// execution fails, `ReturnCode::XcmExecutionFailed` is returned + fn xcm_execute(msg: &[u8]) -> Result; + + /// Send an XCM program from the contract to the specified destination. + /// This is equivalent to dispatching `pallet_xcm::send` through `call_runtime`, except that + /// the function is called directly instead of being dispatched. + /// + /// # Parameters + /// + /// - `dest`: The XCM destination, should be decodable as [VersionedLocation](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/enum.VersionedLocation.html), + /// traps otherwise. + /// - `msg`: The message, should be decodable as a [VersionedXcm](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/enum.VersionedXcm.html), + /// traps otherwise. + /// + /// # Return + /// + /// Returns `ReturnCode::Success` when the message was successfully sent. When the XCM + /// execution fails, `ReturnErrorCode::XcmSendFailed` is returned. + fn xcm_send(dest: &[u8], msg: &[u8], output: &mut [u8; 32]) -> Result; + + /// Stores the size of the returned data of the last contract call or instantiation. + /// + /// # Parameters + /// + /// - `output`: A reference to the output buffer to write the size. + fn return_data_size(output: &mut [u8; 32]); + + /// Stores the returned data of the last contract call or contract instantiation. + /// + /// # Parameters + /// - `output`: A reference to the output buffer to write the data. + /// - `offset`: Byte offset into the returned data + fn return_data_copy(output: &mut &mut [u8], offset: u32); +} + +mod private { + pub trait Sealed {} + impl Sealed for super::HostFnImpl {} +} diff --git a/pallets/revive/uapi/src/host/riscv32.rs b/pallets/revive/uapi/src/host/riscv32.rs new file mode 100644 index 00000000..3bcd6a7d --- /dev/null +++ b/pallets/revive/uapi/src/host/riscv32.rs @@ -0,0 +1,568 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// +// 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. + +#![allow(unused_variables)] + +use crate::{ + host::{CallFlags, HostFn, HostFnImpl, Result, StorageFlags}, + ReturnFlags, +}; + +mod sys { + use crate::ReturnCode; + + #[polkavm_derive::polkavm_define_abi] + mod abi {} + + impl abi::FromHost for ReturnCode { + type Regs = (u32,); + + fn from_host((a0,): Self::Regs) -> Self { + ReturnCode(a0) + } + } + + #[polkavm_derive::polkavm_import(abi = self::abi)] + extern "C" { + pub fn set_storage( + flags: u32, + key_ptr: *const u8, + key_len: u32, + value_ptr: *const u8, + value_len: u32, + ) -> ReturnCode; + pub fn clear_storage(flags: u32, key_ptr: *const u8, key_len: u32) -> ReturnCode; + pub fn get_storage( + flags: u32, + key_ptr: *const u8, + key_len: u32, + out_ptr: *mut u8, + out_len_ptr: *mut u32, + ) -> ReturnCode; + pub fn contains_storage(flags: u32, key_ptr: *const u8, key_len: u32) -> ReturnCode; + pub fn take_storage( + flags: u32, + key_ptr: *const u8, + key_len: u32, + out_ptr: *mut u8, + out_len_ptr: *mut u32, + ) -> ReturnCode; + pub fn transfer(address_ptr: *const u8, value_ptr: *const u8) -> ReturnCode; + pub fn call(ptr: *const u8) -> ReturnCode; + pub fn delegate_call( + flags: u32, + code_hash_ptr: *const u8, + input_data_ptr: *const u8, + input_data_len: u32, + out_ptr: *mut u8, + out_len_ptr: *mut u32, + ) -> ReturnCode; + pub fn instantiate(ptr: *const u8) -> ReturnCode; + pub fn terminate(beneficiary_ptr: *const u8); + pub fn input(out_ptr: *mut u8, out_len_ptr: *mut u32); + pub fn seal_return(flags: u32, data_ptr: *const u8, data_len: u32); + pub fn caller(out_ptr: *mut u8); + pub fn is_contract(account_ptr: *const u8) -> ReturnCode; + pub fn code_hash(address_ptr: *const u8, out_ptr: *mut u8) -> ReturnCode; + pub fn own_code_hash(out_ptr: *mut u8); + pub fn caller_is_origin() -> ReturnCode; + pub fn caller_is_root() -> ReturnCode; + pub fn address(out_ptr: *mut u8); + pub fn weight_to_fee(ref_time: u64, proof_size: u64, out_ptr: *mut u8); + pub fn weight_left(out_ptr: *mut u8, out_len_ptr: *mut u32); + pub fn balance(out_ptr: *mut u8); + pub fn balance_of(addr_ptr: *const u8, out_ptr: *mut u8); + pub fn chain_id(out_ptr: *mut u8); + pub fn value_transferred(out_ptr: *mut u8); + pub fn now(out_ptr: *mut u8); + pub fn minimum_balance(out_ptr: *mut u8); + pub fn deposit_event( + topics_ptr: *const [u8; 32], + num_topic: u32, + data_ptr: *const u8, + data_len: u32, + ); + pub fn block_number(out_ptr: *mut u8); + pub fn hash_sha2_256(input_ptr: *const u8, input_len: u32, out_ptr: *mut u8); + pub fn hash_keccak_256(input_ptr: *const u8, input_len: u32, out_ptr: *mut u8); + pub fn hash_blake2_256(input_ptr: *const u8, input_len: u32, out_ptr: *mut u8); + pub fn hash_blake2_128(input_ptr: *const u8, input_len: u32, out_ptr: *mut u8); + pub fn call_chain_extension( + id: u32, + input_ptr: *const u8, + input_len: u32, + out_ptr: *mut u8, + out_len_ptr: *mut u32, + ) -> ReturnCode; + pub fn debug_message(str_ptr: *const u8, str_len: u32) -> ReturnCode; + pub fn call_runtime(call_ptr: *const u8, call_len: u32) -> ReturnCode; + pub fn ecdsa_recover( + signature_ptr: *const u8, + message_hash_ptr: *const u8, + out_ptr: *mut u8, + ) -> ReturnCode; + pub fn sr25519_verify( + signature_ptr: *const u8, + pub_key_ptr: *const u8, + message_len: u32, + message_ptr: *const u8, + ) -> ReturnCode; + pub fn set_code_hash(code_hash_ptr: *const u8) -> ReturnCode; + pub fn ecdsa_to_eth_address(key_ptr: *const u8, out_ptr: *mut u8) -> ReturnCode; + pub fn instantiation_nonce() -> u64; + pub fn lock_delegate_dependency(code_hash_ptr: *const u8); + pub fn unlock_delegate_dependency(code_hash_ptr: *const u8); + pub fn xcm_execute(msg_ptr: *const u8, msg_len: u32) -> ReturnCode; + pub fn xcm_send( + dest_ptr: *const u8, + dest_len: *const u8, + msg_ptr: *const u8, + msg_len: u32, + out_ptr: *mut u8, + ) -> ReturnCode; + pub fn return_data_size(out_ptr: *mut u8); + pub fn return_data_copy(out_ptr: *mut u8, out_len_ptr: *mut u32, offset: u32); + } +} + +/// A macro to implement all Host functions with a signature of `fn(&mut [u8; n])`. +macro_rules! impl_wrapper_for { + (@impl_fn $name:ident, $n: literal) => { + fn $name(output: &mut [u8; $n]) { + unsafe { sys::$name(output.as_mut_ptr()) } + } + }; + + () => {}; + + ([u8; $n: literal] => $($name:ident),*; $($tail:tt)*) => { + $(impl_wrapper_for!(@impl_fn $name, $n);)* + impl_wrapper_for!($($tail)*); + }; +} + +macro_rules! impl_hash_fn { + ( $name:ident, $bytes_result:literal ) => { + paste::item! { + fn [](input: &[u8], output: &mut [u8; $bytes_result]) { + unsafe { + sys::[]( + input.as_ptr(), + input.len() as u32, + output.as_mut_ptr(), + ) + } + } + } + }; +} + +#[inline(always)] +fn extract_from_slice(output: &mut &mut [u8], new_len: usize) { + debug_assert!(new_len <= output.len()); + let tmp = core::mem::take(output); + *output = &mut tmp[..new_len]; +} + +#[inline(always)] +fn ptr_len_or_sentinel(data: &mut Option<&mut &mut [u8]>) -> (*mut u8, u32) { + match data { + Some(ref mut data) => (data.as_mut_ptr(), data.len() as _), + None => (crate::SENTINEL as _, 0), + } +} + +#[inline(always)] +fn ptr_or_sentinel(data: &Option<&[u8; 32]>) -> *const u8 { + match data { + Some(ref data) => data.as_ptr(), + None => crate::SENTINEL as _, + } +} + +impl HostFn for HostFnImpl { + impl_wrapper_for! { + [u8; 32] => block_number, balance, value_transferred, now, minimum_balance, chain_id; + [u8; 20] => address, caller; + } + + impl_hash_fn!(sha2_256, 32); + + impl_hash_fn!(keccak_256, 32); + + impl_hash_fn!(blake2_256, 32); + + impl_hash_fn!(blake2_128, 16); + + fn instantiate( + code_hash: &[u8; 32], + ref_time_limit: u64, + proof_size_limit: u64, + deposit_limit: Option<&[u8; 32]>, + value: &[u8; 32], + input: &[u8], + mut address: Option<&mut [u8; 20]>, + mut output: Option<&mut &mut [u8]>, + salt: Option<&[u8; 32]>, + ) -> Result { + let address = match address { + Some(ref mut data) => data.as_mut_ptr(), + None => crate::SENTINEL as _, + }; + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let deposit_limit_ptr = ptr_or_sentinel(&deposit_limit); + let salt_ptr = ptr_or_sentinel(&salt); + #[repr(packed)] + #[allow(dead_code)] + struct Args { + code_hash: *const u8, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_limit: *const u8, + value: *const u8, + input: *const u8, + input_len: u32, + address: *const u8, + output: *mut u8, + output_len: *mut u32, + salt: *const u8, + } + let args = Args { + code_hash: code_hash.as_ptr(), + ref_time_limit, + proof_size_limit, + deposit_limit: deposit_limit_ptr, + value: value.as_ptr(), + input: input.as_ptr(), + input_len: input.len() as _, + address, + output: output_ptr, + output_len: &mut output_len as *mut _, + salt: salt_ptr, + }; + + let ret_code = { unsafe { sys::instantiate(&args as *const Args as *const _) } }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + + ret_code.into() + } + + fn call( + flags: CallFlags, + callee: &[u8; 20], + ref_time_limit: u64, + proof_size_limit: u64, + deposit_limit: Option<&[u8; 32]>, + value: &[u8; 32], + input: &[u8], + mut output: Option<&mut &mut [u8]>, + ) -> Result { + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let deposit_limit_ptr = ptr_or_sentinel(&deposit_limit); + #[repr(packed)] + #[allow(dead_code)] + struct Args { + flags: u32, + callee: *const u8, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_limit: *const u8, + value: *const u8, + input: *const u8, + input_len: u32, + output: *mut u8, + output_len: *mut u32, + } + let args = Args { + flags: flags.bits(), + callee: callee.as_ptr(), + ref_time_limit, + proof_size_limit, + deposit_limit: deposit_limit_ptr, + value: value.as_ptr(), + input: input.as_ptr(), + input_len: input.len() as _, + output: output_ptr, + output_len: &mut output_len as *mut _, + }; + + let ret_code = { unsafe { sys::call(&args as *const Args as *const _) } }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + + ret_code.into() + } + + fn caller_is_root() -> u32 { + unsafe { sys::caller_is_root() }.into_u32() + } + + fn delegate_call( + flags: CallFlags, + code_hash: &[u8; 32], + input: &[u8], + mut output: Option<&mut &mut [u8]>, + ) -> Result { + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let ret_code = { + unsafe { + sys::delegate_call( + flags.bits(), + code_hash.as_ptr(), + input.as_ptr(), + input.len() as u32, + output_ptr, + &mut output_len, + ) + } + }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + + ret_code.into() + } + + fn transfer(address: &[u8; 20], value: &[u8; 32]) -> Result { + let ret_code = unsafe { sys::transfer(address.as_ptr(), value.as_ptr()) }; + ret_code.into() + } + + fn deposit_event(topics: &[[u8; 32]], data: &[u8]) { + unsafe { + sys::deposit_event( + topics.as_ptr(), + topics.len() as u32, + data.as_ptr(), + data.len() as u32, + ) + } + } + + fn set_storage(flags: StorageFlags, key: &[u8], encoded_value: &[u8]) -> Option { + let ret_code = unsafe { + sys::set_storage( + flags.bits(), + key.as_ptr(), + key.len() as u32, + encoded_value.as_ptr(), + encoded_value.len() as u32, + ) + }; + ret_code.into() + } + + fn clear_storage(flags: StorageFlags, key: &[u8]) -> Option { + let ret_code = unsafe { sys::clear_storage(flags.bits(), key.as_ptr(), key.len() as u32) }; + ret_code.into() + } + + fn contains_storage(flags: StorageFlags, key: &[u8]) -> Option { + let ret_code = + unsafe { sys::contains_storage(flags.bits(), key.as_ptr(), key.len() as u32) }; + ret_code.into() + } + + fn get_storage(flags: StorageFlags, key: &[u8], output: &mut &mut [u8]) -> Result { + let mut output_len = output.len() as u32; + let ret_code = { + unsafe { + sys::get_storage( + flags.bits(), + key.as_ptr(), + key.len() as u32, + output.as_mut_ptr(), + &mut output_len, + ) + } + }; + extract_from_slice(output, output_len as usize); + ret_code.into() + } + + fn take_storage(flags: StorageFlags, key: &[u8], output: &mut &mut [u8]) -> Result { + let mut output_len = output.len() as u32; + let ret_code = { + unsafe { + sys::take_storage( + flags.bits(), + key.as_ptr(), + key.len() as u32, + output.as_mut_ptr(), + &mut output_len, + ) + } + }; + extract_from_slice(output, output_len as usize); + ret_code.into() + } + + fn debug_message(str: &[u8]) -> Result { + let ret_code = unsafe { sys::debug_message(str.as_ptr(), str.len() as u32) }; + ret_code.into() + } + + fn terminate(beneficiary: &[u8; 20]) -> ! { + unsafe { sys::terminate(beneficiary.as_ptr()) } + panic!("terminate does not return"); + } + + fn call_chain_extension(func_id: u32, input: &[u8], mut output: Option<&mut &mut [u8]>) -> u32 { + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let ret_code = { + unsafe { + sys::call_chain_extension( + func_id, + input.as_ptr(), + input.len() as u32, + output_ptr, + &mut output_len, + ) + } + }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + ret_code.into_u32() + } + + fn input(output: &mut &mut [u8]) { + let mut output_len = output.len() as u32; + { + unsafe { sys::input(output.as_mut_ptr(), &mut output_len) }; + } + extract_from_slice(output, output_len as usize); + } + + fn return_value(flags: ReturnFlags, return_value: &[u8]) -> ! { + unsafe { sys::seal_return(flags.bits(), return_value.as_ptr(), return_value.len() as u32) } + panic!("seal_return does not return"); + } + + fn call_runtime(call: &[u8]) -> Result { + let ret_code = unsafe { sys::call_runtime(call.as_ptr(), call.len() as u32) }; + ret_code.into() + } + + fn weight_left(output: &mut &mut [u8]) { + let mut output_len = output.len() as u32; + unsafe { sys::weight_left(output.as_mut_ptr(), &mut output_len) } + extract_from_slice(output, output_len as usize) + } + + fn weight_to_fee(ref_time_limit: u64, proof_size_limit: u64, output: &mut [u8; 32]) { + unsafe { sys::weight_to_fee(ref_time_limit, proof_size_limit, output.as_mut_ptr()) }; + } + + fn ecdsa_recover( + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], + ) -> Result { + let ret_code = unsafe { + sys::ecdsa_recover(signature.as_ptr(), message_hash.as_ptr(), output.as_mut_ptr()) + }; + ret_code.into() + } + + fn ecdsa_to_eth_address(pubkey: &[u8; 33], output: &mut [u8; 20]) -> Result { + let ret_code = unsafe { sys::ecdsa_to_eth_address(pubkey.as_ptr(), output.as_mut_ptr()) }; + ret_code.into() + } + + fn sr25519_verify(signature: &[u8; 64], message: &[u8], pub_key: &[u8; 32]) -> Result { + let ret_code = unsafe { + sys::sr25519_verify( + signature.as_ptr(), + pub_key.as_ptr(), + message.len() as u32, + message.as_ptr(), + ) + }; + ret_code.into() + } + + fn is_contract(address: &[u8; 20]) -> bool { + let ret_val = unsafe { sys::is_contract(address.as_ptr()) }; + ret_val.into_bool() + } + + fn balance_of(address: &[u8; 20], output: &mut [u8; 32]) { + unsafe { sys::balance_of(address.as_ptr(), output.as_mut_ptr()) }; + } + + fn caller_is_origin() -> bool { + let ret_val = unsafe { sys::caller_is_origin() }; + ret_val.into_bool() + } + + fn set_code_hash(code_hash: &[u8; 32]) -> Result { + let ret_val = unsafe { sys::set_code_hash(code_hash.as_ptr()) }; + ret_val.into() + } + + fn code_hash(address: &[u8; 20], output: &mut [u8; 32]) -> Result { + let ret_val = unsafe { sys::code_hash(address.as_ptr(), output.as_mut_ptr()) }; + ret_val.into() + } + + fn own_code_hash(output: &mut [u8; 32]) { + unsafe { sys::own_code_hash(output.as_mut_ptr()) } + } + + fn lock_delegate_dependency(code_hash: &[u8; 32]) { + unsafe { sys::lock_delegate_dependency(code_hash.as_ptr()) } + } + + fn unlock_delegate_dependency(code_hash: &[u8; 32]) { + unsafe { sys::unlock_delegate_dependency(code_hash.as_ptr()) } + } + + fn xcm_execute(msg: &[u8]) -> Result { + let ret_code = unsafe { sys::xcm_execute(msg.as_ptr(), msg.len() as _) }; + ret_code.into() + } + + fn xcm_send(dest: &[u8], msg: &[u8], output: &mut [u8; 32]) -> Result { + let ret_code = unsafe { + sys::xcm_send( + dest.as_ptr(), + dest.len() as _, + msg.as_ptr(), + msg.len() as _, + output.as_mut_ptr(), + ) + }; + ret_code.into() + } + + fn return_data_size(output: &mut [u8; 32]) { + unsafe { sys::return_data_size(output.as_mut_ptr()) }; + } + + fn return_data_copy(output: &mut &mut [u8], offset: u32) { + let mut output_len = output.len() as u32; + { + unsafe { sys::return_data_copy(output.as_mut_ptr(), &mut output_len, offset) }; + } + extract_from_slice(output, output_len as usize); + } +} diff --git a/pallets/revive/uapi/src/lib.rs b/pallets/revive/uapi/src/lib.rs new file mode 100644 index 00000000..825da8b1 --- /dev/null +++ b/pallets/revive/uapi/src/lib.rs @@ -0,0 +1,132 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// +// 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. + +//! External C API to communicate with substrate contracts runtime module. +//! +//! Refer to substrate FRAME contract module for more documentation. + +#![no_std] + +mod flags; +pub use flags::*; +mod host; + +pub use host::{HostFn, HostFnImpl}; + +macro_rules! define_error_codes { + ( + $( + $( #[$attr:meta] )* + $name:ident = $discr:literal, + )* + ) => { + /// Every error that can be returned to a contract when it calls any of the host functions. + #[derive(Debug, PartialEq, Eq)] + #[repr(u32)] + pub enum ReturnErrorCode { + /// API call successful. + Success = 0, + $( + $( #[$attr] )* + $name = $discr, + )* + /// Returns if an unknown error was received from the host module. + Unknown, + } + + impl From for Result { + fn from(return_code: ReturnCode) -> Self { + match return_code.0 { + 0 => Ok(()), + $( + $discr => Err(ReturnErrorCode::$name), + )* + _ => Err(ReturnErrorCode::Unknown), + } + } + } + }; +} + +impl From for u32 { + fn from(code: ReturnErrorCode) -> u32 { + code as u32 + } +} + +define_error_codes! { + /// The called function trapped and has its state changes reverted. + /// In this case no output buffer is returned. + /// Can only be returned from `call` and `instantiate`. + CalleeTrapped = 1, + /// The called function ran to completion but decided to revert its state. + /// An output buffer is returned when one was supplied. + /// Can only be returned from `call` and `instantiate`. + CalleeReverted = 2, + /// The passed key does not exist in storage. + KeyNotFound = 3, + /// Transfer failed for other not further specified reason. Most probably + /// reserved or locked balance of the sender that was preventing the transfer. + TransferFailed = 4, + /// No code could be found at the supplied code hash. + CodeNotFound = 5, + /// The account that was called is no contract. + NotCallable = 6, + /// The call to `debug_message` had no effect because debug message + /// recording was disabled. + LoggingDisabled = 7, + /// The call dispatched by `call_runtime` was executed but returned an error. + CallRuntimeFailed = 8, + /// ECDSA public key recovery failed. Most probably wrong recovery id or signature. + EcdsaRecoveryFailed = 9, + /// sr25519 signature verification failed. + Sr25519VerifyFailed = 10, + /// The `xcm_execute` call failed. + XcmExecutionFailed = 11, + /// The `xcm_send` call failed. + XcmSendFailed = 12, +} + +/// The raw return code returned by the host side. +#[repr(transparent)] +pub struct ReturnCode(u32); + +/// Used as a sentinel value when reading and writing contract memory. +/// +/// We use this value to signal `None` to a contract when only a primitive is +/// allowed and we don't want to go through encoding a full Rust type. +/// Using `u32::Max` is a safe sentinel because contracts are never +/// allowed to use such a large amount of resources. So this value doesn't +/// make sense for a memory location or length. +const SENTINEL: u32 = u32::MAX; + +impl From for Option { + fn from(code: ReturnCode) -> Self { + (code.0 < SENTINEL).then_some(code.0) + } +} + +impl ReturnCode { + /// Returns the raw underlying `u32` representation. + pub fn into_u32(self) -> u32 { + self.0 + } + + /// Returns the underlying `u32` converted into `bool`. + pub fn into_bool(self) -> bool { + self.0.ne(&0) + } +} + +type Result = core::result::Result<(), ReturnErrorCode>; diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index 4caf8eaa..25a2bedd 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -6,13 +6,14 @@ name = "pop-api" version = "0.0.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/peterwht/ink", branch = "peter/riscv", default-features = false } +polkavm-derive = "0.11.0" pop-primitives = { path = "../primitives", default-features = false } -sp-io = { version = "37.0.0", default-features = false, features = [ - "disable_allocator", - "disable_oom", - "disable_panic_handler", -] } +#sp-io = { version = "37.0.0", default-features = false, features = [ +# "disable_allocator", +# "disable_oom", +# "disable_panic_handler", +#] } [lib] crate-type = [ "rlib" ] @@ -22,4 +23,4 @@ path = "src/lib.rs" [features] default = [ "std" ] fungibles = [ ] -std = [ "ink/std", "pop-primitives/std", "sp-io/std" ] +std = [ "ink/std", "pop-primitives/std" ] diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml index 536a8a17..03c19027 100644 --- a/pop-api/integration-tests/Cargo.toml +++ b/pop-api/integration-tests/Cargo.toml @@ -1,12 +1,12 @@ [package] -build = "build.rs" +#build = "build.rs" edition = "2021" name = "integration-tests" version = "0.1.0" -[build-dependencies] -contract-build = "4.1.1" - +#[build-dependencies] +#contract-build = "4.1.1" +# [dev-dependencies] env_logger = "0.11.2" frame-support = { version = "36.0.0", default-features = false } @@ -15,7 +15,7 @@ frame-system = { version = "36.1.0", default-features = false } log = "0.4.22" pallet-assets = { version = "37.0.0", default-features = false } pallet-balances = { version = "37.0.0", default-features = false } -pallet-contracts = { version = "35.0.0", default-features = false } +pallet-revive = { path = "../../pallets/revive", default-features = false } pop-api = { path = "../../pop-api", default-features = false, features = [ "fungibles", ] } @@ -36,7 +36,7 @@ std = [ "frame-system/std", "pallet-assets/std", "pallet-balances/std", - "pallet-contracts/std", + "pallet-revive/std", "pop-api/std", "pop-primitives/std", "pop-runtime-devnet/std", diff --git a/pop-api/integration-tests/build.rs b/pop-api/integration-tests/build.rs deleted file mode 100644 index 21334ac2..00000000 --- a/pop-api/integration-tests/build.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::{ - fs, - path::{Path, PathBuf}, - process, -}; - -use contract_build::{ - execute, BuildArtifacts, BuildMode, BuildResult, ExecuteArgs, ManifestPath, OutputType, - Verbosity, -}; - -fn main() { - let contracts_dir = PathBuf::from("./contracts/"); - let contract_dirs = match get_subcontract_directories(&contracts_dir) { - Ok(dirs) => dirs, - Err(e) => { - eprintln!("Failed to read contracts directory: {}", e); - process::exit(1); - }, - }; - - for contract in contract_dirs { - if let Err(e) = build_contract(&contract) { - eprintln!("Failed to build contract {}: {}", contract.display(), e); - process::exit(1); - } - } -} - -// Function to retrieve all subdirectories in a given directory. -fn get_subcontract_directories(contracts_dir: &Path) -> Result, String> { - fs::read_dir(contracts_dir) - .map_err(|e| format!("Could not read directory '{}': {}", contracts_dir.display(), e))? - .filter_map(|entry| match entry { - Ok(entry) if entry.path().is_dir() => Some(Ok(entry.path())), - Ok(_) => None, - Err(e) => Some(Err(format!("Error reading directory entry: {}", e))), - }) - .collect() -} - -// Function to build a contract given its directory. -fn build_contract(contract_dir: &Path) -> Result { - let manifest_path = ManifestPath::new(contract_dir.join("Cargo.toml")).map_err(|e| { - format!("Could not retrieve manifest path for {}: {}", contract_dir.display(), e) - })?; - - let args = ExecuteArgs { - build_artifact: BuildArtifacts::CodeOnly, - build_mode: BuildMode::Debug, - manifest_path, - output_type: OutputType::HumanReadable, - verbosity: Verbosity::Default, - ..Default::default() - }; - - execute(args).map_err(|e| format!("Build failed for {}: {}", contract_dir.display(), e)) -} diff --git a/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml index a1517cdd..71ab704f 100755 --- a/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml +++ b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml @@ -1,10 +1,12 @@ [package] +authors = [ "r0gue " ] edition = "2021" name = "create_token_in_constructor" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/peterwht/ink", branch = "peter/riscv", default-features = false } +polkavm-derive = "0.11.0" pop-api = { path = "../../..", default-features = false, features = [ "fungibles" ] } [lib] diff --git a/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs b/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs index 3ca787be..a2a515cd 100755 --- a/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs +++ b/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs @@ -1,5 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] +use ink::env::DefaultEnvironment; +use ink::env::Environment; use pop_api::{ fungibles::{self as api, events::Created}, primitives::TokenId, @@ -8,6 +10,15 @@ use pop_api::{ pub type Result = core::result::Result; +type AccountId = ::AccountId; + +fn to_account_id(address: &AccountId) -> AccountId { + let mut account_id = AccountId::from([0xEE; 32]); + >::as_mut(&mut account_id)[..20] + .copy_from_slice(&>::as_ref(&address)[..20]); + account_id +} + #[ink::contract] mod create_token_in_constructor { use super::*; @@ -20,17 +31,40 @@ mod create_token_in_constructor { impl Fungible { #[ink(constructor, payable)] pub fn new(id: TokenId, min_balance: Balance) -> Result { + Self::env().caller(); let contract = Self { token: id }; // AccountId of the contract which will be set to the owner of the fungible token. - let owner = contract.env().account_id(); + let owner = to_account_id(&contract.env().account_id()); + ink::env::debug_println!("{}", &format!("owner: {:?}", owner)); api::create(id, owner, min_balance)?; contract.env().emit_event(Created { id, creator: owner, admin: owner }); Ok(contract) } - #[ink(message)] + #[ink(constructor, payable)] + pub fn new_default() -> Result { + let contract = Self { token: 0 }; + // AccountId of the contract which will be set to the owner of the fungible token. + let owner = contract.env().account_id(); + api::create(0, owner, 1)?; + contract.env().emit_event(Created { id: 0, creator: owner, admin: owner }); + Ok(contract) + } + + #[ink(message, payable)] pub fn token_exists(&self) -> Result { api::token_exists(self.token) } } + + #[cfg(test)] + mod tests { + use super::*; + + #[ink::test] + fn new_works() { + let contract = Fungible::new(0, 1).unwrap(); + // assert_eq!(contract.token_exists().unwrap(), true); + } + } } diff --git a/pop-api/integration-tests/contracts/fungibles/Cargo.toml b/pop-api/integration-tests/contracts/fungibles/Cargo.toml index dce9b257..b57ad5c4 100755 --- a/pop-api/integration-tests/contracts/fungibles/Cargo.toml +++ b/pop-api/integration-tests/contracts/fungibles/Cargo.toml @@ -1,11 +1,13 @@ [package] +authors = [ "R0GUE " ] edition = "2021" name = "fungibles" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../../pop-api", default-features = false, features = [ "fungibles" ] } +ink = { git = "https://github.com/peterwht/ink", branch = "peter/riscv", default-features = false } +polkavm-derive = "0.11.0" +pop-api = { path = "../../..", default-features = false, features = [ "fungibles" ] } [lib] path = "lib.rs" diff --git a/pop-api/integration-tests/contracts/fungibles/lib.rs b/pop-api/integration-tests/contracts/fungibles/lib.rs index b029413e..86c55193 100755 --- a/pop-api/integration-tests/contracts/fungibles/lib.rs +++ b/pop-api/integration-tests/contracts/fungibles/lib.rs @@ -14,8 +14,19 @@ use pop_api::{ StatusCode, }; +use ink::env::DefaultEnvironment; +use ink::env::Environment; + pub type Result = core::result::Result; +type AccountId = ::AccountId; + +fn to_account_id(address: &AccountId) -> AccountId { + let mut account_id = AccountId::from([0xEE; 32]); + >::as_mut(&mut account_id)[..20] + .copy_from_slice(&>::as_ref(&address)[..20]); + account_id +} #[ink::contract] mod fungibles { use super::*; @@ -65,7 +76,7 @@ mod fungibles { pub fn transfer(&mut self, token: TokenId, to: AccountId, value: Balance) -> Result<()> { api::transfer(token, to, value)?; self.env().emit_event(Transfer { - from: Some(self.env().account_id()), + from: Some(to_account_id(&self.env().account_id())), to: Some(to), value, }); @@ -95,8 +106,11 @@ mod fungibles { value: Balance, ) -> Result<()> { api::approve(token, spender, value)?; - self.env() - .emit_event(Approval { owner: self.env().account_id(), spender, value }); + self.env().emit_event(Approval { + owner: to_account_id(&self.env().account_id()), + spender, + value, + }); Ok(()) } diff --git a/pop-api/integration-tests/src/fungibles/mod.rs b/pop-api/integration-tests/src/fungibles/mod.rs index aa2048a9..942346a9 100644 --- a/pop-api/integration-tests/src/fungibles/mod.rs +++ b/pop-api/integration-tests/src/fungibles/mod.rs @@ -9,7 +9,7 @@ use super::*; mod utils; const TOKEN_ID: TokenId = 1; -const CONTRACT: &str = "contracts/fungibles/target/ink/fungibles.wasm"; +const CONTRACT: &str = "contracts/fungibles/target/ink/fungibles.riscv"; /// 1. PSP-22 Interface: /// - total_supply @@ -24,14 +24,14 @@ const CONTRACT: &str = "contracts/fungibles/target/ink/fungibles.wasm"; #[test] fn total_supply_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); // No tokens in circulation. assert_eq!(total_supply(&addr, TOKEN_ID), Ok(Assets::total_supply(TOKEN_ID))); assert_eq!(total_supply(&addr, TOKEN_ID), Ok(0)); // Tokens in circulation. - assets::create_and_mint_to(&addr, TOKEN_ID, &BOB, 100); + assets::create_and_mint_to(&account_id, TOKEN_ID, &BOB, 100); assert_eq!(total_supply(&addr, TOKEN_ID), Ok(Assets::total_supply(TOKEN_ID))); assert_eq!(total_supply(&addr, TOKEN_ID), Ok(100)); }); @@ -40,14 +40,14 @@ fn total_supply_works() { #[test] fn balance_of_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); // No tokens in circulation. assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(Assets::balance(TOKEN_ID, BOB))); assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(0)); // Tokens in circulation. - assets::create_and_mint_to(&addr, TOKEN_ID, &BOB, 100); + assets::create_and_mint_to(&account_id, TOKEN_ID, &BOB, 100); assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(Assets::balance(TOKEN_ID, BOB))); assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(100)); }); @@ -56,7 +56,7 @@ fn balance_of_works() { #[test] fn allowance_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); // No tokens in circulation. assert_eq!( @@ -66,7 +66,7 @@ fn allowance_works() { assert_eq!(allowance(&addr, TOKEN_ID, BOB, ALICE), Ok(0)); // Tokens in circulation. - assets::create_mint_and_approve(&addr, TOKEN_ID, &BOB, 100, &ALICE, 50); + assets::create_mint_and_approve(&account_id, TOKEN_ID, &BOB, 100, &ALICE, 50); assert_eq!( allowance(&addr, TOKEN_ID, BOB, ALICE), Ok(Assets::allowance(TOKEN_ID, &BOB, &ALICE)) @@ -78,13 +78,13 @@ fn allowance_works() { #[test] fn transfer_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. assert_eq!(transfer(&addr, 1, BOB, amount), Err(Module { index: 52, error: [3, 0] })); // Create token with Alice as owner and mint `amount` to contract address. - let token = assets::create_and_mint_to(&ALICE, 1, &addr, amount); + let token = assets::create_and_mint_to(&ALICE, 1, &account_id, amount); // Token is not live, i.e. frozen or being destroyed. assets::freeze(&ALICE, token); assert_eq!(transfer(&addr, token, BOB, amount), Err(Module { index: 52, error: [16, 0] })); @@ -102,7 +102,7 @@ fn transfer_works() { let balance_after_transfer = Assets::balance(token, &BOB); assert_eq!(balance_after_transfer, balance_before_transfer + amount / 2); // Successfully emit event. - let from = account_id_from_slice(addr.as_ref()); + let from = account_id_from_slice(account_id.as_ref()); let to = account_id_from_slice(BOB.as_ref()); let expected = Transfer { from: Some(from), to: Some(to), value: amount / 2 }.encode(); assert_eq!(last_contract_event(), expected.as_slice()); @@ -120,7 +120,7 @@ fn transfer_works() { #[test] fn transfer_from_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. @@ -138,7 +138,7 @@ fn transfer_from_works() { assert_ok!(Assets::approve_transfer( RuntimeOrigin::signed(ALICE.into()), token.into(), - addr.clone().into(), + account_id.clone().into(), amount + 1 * UNIT, )); // Token is not live, i.e. frozen or being destroyed. @@ -169,34 +169,34 @@ fn transfer_from_works() { #[test] fn approve_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, 0, vec![]); + let (addr, account_id) = instantiate(CONTRACT, 0, vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. assert_eq!(approve(&addr, 0, &BOB, amount), Err(Module { index: 52, error: [3, 0] })); - let token = assets::create_and_mint_to(&ALICE, 0, &addr, amount); + let token = assets::create_and_mint_to(&ALICE, 0, &account_id, amount); assert_eq!(approve(&addr, token, &BOB, amount), Err(ConsumerRemaining)); - let addr = instantiate(CONTRACT, INIT_VALUE, vec![1]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![1]); // Create token with Alice as owner and mint `amount` to contract address. - let token = assets::create_and_mint_to(&ALICE, 1, &addr, amount); + let token = assets::create_and_mint_to(&ALICE, 1, &account_id, amount); // Token is not live, i.e. frozen or being destroyed. assets::freeze(&ALICE, token); assert_eq!(approve(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); assets::thaw(&ALICE, token); // Successful approvals. - assert_eq!(0, Assets::allowance(token, &addr, &BOB)); + assert_eq!(0, Assets::allowance(token, &account_id, &BOB)); assert_ok!(approve(&addr, token, &BOB, amount)); - assert_eq!(Assets::allowance(token, &addr, &BOB), amount); + assert_eq!(Assets::allowance(token, &account_id, &BOB), amount); // Successfully emit event. - let owner = account_id_from_slice(addr.as_ref()); + let owner = account_id_from_slice(account_id.as_ref()); let spender = account_id_from_slice(BOB.as_ref()); let expected = Approval { owner, spender, value: amount }.encode(); assert_eq!(last_contract_event(), expected.as_slice()); // Non-additive, sets new value. assert_ok!(approve(&addr, token, &BOB, amount / 2)); - assert_eq!(Assets::allowance(token, &addr, &BOB), amount / 2); + assert_eq!(Assets::allowance(token, &account_id, &BOB), amount / 2); // Successfully emit event. - let owner = account_id_from_slice(addr.as_ref()); + let owner = account_id_from_slice(account_id.as_ref()); let spender = account_id_from_slice(BOB.as_ref()); let expected = Approval { owner, spender, value: amount / 2 }.encode(); assert_eq!(last_contract_event(), expected.as_slice()); @@ -211,19 +211,19 @@ fn increase_allowance_works() { new_test_ext().execute_with(|| { let amount: Balance = 100 * UNIT; // Instantiate a contract without balance - test `ConsumerRemaining. - let addr = instantiate(CONTRACT, 0, vec![]); + let (addr, account_id) = instantiate(CONTRACT, 0, vec![]); // Token does not exist. assert_eq!( increase_allowance(&addr, 0, &BOB, amount), Err(Module { index: 52, error: [3, 0] }) ); - let token = assets::create_and_mint_to(&ALICE, 0, &addr, amount); + let token = assets::create_and_mint_to(&ALICE, 0, &account_id, amount); assert_eq!(increase_allowance(&addr, token, &BOB, amount), Err(ConsumerRemaining)); // Instantiate a contract with balance. - let addr = instantiate(CONTRACT, INIT_VALUE, vec![1]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![1]); // Create token with Alice as owner and mint `amount` to contract address. - let token = assets::create_and_mint_to(&ALICE, 1, &addr, amount); + let token = assets::create_and_mint_to(&ALICE, 1, &account_id, amount); // Token is not live, i.e. frozen or being destroyed. assets::freeze(&ALICE, token); assert_eq!( @@ -232,12 +232,12 @@ fn increase_allowance_works() { ); assets::thaw(&ALICE, token); // Successful approvals: - assert_eq!(0, Assets::allowance(token, &addr, &BOB)); + assert_eq!(0, Assets::allowance(token, &account_id, &BOB)); assert_ok!(increase_allowance(&addr, token, &BOB, amount)); - assert_eq!(Assets::allowance(token, &addr, &BOB), amount); + assert_eq!(Assets::allowance(token, &account_id, &BOB), amount); // Additive. assert_ok!(increase_allowance(&addr, token, &BOB, amount)); - assert_eq!(Assets::allowance(token, &addr, &BOB), amount * 2); + assert_eq!(Assets::allowance(token, &account_id, &BOB), amount * 2); // Token is not live, i.e. frozen or being destroyed. assets::start_destroy(&ALICE, token); assert_eq!( @@ -250,30 +250,31 @@ fn increase_allowance_works() { #[test] fn decrease_allowance_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); let amount: Balance = 100 * UNIT; // Create token and mint `amount` to contract address, then approve Bob to spend `amount`. - let token = assets::create_mint_and_approve(&addr, 0, &addr, amount, &BOB, amount); + let token = + assets::create_mint_and_approve(&account_id, 0, &account_id, amount, &BOB, amount); // Token is not live, i.e. frozen or being destroyed. - assets::freeze(&addr, token); + assets::freeze(&account_id, token); assert_eq!( decrease_allowance(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] }), ); - assets::thaw(&addr, token); + assets::thaw(&account_id, token); // "Unapproved" error is returned if the current allowance is less than `value`. assert_eq!( decrease_allowance(&addr, token, &BOB, amount * 2), Err(Module { index: 52, error: [10, 0] }), ); // Successfully decrease allowance. - let allowance_before = Assets::allowance(token, &addr, &BOB); - assert_ok!(decrease_allowance(&addr, token, &BOB, amount / 2 - 1 * UNIT)); - let allowance_after = Assets::allowance(token, &addr, &BOB); + let allowance_before = Assets::allowance(token, &account_id, &BOB); + assert_ok!(decrease_allowance(&addr, 0, &BOB, amount / 2 - 1 * UNIT)); + let allowance_after = Assets::allowance(token, &account_id, &BOB); assert_eq!(allowance_before - allowance_after, amount / 2 - 1 * UNIT); // Token is not live, i.e. frozen or being destroyed. - assets::start_destroy(&addr, token); + assets::start_destroy(&account_id, token); assert_eq!( decrease_allowance(&addr, token, &BOB, 1 * UNIT), Err(Module { index: 52, error: [16, 0] }), @@ -289,7 +290,7 @@ fn decrease_allowance_works() { #[test] fn token_metadata_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); let name: Vec = vec![11, 12, 13]; let symbol: Vec = vec![21, 22, 23]; let decimals: u8 = 69; @@ -299,7 +300,13 @@ fn token_metadata_works() { assert_eq!(token_symbol(&addr, TOKEN_ID), Ok(None)); assert_eq!(token_decimals(&addr, TOKEN_ID), Ok(0)); // Create Token. - assets::create_and_set_metadata(&addr, TOKEN_ID, name.clone(), symbol.clone(), decimals); + assets::create_and_set_metadata( + &account_id, + TOKEN_ID, + name.clone(), + symbol.clone(), + decimals, + ); assert_eq!(token_name(&addr, TOKEN_ID), Ok(Some(Assets::name(TOKEN_ID)))); assert_eq!(token_name(&addr, TOKEN_ID), Ok(Some(name))); assert_eq!(token_symbol(&addr, TOKEN_ID), Ok(Some(Assets::symbol(TOKEN_ID)))); @@ -319,23 +326,29 @@ fn token_metadata_works() { fn create_works() { new_test_ext().execute_with(|| { // Instantiate a contract without balance for fees. - let addr = instantiate(CONTRACT, 0, vec![0]); + let (addr, account_id) = instantiate(CONTRACT, 0, vec![0]); // No balance to pay for fees. - assert_eq!(create(&addr, TOKEN_ID, &addr, 1), Err(Module { index: 10, error: [2, 0] }),); + assert_eq!( + create(&addr, TOKEN_ID, &account_id, 1), + Err(Module { index: 10, error: [2, 0] }), + ); // Instantiate a contract without balance for deposit. - let addr = instantiate(CONTRACT, 100, vec![1]); + let (addr, account_id) = instantiate(CONTRACT, 100, vec![1]); // No balance to pay the deposit. - assert_eq!(create(&addr, TOKEN_ID, &addr, 1), Err(Module { index: 10, error: [2, 0] }),); + assert_eq!( + create(&addr, TOKEN_ID, &account_id, 1), + Err(Module { index: 10, error: [2, 0] }), + ); // Instantiate a contract with enough balance. - let addr = instantiate(CONTRACT, INIT_VALUE, vec![2]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![2]); assert_eq!(create(&addr, TOKEN_ID, &BOB, 0), Err(Module { index: 52, error: [7, 0] }),); // The minimal balance for a token must be non zero. assert_eq!(create(&addr, TOKEN_ID, &BOB, 0), Err(Module { index: 52, error: [7, 0] }),); // Create token successfully. assert_ok!(create(&addr, TOKEN_ID, &BOB, 1)); - assert_eq!(Assets::owner(TOKEN_ID), Some(addr.clone())); + assert_eq!(Assets::owner(TOKEN_ID), Some(account_id.clone())); // Successfully emit event. let admin = account_id_from_slice(BOB.as_ref()); let expected = Created { id: TOKEN_ID, creator: admin, admin }.encode(); @@ -350,7 +363,7 @@ fn create_works() { fn instantiate_and_create_fungible_works() { new_test_ext().execute_with(|| { let contract = - "contracts/create_token_in_constructor/target/ink/create_token_in_constructor.wasm"; + "contracts/create_token_in_constructor/target/ink/create_token_in_constructor.riscv"; // Token already exists. assets::create(&ALICE, 0, 1); assert_eq!( @@ -374,7 +387,7 @@ fn instantiate_and_create_fungible_works() { #[test] fn start_destroy_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![2]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![2]); // Token does not exist. assert_eq!(start_destroy(&addr, TOKEN_ID), Err(Module { index: 52, error: [3, 0] }),); @@ -382,7 +395,7 @@ fn start_destroy_works() { let token = assets::create(&ALICE, 0, 1); // No Permission. assert_eq!(start_destroy(&addr, token), Err(Module { index: 52, error: [2, 0] }),); - let token = assets::create(&addr, TOKEN_ID, 1); + let token = assets::create(&account_id, TOKEN_ID, 1); assert_ok!(start_destroy(&addr, token)); // Successfully emit event. let expected = DestroyStarted { token: TOKEN_ID }.encode(); @@ -396,7 +409,7 @@ fn set_metadata_works() { let name = vec![42]; let symbol = vec![42]; let decimals = 42u8; - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); // Token does not exist. assert_eq!( @@ -410,14 +423,14 @@ fn set_metadata_works() { set_metadata(&addr, token, vec![0], vec![0], 0u8), Err(Module { index: 52, error: [2, 0] }), ); - let token = assets::create(&addr, TOKEN_ID, 1); + let token = assets::create(&account_id, TOKEN_ID, 1); // Token is not live, i.e. frozen or being destroyed. - assets::freeze(&addr, token); + assets::freeze(&account_id, token); assert_eq!( set_metadata(&addr, TOKEN_ID, vec![0], vec![0], 0u8), Err(Module { index: 52, error: [16, 0] }), ); - assets::thaw(&addr, token); + assets::thaw(&account_id, token); // TODO: calling the below with a vector of length `100_000` errors in pallet contracts // `OutputBufferTooSmall. Added to security analysis issue #131 to revisit. // Set bad metadata - too large values. @@ -431,7 +444,7 @@ fn set_metadata_works() { let expected = MetadataSet { token: TOKEN_ID, name, symbol, decimals }.encode(); assert_eq!(last_contract_event(), expected.as_slice()); // Token is not live, i.e. frozen or being destroyed. - assets::start_destroy(&addr, token); + assets::start_destroy(&account_id, token); assert_eq!( set_metadata(&addr, TOKEN_ID, vec![0], vec![0], 0), Err(Module { index: 52, error: [16, 0] }), @@ -445,7 +458,7 @@ fn clear_metadata_works() { let name = vec![42]; let symbol = vec![42]; let decimals = 42u8; - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); // Token does not exist. assert_eq!(clear_metadata(&addr, 0), Err(Module { index: 52, error: [3, 0] }),); @@ -453,21 +466,21 @@ fn clear_metadata_works() { let token = assets::create_and_set_metadata(&ALICE, 0, vec![0], vec![0], 0); // No Permission. assert_eq!(clear_metadata(&addr, token), Err(Module { index: 52, error: [2, 0] }),); - let token = assets::create(&addr, TOKEN_ID, 1); + let token = assets::create(&account_id, TOKEN_ID, 1); // Token is not live, i.e. frozen or being destroyed. - assets::freeze(&addr, token); + assets::freeze(&account_id, token); assert_eq!(clear_metadata(&addr, token), Err(Module { index: 52, error: [16, 0] }),); - assets::thaw(&addr, token); + assets::thaw(&account_id, token); // No metadata set. assert_eq!(clear_metadata(&addr, token), Err(Module { index: 52, error: [3, 0] }),); - assets::set_metadata(&addr, token, name, symbol, decimals); + assets::set_metadata(&account_id, token, name, symbol, decimals); // Clear metadata successfully. assert_ok!(clear_metadata(&addr, TOKEN_ID)); // Successfully emit event. let expected = MetadataCleared { token: TOKEN_ID }.encode(); assert_eq!(last_contract_event(), expected.as_slice()); // Token is not live, i.e. frozen or being destroyed. - assets::start_destroy(&addr, token); + assets::start_destroy(&account_id, token); assert_eq!( set_metadata(&addr, TOKEN_ID, vec![0], vec![0], decimals), Err(Module { index: 52, error: [16, 0] }), @@ -478,13 +491,13 @@ fn clear_metadata_works() { #[test] fn token_exists_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); // No tokens in circulation. assert_eq!(token_exists(&addr, TOKEN_ID), Ok(Assets::asset_exists(TOKEN_ID))); // Tokens in circulation. - assets::create(&addr, TOKEN_ID, 1); + assets::create(&account_id, TOKEN_ID, 1); assert_eq!(token_exists(&addr, TOKEN_ID), Ok(Assets::asset_exists(TOKEN_ID))); }); } @@ -492,7 +505,7 @@ fn token_exists_works() { #[test] fn mint_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. @@ -500,13 +513,13 @@ fn mint_works() { let token = assets::create(&ALICE, 1, 1); // Minting can only be done by the owner. assert_eq!(mint(&addr, token, &BOB, 1), Err(Module { index: 52, error: [2, 0] })); - let token = assets::create(&addr, 2, 2); + let token = assets::create(&account_id, 2, 2); // Minimum balance of a token can not be zero. assert_eq!(mint(&addr, token, &BOB, 1), Err(Token(BelowMinimum))); // Token is not live, i.e. frozen or being destroyed. - assets::freeze(&addr, token); + assets::freeze(&account_id, token); assert_eq!(mint(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); - assets::thaw(&addr, token); + assets::thaw(&account_id, token); // Successful mint. let balance_before_mint = Assets::balance(token, &BOB); assert_ok!(mint(&addr, token, &BOB, amount)); @@ -515,7 +528,7 @@ fn mint_works() { // Account can not hold more tokens than Balance::MAX. assert_eq!(mint(&addr, token, &BOB, Balance::MAX,), Err(Arithmetic(Overflow))); // Token is not live, i.e. frozen or being destroyed. - assets::start_destroy(&addr, token); + assets::start_destroy(&account_id, token); assert_eq!(mint(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); }); } @@ -523,7 +536,7 @@ fn mint_works() { #[test] fn burn_works() { new_test_ext().execute_with(|| { - let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. @@ -534,18 +547,18 @@ fn burn_works() { // Burning can only be done by the manager. assets::mint(&ALICE, token, &BOB, amount); assert_eq!(burn(&addr, token, &BOB, 1), Err(Module { index: 52, error: [2, 0] })); - let token = assets::create_and_mint_to(&addr, 2, &BOB, amount * 2); + let token = assets::create_and_mint_to(&account_id, 2, &BOB, amount * 2); // Token is not live, i.e. frozen or being destroyed. - assets::freeze(&addr, token); + assets::freeze(&account_id, token); assert_eq!(burn(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); - assets::thaw(&addr, token); + assets::thaw(&account_id, token); // Successful mint. let balance_before_burn = Assets::balance(token, &BOB); assert_ok!(burn(&addr, token, &BOB, amount)); let balance_after_burn = Assets::balance(token, &BOB); assert_eq!(balance_after_burn, balance_before_burn - amount); // Token is not live, i.e. frozen or being destroyed. - assets::start_destroy(&addr, token); + assets::start_destroy(&account_id, token); assert_eq!(burn(&addr, token, &BOB, amount), Err(Module { index: 52, error: [17, 0] })); }); } diff --git a/pop-api/integration-tests/src/fungibles/utils.rs b/pop-api/integration-tests/src/fungibles/utils.rs index 07be866e..551bcf06 100644 --- a/pop-api/integration-tests/src/fungibles/utils.rs +++ b/pop-api/integration-tests/src/fungibles/utils.rs @@ -1,6 +1,9 @@ use super::*; +use crate::sp_core::H160; +use pallet_revive::AddressMapper; +use pallet_revive::DefaultAddressMapper; -fn do_bare_call(function: &str, addr: &AccountId32, params: Vec) -> ExecReturnValue { +fn do_bare_call(function: &str, addr: &H160, params: Vec) -> ExecReturnValue { let function = function_selector(function); let params = [function, params].concat(); bare_call(addr.clone(), params, 0).expect("should work") @@ -11,14 +14,14 @@ pub(super) fn decoded(result: ExecReturnValue) -> Result::decode(&mut &result.data[1..]).map_err(|_| result) } -pub(super) fn total_supply(addr: &AccountId32, token_id: TokenId) -> Result { +pub(super) fn total_supply(addr: &H160, token_id: TokenId) -> Result { let result = do_bare_call("total_supply", addr, token_id.encode()); decoded::>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } pub(super) fn balance_of( - addr: &AccountId32, + addr: &H160, token_id: TokenId, owner: AccountId32, ) -> Result { @@ -29,7 +32,7 @@ pub(super) fn balance_of( } pub(super) fn allowance( - addr: &AccountId32, + addr: &H160, token_id: TokenId, owner: AccountId32, spender: AccountId32, @@ -40,14 +43,14 @@ pub(super) fn allowance( .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } -pub(super) fn token_name(addr: &AccountId32, token_id: TokenId) -> Result>, Error> { +pub(super) fn token_name(addr: &H160, token_id: TokenId) -> Result>, Error> { let result = do_bare_call("token_name", addr, token_id.encode()); decoded::>, Error>>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } pub(super) fn token_symbol( - addr: &AccountId32, + addr: &H160, token_id: TokenId, ) -> Result>, Error> { let result = do_bare_call("token_symbol", addr, token_id.encode()); @@ -55,20 +58,20 @@ pub(super) fn token_symbol( .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } -pub(super) fn token_decimals(addr: &AccountId32, token_id: TokenId) -> Result { +pub(super) fn token_decimals(addr: &H160, token_id: TokenId) -> Result { let result = do_bare_call("token_decimals", addr, token_id.encode()); decoded::>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } -pub(super) fn token_exists(addr: &AccountId32, token_id: TokenId) -> Result { +pub(super) fn token_exists(addr: &H160, token_id: TokenId) -> Result { let result = do_bare_call("token_exists", addr, token_id.encode()); decoded::>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } pub(super) fn transfer( - addr: &AccountId32, + addr: &H160, token_id: TokenId, to: AccountId32, value: Balance, @@ -80,7 +83,7 @@ pub(super) fn transfer( } pub(super) fn transfer_from( - addr: &AccountId32, + addr: &H160, token_id: TokenId, from: AccountId32, to: AccountId32, @@ -95,7 +98,7 @@ pub(super) fn transfer_from( } pub(super) fn approve( - addr: &AccountId32, + addr: &H160, token_id: TokenId, spender: &AccountId32, value: Balance, @@ -107,7 +110,7 @@ pub(super) fn approve( } pub(super) fn increase_allowance( - addr: &AccountId32, + addr: &H160, token_id: TokenId, spender: &AccountId32, value: Balance, @@ -119,7 +122,7 @@ pub(super) fn increase_allowance( } pub(super) fn decrease_allowance( - addr: &AccountId32, + addr: &H160, token_id: TokenId, spender: &AccountId32, value: Balance, @@ -131,7 +134,7 @@ pub(super) fn decrease_allowance( } pub(super) fn create( - addr: &AccountId32, + addr: &H160, token_id: TokenId, admin: &AccountId32, min_balance: Balance, @@ -142,7 +145,7 @@ pub(super) fn create( .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } -pub(super) fn start_destroy(addr: &AccountId32, token_id: TokenId) -> Result<(), Error> { +pub(super) fn start_destroy(addr: &H160, token_id: TokenId) -> Result<(), Error> { let result = do_bare_call("start_destroy", addr, token_id.encode()); match decoded::>(result) { Ok(x) => x, @@ -151,7 +154,7 @@ pub(super) fn start_destroy(addr: &AccountId32, token_id: TokenId) -> Result<(), } pub(super) fn set_metadata( - addr: &AccountId32, + addr: &H160, token_id: TokenId, name: Vec, symbol: Vec, @@ -163,14 +166,14 @@ pub(super) fn set_metadata( .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } -pub(super) fn clear_metadata(addr: &AccountId32, token_id: TokenId) -> Result<(), Error> { +pub(super) fn clear_metadata(addr: &H160, token_id: TokenId) -> Result<(), Error> { let result = do_bare_call("clear_metadata", addr, token_id.encode()); decoded::>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) } pub(super) fn mint( - addr: &AccountId32, + addr: &H160, token_id: TokenId, account: &AccountId32, amount: Balance, @@ -182,7 +185,7 @@ pub(super) fn mint( } pub(super) fn burn( - addr: &AccountId32, + addr: &H160, token_id: TokenId, account: &AccountId32, amount: Balance, @@ -322,33 +325,32 @@ pub(super) fn instantiate_and_create_fungible( let input = [function, token_id.encode(), min_balance.encode()].concat(); let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); let result = Contracts::bare_instantiate( - ALICE, + RuntimeOrigin::signed(ALICE), INIT_VALUE, GAS_LIMIT, - None, + 1 * 1_000_000_000_000, Code::Upload(wasm_binary), input, - vec![], + None, DEBUG_OUTPUT, CollectEvents::Skip, ) .result .expect("should work"); - let address = result.account_id; + let address = result.addr; let result = result.result; decoded::>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) - .map(|_| address) + .map(|_| DefaultAddressMapper::to_account_id(&address)) } /// Get the last event from pallet contracts. pub(super) fn last_contract_event() -> Vec { - let events = System::read_events_for_pallet::>(); + let events = System::read_events_for_pallet::>(); let contract_events = events .iter() .filter_map(|event| match event { - pallet_contracts::Event::::ContractEmitted { data, .. } => - Some(data.as_slice()), + pallet_revive::Event::::ContractEmitted { data, .. } => Some(data.as_slice()), _ => None, }) .collect::>(); diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index 9e6e20fc..fd029c93 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -7,9 +7,11 @@ use frame_support::{ }, weights::Weight, }; -use pallet_contracts::{Code, CollectEvents, Determinism, ExecReturnValue}; -use pop_runtime_devnet::{Assets, Contracts, Runtime, RuntimeOrigin, System, UNIT}; +use pallet_revive::AddressMapper; +use pallet_revive::{Code, CollectEvents, ExecReturnValue}; +use pop_runtime_devnet::{Assets, Revive as Contracts, Runtime, RuntimeOrigin, System, UNIT}; use scale::{Decode, Encode}; +use sp_runtime::app_crypto::sp_core; use sp_runtime::{AccountId32, BuildStorage, DispatchError}; mod fungibles; @@ -18,7 +20,7 @@ type Balance = u128; const ALICE: AccountId32 = AccountId32::new([1_u8; 32]); const BOB: AccountId32 = AccountId32::new([2_u8; 32]); -const DEBUG_OUTPUT: pallet_contracts::DebugInfo = pallet_contracts::DebugInfo::UnsafeDebug; +const DEBUG_OUTPUT: pallet_revive::DebugInfo = pallet_revive::DebugInfo::UnsafeDebug; const FERDIE: AccountId32 = AccountId32::new([3_u8; 32]); const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024); const INIT_AMOUNT: Balance = 100_000_000 * UNIT; @@ -49,42 +51,45 @@ fn function_selector(name: &str) -> Vec { } fn bare_call( - addr: AccountId32, + addr: sp_core::H160, input: Vec, value: u128, ) -> Result { let result = Contracts::bare_call( - ALICE, + RuntimeOrigin::signed(ALICE), addr.into(), value.into(), GAS_LIMIT, - None, + 1 * 1_000_000_000_000, input, DEBUG_OUTPUT, CollectEvents::Skip, - Determinism::Enforced, ); log::info!("contract exec result={result:?}"); result.result } // Deploy, instantiate and return contract address. -fn instantiate(contract: &str, init_value: u128, salt: Vec) -> AccountId32 { +fn instantiate( + contract: &str, + init_value: u128, + salt: Vec, +) -> (crate::sp_core::H160, AccountId32) { let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); let result = Contracts::bare_instantiate( - ALICE, + RuntimeOrigin::signed(ALICE), init_value, GAS_LIMIT, - None, + 1 * 1_000_000_000_000, Code::Upload(wasm_binary), function_selector("new"), - salt, + None, DEBUG_OUTPUT, CollectEvents::Skip, ) .result .unwrap(); assert!(!result.result.did_revert(), "deploying contract reverted {:?}", result); - result.account_id + (result.addr, pallet_revive::DefaultAddressMapper::to_account_id(&result.addr)) } diff --git a/runtime/devnet/Cargo.toml b/runtime/devnet/Cargo.toml index 2b4fe41c..300b1cc6 100644 --- a/runtime/devnet/Cargo.toml +++ b/runtime/devnet/Cargo.toml @@ -99,6 +99,9 @@ ismp-parachain-runtime-api.workspace = true pallet-ismp = { workspace = true, features = [ "unsigned" ] } pallet-ismp-runtime-api.workspace = true +# revive +pallet-revive.workspace = true + [dev-dependencies] enumflags2.workspace = true env_logger.workspace = true @@ -145,6 +148,7 @@ std = [ "pallet-nfts/std", "pallet-preimage/std", "pallet-proxy/std", + "pallet-revive/std", "pallet-scheduler/std", "pallet-session/std", "pallet-sudo/std", @@ -200,6 +204,7 @@ runtime-benchmarks = [ "pallet-nfts/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", + "pallet-revive/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", "pallet-sudo/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", @@ -239,6 +244,7 @@ try-runtime = [ "pallet-nfts/try-runtime", "pallet-preimage/try-runtime", "pallet-proxy/try-runtime", + "pallet-revive/try-runtime", "pallet-scheduler/try-runtime", "pallet-session/try-runtime", "pallet-sudo/try-runtime", @@ -251,6 +257,8 @@ try-runtime = [ "sp-runtime/try-runtime", ] +riscv = [ "pallet-revive/riscv" ] + # Enable the metadata hash generation. # # This is hidden behind a feature because it increases the compile time. diff --git a/runtime/devnet/src/config/api/mod.rs b/runtime/devnet/src/config/api/mod.rs index 035a6014..10f6b540 100644 --- a/runtime/devnet/src/config/api/mod.rs +++ b/runtime/devnet/src/config/api/mod.rs @@ -31,7 +31,7 @@ type DecodesAs = pallet_api::extension::DecodesAs< pub enum RuntimeRead { /// Fungible token queries. #[codec(index = 150)] - Fungibles(fungibles::Read), + FungiblesRevive(fungibles::Read), } impl Readable for RuntimeRead { @@ -42,14 +42,15 @@ impl Readable for RuntimeRead { /// performed. fn weight(&self) -> Weight { match self { - RuntimeRead::Fungibles(key) => fungibles::Pallet::weight(key), + RuntimeRead::FungiblesRevive(key) => fungibles::Pallet::weight(key), } } /// Performs the read and returns the result. fn read(self) -> Self::Result { match self { - RuntimeRead::Fungibles(key) => RuntimeResult::Fungibles(fungibles::Pallet::read(key)), + RuntimeRead::FungiblesRevive(key) => + RuntimeResult::Fungibles(fungibles::Pallet::read(key)), } } } @@ -153,7 +154,7 @@ impl Contains for Filter { use fungibles::Read::*; matches!( r, - RuntimeRead::Fungibles( + RuntimeRead::FungiblesRevive( TotalSupply(..) | BalanceOf { .. } | Allowance { .. } | diff --git a/runtime/devnet/src/config/api/versioning.rs b/runtime/devnet/src/config/api/versioning.rs index daa2a0a9..76e59e6a 100644 --- a/runtime/devnet/src/config/api/versioning.rs +++ b/runtime/devnet/src/config/api/versioning.rs @@ -54,7 +54,7 @@ impl TryFrom<(RuntimeResult, Version)> for VersionedRuntimeResult { match version { // Allows mapping from current `RuntimeResult` to a specific/prior version 0 => Ok(VersionedRuntimeResult::V0(result)), - _ => Err(pallet_contracts::Error::::DecodingFailed.into()), + _ => Err(pallet_revive::Error::::DecodingFailed.into()), } } } @@ -84,7 +84,7 @@ impl TryFrom<(DispatchError, Version)> for VersionedError { match version { // Allows mapping from current `DispatchError` to a specific/prior version of `Error` 0 => Ok(VersionedError::V0(V0Error::from(error).0)), - _ => Err(pallet_contracts::Error::::DecodingFailed.into()), + _ => Err(pallet_revive::Error::::DecodingFailed.into()), } } } @@ -119,7 +119,7 @@ impl From for V0Error { let ModuleError { index, error, message: _message } = error; // Map `pallet-contracts::Error::DecodingFailed` to `Error::DecodingFailed` if index as usize == - ::index() && + ::index() && error == DECODING_FAILED_ERROR { Error::DecodingFailed @@ -202,7 +202,7 @@ mod tests { RuntimeResult::Fungibles(fungibles::ReadResult::::TotalSupply(1_000)), 1 )), - Err(pallet_contracts::Error::::DecodingFailed.into()) + Err(pallet_revive::Error::::DecodingFailed.into()) ); } @@ -229,7 +229,7 @@ mod tests { // Unknown version 1. assert_eq!( VersionedError::try_from((BadOrigin, 1)), - Err(pallet_contracts::Error::::DecodingFailed.into()) + Err(pallet_revive::Error::::DecodingFailed.into()) ); } @@ -264,7 +264,7 @@ mod tests { Module(ModuleError { index: 1, error: [2, 2, 2, 4], message: Some("hallo") }), Error::Module { index: 1, error: [2, 2] }, ), - (pallet_contracts::Error::::DecodingFailed.into(), Error::DecodingFailed), + (pallet_revive::Error::::DecodingFailed.into(), Error::DecodingFailed), (ConsumerRemaining, Error::ConsumerRemaining), (NoProviders, Error::NoProviders), (TooManyConsumers, Error::TooManyConsumers), @@ -287,7 +287,7 @@ mod tests { #[test] fn decoding_failed_error_encoding_works() { - let Module(error) = pallet_contracts::Error::::DecodingFailed.into() else { + let Module(error) = pallet_revive::Error::::DecodingFailed.into() else { unreachable!() }; assert_eq!(error.error, DECODING_FAILED_ERROR) diff --git a/runtime/devnet/src/config/contracts.rs b/runtime/devnet/src/config/contracts.rs deleted file mode 100644 index b3701498..00000000 --- a/runtime/devnet/src/config/contracts.rs +++ /dev/null @@ -1,90 +0,0 @@ -use frame_support::{ - parameter_types, - traits::{ConstBool, ConstU32, Nothing, Randomness}, -}; -use frame_system::{pallet_prelude::BlockNumberFor, EnsureSigned}; - -use super::api::{self, Config}; -use crate::{ - deposit, Balance, Balances, Perbill, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, - Timestamp, -}; - -fn schedule() -> pallet_contracts::Schedule { - pallet_contracts::Schedule { - limits: pallet_contracts::Limits { - runtime_memory: 1024 * 1024 * 1024, - ..Default::default() - }, - ..Default::default() - } -} - -// randomness-collective-flip is insecure. Provide dummy randomness as placeholder for the -// deprecated trait. https://github.com/paritytech/polkadot-sdk/blob/9bf1a5e23884921498b381728bfddaae93f83744/substrate/frame/contracts/mock-network/src/parachain/contracts_config.rs#L45 -pub struct DummyRandomness(sp_std::marker::PhantomData); - -impl Randomness> for DummyRandomness { - fn random(_subject: &[u8]) -> (T::Hash, BlockNumberFor) { - (Default::default(), Default::default()) - } -} - -parameter_types! { - pub const DepositPerItem: Balance = deposit(1, 0); - pub const DepositPerByte: Balance = deposit(0, 1); - pub Schedule: pallet_contracts::Schedule = schedule::(); - pub const DefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); - pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); -} - -impl pallet_contracts::Config for Runtime { - type AddressGenerator = pallet_contracts::DefaultAddressGenerator; - type ApiVersion = (); - // IMPORTANT: only runtime calls through the api are allowed. - type CallFilter = Nothing; - type CallStack = [pallet_contracts::Frame; 23]; - type ChainExtension = api::Extension; - type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; - type Currency = Balances; - type Debug = (); - type DefaultDepositLimit = DefaultDepositLimit; - type DepositPerByte = DepositPerByte; - type DepositPerItem = DepositPerItem; - type Environment = (); - type InstantiateOrigin = EnsureSigned; - // This node is geared towards development and testing of contracts. - // We decided to increase the default allowed contract size for this - // reason (the default is `128 * 1024`). - // - // Our reasoning is that the error code `CodeTooLarge` is thrown - // if a too-large contract is uploaded. We noticed that it poses - // less friction during development when the requirement here is - // just more lax. - type MaxCodeLen = ConstU32<{ 256 * 1024 }>; - type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; - type MaxDelegateDependencies = ConstU32<32>; - type MaxStorageKeyLen = ConstU32<128>; - type Migrations = (); - type Randomness = DummyRandomness; - type RuntimeCall = RuntimeCall; - type RuntimeEvent = RuntimeEvent; - type RuntimeHoldReason = RuntimeHoldReason; - type Schedule = Schedule; - type Time = Timestamp; - type UnsafeUnstableInterface = ConstBool; - type UploadOrigin = EnsureSigned; - type WeightInfo = pallet_contracts::weights::SubstrateWeight; - type WeightPrice = pallet_transaction_payment::Pallet; - type Xcm = pallet_xcm::Pallet; -} - -// IMPORTANT: only runtime calls through the api are allowed. -#[test] -fn contracts_prevents_runtime_calls() { - use std::any::TypeId; - assert_eq!( - TypeId::of::<::CallFilter>(), - TypeId::of::() - ); -} diff --git a/runtime/devnet/src/config/mod.rs b/runtime/devnet/src/config/mod.rs index 61ae0827..7fb3abcb 100644 --- a/runtime/devnet/src/config/mod.rs +++ b/runtime/devnet/src/config/mod.rs @@ -1,8 +1,9 @@ mod api; // Public due to pop api integration tests crate. pub mod assets; -mod contracts; mod ismp; mod proxy; // Public due to integration tests crate. pub mod xcm; + +mod revive; diff --git a/runtime/devnet/src/config/revive.rs b/runtime/devnet/src/config/revive.rs new file mode 100644 index 00000000..3a89d785 --- /dev/null +++ b/runtime/devnet/src/config/revive.rs @@ -0,0 +1,51 @@ +use frame_support::{ + parameter_types, + traits::{ConstBool, ConstU32, ConstU64, Nothing}, +}; +use frame_system::EnsureSigned; +use pallet_api::Extension; +use pallet_revive::{ + chain_extension::{ + ChainExtension, Environment, Ext, RegisteredChainExtension, Result as ExtensionResult, + RetVal, ReturnFlags, + }, + wasm::Memory, +}; +use sp_std::vec; + +use super::api::{self, Config}; +use crate::{ + deposit, Balance, Balances, Perbill, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, + Timestamp, +}; + +parameter_types! { + pub const DepositPerItem: Balance = deposit(1, 0); + pub const DepositPerByte: Balance = deposit(0, 1); + pub const DefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); + pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); +} + +impl pallet_revive::Config for Runtime { + type AddressMapper = pallet_revive::DefaultAddressMapper; + type CallFilter = Nothing; + type ChainExtension = api::Extension; + type ChainId = ConstU64<4001>; + type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; + type Currency = Balances; + type Debug = (); + type DepositPerByte = DepositPerByte; + type DepositPerItem = DepositPerItem; + type InstantiateOrigin = EnsureSigned; + type PVFMemory = ConstU32<{ 512 * 1024 * 1024 }>; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeMemory = ConstU32<{ 128 * 1024 * 1024 }>; + type Time = Timestamp; + type UnsafeUnstableInterface = ConstBool; + type UploadOrigin = EnsureSigned; + type WeightInfo = pallet_revive::weights::SubstrateWeight; + type WeightPrice = pallet_transaction_payment::Pallet; + type Xcm = (); +} diff --git a/runtime/devnet/src/lib.rs b/runtime/devnet/src/lib.rs index 16f168fa..c25213cb 100644 --- a/runtime/devnet/src/lib.rs +++ b/runtime/devnet/src/lib.rs @@ -55,7 +55,7 @@ pub use pop_runtime_common::{ }; use smallvec::smallvec; use sp_api::impl_runtime_apis; -use sp_core::{crypto::KeyTypeId, Get, OpaqueMetadata, H256}; +use sp_core::{crypto::KeyTypeId, Get, OpaqueMetadata, H160, H256}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; use sp_runtime::{ @@ -610,8 +610,8 @@ mod runtime { pub type IsmpParachain = ismp_parachain::Pallet; // Contracts - #[runtime::pallet_index(40)] - pub type Contracts = pallet_contracts::Pallet; + // #[runtime::pallet_index(40)] + // pub type Contracts = pallet_contracts::Pallet; // Proxy #[runtime::pallet_index(41)] @@ -634,6 +634,10 @@ mod runtime { // Pop API #[runtime::pallet_index(150)] pub type Fungibles = fungibles::Pallet; + + // Revive + #[runtime::pallet_index(255)] + pub type Revive = pallet_revive::Pallet; } #[cfg(feature = "runtime-benchmarks")] @@ -791,28 +795,26 @@ impl_runtime_apis! { } } - impl pallet_contracts::ContractsApi - for Runtime + + impl pallet_revive::ReviveApi for Runtime { fn call( origin: AccountId, - dest: AccountId, + dest: H160, value: Balance, gas_limit: Option, storage_deposit_limit: Option, input_data: Vec, - ) -> pallet_contracts::ContractExecResult { - let gas_limit = gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block); - Contracts::bare_call( - origin, + ) -> pallet_revive::ContractExecResult { + Revive::bare_call( + RuntimeOrigin::signed(origin), dest, value, - gas_limit, - storage_deposit_limit, + gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block), + storage_deposit_limit.unwrap_or(u128::MAX), input_data, - CONTRACTS_DEBUG_OUTPUT, - CONTRACTS_EVENTS, - pallet_contracts::Determinism::Enforced, + pallet_revive::DebugInfo::UnsafeDebug, + pallet_revive::CollectEvents::UnsafeCollect, ) } @@ -821,22 +823,21 @@ impl_runtime_apis! { value: Balance, gas_limit: Option, storage_deposit_limit: Option, - code: pallet_contracts::Code, + code: pallet_revive::Code, data: Vec, - salt: Vec, - ) -> pallet_contracts::ContractInstantiateResult + salt: Option<[u8; 32]>, + ) -> pallet_revive::ContractInstantiateResult { - let gas_limit = gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block); - Contracts::bare_instantiate( - origin, + Revive::bare_instantiate( + RuntimeOrigin::signed(origin), value, - gas_limit, - storage_deposit_limit, + gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block), + storage_deposit_limit.unwrap_or(u128::MAX), code, data, salt, - CONTRACTS_DEBUG_OUTPUT, - CONTRACTS_EVENTS, + pallet_revive::DebugInfo::UnsafeDebug, + pallet_revive::CollectEvents::UnsafeCollect, ) } @@ -844,17 +845,23 @@ impl_runtime_apis! { origin: AccountId, code: Vec, storage_deposit_limit: Option, - determinism: pallet_contracts::Determinism, - ) -> pallet_contracts::CodeUploadResult + ) -> pallet_revive::CodeUploadResult { - Contracts::bare_upload_code(origin, code, storage_deposit_limit, determinism) + Revive::bare_upload_code( + RuntimeOrigin::signed(origin), + code, + storage_deposit_limit.unwrap_or(u128::MAX), + ) } fn get_storage( - address: AccountId, - key: Vec, - ) -> pallet_contracts::GetStorageResult { - Contracts::get_storage(address, key) + address: H160, + key: [u8; 32], + ) -> pallet_revive::GetStorageResult { + Revive::get_storage( + address, + key + ) } } From 30db9e3bab3683876373bfa5d720e8ddd353f8b2 Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:02:12 +0100 Subject: [PATCH 2/8] chore(devnet): enable trace logging for revive (#349) --- networks/devnet.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/networks/devnet.toml b/networks/devnet.toml index 957a4716..4b630ec1 100644 --- a/networks/devnet.toml +++ b/networks/devnet.toml @@ -36,7 +36,13 @@ balances = [ [[parachains.collators]] name = "pop" rpc_port = 9944 -args = ["-lruntime::contracts=debug", "-lpopapi::extension=debug", "--enable-offchain-indexing=true"] +args = [ + "-lpop-api::extension=debug", + "-lruntime::contracts=debug", + "-lruntime::revive=trace", + "-lruntime::revive::strace=trace", + "--enable-offchain-indexing=true" +] [[parachains]] id = 1000 From 78dae751ed91bacb3a718171d3717a0bfecace5a Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:28:26 +0100 Subject: [PATCH 3/8] feat(revive): stateful mapping between h160 -> h256 (#355) Co-authored-by: Peter White --- extension/src/environment.rs | 28 +- extension/src/lib.rs | 5 +- pallets/revive/src/address.rs | 286 ++++++++++++++++-- pallets/revive/src/exec.rs | 23 +- pallets/revive/src/lib.rs | 77 ++++- pallets/revive/src/tests.rs | 8 +- pallets/revive/src/wasm/runtime.rs | 21 ++ pallets/revive/src/weights.rs | 79 +++++ pallets/revive/uapi/src/host.rs | 3 + pallets/revive/uapi/src/host/riscv32.rs | 6 + .../integration-tests/src/fungibles/utils.rs | 11 +- pop-api/integration-tests/src/lib.rs | 14 +- runtime/devnet/src/config/revive.rs | 2 +- 13 files changed, 505 insertions(+), 58 deletions(-) diff --git a/extension/src/environment.rs b/extension/src/environment.rs index 015e78c0..6da88cdd 100644 --- a/extension/src/environment.rs +++ b/extension/src/environment.rs @@ -5,11 +5,12 @@ use frame_support::pallet_prelude::Weight; use pallet_revive::{ chain_extension::{ChargedAmount, Result}, wasm::Memory, - AddressMapper, DefaultAddressMapper, + AccountId32Mapper, AddressMapper, }; +use sp_core::{crypto::AccountId32, H160}; use sp_std::vec::Vec; -use crate::AccountIdOf; +use crate::{AccountIdOf, Config}; /// Provides access to the parameters passed to a chain extension and its execution environment. /// @@ -67,6 +68,11 @@ pub(crate) struct Env<'a, 'b, E: pallet_revive::chain_extension::Ext, S: ?Sized impl<'a, 'b, E: pallet_revive::chain_extension::Ext, S: ?Sized + Memory> Environment for Env<'a, 'b, E, S> +where + E: pallet_revive::chain_extension::Ext, + E::T: frame_system::Config, + ::AccountId: From + Decode, + E::T: SysConfig, { type AccountId = AccountIdOf; type ChargedAmount = ChargedAmount; @@ -189,13 +195,25 @@ impl Ext for () { /// A wrapper type for a type implementing `pallet_revive::chain_extension::Ext`. pub(crate) struct ExternalEnvironment<'a, T: pallet_revive::chain_extension::Ext>(&'a mut T); -impl<'a, E: pallet_revive::chain_extension::Ext> Ext for ExternalEnvironment<'a, E> { +use pallet_revive::chain_extension::SysConfig; + +impl<'a, E> Ext for ExternalEnvironment<'a, E> +where + E: pallet_revive::chain_extension::Ext, + E::T: frame_system::Config, + ::AccountId: From + Decode, + E::T: SysConfig, +{ type AccountId = AccountIdOf; fn address(&self) -> Self::AccountId { + // Get the H160 address + let h160_addr = self.0.address(); + let account_id = AccountId32Mapper::::to_account_id(&h160_addr); // TODO: hacky way to get AccountId32 to match the Self::AccountId type - let encoded_addr = DefaultAddressMapper::to_account_id(&self.0.address()).encode(); - Self::AccountId::decode(&mut &encoded_addr[..]).unwrap() + // let encoded_addr = AccountId32Mapper::::to_account_id(&self.0.address()).encode(); + // Self::AccountId::decode(&mut &encoded_addr[..]).unwrap() + account_id.into() } } diff --git a/extension/src/lib.rs b/extension/src/lib.rs index 0da73fc8..bf966032 100644 --- a/extension/src/lib.rs +++ b/extension/src/lib.rs @@ -15,14 +15,14 @@ pub use functions::{ }; pub use matching::{Equals, FunctionId, Matches}; use pallet_revive::{ - chain_extension::{ChainExtension, RetVal::Converging}, + chain_extension::{ChainExtension, RetVal::Converging, SysConfig}, WeightInfo, }; pub use pallet_revive::{ chain_extension::{Result, RetVal}, wasm::Memory, }; -use sp_core::Get; +use sp_core::{crypto::AccountId32, Get}; use sp_runtime::{traits::Dispatchable, DispatchError}; use sp_std::vec::Vec; @@ -51,6 +51,7 @@ where RuntimeCall: GetDispatchInfo + Dispatchable, >, Config: self::Config> + 'static, + Runtime: SysConfig, { /// Call the chain extension logic. /// diff --git a/pallets/revive/src/address.rs b/pallets/revive/src/address.rs index 928896e8..4f9e2b53 100644 --- a/pallets/revive/src/address.rs +++ b/pallets/revive/src/address.rs @@ -18,61 +18,173 @@ //! Functions that deal contract addresses. use alloc::vec::Vec; +use core::marker::PhantomData; -use sp_core::H160; +use frame_support::traits::{fungible::MutateHold, tokens::Precision}; +use sp_core::{Get, H160}; use sp_io::hashing::keccak_256; -use sp_runtime::AccountId32; +use sp_runtime::{AccountId32, DispatchResult, SaturatedConversion, Saturating}; + +use crate::{ensure, AddressSuffix, Config, Error, HoldReason}; /// Map between the native chain account id `T` and an Ethereum [`H160`]. /// /// This trait exists only to emulate specialization for different concrete /// native account ids. **Not** to make the mapping user configurable. Hence -/// the trait is `Sealed` and only one mandatory implementor [`DefaultAddressMapper`] -/// exists. +/// the trait is `Sealed` and depending on your runtime configuration you need +/// to pick either [`AccountId32Mapper`] or [`H160Mapper`]. Picking the wrong +/// one will result in a compilation error. No footguns here. /// /// Please note that we assume that the native account is at least 20 bytes and /// only implement this type for a `T` where this is the case. Luckily, this is the -/// case for all existing runtimes as of right now. Reasing is that this will allow +/// case for all existing runtimes as of right now. Reasoning is that this will allow /// us to reverse an address -> account_id mapping by just stripping the prefix. -pub trait AddressMapper: private::Sealed { +/// +/// We require the mapping to be reversible. Since we are potentially dealing with types of +/// different sizes one direction of the mapping is necessarily lossy. This requires the mapping to +/// make use of the [`AddressSuffix`] storage item to reverse the mapping. +pub trait AddressMapper: private::Sealed { /// Convert an account id to an ethereum adress. - /// - /// This mapping is **not** required to be reversible. - fn to_address(account_id: &T) -> H160; + fn to_address(account_id: &T::AccountId) -> H160; /// Convert an ethereum address to a native account id. + fn to_account_id(address: &H160) -> T::AccountId; + + /// Same as [`Self::to_account_id`] but always returns the fallback account. + /// + /// This skips the query into [`AddressSuffix`] and always returns the stateless + /// fallback account. This is useful when we know for a fact that the `address` + /// in question is originally a `H160`. This is usually only the case when we + /// generated a new contract address. + fn to_fallback_account_id(address: &H160) -> T::AccountId; + + /// Create a stateful mapping for `account_id` + /// + /// This will enable `to_account_id` to map back to the original + /// `account_id` instead of the fallback account id. + fn map(account_id: &T::AccountId) -> DispatchResult; + + /// Remove the mapping in order to reclaim the deposit. /// - /// This mapping is **required** to be reversible. - fn to_account_id(address: &H160) -> T; + /// There is no reason why one would unmap their `account_id` except + /// for reclaiming the deposit. + fn unmap(account_id: &T::AccountId) -> DispatchResult; - /// Same as [`Self::to_account_id`] but when we know the address is a contract. + /// Returns true if the `account_id` is useable as an origin. /// - /// This is only the case when we just generated the new address. - fn to_account_id_contract(address: &H160) -> T; + /// This means either the `account_id` doesn't require a stateful mapping + /// or a stateful mapping exists. + fn is_mapped(account_id: &T::AccountId) -> bool; } mod private { pub trait Sealed {} - impl Sealed for super::DefaultAddressMapper {} + impl Sealed for super::AccountId32Mapper {} + impl Sealed for super::H160Mapper {} } -/// The only implementor for `AddressMapper`. -pub enum DefaultAddressMapper {} +/// The mapper to be used if the account id is `AccountId32`. +/// +/// It converts between addresses by either truncating the last 12 bytes or +/// suffixing them. The suffix is queried from [`AddressSuffix`] and will fall +/// back to all `0xEE` if no suffix was registered. This means contracts and +/// plain wallets controlled by an `secp256k1` always have a `0xEE` suffixed +/// account. +pub struct AccountId32Mapper(PhantomData); + +/// The mapper to be used if the account id is `H160`. +/// +/// It just trivially returns its inputs and doesn't make use of any state. +pub struct H160Mapper(PhantomData); -impl AddressMapper for DefaultAddressMapper { +impl AddressMapper for AccountId32Mapper +where + T: Config, +{ fn to_address(account_id: &AccountId32) -> H160 { H160::from_slice(&>::as_ref(&account_id)[..20]) } fn to_account_id(address: &H160) -> AccountId32 { + if let Some(suffix) = >::get(address) { + let mut account_id = Self::to_fallback_account_id(address); + let account_bytes: &mut [u8; 32] = account_id.as_mut(); + account_bytes[20..].copy_from_slice(suffix.as_slice()); + account_id + } else { + Self::to_fallback_account_id(address) + } + } + + fn to_fallback_account_id(address: &H160) -> AccountId32 { let mut account_id = AccountId32::new([0xEE; 32]); - >::as_mut(&mut account_id)[..20] - .copy_from_slice(address.as_bytes()); + let account_bytes: &mut [u8; 32] = account_id.as_mut(); + account_bytes[..20].copy_from_slice(address.as_bytes()); account_id } - fn to_account_id_contract(address: &H160) -> AccountId32 { - Self::to_account_id(address) + fn map(account_id: &T::AccountId) -> DispatchResult { + ensure!(!Self::is_mapped(account_id), >::AccountAlreadyMapped); + + let account_bytes: &[u8; 32] = account_id.as_ref(); + + // each mapping entry stores one AccountId32 distributed between key and value + let deposit = T::DepositPerByte::get() + .saturating_mul(account_bytes.len().saturated_into()) + .saturating_add(T::DepositPerItem::get()); + + let suffix: [u8; 12] = account_bytes[20..] + .try_into() + .expect("Skipping 20 byte of a an 32 byte array will fit into 12 bytes; qed"); + T::Currency::hold(&HoldReason::AddressMapping.into(), account_id, deposit)?; + >::insert(Self::to_address(account_id), suffix); + Ok(()) + } + + fn unmap(account_id: &T::AccountId) -> DispatchResult { + // will do nothing if address is not mapped so no check required + >::remove(Self::to_address(account_id)); + T::Currency::release_all( + &HoldReason::AddressMapping.into(), + account_id, + Precision::BestEffort, + )?; + Ok(()) + } + + fn is_mapped(account_id: &T::AccountId) -> bool { + let account_bytes: &[u8; 32] = account_id.as_ref(); + &account_bytes[20..] == &[0xEE; 12] || + >::contains_key(Self::to_address(account_id)) + } +} + +impl AddressMapper for H160Mapper +where + T: Config, +{ + fn to_address(account_id: &H160) -> H160 { + *account_id + } + + fn to_account_id(address: &H160) -> H160 { + *address + } + + fn to_fallback_account_id(address: &H160) -> H160 { + *address + } + + fn map(_account_id: &T::AccountId) -> DispatchResult { + Ok(()) + } + + fn unmap(_account_id: &T::AccountId) -> DispatchResult { + Ok(()) + } + + fn is_mapped(_account_id: &T::AccountId) -> bool { + true } } @@ -102,10 +214,19 @@ pub fn create2(deployer: &H160, code: &[u8], input_data: &[u8], salt: &[u8; 32]) #[cfg(test)] mod test { + use frame_support::{ + assert_err, + traits::fungible::{InspectHold, Mutate}, + }; + use pretty_assertions::assert_eq; use sp_core::{hex2array, H160}; use super::*; - use crate::test_utils::ALICE_ADDR; + use crate::{ + test_utils::*, + tests::{ExtBuilder, Test}, + AddressMapper, Error, + }; #[test] fn create1_works() { @@ -127,4 +248,123 @@ mod test { H160(hex2array!("7f31e795e5836a19a8f919ab5a9de9a197ecd2b6")), ) } + + #[test] + fn fallback_map_works() { + assert!(::AddressMapper::is_mapped(&ALICE)); + assert_eq!( + ALICE_FALLBACK, + ::AddressMapper::to_fallback_account_id(&ALICE_ADDR) + ); + assert_eq!(ALICE_ADDR, ::AddressMapper::to_address(&ALICE_FALLBACK)); + } + + #[test] + fn map_works() { + ExtBuilder::default().build().execute_with(|| { + ::Currency::set_balance(&EVE, 1_000_000); + // before mapping the fallback account is returned + assert!(!::AddressMapper::is_mapped(&EVE)); + assert_eq!(EVE_FALLBACK, ::AddressMapper::to_account_id(&EVE_ADDR)); + assert_eq!( + ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &EVE + ), + 0 + ); + + // when mapped the full account id is returned + ::AddressMapper::map(&EVE).unwrap(); + assert!(::AddressMapper::is_mapped(&EVE)); + assert_eq!(EVE, ::AddressMapper::to_account_id(&EVE_ADDR)); + assert!( + ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &EVE + ) > 0 + ); + }); + } + + #[test] + fn map_fallback_account_fails() { + ExtBuilder::default().build().execute_with(|| { + assert!(::AddressMapper::is_mapped(&ALICE)); + // alice is an e suffixed account and hence cannot be mapped + assert_err!( + ::AddressMapper::map(&ALICE), + >::AccountAlreadyMapped, + ); + assert_eq!( + ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &ALICE + ), + 0 + ); + }); + } + + #[test] + fn double_map_fails() { + ExtBuilder::default().build().execute_with(|| { + assert!(!::AddressMapper::is_mapped(&EVE)); + ::Currency::set_balance(&EVE, 1_000_000); + ::AddressMapper::map(&EVE).unwrap(); + assert!(::AddressMapper::is_mapped(&EVE)); + let deposit = ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &EVE, + ); + assert_err!( + ::AddressMapper::map(&EVE), + >::AccountAlreadyMapped, + ); + assert!(::AddressMapper::is_mapped(&EVE)); + assert_eq!( + ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &EVE + ), + deposit + ); + }); + } + + #[test] + fn unmap_works() { + ExtBuilder::default().build().execute_with(|| { + ::Currency::set_balance(&EVE, 1_000_000); + ::AddressMapper::map(&EVE).unwrap(); + assert!(::AddressMapper::is_mapped(&EVE)); + assert!( + ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &EVE + ) > 0 + ); + + ::AddressMapper::unmap(&EVE).unwrap(); + assert!(!::AddressMapper::is_mapped(&EVE)); + assert_eq!( + ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &EVE + ), + 0 + ); + + // another unmap is a noop + ::AddressMapper::unmap(&EVE).unwrap(); + assert!(!::AddressMapper::is_mapped(&EVE)); + assert_eq!( + ::Currency::balance_on_hold( + &HoldReason::AddressMapping.into(), + &EVE + ), + 0 + ); + }); + } } diff --git a/pallets/revive/src/exec.rs b/pallets/revive/src/exec.rs index 4d57422b..b612be2a 100644 --- a/pallets/revive/src/exec.rs +++ b/pallets/revive/src/exec.rs @@ -169,6 +169,18 @@ impl Origin { Origin::Root => Err(DispatchError::RootNotAllowed), } } + + /// Make sure that this origin is mapped. + /// + /// We require an origin to be mapped in order to be used in a `Stack`. Otherwise + /// [`Stack::caller`] returns an address that can't be reverted to the original address. + fn ensure_mapped(&self) -> DispatchResult { + match self { + Self::Root => Ok(()), + Self::Signed(account_id) if T::AddressMapper::is_mapped(account_id) => Ok(()), + Self::Signed(_) => Err(>::AccountUnmapped.into()), + } + } } /// An interface that provides access to the external environment in which the @@ -823,6 +835,7 @@ where value: BalanceOf, debug_message: Option<&'a mut DebugBuffer>, ) -> Result, ExecError> { + origin.ensure_mapped()?; let Some((first_frame, executable)) = Self::new_frame( args, value, @@ -903,7 +916,7 @@ where *executable.code_hash(), )?; ( - T::AddressMapper::to_account_id_contract(&address), + T::AddressMapper::to_fallback_account_id(&address), contract, executable, None, @@ -1317,14 +1330,18 @@ where // is caught by it. self.top_frame_mut().allows_reentry = allows_reentry; - let dest = T::AddressMapper::to_account_id(dest); - let value = value.try_into().map_err(|_| Error::::BalanceConversionFailed)?; + // We reset the return data now, so it is cleared out even if no new frame was executed. + // This is for example the case for balance transfers or when creating the frame fails. + *self.last_frame_output_mut() = Default::default(); let try_call = || { + let dest = T::AddressMapper::to_account_id(dest); if !self.allows_reentry(&dest) { return Err(>::ReentranceDenied.into()); } + let value = value.try_into().map_err(|_| Error::::BalanceConversionFailed)?; + // We ignore instantiate frames in our search for a cached contract. // Otherwise it would be possible to recursively call a contract from its own // constructor: We disallow calling not fully constructed contracts. diff --git a/pallets/revive/src/lib.rs b/pallets/revive/src/lib.rs index 3b764ed9..660e9c4c 100644 --- a/pallets/revive/src/lib.rs +++ b/pallets/revive/src/lib.rs @@ -76,7 +76,7 @@ pub use weights::WeightInfo; #[cfg(doc)] pub use crate::wasm::SyscallDoc; pub use crate::{ - address::{AddressMapper, DefaultAddressMapper}, + address::{create1, create2, AccountId32Mapper, AddressMapper}, debug::Tracing, pallet::*, }; @@ -157,10 +157,9 @@ pub mod pallet { /// The overarching call type. #[pallet::no_default_bounds] - type RuntimeCall: Dispatchable - + GetDispatchInfo - + codec::Decode - + IsType<::RuntimeCall>; + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo; /// Overarching hold reason. #[pallet::no_default_bounds] @@ -229,9 +228,9 @@ pub mod pallet { #[pallet::constant] type CodeHashLockupDepositPercent: Get; - /// Only valid type is [`DefaultAddressMapper`]. - #[pallet::no_default_bounds] - type AddressMapper: AddressMapper>; + /// Use either valid type is [`address::AccountId32Mapper`] or [`address::H160Mapper`]. + #[pallet::no_default] + type AddressMapper: AddressMapper; /// Make contract callable functions marked as `#[unstable]` available. /// @@ -354,7 +353,6 @@ pub mod pallet { #[frame_support::register_default_impl(TestDefaultConfig)] impl DefaultConfig for TestDefaultConfig { - type AddressMapper = DefaultAddressMapper; type CallFilter = (); type ChainExtension = (); type ChainId = ConstU64<{ 0 }>; @@ -554,6 +552,12 @@ pub mod pallet { ExecutionFailed, /// Failed to convert a U256 to a Balance. BalanceConversionFailed, + /// An `AccountID32` account tried to interact with the pallet without having a mapping. + /// + /// Call [`Pallet::map_account`] in order to create a mapping for the account. + AccountUnmapped, + /// Tried to map an account that is already mapped. + AccountAlreadyMapped, } /// A reason for the pallet contracts placing a hold on funds. @@ -563,6 +567,8 @@ pub mod pallet { CodeUploadDepositReserve, /// The Pallet has reserved it for storage deposit. StorageDepositReserve, + /// Deposit for creating an address mapping in [`AddressSuffix`]. + AddressMapping, } /// A mapping from a contract's code hash to its code. @@ -590,6 +596,14 @@ pub mod pallet { pub(crate) type DeletionQueueCounter = StorageValue<_, DeletionQueueManager, ValueQuery>; + /// Map a Ethereum address to its original `AccountId32`. + /// + /// Stores the last 12 byte for addresses that were originally an `AccountId32` instead + /// of an `H160`. Register your `AccountId32` using [`Pallet::map_account`] in order to + /// use it with this pallet. + #[pallet::storage] + pub(crate) type AddressSuffix = StorageMap<_, Identity, H160, [u8; 12]>; + #[pallet::extra_constants] impl Pallet { #[pallet::constant_name(ApiVersion)] @@ -944,6 +958,51 @@ pub mod pallet { Ok(()) }) } + + /// Register the callers account id so that it can be used in contract interactions. + /// + /// This will error if the origin is already mapped or is a eth native `Address20`. It will + /// take a deposit that can be released by calling [`Self::unmap_account`]. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::map_account())] + pub fn map_account(origin: OriginFor) -> DispatchResult { + let origin = ensure_signed(origin)?; + T::AddressMapper::map(&origin) + } + + /// Unregister the callers account id in order to free the deposit. + /// + /// There is no reason to ever call this function other than freeing up the deposit. + /// This is only useful when the account should no longer be used. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::unmap_account())] + pub fn unmap_account(origin: OriginFor) -> DispatchResult { + let origin = ensure_signed(origin)?; + T::AddressMapper::unmap(&origin) + } + + /// Dispatch an `call` with the origin set to the callers fallback address. + /// + /// Every `AccountId32` can control its corresponding fallback account. The fallback account + /// is the `AccountId20` with the last 12 bytes set to `0xEE`. This is essentially a + /// recovery function in case an `AccountId20` was used without creating a mapping first. + #[pallet::call_index(9)] + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + ( + T::WeightInfo::dispatch_as_fallback_account().saturating_add(dispatch_info.weight), + dispatch_info.class + ) + })] + pub fn dispatch_as_fallback_account( + origin: OriginFor, + call: alloc::boxed::Box<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + let origin = ensure_signed(origin)?; + let unmapped_account = + T::AddressMapper::to_fallback_account_id(&T::AddressMapper::to_address(&origin)); + call.dispatch(RawOrigin::Signed(unmapped_account).into()) + } } } diff --git a/pallets/revive/src/tests.rs b/pallets/revive/src/tests.rs index 5385a5d4..7431dcdd 100644 --- a/pallets/revive/src/tests.rs +++ b/pallets/revive/src/tests.rs @@ -65,9 +65,9 @@ use crate::{ tests::test_utils::{get_contract, get_contract_checked}, wasm::Memory, weights::WeightInfo, - BalanceOf, Code, CodeInfoOf, CollectEvents, Config, ContractInfo, ContractInfoOf, DebugInfo, - DefaultAddressMapper, DeletionQueueCounter, Error, HoldReason, Origin, Pallet, PristineCode, - H160, + AccountId32Mapper, BalanceOf, Code, CodeInfoOf, CollectEvents, Config, ContractInfo, + ContractInfoOf, DebugInfo, DefaultAddressMapper, DeletionQueueCounter, Error, HoldReason, + Origin, Pallet, PristineCode, H160, }; type Block = frame_system::mocking::MockBlock; @@ -485,7 +485,7 @@ parameter_types! { #[derive_impl(crate::config_preludes::TestDefaultConfig)] impl Config for Test { - type AddressMapper = DefaultAddressMapper; + type AddressMapper = AccountId32Mapper; type CallFilter = TestFilter; type ChainExtension = (TestExtension, DisabledExtension, RevertingExtension, TempStorageExtension); diff --git a/pallets/revive/src/wasm/runtime.rs b/pallets/revive/src/wasm/runtime.rs index 6d6fef37..b006214e 100644 --- a/pallets/revive/src/wasm/runtime.rs +++ b/pallets/revive/src/wasm/runtime.rs @@ -2041,4 +2041,25 @@ pub mod env { *self.ext.last_frame_output_mut() = output; Ok(result?) } + + #[api_version(0)] + pub fn to_account_id( + &mut self, + memory: &mut M, + addr_ptr: u32, + out_ptr: u32, + ) -> Result<(), TrapReason> { + // TODO: add specific runtime cost + self.charge_gas(RuntimeCosts::Caller)?; + let mut address = H160::zero(); + memory.read_into_buf(addr_ptr, address.as_bytes_mut())?; + let account = ::AddressMapper::to_account_id(&address); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + account.encode().as_ref(), + false, + already_charged, + )?) + } } diff --git a/pallets/revive/src/weights.rs b/pallets/revive/src/weights.rs index b66c28bd..f093591b 100644 --- a/pallets/revive/src/weights.rs +++ b/pallets/revive/src/weights.rs @@ -59,6 +59,9 @@ pub trait WeightInfo { fn upload_code_determinism_relaxed(c: u32, ) -> Weight; fn remove_code() -> Weight; fn set_code() -> Weight; + fn map_account() -> Weight; + fn unmap_account() -> Weight; + fn dispatch_as_fallback_account() -> Weight; fn noop_host_fn(r: u32, ) -> Weight; fn seal_caller() -> Weight; fn seal_is_contract() -> Weight; @@ -331,6 +334,44 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// Storage: `Revive::AddressSuffix` (r:1 w:1) + /// Proof: `Revive::AddressSuffix` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `Measured`) + fn map_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `109` + // Estimated: `3574` + // Minimum execution time: 44_031_000 picoseconds. + Weight::from_parts(45_133_000, 3574) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `Measured`) + /// Storage: `Revive::AddressSuffix` (r:0 w:1) + /// Proof: `Revive::AddressSuffix` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `Measured`) + fn unmap_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `56` + // Estimated: `3521` + // Minimum execution time: 35_681_000 picoseconds. + Weight::from_parts(36_331_000, 3521) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `TxPause::PausedCalls` (r:1 w:0) + /// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `Measured`) + fn dispatch_as_fallback_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `145` + // Estimated: `3610` + // Minimum execution time: 11_550_000 picoseconds. + Weight::from_parts(12_114_000, 3610) + .saturating_add(T::DbWeight::get().reads(2_u64)) + } /// The range of component `r` is `[0, 1600]`. fn noop_host_fn(r: u32, ) -> Weight { // Proof Size summary in bytes: @@ -1155,6 +1196,44 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + /// Storage: `Revive::AddressSuffix` (r:1 w:1) + /// Proof: `Revive::AddressSuffix` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `Measured`) + fn map_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `109` + // Estimated: `3574` + // Minimum execution time: 44_031_000 picoseconds. + Weight::from_parts(45_133_000, 3574) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `Measured`) + /// Storage: `Revive::AddressSuffix` (r:0 w:1) + /// Proof: `Revive::AddressSuffix` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `Measured`) + fn unmap_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `56` + // Estimated: `3521` + // Minimum execution time: 35_681_000 picoseconds. + Weight::from_parts(36_331_000, 3521) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `TxPause::PausedCalls` (r:1 w:0) + /// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `Measured`) + fn dispatch_as_fallback_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `145` + // Estimated: `3610` + // Minimum execution time: 11_550_000 picoseconds. + Weight::from_parts(12_114_000, 3610) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + } /// The range of component `r` is `[0, 1600]`. fn noop_host_fn(r: u32, ) -> Weight { // Proof Size summary in bytes: diff --git a/pallets/revive/uapi/src/host.rs b/pallets/revive/uapi/src/host.rs index eca006c3..e3af1a72 100644 --- a/pallets/revive/uapi/src/host.rs +++ b/pallets/revive/uapi/src/host.rs @@ -635,6 +635,9 @@ pub trait HostFn: private::Sealed { /// - `output`: A reference to the output buffer to write the data. /// - `offset`: Byte offset into the returned data fn return_data_copy(output: &mut &mut [u8], offset: u32); + + /// Convert an ethereum address to a native account id. + fn to_account_id(addr: &[u8; 20], output: &mut [u8; 32]); } mod private { diff --git a/pallets/revive/uapi/src/host/riscv32.rs b/pallets/revive/uapi/src/host/riscv32.rs index 3bcd6a7d..8b9cab8a 100644 --- a/pallets/revive/uapi/src/host/riscv32.rs +++ b/pallets/revive/uapi/src/host/riscv32.rs @@ -133,6 +133,7 @@ mod sys { ) -> ReturnCode; pub fn return_data_size(out_ptr: *mut u8); pub fn return_data_copy(out_ptr: *mut u8, out_len_ptr: *mut u32, offset: u32); + pub fn to_account_id(addr_ptr: *const u8, out_ptr: *mut u8); } } @@ -565,4 +566,9 @@ impl HostFn for HostFnImpl { } extract_from_slice(output, output_len as usize); } + + /// Convert an ethereum address to a native account id. + fn to_account_id(addr: &[u8; 20], output: &mut [u8; 32]) { + unsafe { sys::to_account_id(addr.as_ptr(), output.as_mut_ptr()) }; + } } diff --git a/pop-api/integration-tests/src/fungibles/utils.rs b/pop-api/integration-tests/src/fungibles/utils.rs index 551bcf06..953c2044 100644 --- a/pop-api/integration-tests/src/fungibles/utils.rs +++ b/pop-api/integration-tests/src/fungibles/utils.rs @@ -1,7 +1,7 @@ use super::*; use crate::sp_core::H160; +use pallet_revive::AccountId32Mapper; use pallet_revive::AddressMapper; -use pallet_revive::DefaultAddressMapper; fn do_bare_call(function: &str, addr: &H160, params: Vec) -> ExecReturnValue { let function = function_selector(function); @@ -49,10 +49,7 @@ pub(super) fn token_name(addr: &H160, token_id: TokenId) -> Result Result>, Error> { +pub(super) fn token_symbol(addr: &H160, token_id: TokenId) -> Result>, Error> { let result = do_bare_call("token_symbol", addr, token_id.encode()); decoded::>, Error>>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) @@ -324,7 +321,7 @@ pub(super) fn instantiate_and_create_fungible( let function = function_selector("new"); let input = [function, token_id.encode(), min_balance.encode()].concat(); let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); - let result = Contracts::bare_instantiate( + let result = Revive::bare_instantiate( RuntimeOrigin::signed(ALICE), INIT_VALUE, GAS_LIMIT, @@ -341,7 +338,7 @@ pub(super) fn instantiate_and_create_fungible( let result = result.result; decoded::>(result.clone()) .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) - .map(|_| DefaultAddressMapper::to_account_id(&address)) + .map(|_| AccountId32Mapper::::to_account_id(&address)) } /// Get the last event from pallet contracts. diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index fd029c93..e5728b15 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -9,7 +9,7 @@ use frame_support::{ }; use pallet_revive::AddressMapper; use pallet_revive::{Code, CollectEvents, ExecReturnValue}; -use pop_runtime_devnet::{Assets, Revive as Contracts, Runtime, RuntimeOrigin, System, UNIT}; +use pop_runtime_devnet::{Assets, Revive, Runtime, RuntimeOrigin, System, UNIT}; use scale::{Decode, Encode}; use sp_runtime::app_crypto::sp_core; use sp_runtime::{AccountId32, BuildStorage, DispatchError}; @@ -42,6 +42,12 @@ fn new_test_ext() -> sp_io::TestExternalities { let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); + // register account mappings + ext.execute_with(|| { + Revive::map_account(RuntimeOrigin::signed(ALICE)); + Revive::map_account(RuntimeOrigin::signed(BOB)); + Revive::map_account(RuntimeOrigin::signed(FERDIE)); + }); ext } @@ -55,7 +61,7 @@ fn bare_call( input: Vec, value: u128, ) -> Result { - let result = Contracts::bare_call( + let result = Revive::bare_call( RuntimeOrigin::signed(ALICE), addr.into(), value.into(), @@ -77,7 +83,7 @@ fn instantiate( ) -> (crate::sp_core::H160, AccountId32) { let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); - let result = Contracts::bare_instantiate( + let result = Revive::bare_instantiate( RuntimeOrigin::signed(ALICE), init_value, GAS_LIMIT, @@ -91,5 +97,5 @@ fn instantiate( .result .unwrap(); assert!(!result.result.did_revert(), "deploying contract reverted {:?}", result); - (result.addr, pallet_revive::DefaultAddressMapper::to_account_id(&result.addr)) + (result.addr, pallet_revive::AccountId32Mapper::::to_account_id(&result.addr)) } diff --git a/runtime/devnet/src/config/revive.rs b/runtime/devnet/src/config/revive.rs index 3a89d785..412557f2 100644 --- a/runtime/devnet/src/config/revive.rs +++ b/runtime/devnet/src/config/revive.rs @@ -27,7 +27,7 @@ parameter_types! { } impl pallet_revive::Config for Runtime { - type AddressMapper = pallet_revive::DefaultAddressMapper; + type AddressMapper = pallet_revive::AccountId32Mapper; type CallFilter = Nothing; type ChainExtension = api::Extension; type ChainId = ConstU64<4001>; From ce5d682677054c0f6ecc92c1f0026bc762250af5 Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:12:45 +0100 Subject: [PATCH 4/8] test(revive): environment integration tests (#356) --- extension/contract/Cargo.toml | 3 +- pop-api/Cargo.toml | 2 +- pop-api/examples/balance-transfer/Cargo.toml | 3 +- pop-api/examples/fungibles/Cargo.toml | 3 +- pop-api/examples/nfts/Cargo.toml | 3 +- pop-api/examples/place-spot-order/Cargo.toml | 3 +- .../examples/read-runtime-state/Cargo.toml | 3 +- pop-api/integration-tests/Cargo.toml | 2 + .../create_token_in_constructor/Cargo.toml | 2 +- .../create_token_in_constructor/lib.rs | 3 +- .../contracts/environment/Cargo.toml | 23 ++ .../contracts/environment/lib.rs | 150 +++++++++ .../contracts/fungibles/Cargo.toml | 2 +- .../contracts/fungibles/lib.rs | 20 +- pop-api/integration-tests/src/environment.rs | 296 ++++++++++++++++++ pop-api/integration-tests/src/lib.rs | 7 +- pop-api/src/primitives.rs | 4 +- tests/contracts/filtered-call/Cargo.toml | 3 +- 18 files changed, 498 insertions(+), 34 deletions(-) create mode 100755 pop-api/integration-tests/contracts/environment/Cargo.toml create mode 100755 pop-api/integration-tests/contracts/environment/lib.rs create mode 100644 pop-api/integration-tests/src/environment.rs diff --git a/extension/contract/Cargo.toml b/extension/contract/Cargo.toml index 52c56f66..84b822f8 100755 --- a/extension/contract/Cargo.toml +++ b/extension/contract/Cargo.toml @@ -6,7 +6,8 @@ name = "proxy" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" [lib] path = "lib.rs" diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index 25a2bedd..88f0597d 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -6,7 +6,7 @@ name = "pop-api" version = "0.0.0" [dependencies] -ink = { git = "https://github.com/peterwht/ink", branch = "peter/riscv", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } polkavm-derive = "0.11.0" pop-primitives = { path = "../primitives", default-features = false } #sp-io = { version = "37.0.0", default-features = false, features = [ diff --git a/pop-api/examples/balance-transfer/Cargo.toml b/pop-api/examples/balance-transfer/Cargo.toml index 2a12e532..1339ac53 100755 --- a/pop-api/examples/balance-transfer/Cargo.toml +++ b/pop-api/examples/balance-transfer/Cargo.toml @@ -5,7 +5,8 @@ name = "balance_transfer" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" pop-api = { path = "../../../pop-api", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } diff --git a/pop-api/examples/fungibles/Cargo.toml b/pop-api/examples/fungibles/Cargo.toml index 0b79e1b2..bded6a05 100755 --- a/pop-api/examples/fungibles/Cargo.toml +++ b/pop-api/examples/fungibles/Cargo.toml @@ -5,7 +5,8 @@ name = "fungibles" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" pop-api = { path = "../../../pop-api", default-features = false, features = [ "fungibles" ] } [lib] diff --git a/pop-api/examples/nfts/Cargo.toml b/pop-api/examples/nfts/Cargo.toml index ef50b7ec..3b77f5df 100755 --- a/pop-api/examples/nfts/Cargo.toml +++ b/pop-api/examples/nfts/Cargo.toml @@ -5,7 +5,8 @@ name = "nfts" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" pop-api = { path = "../../../pop-api", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } diff --git a/pop-api/examples/place-spot-order/Cargo.toml b/pop-api/examples/place-spot-order/Cargo.toml index f523bea7..17f20f98 100755 --- a/pop-api/examples/place-spot-order/Cargo.toml +++ b/pop-api/examples/place-spot-order/Cargo.toml @@ -5,7 +5,8 @@ name = "spot_order" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" pop-api = { path = "../../../pop-api", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } diff --git a/pop-api/examples/read-runtime-state/Cargo.toml b/pop-api/examples/read-runtime-state/Cargo.toml index f5464730..723bbb48 100755 --- a/pop-api/examples/read-runtime-state/Cargo.toml +++ b/pop-api/examples/read-runtime-state/Cargo.toml @@ -5,7 +5,8 @@ name = "read_relay_blocknumber" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" pop-api = { path = "../../../pop-api", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml index 03c19027..0306acd8 100644 --- a/pop-api/integration-tests/Cargo.toml +++ b/pop-api/integration-tests/Cargo.toml @@ -16,6 +16,7 @@ log = "0.4.22" pallet-assets = { version = "37.0.0", default-features = false } pallet-balances = { version = "37.0.0", default-features = false } pallet-revive = { path = "../../pallets/revive", default-features = false } +pallet-timestamp = { version = "35.0.0", default-features = false } pop-api = { path = "../../pop-api", default-features = false, features = [ "fungibles", ] } @@ -37,6 +38,7 @@ std = [ "pallet-assets/std", "pallet-balances/std", "pallet-revive/std", + "pallet-timestamp/std", "pop-api/std", "pop-primitives/std", "pop-runtime-devnet/std", diff --git a/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml index 71ab704f..5e56acd8 100755 --- a/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml +++ b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml @@ -5,7 +5,7 @@ name = "create_token_in_constructor" version = "0.1.0" [dependencies] -ink = { git = "https://github.com/peterwht/ink", branch = "peter/riscv", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } polkavm-derive = "0.11.0" pop-api = { path = "../../..", default-features = false, features = [ "fungibles" ] } diff --git a/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs b/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs index a2a515cd..5bbbab54 100755 --- a/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs +++ b/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs @@ -1,7 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -use ink::env::DefaultEnvironment; -use ink::env::Environment; +use ink::{env::Environment, prelude::*}; use pop_api::{ fungibles::{self as api, events::Created}, primitives::TokenId, diff --git a/pop-api/integration-tests/contracts/environment/Cargo.toml b/pop-api/integration-tests/contracts/environment/Cargo.toml new file mode 100755 index 00000000..eeeb04ba --- /dev/null +++ b/pop-api/integration-tests/contracts/environment/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = [ "R0GUE " ] +description = "Contract for testing environment integration." +edition = "2021" +name = "environment" +version = "0.1.0" + +[dependencies] +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" +pop-api = { path = "../../..", default-features = false, features = [ "fungibles" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/integration-tests/contracts/environment/lib.rs b/pop-api/integration-tests/contracts/environment/lib.rs new file mode 100755 index 00000000..a5c34162 --- /dev/null +++ b/pop-api/integration-tests/contracts/environment/lib.rs @@ -0,0 +1,150 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use pop_api::StatusCode; + +pub type Result = core::result::Result; + +#[ink::contract] +mod environment { + use super::*; + + #[ink(storage)] + #[derive(Default)] + pub struct Contract; + + impl Contract { + #[ink(constructor, payable)] + pub fn new() -> Self { + Default::default() + } + + #[ink(message)] + pub fn caller(&self) -> AccountId { + self.env().caller() + } + + #[ink(message, payable)] + pub fn transferred_value(&self) -> Balance { + self.env().transferred_value() + } + + #[ink(message)] + pub fn weight_to_fee(&self, gas: u64) -> Balance { + self.env().weight_to_fee(gas) + } + + #[ink(message)] + pub fn block_timestamp(&self) -> Timestamp { + self.env().block_timestamp() + } + + #[ink(message)] + pub fn account_id(&self) -> AccountId { + self.env().account_id() + } + + #[ink(message)] + pub fn balance(&self) -> Balance { + self.env().balance() + } + + #[ink(message)] + pub fn block_number(&self) -> BlockNumber { + self.env().block_number() + } + + #[ink(message)] + pub fn minimum_balance(&self) -> Balance { + self.env().minimum_balance() + } + + #[ink(message)] + pub fn invoke_contract(&self) -> Result<()> { + todo!() + } + + #[ink(message)] + pub fn invoke_contract_delegate(&self) -> Result<()> { + todo!() + } + + #[ink(message)] + pub fn instantiate_contract(&self) -> Result<()> { + todo!() + } + + #[ink(message)] + pub fn terminate_contract(&self, beneficiary: AccountId) { + self.env().terminate_contract(beneficiary) + } + + #[ink(message, payable)] + pub fn transfer(&self, destination: AccountId) -> Result<()> { + let transferred_value = self.env().transferred_value(); + self.env().transfer(destination, transferred_value).unwrap(); + self.env().emit_event(Transferred { transferred_value, destination }); + Ok(()) + } + + #[ink(message)] + pub fn is_contract(&self, account: AccountId) -> bool { + self.env().is_contract(&account) + } + + #[ink(message)] + pub fn caller_is_origin(&self) -> bool { + self.env().caller_is_origin() + } + + #[ink(message)] + pub fn code_hash(&self, account: AccountId) -> Option { + self.env().code_hash(&account).ok() + } + + #[ink(message)] + pub fn own_code_hash(&self) -> Option { + self.env().own_code_hash().ok() + } + + #[ink(message)] + pub fn call_runtime(&self) -> Result<()> { + todo!() + } + + #[ink(message)] + pub fn lock_delegate_dependency(&self) -> Result<()> { + todo!() + } + + #[ink(message)] + pub fn unlock_delegate_dependency(&self) -> Result<()> { + todo!() + } + + #[ink(message)] + pub fn xcm_execute(&self) -> Result<()> { + todo!() + } + + #[ink(message)] + pub fn xcm_send(&self) -> Result<()> { + todo!() + } + } + + #[ink::event] + pub struct Transferred { + pub transferred_value: Balance, + pub destination: AccountId, + } + + #[cfg(test)] + mod tests { + use super::*; + + #[ink::test] + fn default_works() { + Contract::new(); + } + } +} diff --git a/pop-api/integration-tests/contracts/fungibles/Cargo.toml b/pop-api/integration-tests/contracts/fungibles/Cargo.toml index b57ad5c4..5a48c7e8 100755 --- a/pop-api/integration-tests/contracts/fungibles/Cargo.toml +++ b/pop-api/integration-tests/contracts/fungibles/Cargo.toml @@ -5,7 +5,7 @@ name = "fungibles" version = "0.1.0" [dependencies] -ink = { git = "https://github.com/peterwht/ink", branch = "peter/riscv", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } polkavm-derive = "0.11.0" pop-api = { path = "../../..", default-features = false, features = [ "fungibles" ] } diff --git a/pop-api/integration-tests/contracts/fungibles/lib.rs b/pop-api/integration-tests/contracts/fungibles/lib.rs index 86c55193..b029413e 100755 --- a/pop-api/integration-tests/contracts/fungibles/lib.rs +++ b/pop-api/integration-tests/contracts/fungibles/lib.rs @@ -14,19 +14,8 @@ use pop_api::{ StatusCode, }; -use ink::env::DefaultEnvironment; -use ink::env::Environment; - pub type Result = core::result::Result; -type AccountId = ::AccountId; - -fn to_account_id(address: &AccountId) -> AccountId { - let mut account_id = AccountId::from([0xEE; 32]); - >::as_mut(&mut account_id)[..20] - .copy_from_slice(&>::as_ref(&address)[..20]); - account_id -} #[ink::contract] mod fungibles { use super::*; @@ -76,7 +65,7 @@ mod fungibles { pub fn transfer(&mut self, token: TokenId, to: AccountId, value: Balance) -> Result<()> { api::transfer(token, to, value)?; self.env().emit_event(Transfer { - from: Some(to_account_id(&self.env().account_id())), + from: Some(self.env().account_id()), to: Some(to), value, }); @@ -106,11 +95,8 @@ mod fungibles { value: Balance, ) -> Result<()> { api::approve(token, spender, value)?; - self.env().emit_event(Approval { - owner: to_account_id(&self.env().account_id()), - spender, - value, - }); + self.env() + .emit_event(Approval { owner: self.env().account_id(), spender, value }); Ok(()) } diff --git a/pop-api/integration-tests/src/environment.rs b/pop-api/integration-tests/src/environment.rs new file mode 100644 index 00000000..d401de87 --- /dev/null +++ b/pop-api/integration-tests/src/environment.rs @@ -0,0 +1,296 @@ +use frame_support::traits::fungible::Inspect; +use pop_api::primitives::{Error, Hash, Timestamp}; +use pop_runtime_devnet::EXISTENTIAL_DEPOSIT; +use sp_runtime::app_crypto::sp_core::H160; + +use super::*; + +const CONTRACT: &str = "contracts/environment/target/ink/environment.riscv"; + +#[test] +fn caller_works() { + let caller = ALICE; + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert_eq!(contract.caller(), caller); + }); +} + +#[test] +fn transferred_value_works() { + let value = 10_000_000_000; + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert_eq!(contract.transferred_value(value), value); + }); +} + +#[test] +fn weight_to_fee_works() { + let gas = u64::MAX; + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert_eq!(contract.weight_to_fee(gas), 160_000_000); + }); +} + +#[test] +fn block_timestamp_works() { + let timestamp = Timestamp::MAX; + new_test_ext().execute_with(|| { + pallet_timestamp::Now::::put(timestamp); + let contract = Contract::new(); + assert_eq!(contract.block_timestamp(), timestamp); + }); +} + +#[test] +fn account_id_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert_eq!(contract.account_id(), contract.0 .1); + }); +} + +#[test] +fn balance_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert_eq!(contract.balance(), INIT_VALUE); + }); +} + +#[test] +fn block_number_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert_eq!(contract.block_number(), 1); + }); +} + +#[test] +fn minimum_balance_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert_eq!(contract.minimum_balance(), EXISTENTIAL_DEPOSIT); + }); +} + +#[test] +#[ignore] +fn invoke_contract_works() { + todo!() +} + +#[test] +#[ignore] +fn invoke_contract_delegate_works() { + todo!() +} + +#[test] +#[ignore] +fn instantiate_contract_works() { + todo!() +} + +#[test] +fn terminate_contract_works() { + let beneficiary = BOB; + new_test_ext().execute_with(|| { + let balance = pallet_balances::Pallet::::balance(&beneficiary); + let contract = Contract::new(); + let contract_balance = contract.balance(); + contract.terminate_contract(&beneficiary); + assert!( + // Storage deposit also goes to beneficiary. + pallet_balances::Pallet::::balance(&beneficiary) > balance + contract_balance + ); + }); +} + +#[test] +fn transfer_works() { + let destination = BOB; + let value = 10_000_000_000; + new_test_ext().execute_with(|| { + let balance = pallet_balances::Pallet::::balance(&destination); + let contract = Contract::new(); + assert_ok!(contract.transfer(destination.clone(), value)); + assert_eq!(pallet_balances::Pallet::::balance(&destination), balance + value); + assert_eq!( + contract.last_event(), + Transferred { transferred_value: value, destination }.encode() + ); + }); +} + +#[test] +fn is_contract_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert!(contract.is_contract(&contract.0 .1)); + assert_eq!(contract.is_contract(&ALICE), false); + }); +} + +#[test] +fn caller_is_origin_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert!(contract.caller_is_origin()); + }); +} + +#[test] +fn code_hash_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert!(contract.code_hash(&contract.0 .1).is_some()); + assert!(contract.code_hash(&ALICE).is_none()); + }); +} + +#[test] +fn own_code_hash_works() { + new_test_ext().execute_with(|| { + let contract = Contract::new(); + assert!(contract.own_code_hash().is_some()); + }); +} + +#[test] +#[ignore] +fn call_runtime_works() { + todo!() +} + +#[test] +#[ignore] +fn lock_delegate_dependency_works() { + todo!() +} + +#[test] +#[ignore] +fn unlock_delegate_dependency_works() { + todo!() +} + +#[test] +#[ignore] +fn xcm_execute_works() { + todo!() +} + +#[test] +#[ignore] +fn xcm_send_works() { + todo!() +} + +// A simple, strongly typed wrapper for the contract. +struct Contract((H160, AccountId32)); +impl Contract { + fn new() -> Self { + Self(instantiate(CONTRACT, INIT_VALUE, vec![])) + } + + fn caller(&self) -> AccountId32 { + let result = self.call("caller", Default::default(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn transferred_value(&self, value: Balance) -> Balance { + let result = self.call("transferred_value", Default::default(), value); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn weight_to_fee(&self, gas: u64) -> Balance { + let result = self.call("weight_to_fee", gas.encode(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn block_timestamp(&self) -> Timestamp { + let result = self.call("block_timestamp", Default::default(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn account_id(&self) -> AccountId32 { + let result = self.call("account_id", Default::default(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn balance(&self) -> Balance { + let result = self.call("balance", Default::default(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn block_number(&self) -> u32 { + let result = self.call("block_number", Default::default(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn minimum_balance(&self) -> Balance { + let result = self.call("minimum_balance", Default::default(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn terminate_contract(&self, beneficiary: &AccountId32) { + self.call("terminate_contract", beneficiary.encode(), 0); + } + + fn transfer(&self, destination: AccountId32, value: Balance) -> Result<(), Error> { + let result = self.call("transfer", destination.encode(), value); + >::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn is_contract(&self, account: &AccountId32) -> bool { + let result = self.call("is_contract", account.encode(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn caller_is_origin(&self) -> bool { + let result = self.call("caller_is_origin", Default::default(), 0); + ::decode(&mut &result.data[1..]).unwrap() + } + + fn code_hash(&self, account: &AccountId32) -> Option { + let result = self.call("code_hash", account.encode(), 0); + >::decode(&mut &result.data[1..]).unwrap() + } + + fn own_code_hash(&self) -> Option { + let result = self.call("own_code_hash", Default::default(), 0); + >::decode(&mut &result.data[1..]).unwrap() + } + + fn call(&self, function: &str, params: Vec, value: u128) -> ExecReturnValue { + let function = function_selector(function); + let params = [function, params].concat(); + let result = bare_call(self.0.clone().0, params, value).expect("should work"); + assert!(!result.did_revert(), "calling contract reverted {:?}", result); + result + } + + fn last_event(&self) -> Vec { + let events = System::read_events_for_pallet::>(); + let contract_events = events + .iter() + .filter_map(|event| match event { + pallet_revive::Event::::ContractEmitted { contract, data, .. } + if contract == &self.0 .0 => + Some(data.as_slice()), + _ => None, + }) + .collect::>(); + contract_events.last().unwrap().to_vec() + } +} + +#[derive(Encode)] +pub struct Transferred { + pub transferred_value: Balance, + pub destination: AccountId32, +} diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index e5728b15..c7e91c75 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -7,13 +7,12 @@ use frame_support::{ }, weights::Weight, }; -use pallet_revive::AddressMapper; -use pallet_revive::{Code, CollectEvents, ExecReturnValue}; +use pallet_revive::{AddressMapper, Code, CollectEvents, ExecReturnValue}; use pop_runtime_devnet::{Assets, Revive, Runtime, RuntimeOrigin, System, UNIT}; use scale::{Decode, Encode}; -use sp_runtime::app_crypto::sp_core; -use sp_runtime::{AccountId32, BuildStorage, DispatchError}; +use sp_runtime::{app_crypto::sp_core, AccountId32, BuildStorage, DispatchError}; +mod environment; mod fungibles; type Balance = u128; diff --git a/pop-api/src/primitives.rs b/pop-api/src/primitives.rs index 2fcb8a95..b0488ba2 100644 --- a/pop-api/src/primitives.rs +++ b/pop-api/src/primitives.rs @@ -3,4 +3,6 @@ pub use pop_primitives::*; // Public due to integration tests crate. pub type AccountId = ::AccountId; -pub(crate) type Balance = ::Balance; +pub type Balance = ::Balance; +pub type Hash = ::Hash; +pub type Timestamp = ::Timestamp; diff --git a/tests/contracts/filtered-call/Cargo.toml b/tests/contracts/filtered-call/Cargo.toml index 091beabe..e0e46506 100755 --- a/tests/contracts/filtered-call/Cargo.toml +++ b/tests/contracts/filtered-call/Cargo.toml @@ -3,7 +3,8 @@ edition = "2021" name = "pop_api_filtered_call" [dependencies] -ink = { version = "5.0.0", default-features = false } +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } From 7f83eb962c31f14d390a51d15df6bf1837874fde Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Fri, 25 Oct 2024 06:24:20 +0100 Subject: [PATCH 5/8] feat: messaging api (#338) --- Cargo.lock | 4 + networks/devnet.toml | 32 +- node/Cargo.toml | 1 + node/src/chain_spec.rs | 2 +- pallets/api/Cargo.toml | 10 +- pallets/api/src/lib.rs | 1 + pallets/api/src/messaging/mod.rs | 407 ++++++++++++++++++ pallets/api/src/messaging/transports/ismp.rs | 235 ++++++++++ pallets/api/src/messaging/transports/mod.rs | 4 + pallets/api/src/messaging/transports/xcm.rs | 49 +++ pop-api/Cargo.toml | 1 + pop-api/examples/messaging/Cargo.toml | 22 + pop-api/examples/messaging/lib.rs | 226 ++++++++++ pop-api/integration-tests/Cargo.toml | 4 + .../create_token_in_constructor/Cargo.toml | 2 +- .../contracts/messaging/Cargo.toml | 22 + .../contracts/messaging/lib.rs | 91 ++++ pop-api/integration-tests/src/lib.rs | 15 +- pop-api/integration-tests/src/messaging.rs | 255 +++++++++++ pop-api/src/primitives.rs | 1 + pop-api/src/v0/messaging/ismp.rs | 50 +++ pop-api/src/v0/messaging/mod.rs | 69 +++ pop-api/src/v0/messaging/xcm.rs | 42 ++ pop-api/src/v0/mod.rs | 3 + runtime/devnet/src/config/api/mod.rs | 53 ++- runtime/devnet/src/config/ismp.rs | 5 + runtime/devnet/src/config/mod.rs | 2 +- runtime/devnet/src/config/revive.rs | 11 +- runtime/devnet/src/config/xcm.rs | 44 +- runtime/devnet/src/lib.rs | 20 +- 30 files changed, 1632 insertions(+), 51 deletions(-) create mode 100644 pallets/api/src/messaging/mod.rs create mode 100644 pallets/api/src/messaging/transports/ismp.rs create mode 100644 pallets/api/src/messaging/transports/mod.rs create mode 100644 pallets/api/src/messaging/transports/xcm.rs create mode 100755 pop-api/examples/messaging/Cargo.toml create mode 100755 pop-api/examples/messaging/lib.rs create mode 100755 pop-api/integration-tests/contracts/messaging/Cargo.toml create mode 100755 pop-api/integration-tests/contracts/messaging/lib.rs create mode 100644 pop-api/integration-tests/src/messaging.rs create mode 100644 pop-api/src/v0/messaging/ismp.rs create mode 100644 pop-api/src/v0/messaging/mod.rs create mode 100644 pop-api/src/v0/messaging/xcm.rs diff --git a/Cargo.lock b/Cargo.lock index ce9196c6..8e00dff6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7375,9 +7375,11 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "ismp", "log", "pallet-assets", "pallet-balances", + "pallet-ismp", "parity-scale-codec", "pop-chain-extension", "scale-info", @@ -7385,6 +7387,8 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", + "staging-xcm", + "staging-xcm-executor", ] [[package]] diff --git a/networks/devnet.toml b/networks/devnet.toml index 4b630ec1..0f19c51d 100644 --- a/networks/devnet.toml +++ b/networks/devnet.toml @@ -3,12 +3,6 @@ [relaychain] chain = "paseo-local" -[relaychain.runtime_genesis_patch.balances] -balances = [ - # Pop sovereign account - ["5Ec4AhPKXY9B4ayGshkz2wFMh7N8gP7XKfAvtt1cigpG9FkJ", 60000000000000000], -] - [[relaychain.nodes]] name = "alice" rpc_port = 8833 @@ -18,6 +12,7 @@ validator = true name = "bob" validator = true +# Pop [[parachains]] id = 4001 default_command = "./target/release/pop-node" @@ -41,14 +36,35 @@ args = [ "-lruntime::contracts=debug", "-lruntime::revive=trace", "-lruntime::revive::strace=trace", + "-lxcm=trace", "--enable-offchain-indexing=true" ] +# Asset Hub [[parachains]] id = 1000 -chain = "asset-hub-rococo-local" +chain = "asset-hub-paseo-local" + +[parachains.genesis_overrides.balances] +balances = [ + # Pop sovereign account + ["5Eg2fnt8cGL5CBhRRhi59abAwb3SPoAdPJpN9qY7bQqpzpf6", 60000000000000000], +] [[parachains.collators]] name = "asset-hub" args = ["-lxcm=trace"] -rpc_port = 9977 \ No newline at end of file +rpc_port = 9977 + +# Channels +[[hrmp_channels]] +sender = 1000 +recipient = 4001 +max_capacity = 1000 +max_message_size = 1024 + +[[hrmp_channels]] +sender = 4001 +recipient = 1000 +max_capacity = 1000 +max_message_size = 1024 \ No newline at end of file diff --git a/node/Cargo.toml b/node/Cargo.toml index 4948cb18..6f75c6a4 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -92,6 +92,7 @@ substrate-build-script-utils.workspace = true pallet-multisig.workspace = true [features] +default = [ "ismp" ] runtime-benchmarks = [ "cumulus-primitives-core/runtime-benchmarks", "frame-benchmarking-cli/runtime-benchmarks", diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 64dc6368..f48693b2 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -315,7 +315,7 @@ fn devnet_genesis( id: ParaId, ) -> serde_json::Value { use pop_runtime_devnet::EXISTENTIAL_DEPOSIT; - let asset_hub = ismp_parachain::ParachainData { id: 1000, slot_duration: 6000 }; + let asset_hub = ismp_parachain::ParachainData { id: 1000, slot_duration: 12_000 }; serde_json::json!({ "balances": { diff --git a/pallets/api/Cargo.toml b/pallets/api/Cargo.toml index 55a00789..b626cafc 100644 --- a/pallets/api/Cargo.toml +++ b/pallets/api/Cargo.toml @@ -22,12 +22,18 @@ frame-benchmarking.workspace = true frame-support.workspace = true frame-system.workspace = true pallet-assets.workspace = true +sp-core.workspace = true sp-runtime.workspace = true sp-std.workspace = true +# Cross chain +ismp.workspace = true +pallet-ismp.workspace = true +xcm.workspace = true +xcm-executor.workspace = true + [dev-dependencies] pallet-balances.workspace = true -sp-core.workspace = true sp-io.workspace = true [features] @@ -45,8 +51,10 @@ std = [ "frame-benchmarking/std", "frame-support/std", "frame-system/std", + "ismp/std", "pallet-assets/std", "pallet-balances/std", + "pallet-ismp/std", "pop-chain-extension/std", "scale-info/std", "sp-core/std", diff --git a/pallets/api/src/lib.rs b/pallets/api/src/lib.rs index d94d1978..65b69598 100644 --- a/pallets/api/src/lib.rs +++ b/pallets/api/src/lib.rs @@ -5,6 +5,7 @@ use frame_support::pallet_prelude::Weight; pub mod extension; pub mod fungibles; +pub mod messaging; #[cfg(test)] mod mock; diff --git a/pallets/api/src/messaging/mod.rs b/pallets/api/src/messaging/mod.rs new file mode 100644 index 00000000..5f692dba --- /dev/null +++ b/pallets/api/src/messaging/mod.rs @@ -0,0 +1,407 @@ +//! TODO: pallet docs. + +use codec::{Decode, Encode}; +use frame_support::{ + pallet_prelude::MaxEncodedLen, + traits::{fungible::Inspect, Get, OriginTrait}, +}; +use frame_system::pallet_prelude::*; +pub use pallet::*; +use scale_info::TypeInfo; +use sp_core::H256; +use sp_runtime::{traits::Saturating, BoundedVec, SaturatedConversion}; +use sp_std::vec::Vec; +use transports::{ + ismp::{self as ismp, FeeMetadata, IsmpDispatcher}, + xcm::{self as xcm, Location, QueryHandler, QueryId, VersionedLocation}, +}; +use xcm::VersionedResponse; + +use super::Weight; + +/// Messaging transports. +pub mod transports; + +type AccountIdOf = ::AccountId; +type BlockNumberOf = BlockNumberFor; +type BalanceOf = <::Deposit as Inspect>>::Balance; +type MessageId = u64; + +#[frame_support::pallet] +pub mod pallet { + + use frame_support::{ + dispatch::DispatchResult, + pallet_prelude::*, + traits::tokens::{fungible::hold::Mutate, Precision::Exact}, + }; + use sp_core::H256; + use sp_runtime::traits::TryConvert; + + use super::*; + + /// Configuration of the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: frame_system::Config { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type OriginConverter: TryConvert; + + #[pallet::constant] + type ByteFee: Get>; + + /// The deposit mechanism. + type Deposit: Mutate + + Inspect; + + #[pallet::constant] + type IsmpByteFee: Get>; + + /// The ISMP message dispatcher. + type IsmpDispatcher: IsmpDispatcher>; + + /// The maximum length of any additional application-specific metadata relating to a + /// request. + #[pallet::constant] + type MaxContextLen: Get; + /// The maximum length of outbound (posted) data. + #[pallet::constant] + type MaxDataLen: Get; + #[pallet::constant] + type MaxKeys: Get; + #[pallet::constant] + type MaxKeyLen: Get; + + #[pallet::constant] + type MaxResponseLen: Get; + #[pallet::constant] + type MaxRemovals: Get; + + /// Overarching hold reason. + type RuntimeHoldReason: From; + + type Xcm: QueryHandler>; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type Messages = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + MessageId, + Message, + OptionQuery, + >; + + #[pallet::storage] + pub(super) type IsmpRequests = + StorageMap<_, Identity, H256, (T::AccountId, MessageId), OptionQuery>; + + #[pallet::storage] + pub(super) type XcmQueries = + StorageMap<_, Identity, QueryId, (T::AccountId, MessageId), OptionQuery>; + + /// The events that can be emitted. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + IsmpGetDispatched { origin: T::AccountId, id: MessageId }, + IsmpGetResponseReceived { dest: T::AccountId, id: MessageId }, + IsmpPostDispatched { origin: T::AccountId, id: MessageId }, + IsmpPostResponseReceived { dest: T::AccountId, id: MessageId }, + XcmQueryCreated { origin: T::AccountId, id: MessageId }, + XcmResponseReceived { dest: T::AccountId, id: MessageId }, + Removed { origin: T::AccountId, messages: Vec }, + } + + #[pallet::error] + pub enum Error { + DispatchFailed, + InvalidMessage, + OriginConversionFailed, + MessageExists, + RequestPending, + } + + /// A reason for the pallet placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// Held for the duration of a message's lifespan. + #[codec(index = 0)] + Messaging, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(Weight::zero())] // todo: benchmarking after consolidating storage + pub fn send(_origin: OriginFor, _id: MessageId) -> DispatchResult { + // e.g. Message::StateQuery { dest: Parachain(1000), storage_keys: vec![] } + // e.g. Message::Transact { dest: Parachain(1000), call: vec![] } + todo!("Reserved for messaging abstractions") + } + + // TODO: does ismp allow querying to ensure that specified para id is supported? + #[pallet::call_index(1)] + #[pallet::weight(Weight::zero())] // todo: benchmarking after consolidating storage + pub fn ismp_get( + origin: OriginFor, + id: MessageId, + message: ismp::Get, + fee: BalanceOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + // Calculate deposit and place on hold. + let deposit = Self::calculate_deposit(message.calculate_deposit()); + T::Deposit::hold(&HoldReason::Messaging.into(), &origin, deposit)?; + + // Process message by dispatching request via ISMP. + ensure!(!Messages::::contains_key(&origin, &id), Error::::MessageExists); + let commitment = T::IsmpDispatcher::default() + .dispatch_request(message.into(), FeeMetadata { payer: origin.clone(), fee }) + .map_err(|_| Error::::DispatchFailed)?; + + // Store commitment for lookup on response, message for querying, response/timeout + // handling. + IsmpRequests::::insert(&commitment, (&origin, id)); + Messages::::insert(&origin, id, Message::Ismp { deposit, commitment }); + Pallet::::deposit_event(Event::::IsmpGetDispatched { origin, id }); + Ok(()) + } + + // TODO: does ismp allow querying to ensure that specified para id is supported? + #[pallet::call_index(2)] + #[pallet::weight(Weight::zero())] // todo: benchmarking after consolidating storage + pub fn ismp_post( + origin: OriginFor, + id: MessageId, + message: ismp::Post, + fee: BalanceOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + // Calculate deposit and place on hold. + let deposit = Self::calculate_deposit(message.calculate_deposit()); + T::Deposit::hold(&HoldReason::Messaging.into(), &origin, deposit)?; + + // Process message by dispatching request via ISMP. + ensure!(!Messages::::contains_key(&origin, &id), Error::::MessageExists); + let commitment = T::IsmpDispatcher::default() + .dispatch_request(message.into(), FeeMetadata { payer: origin.clone(), fee }) + .map_err(|_| Error::::DispatchFailed)?; + + // Store commitment for lookup on response, message for querying, response/timeout + // handling. + IsmpRequests::::insert(&commitment, (&origin, id)); + Messages::::insert(&origin, id, Message::Ismp { deposit, commitment }); + Pallet::::deposit_event(Event::::IsmpPostDispatched { origin, id }); + Ok(()) + } + + #[pallet::call_index(3)] + #[pallet::weight(Weight::zero())] // todo: benchmarking after consolidating storage + pub fn xcm_new_query( + origin: OriginFor, + id: u64, + responder: VersionedLocation, + timeout: BlockNumberOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + // Calculate deposit and place on hold. + let deposit = Self::calculate_deposit(Default::default()); + T::Deposit::hold(&HoldReason::Messaging.into(), &origin, deposit)?; + + // Process message by creating new query via XCM. + ensure!(!Messages::::contains_key(&origin, &id), Error::::MessageExists); + let responder = + Location::try_from(responder).map_err(|_| Error::::OriginConversionFailed)?; + // TODO: neater way of doing this + let querier = T::OriginConverter::try_convert(T::RuntimeOrigin::signed(origin.clone())) + .map_err(|_| Error::::OriginConversionFailed)?; + let query_id = T::Xcm::new_query(responder, timeout, querier); + + // Store query id for later lookup on response, message for querying, response/timeout + // handling. + XcmQueries::::insert(&query_id, (&origin, id)); + Messages::::insert(&origin, id, Message::XcmQuery { deposit, query_id }); + Pallet::::deposit_event(Event::::XcmQueryCreated { origin, id }); + Ok(()) + } + + // Remove a request/response, returning any deposit previously taken. + #[pallet::call_index(4)] + #[pallet::weight(Weight::zero())] // todo: benchmarking after consolidating storage + pub fn remove( + origin: OriginFor, + messages: BoundedVec, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + for id in &messages { + // Ensure request exists and is not pending. + let deposit = match Messages::::take(&origin, id) { + Some(message) => match message { + Message::Ismp { .. } | Message::XcmQuery { .. } => { + return Err(Error::::RequestPending.into()); + }, + Message::IsmpResponse { commitment, deposit, .. } => { + IsmpRequests::::remove(commitment); + deposit + }, + Message::XcmResponse { query_id, deposit, .. } => { + XcmQueries::::remove(query_id); + deposit + }, + Message::IsmpTimedOut { .. } => { + todo!() + }, + }, + None => { + return Err(Error::::InvalidMessage.into()); + }, + }; + // Return deposit. + T::Deposit::release(&HoldReason::Messaging.into(), &origin, deposit, Exact)?; + } + Pallet::::deposit_event(Event::::Removed { + origin, + messages: messages.into_inner(), + }); + Ok(()) + } + } +} + +impl Pallet { + // Calculate the deposit required for a particular request. + fn calculate_deposit(mut deposit: BalanceOf) -> BalanceOf { + // Add amount for `Requests` key. + let request_key_len: BalanceOf = + (T::AccountId::max_encoded_len() + MessageId::max_encoded_len()).saturated_into(); + deposit.saturating_accrue(T::ByteFee::get() * request_key_len); + + // Add amount for `Requests` value. + let request_value_len = Status::max_encoded_len() + + BalanceOf::::max_encoded_len() + + // todo: could be optimised away + Option::::max_encoded_len(); + deposit.saturating_accrue(T::ByteFee::get() * request_value_len.saturated_into()); + + // Add amount for storing response. + deposit.saturating_accrue(T::ByteFee::get() * request_key_len); + deposit.saturating_accrue(T::ByteFee::get() * T::MaxResponseLen::get().saturated_into()); + + deposit + } +} + +#[derive(Clone, Decode, Debug, Encode, MaxEncodedLen, PartialEq, TypeInfo)] +pub enum Status { + Pending, + TimedOut, + Complete, +} + +#[derive(Encode, Decode, Debug, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(PartialEq, Clone))] +#[repr(u8)] +#[allow(clippy::unnecessary_cast)] +pub enum Read { + #[codec(index = 0)] + Poll((T::AccountId, MessageId)), + #[codec(index = 1)] + Get((T::AccountId, MessageId)), + #[codec(index = 2)] + QueryId((T::AccountId, MessageId)), +} + +#[derive(Debug)] +#[cfg_attr(feature = "std", derive(PartialEq, Clone))] +pub enum ReadResult { + Poll(Option), + Get(Option>), + QueryId(Option), +} + +impl ReadResult { + pub fn encode(&self) -> Vec { + use ReadResult::*; + match self { + Poll(status) => status.encode(), + Get(response) => response.encode(), + QueryId(query_id) => query_id.encode(), + } + } +} + +impl crate::Read for Pallet { + type Read = Read; + type Result = ReadResult; + + fn weight(_read: &Self::Read) -> Weight { + // TODO: implement benchmarks + Weight::zero() + } + + fn read(request: Self::Read) -> Self::Result { + match request { + Read::Poll(request) => + ReadResult::Poll(Messages::::get(request.0, request.1).map(|m| match m { + Message::Ismp { .. } | Message::XcmQuery { .. } => Status::Pending, + Message::IsmpTimedOut { .. } => Status::TimedOut, + Message::IsmpResponse { .. } | Message::XcmResponse { .. } => Status::Complete, + })), + Read::Get(request) => + ReadResult::Get(Messages::::get(request.0, request.1).and_then(|m| match m { + Message::IsmpResponse { response, .. } => Some(response), + Message::XcmResponse { response, .. } => + Some(response.encode().try_into().unwrap()), // todo: handle error + _ => None, + })), + Read::QueryId(request) => ReadResult::QueryId( + Messages::::get(request.0, request.1).and_then(|m| match m { + Message::XcmQuery { query_id, .. } | Message::XcmResponse { query_id, .. } => + Some(query_id), + _ => None, + }), + ), + } + } +} + +trait CalculateDeposit { + fn calculate_deposit(&self) -> Deposit; +} + +#[derive(Clone, Debug, Encode, Eq, Decode, MaxEncodedLen, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(T))] +enum Message { + Ismp { + commitment: H256, + deposit: BalanceOf, + }, + IsmpTimedOut { + commitment: H256, + deposit: BalanceOf, + }, + IsmpResponse { + commitment: H256, + deposit: BalanceOf, + response: BoundedVec, + }, + XcmQuery { + query_id: QueryId, + deposit: BalanceOf, + }, + XcmResponse { + query_id: QueryId, + deposit: BalanceOf, + response: VersionedResponse, + }, +} diff --git a/pallets/api/src/messaging/transports/ismp.rs b/pallets/api/src/messaging/transports/ismp.rs new file mode 100644 index 00000000..cb96bb34 --- /dev/null +++ b/pallets/api/src/messaging/transports/ismp.rs @@ -0,0 +1,235 @@ +use core::marker::PhantomData; + +pub(crate) use ::ismp::dispatcher::{FeeMetadata, IsmpDispatcher}; +use ::ismp::{ + dispatcher::{ + DispatchGet, + DispatchRequest::{self}, + }, + host::StateMachine, +}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + pallet_prelude::Weight, traits::Get as _, CloneNoBound, DebugNoBound, EqNoBound, + PartialEqNoBound, +}; +use ismp::{ + dispatcher::DispatchPost, + module::IsmpModule, + router::{GetResponse, PostRequest, PostResponse, Request::*, Response, Timeout}, + Error, +}; +use pallet_ismp::weights::IsmpModuleWeight; +use scale_info::TypeInfo; +use sp_core::{keccak_256, H256}; +use sp_runtime::{BoundedVec, SaturatedConversion, Saturating}; + +use crate::messaging::{ + pallet::{Config, Event, IsmpRequests, Messages, Pallet}, + BalanceOf, CalculateDeposit, MessageId, +}; + +pub const ID: [u8; 3] = *b"pop"; + +type DbWeightOf = ::DbWeight; + +#[derive(Encode, EqNoBound, CloneNoBound, DebugNoBound, Decode, PartialEqNoBound, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub enum Message { + Get(Get), + Post(Post), +} + +impl From> for DispatchRequest { + fn from(value: Message) -> Self { + match value { + Message::Get(get) => get.into(), + Message::Post(post) => post.into(), + } + } +} + +#[derive(Encode, EqNoBound, CloneNoBound, DebugNoBound, Decode, PartialEqNoBound, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct Get { + // TODO: Option to support relay? + pub(crate) dest: u32, + pub(crate) height: u32, + pub(crate) timeout: u64, + pub(crate) context: BoundedVec, + pub(crate) keys: BoundedVec, T::MaxKeys>, +} + +impl From> for DispatchGet { + fn from(value: Get) -> Self { + DispatchGet { + dest: StateMachine::Polkadot(value.dest), + from: ID.into(), + keys: value.keys.into_iter().map(|key| key.into_inner()).collect(), + height: value.height.into(), + context: value.context.into_inner(), + timeout: value.timeout, + } + } +} + +impl From> for DispatchRequest { + fn from(value: Get) -> Self { + DispatchRequest::Get(value.into()) + } +} + +impl CalculateDeposit> for Get { + fn calculate_deposit(&self) -> BalanceOf { + let len = self.dest.encoded_size() + + self.height.encoded_size() + + self.timeout.encoded_size() + + self.context.len() + + self.keys.iter().map(|k| k.len()).sum::(); + calculate_deposit::(T::IsmpByteFee::get() * len.saturated_into()) + } +} + +#[derive(Encode, EqNoBound, CloneNoBound, DebugNoBound, Decode, PartialEqNoBound, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct Post { + // TODO: Option to support relay? + pub(crate) dest: u32, + pub(crate) timeout: u64, + pub(crate) data: BoundedVec, +} + +impl From> for DispatchPost { + fn from(value: Post) -> Self { + DispatchPost { + dest: StateMachine::Polkadot(value.dest), + from: ID.into(), + to: ID.into(), + timeout: value.timeout, + body: value.data.into_inner(), + } + } +} + +impl From> for DispatchRequest { + fn from(value: Post) -> Self { + DispatchRequest::Post(value.into()) + } +} + +impl CalculateDeposit> for Post { + fn calculate_deposit(&self) -> BalanceOf { + let len = self.dest.encoded_size() + self.timeout.encoded_size() + self.data.len(); + calculate_deposit::(T::IsmpByteFee::get() * len.saturated_into()) + } +} + +pub struct Handler(PhantomData); +impl Handler { + pub fn new() -> Self { + Self(PhantomData) + } +} + +impl IsmpModule for Handler { + fn on_accept(&self, _request: PostRequest) -> Result<(), Error> { + todo!() + } + + fn on_response(&self, response: Response) -> Result<(), Error> { + let ((origin, id), response) = match response { + Response::Get(GetResponse { get, values }) => { + // hash request to determine key for original message id lookup + let id = IsmpRequests::::get(H256::from(keccak_256( + &ismp::router::Request::Get(get).encode(), + ))) + .ok_or(Error::Custom("request not found".into()))?; + Pallet::::deposit_event(Event::::IsmpGetResponseReceived { + dest: id.0.clone(), + id: id.1, + }); + (id, values.encode()) + }, + Response::Post(PostResponse { post, response, .. }) => { + // hash request to determine key for original message id lookup + let id = IsmpRequests::::get(H256::from(keccak_256(&Post(post).encode()))) + .ok_or(Error::Custom("request not found".into()))?; + Pallet::::deposit_event(Event::::IsmpPostResponseReceived { + dest: id.0.clone(), + id: id.1, + }); + (id, response) + }, + }; + + // Store values for later retrieval + let response: BoundedVec = + response.try_into().map_err(|_| Error::Custom("response exceeds max".into()))?; + Messages::::try_mutate(&origin, &id, |message| { + let Some(super::super::Message::Ismp { deposit, commitment }) = message else { + return Err(Error::Custom("message not found".into())) + }; + *message = Some(super::super::Message::IsmpResponse { + deposit: *deposit, + commitment: *commitment, + response, + }); + Ok(()) + })?; + Ok(()) + } + + fn on_timeout(&self, timeout: Timeout) -> Result<(), Error> { + match timeout { + Timeout::Request(request) => { + // hash request to determine key for original request id lookup + let id = match request { + Get(get) => H256::from(keccak_256(&get.encode())), + Post(post) => H256::from(keccak_256(&post.encode())), + }; + let key = + IsmpRequests::::get(id).ok_or(Error::Custom("request not found".into()))?; + Messages::::try_mutate(key.0, key.1, |message| { + let Some(super::super::Message::Ismp { deposit, commitment }) = message else { + return Err(Error::Custom("message not found".into())) + }; + *message = Some(super::super::Message::IsmpTimedOut { + deposit: *deposit, + commitment: *commitment, + }); + Ok(()) + })?; + Ok(()) + }, + Timeout::Response(_response) => { + todo!() + }, + } + } +} + +// TODO: replace with benchmarked weight functions +impl IsmpModuleWeight for Pallet { + fn on_accept(&self, _request: &PostRequest) -> Weight { + todo!() + } + + fn on_timeout(&self, _timeout: &Timeout) -> Weight { + DbWeightOf::::get().reads_writes(2, 1) + } + + fn on_response(&self, _response: &Response) -> Weight { + DbWeightOf::::get().reads_writes(2, 2) + } +} + +fn calculate_deposit(mut deposit: BalanceOf) -> BalanceOf { + // Add amount for `IsmpRequests` lookup. + let key_len: BalanceOf = + (T::AccountId::max_encoded_len() + MessageId::max_encoded_len()).saturated_into(); + deposit.saturating_accrue( + T::ByteFee::get() * (H256::max_encoded_len().saturated_into::>() + key_len), + ); + + deposit +} diff --git a/pallets/api/src/messaging/transports/mod.rs b/pallets/api/src/messaging/transports/mod.rs new file mode 100644 index 00000000..64f4712a --- /dev/null +++ b/pallets/api/src/messaging/transports/mod.rs @@ -0,0 +1,4 @@ +/// Messaging using the Interoperable State Machine Protocol (ISMP). +pub mod ismp; +/// Messaging using Polkadot's Cross-Consensus Messaging (XCM). +pub(crate) mod xcm; diff --git a/pallets/api/src/messaging/transports/xcm.rs b/pallets/api/src/messaging/transports/xcm.rs new file mode 100644 index 00000000..2d892a40 --- /dev/null +++ b/pallets/api/src/messaging/transports/xcm.rs @@ -0,0 +1,49 @@ +use frame_support::pallet_prelude::Weight; +use xcm::latest::{Response, XcmContext}; +pub(crate) use xcm::{ + latest::{Location, QueryId}, + VersionedLocation, VersionedResponse, +}; +use xcm_executor::traits::OnResponse; +pub(crate) use xcm_executor::traits::QueryHandler; + +use crate::messaging::{ + pallet::{Messages, XcmQueries}, + Config, Event, Pallet, +}; + +impl OnResponse for Pallet { + // todo: check origin and querier + fn expecting_response(_origin: &Location, query_id: u64, _querier: Option<&Location>) -> bool { + // todo: weight? + XcmQueries::::contains_key(query_id) + } + + // todo: check origin and querier + fn on_response( + _origin: &Location, + query_id: u64, + _querier: Option<&Location>, + response: Response, + _max_weight: Weight, + _context: &XcmContext, + ) -> Weight { + let (origin, id) = XcmQueries::::get(query_id).unwrap(); // TODO: handle error + + Messages::::mutate(&origin, &id, |message| { + let Some(super::super::Message::XcmQuery { query_id, deposit }) = message else { + panic!() // TODO: handle error + }; + *message = Some(super::super::Message::XcmResponse { + deposit: *deposit, + query_id: *query_id, + // TODO: remove this in favour of using the data stored in the xcm-pallet until + // taken. + response: VersionedResponse::from(response), + }); + }); + Pallet::::deposit_event(Event::::XcmResponseReceived { dest: origin, id }); + // todo: weight + Weight::zero() + } +} diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index 88f0597d..872a129d 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -23,4 +23,5 @@ path = "src/lib.rs" [features] default = [ "std" ] fungibles = [ ] +messaging = [ ] std = [ "ink/std", "pop-primitives/std" ] diff --git a/pop-api/examples/messaging/Cargo.toml b/pop-api/examples/messaging/Cargo.toml new file mode 100755 index 00000000..d0a800b4 --- /dev/null +++ b/pop-api/examples/messaging/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors = [ "R0GUE " ] +edition = "2021" +name = "messaging" +version = "0.1.0" + +[dependencies] +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" +pop-api = { path = "../../../pop-api", default-features = false, features = [ "messaging" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/examples/messaging/lib.rs b/pop-api/examples/messaging/lib.rs new file mode 100755 index 00000000..83425948 --- /dev/null +++ b/pop-api/examples/messaging/lib.rs @@ -0,0 +1,226 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::{ + env::hash::{Blake2x256, CryptoHash}, + prelude::vec::Vec, + scale::{Compact, Encode}, + xcm::{ + prelude::{All, Asset, Junction::Parachain, Location, Weight, WeightLimit, Xcm}, + VersionedXcm, + }, +}; +use pop_api::{ + messaging::{self as api, ismp, RequestId, Status}, + StatusCode, +}; + +pub type Result = core::result::Result; + +#[ink::contract] +mod messaging { + use ink::xcm::{ + prelude::{AccountId32, OriginKind, QueryId, QueryResponseInfo, XcmHash}, + DoubleEncoded, + }; + use pop_api::messaging::ismp::Get; + + use super::*; + + #[ink(storage)] + #[derive(Default)] + pub struct Messaging { + para: u32, + id: RequestId, + } + + impl Messaging { + #[ink(constructor, payable)] + pub fn new(para: u32) -> Self { + Self { para, id: 0 } + } + + #[ink(message)] + pub fn get(&mut self, key: Vec, height: u32) -> Result<()> { + self.id = self.id.saturating_add(1); + ismp::get( + self.id, + Get::new(self.para, height, 0, Vec::default(), Vec::from([key.clone()])), + 0, + )?; + self.env().emit_event(IsmpRequested { id: self.id, key, height }); + Ok(()) + } + + #[ink(message, payable)] + pub fn fund(&mut self) -> Result<()> { + let dest = Location::new(1, Parachain(self.para)); + + // Reserve transfer specified assets to contract account on destination. + let asset: Asset = (Location::parent(), self.env().transferred_value()).into(); + let beneficiary = hashed_account(4_001, self.env().account_id()); // todo: para id getter + let message: Xcm<()> = Xcm::builder_unsafe() + .withdraw_asset(asset.clone().into()) + .initiate_reserve_withdraw( + asset.clone().into(), + dest.clone(), + Xcm::builder_unsafe() + .buy_execution(asset.clone(), WeightLimit::Unlimited) + .deposit_asset( + All.into(), + Location::new(0, AccountId32 { network: None, id: beneficiary.0 }), + ) + .build(), + ) + .build(); + api::xcm::execute(&VersionedXcm::V4(message)).unwrap(); // todo: handle error + + self.env().emit_event(Funded { + account_id: beneficiary, + value: self.env().transferred_value(), + }); + Ok(()) + } + + #[ink(message, payable)] + pub fn transact(&mut self, call: DoubleEncoded<()>, weight: Weight) -> Result<()> { + let dest = Location::new(1, Parachain(self.para)); + + // Register a new query for receiving a response, used to report transact status. + self.id = self.id.saturating_add(1); + let query_id = api::xcm::new_query( + self.id, + dest.clone().into_versioned(), + self.env().block_number().saturating_add(100), + )? + .unwrap(); // TODO: handle error + + // TODO: provide an api function for determining the local para id and max weight value + // for processing the reported transact status on the local chain. + let response = QueryResponseInfo { + // Route back to this parachain. + destination: Location::new(1, Parachain(4_001)), + query_id, + max_weight: Weight::from_parts(1_000_000, 5_000), + }; + + // Send transact message. + let fees: Asset = (Location::parent(), self.env().transferred_value()).into(); + let message: Xcm<()> = self._transact(call, weight, fees, response); + let hash = api::xcm::send(&dest.into_versioned(), &VersionedXcm::V4(message)).unwrap(); // todo: handle error + + self.env().emit_event(XcmRequested { id: self.id, query_id, hash }); + Ok(()) + } + + #[ink(message)] + pub fn complete(&mut self, request: RequestId) -> Result<()> { + if let Ok(Some(status)) = api::poll((self.env().account_id(), request)) { + if status == Status::Complete { + let result = api::get((self.env().account_id(), request))?; + api::remove([request].to_vec())?; + self.env().emit_event(Completed { id: request, result }); + } + } + Ok(()) + } + + fn _transact( + &self, + call: DoubleEncoded<()>, + weight: Weight, + fees: Asset, + response: QueryResponseInfo, + ) -> Xcm<()> { + let beneficiary = hashed_account(4_001, self.env().account_id()); // todo: para id getter + Xcm::builder_unsafe() + .withdraw_asset(fees.clone().into()) + .buy_execution(fees, WeightLimit::Unlimited) + .set_appendix( + Xcm::builder_unsafe() + .refund_surplus() + .deposit_asset( + All.into(), + Location::new(0, AccountId32 { network: None, id: beneficiary.0 }), + ) + .build(), + ) + .set_error_handler(Xcm::builder_unsafe().report_error(response.clone()).build()) + .transact(OriginKind::SovereignAccount, weight, call) + .report_transact_status(response) + .build() + } + } + + #[ink::event] + pub struct IsmpRequested { + #[ink(topic)] + pub id: RequestId, + pub key: Vec, + pub height: BlockNumber, + } + + #[ink::event] + pub struct Funded { + #[ink(topic)] + pub account_id: AccountId, + pub value: Balance, + } + + #[ink::event] + pub struct XcmRequested { + #[ink(topic)] + pub id: RequestId, + #[ink(topic)] + pub query_id: QueryId, + #[ink(topic)] + pub hash: XcmHash, + } + + #[ink::event] + pub struct Completed { + #[ink(topic)] + pub id: RequestId, + pub result: Option>, + } + + // todo: make hasher generic and move to pop-api + fn hashed_account(para_id: u32, account_id: AccountId) -> AccountId { + let location = ( + b"SiblingChain", + Compact::::from(para_id), + (b"AccountId32", account_id.0).encode(), + ) + .encode(); + let mut output = [0u8; 32]; + Blake2x256::hash(&location, &mut output); + AccountId::from(output) + } + + #[cfg(test)] + mod tests { + + use super::*; + + #[ink::test] + fn default_works() { + Messaging::new(1_000); + } + + #[test] + fn it_works() { + let account_id: [u8; 32] = [ + 27, 2, 24, 17, 104, 5, 173, 98, 25, 32, 36, 0, 82, 159, 11, 212, 178, 11, 39, 219, + 14, 178, 226, 179, 216, 62, 19, 85, 226, 17, 80, 179, + ]; + let location = ( + b"SiblingChain", + Compact::::from(4001), + (b"AccountId32", account_id).encode(), + ) + .encode(); + let mut output = [0u8; 32]; + Blake2x256::hash(&location, &mut output); + println!("{output:?}") + } + } +} diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml index 0306acd8..28745087 100644 --- a/pop-api/integration-tests/Cargo.toml +++ b/pop-api/integration-tests/Cargo.toml @@ -12,13 +12,16 @@ env_logger = "0.11.2" frame-support = { version = "36.0.0", default-features = false } frame-support-procedural = { version = "=30.0.1", default-features = false } frame-system = { version = "36.1.0", default-features = false } +ismp = { git = "https://github.com/r0gue-io/ismp", branch = "polkadot-v1.14.0", default-features = false } log = "0.4.22" pallet-assets = { version = "37.0.0", default-features = false } pallet-balances = { version = "37.0.0", default-features = false } +pallet-ismp = { git = "https://github.com/r0gue-io/ismp", branch = "polkadot-v1.14.0", default-features = false } pallet-revive = { path = "../../pallets/revive", default-features = false } pallet-timestamp = { version = "35.0.0", default-features = false } pop-api = { path = "../../pop-api", default-features = false, features = [ "fungibles", + "messaging", ] } pop-primitives = { path = "../../primitives", default-features = false } pop-runtime-devnet = { path = "../../runtime/devnet", default-features = false } @@ -29,6 +32,7 @@ sp-io = { version = "37.0.0", default-features = false } sp-runtime = { version = "=38.0.0", default-features = false } # TODO: Requires to resolve a dependency version issue. See more at: https://github.com/r0gue-io/pop-node/issues/313. staging-xcm = { version = "=14.1.0", default-features = false } +xcm-executor = { version = "15.0.0", package = "staging-xcm-executor", default-features = false } [features] default = [ "std" ] diff --git a/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml index 5e56acd8..393f4e30 100755 --- a/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml +++ b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = [ "r0gue " ] +authors = [ "R0GUE " ] edition = "2021" name = "create_token_in_constructor" version = "0.1.0" diff --git a/pop-api/integration-tests/contracts/messaging/Cargo.toml b/pop-api/integration-tests/contracts/messaging/Cargo.toml new file mode 100755 index 00000000..28aa1a4c --- /dev/null +++ b/pop-api/integration-tests/contracts/messaging/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors = [ "R0GUE " ] +edition = "2021" +name = "messaging" +version = "0.1.0" + +[dependencies] +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" +pop-api = { path = "../../../../pop-api", default-features = false, features = [ "messaging" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/integration-tests/contracts/messaging/lib.rs b/pop-api/integration-tests/contracts/messaging/lib.rs new file mode 100755 index 00000000..62bc3f70 --- /dev/null +++ b/pop-api/integration-tests/contracts/messaging/lib.rs @@ -0,0 +1,91 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::prelude::vec::Vec; +use pop_api::{ + messaging::{ + self as api, + ismp::{Get, Post}, + xcm::{Junction, Location, QueryId, VersionedLocation}, + RequestId, Status, + }, + StatusCode, +}; + +pub type Result = core::result::Result; + +#[ink::contract] +mod messaging { + use super::*; + + #[ink(storage)] + #[derive(Default)] + pub struct Contract; + + impl Contract { + #[ink(constructor, payable)] + pub fn new() -> Self { + Default::default() + } + + #[ink(message)] + pub fn ismp_get(&mut self, id: RequestId, request: Get, fee: Balance) -> Result<()> { + api::ismp::get(id, request, fee)?; + Ok(()) + } + + #[ink(message)] + pub fn ismp_post(&mut self, id: RequestId, request: Post, fee: Balance) -> Result<()> { + api::ismp::post(id, request, fee)?; + Ok(()) + } + + #[ink(message)] + pub fn xcm_new_query( + &mut self, + id: RequestId, + // responder: VersionedLocation, + // Workaround for 'polkavm::interpreter] Store of 4 bytes to 0xfffdefcc failed! (pc = + // 8904, cycle = 239)' when using VersionedLocation + responder: Option, + timeout: BlockNumber, + ) -> Result> { + let responder = match responder { + Some(para) => Location::new(1, [Junction::Parachain(para)]), + None => Location::parent(), + } + .into_versioned(); + api::xcm::new_query(id, responder, timeout) + } + + #[ink(message)] + pub fn poll(&self, request: RequestId) -> Result> { + api::poll((self.env().account_id(), request)) + } + + #[ink(message)] + pub fn get(&self, request: RequestId) -> Result>> { + api::get((self.env().account_id(), request)) + } + + #[ink(message)] + pub fn remove(&mut self, request: RequestId) -> Result<()> { + api::remove([request].to_vec())?; + Ok(()) + } + + #[ink(message)] + pub fn test(&mut self, one: VersionedLocation, two: VersionedLocation) -> Result<()> { + Ok(()) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[ink::test] + fn default_works() { + Contract::new(); + } + } +} diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index c7e91c75..0dbd4357 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -8,12 +8,15 @@ use frame_support::{ weights::Weight, }; use pallet_revive::{AddressMapper, Code, CollectEvents, ExecReturnValue}; -use pop_runtime_devnet::{Assets, Revive, Runtime, RuntimeOrigin, System, UNIT}; +use pop_runtime_devnet::{config::ismp::Router, Assets, Messaging, Revive, Runtime, RuntimeOrigin, System, UNIT}; use scale::{Decode, Encode}; -use sp_runtime::{app_crypto::sp_core, AccountId32, BuildStorage, DispatchError}; +use sp_runtime::{app_crypto::sp_core, + offchain::{testing::TestOffchainExt, OffchainDbExt}, + AccountId32, BuildStorage, DispatchError}; mod environment; mod fungibles; +mod messaging; type Balance = u128; @@ -40,6 +43,8 @@ fn new_test_ext() -> sp_io::TestExternalities { .expect("Pallet balances storage can be assimilated"); let mut ext = sp_io::TestExternalities::new(t); + let (offchain, _state) = TestOffchainExt::new(); + ext.register_extension(OffchainDbExt::new(offchain)); ext.execute_with(|| System::set_block_number(1)); // register account mappings ext.execute_with(|| { @@ -75,11 +80,7 @@ fn bare_call( } // Deploy, instantiate and return contract address. -fn instantiate( - contract: &str, - init_value: u128, - salt: Vec, -) -> (crate::sp_core::H160, AccountId32) { +fn instantiate(contract: &str, init_value: u128, _salt: Vec) -> (sp_core::H160, AccountId32) { let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); let result = Revive::bare_instantiate( diff --git a/pop-api/integration-tests/src/messaging.rs b/pop-api/integration-tests/src/messaging.rs new file mode 100644 index 00000000..b743a727 --- /dev/null +++ b/pop-api/integration-tests/src/messaging.rs @@ -0,0 +1,255 @@ +use ::ismp::router::{GetResponse, IsmpRouter, PostResponse, Response, StorageValue}; +use pallet_ismp::mmr::Leaf; +use pop_api::{ + messaging::{ + ismp::{Get, Post}, + xcm::{Junction, Location, MaybeErrorCode, QueryId, VersionedResponse, XcmHash}, + RequestId, Status, + }, + primitives::{BlockNumber, Error}, +}; +use sp_io::{hashing::keccak_256, TestExternalities}; +use sp_runtime::{app_crypto::sp_core::H160, offchain::OffchainOverlayedChange}; +use xcm_executor::traits::OnResponse; + +use super::*; + +const CONTRACT: &str = "contracts/messaging/target/ink/messaging.riscv"; +const ASSET_HUB: u32 = 1_000; +const HYPERBRIDGE: u32 = 4_009; +const ISMP_MODULE_ID: [u8; 3] = *b"pop"; + +#[test] +fn ismp_get_request_works() { + let id = 42; + let key = "some_key".as_bytes().to_vec(); + let request = Get::new(ASSET_HUB, 0, 0, "some_context".as_bytes().to_vec(), vec![key.clone()]); + let response = vec![StorageValue { key, value: Some("some_value".as_bytes().to_vec()) }]; + + // Create a get request. + let mut ext = new_test_ext(); + let contract = ext.execute_with(|| { + let contract = Contract::new(); + assert_ok!(contract.ismp_get(id, request, 0)); + assert_eq!(contract.poll(id).unwrap(), Some(Status::Pending)); + + // TODO: assert events from messaging and ismp pallets emitted + println!("{:#?}", System::events()); + + contract + }); + + // Look up the request within offchain state in order to provide a response. + let ::ismp::router::Request::Get(get) = get_ismp_request(&mut ext) else { panic!() }; + + // Provide a response. + ext.execute_with(|| { + let module = Router::default().module_for_id(ISMP_MODULE_ID.to_vec()).unwrap(); + module + .on_response(Response::Get(GetResponse { get, values: response.clone() })) + .unwrap(); + + assert_eq!(contract.poll(id).unwrap(), Some(Status::Complete)); + assert_eq!(contract.get(id).unwrap(), Some(response.encode())); + assert_ok!(contract.remove(id)); + }); +} + +#[test] +fn ismp_post_request_works() { + let id = 42; + let request = Post::new(HYPERBRIDGE, 0, "some_data".as_bytes().to_vec()); + let response = "some_value".as_bytes().to_vec(); + + // Create a post request. + let mut ext = new_test_ext(); + let contract = ext.execute_with(|| { + let contract = Contract::new(); + assert_ok!(contract.ismp_post(id, request, 0)); + assert_eq!(contract.poll(id).unwrap(), Some(Status::Pending)); + + // TODO: assert events from messaging and ismp pallets emitted + println!("{:#?}", System::events()); + contract + }); + + // Look up the request within offchain state in order to provide a response. + let ::ismp::router::Request::Post(post) = get_ismp_request(&mut ext) else { panic!() }; + + // Provide a response. + ext.execute_with(|| { + let module = Router::default().module_for_id(ISMP_MODULE_ID.to_vec()).unwrap(); + module + .on_response(Response::Post(PostResponse { + post, + response: response.clone(), + timeout_timestamp: 0, + })) + .unwrap(); + + assert_eq!(contract.poll(id).unwrap(), Some(Status::Complete)); + assert_eq!(contract.get(id).unwrap(), Some(response)); + assert_ok!(contract.remove(id)); + }); +} + +#[test] +fn xcm_request_works() { + let id = 42u64; + let origin = Location::new(1, [Junction::Parachain(ASSET_HUB)]); + // Workaround for 'polkavm::interpreter] Store of 4 bytes to 0xfffdefcc failed! (pc = 8904, + // cycle = 239)' when using VersionedLocation + // let responder = origin.clone().into_versioned(); + let responder = Some(ASSET_HUB); + let timeout = 100; + let response = pop_api::messaging::xcm::Response::DispatchResult(MaybeErrorCode::Success); + let context = staging_xcm::prelude::XcmContext { + origin: None, + message_id: XcmHash::default(), + topic: None, + }; + new_test_ext().execute_with(|| { + let contract = Contract::new(); + + let query_id = contract.xcm_new_query(id, responder, timeout).unwrap().unwrap(); + assert_eq!(query_id, 0); + assert_eq!(contract.poll(id).unwrap(), Some(Status::Pending)); + + // TODO: assert events from messaging pallet emitted + println!("{:#?}", System::events()); + + // Provide a response. + let querier = staging_xcm::prelude::Location::new( + 0, + [staging_xcm::prelude::Junction::AccountId32 { + network: None, + id: contract.0 .1.clone().into(), + }], + ); + let origin = staging_xcm::prelude::Location::decode(&mut &origin.encode()[..]).unwrap(); + assert!(Messaging::expecting_response(&origin, query_id, Some(&querier))); + // TODO: update weight + assert_eq!( + Messaging::on_response( + &origin, + query_id, + Some(&querier), + staging_xcm::prelude::Response::decode(&mut &response.encode()[..]).unwrap(), + Weight::MAX, + &context + ), + Weight::zero() + ); + + assert_eq!(contract.poll(id).unwrap(), Some(Status::Complete)); + assert_eq!(contract.get(id).unwrap(), Some(VersionedResponse::from(response).encode())); + assert_ok!(contract.remove(id)); + }); +} + +#[test] +#[should_panic(expected = "should work")] +fn versioned_location_fails() { + let location = Location::new(1, [Junction::Parachain(ASSET_HUB)]).into_versioned(); + + new_test_ext().execute_with(|| { + let contract = Contract::new(); + contract.call("test", (&location, &location).encode(), 0); + }); +} + +// Get the last ismp request. +fn get_ismp_request(ext: &mut TestExternalities) -> ::ismp::router::Request { + // Get commitment from last ismp request event. + let commitment = ext.execute_with(|| { + System::read_events_for_pallet::>() + .iter() + .filter_map(|e| match e { + pallet_ismp::Event::::Request { commitment, .. } => + Some(commitment.clone()), + _ => None, + }) + .last() + .unwrap() + }); + // Read value from offchain storage overlay, stored via `NoOpMmrTree`. + let key = ("storage".as_bytes().to_vec(), (b"no_op", commitment).encode()); + let request = ext + .overlayed_changes() + .offchain() + .overlay() + .changes() + .filter_map(|c| { + (c.0 == &key).then(|| match c.1.value_ref() { + OffchainOverlayedChange::SetValue(value) => { + match Leaf::decode(&mut &value[..]).unwrap() { + Leaf::Request(req) => Some(req), + Leaf::Response(_) => None, + } + }, + _ => None, + }) + }) + .last() + .flatten() + .unwrap(); + // Ensure the request matches the commitment. + assert_eq!(commitment.0, keccak_256(&request.encode())); + request +} + +// A simple, strongly typed wrapper for the contract. +struct Contract((H160, AccountId32)); +impl Contract { + fn new() -> Self { + Self(instantiate(CONTRACT, INIT_VALUE, vec![])) + } + + fn ismp_get(&self, id: RequestId, request: Get, fee: Balance) -> Result<(), Error> { + let result = self.call("ismp_get", (id, request, fee).encode(), 0); + >::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn ismp_post(&self, id: RequestId, request: Post, fee: Balance) -> Result<(), Error> { + let result = self.call("ismp_post", (id, request, fee).encode(), 0); + >::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn xcm_new_query( + &self, + id: RequestId, + // responder: VersionedLocation, + responder: Option, + timeout: BlockNumber, + ) -> Result, Error> { + let result = self.call("xcm_new_query", (id, responder, timeout).encode(), 0); + , Error>>::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn poll(&self, request: RequestId) -> Result, Error> { + let result = self.call("poll", request.encode(), 0); + Result::, Error>::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn get(&self, request: RequestId) -> Result>, Error> { + let result = self.call("get", request.encode(), 0); + Result::>, Error>::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn remove(&self, request: RequestId) -> Result<(), Error> { + let result = self.call("remove", request.encode(), 0); + >::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn call(&self, function: &str, params: Vec, value: u128) -> ExecReturnValue { + let function = function_selector(function); + let params = [function, params].concat(); + bare_call(self.0.clone().0, params, value).expect("should work") + } +} diff --git a/pop-api/src/primitives.rs b/pop-api/src/primitives.rs index b0488ba2..26070c54 100644 --- a/pop-api/src/primitives.rs +++ b/pop-api/src/primitives.rs @@ -4,5 +4,6 @@ pub use pop_primitives::*; // Public due to integration tests crate. pub type AccountId = ::AccountId; pub type Balance = ::Balance; +pub type BlockNumber = ::BlockNumber; pub type Hash = ::Hash; pub type Timestamp = ::Timestamp; diff --git a/pop-api/src/v0/messaging/ismp.rs b/pop-api/src/v0/messaging/ismp.rs new file mode 100644 index 00000000..9b352611 --- /dev/null +++ b/pop-api/src/v0/messaging/ismp.rs @@ -0,0 +1,50 @@ +use super::*; + +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct Get { + dest: u32, + height: u32, + timeout: u64, + // TODO: Option + context: Vec, + keys: Vec>, +} + +impl Get { + pub fn new(dest: u32, height: u32, timeout: u64, context: Vec, keys: Vec>) -> Self { + // TODO: validate: dest para id, ensure at least one key + Self { dest, height, timeout, context, keys } + } +} + +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct Post { + dest: u32, + timeout: u64, + data: Vec, +} + +impl Post { + pub fn new(dest: u32, timeout: u64, data: Vec) -> Self { + // TODO: validate: dest para id, ensure data not empty + Self { dest, timeout, data } + } +} + +#[inline] +pub fn get(id: RequestId, request: Get, fee: Balance) -> Result<()> { + build_dispatch(ISMP_GET) + .input::<(RequestId, Get, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(id, request, fee)) +} + +#[inline] +pub fn post(id: RequestId, request: Post, fee: Balance) -> Result<()> { + build_dispatch(ISMP_POST) + .input::<(RequestId, Post, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(id, request, fee)) +} diff --git a/pop-api/src/v0/messaging/mod.rs b/pop-api/src/v0/messaging/mod.rs new file mode 100644 index 00000000..9f42f266 --- /dev/null +++ b/pop-api/src/v0/messaging/mod.rs @@ -0,0 +1,69 @@ +use ink::prelude::vec::Vec; + +use crate::{ + primitives::{AccountId, Balance, BlockNumber}, + ChainExtensionMethodApi, Result, StatusCode, +}; + +/// APIs for messaging using the Interoperable State Machine Protocol (ISMP). +pub mod ismp; +/// APIs for messaging using Polkadot's Cross-Consensus Messaging (XCM). +pub mod xcm; + +pub(crate) const API: u8 = 151; +// Dispatchables +pub(super) const _REQUEST: u8 = 0; +pub(super) const ISMP_GET: u8 = 1; +pub(super) const ISMP_POST: u8 = 2; +pub(super) const XCM_NEW_QUERY: u8 = 3; +pub(super) const REMOVE: u8 = 4; +// Reads +pub(super) const POLL: u8 = 0; +pub(super) const GET: u8 = 1; +pub(super) const QUERY_ID: u8 = 2; + +pub type RequestId = u64; + +fn build_dispatch(dispatchable: u8) -> ChainExtensionMethodApi { + crate::v0::build_dispatch(API, dispatchable) +} + +fn build_read_state(state_query: u8) -> ChainExtensionMethodApi { + crate::v0::build_read_state(API, state_query) +} + +#[inline] +pub fn poll(id: (AccountId, RequestId)) -> Result> { + build_read_state(POLL) + .input::<(AccountId, RequestId)>() + .output::>, true>() + .handle_error_code::() + .call(&id) +} + +#[inline] +pub fn get(id: (AccountId, RequestId)) -> Result>> { + build_read_state(GET) + .input::<(AccountId, RequestId)>() + .output::>>, true>() + .handle_error_code::() + .call(&id) +} + +#[inline] +pub fn remove(requests: Vec) -> Result<()> { + build_dispatch(REMOVE) + .input::>() + .output::, true>() + .handle_error_code::() + .call(&requests) +} + +#[derive(PartialEq)] +#[ink::scale_derive(Decode, Encode, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum Status { + Pending, + TimedOut, + Complete, +} diff --git a/pop-api/src/v0/messaging/xcm.rs b/pop-api/src/v0/messaging/xcm.rs new file mode 100644 index 00000000..8929efd6 --- /dev/null +++ b/pop-api/src/v0/messaging/xcm.rs @@ -0,0 +1,42 @@ +pub use ink::xcm::prelude::{ + Junction, Junctions, Location, MaybeErrorCode, QueryId, Response, VersionedLocation, + VersionedResponse, VersionedXcm, XcmContext, XcmHash, +}; +use ink::{ + env::{account_id, xcm_execute, xcm_send, DefaultEnvironment}, + scale::Encode, +}; + +use super::*; + +#[inline] +pub fn new_query( + id: RequestId, + responder: VersionedLocation, + timeout: BlockNumber, +) -> Result> { + build_dispatch(XCM_NEW_QUERY) + .input::<(RequestId, VersionedLocation, BlockNumber)>() + .output::, true>() + .handle_error_code::() + .call(&(id, responder, timeout))?; + + build_read_state(QUERY_ID) + .input::<(AccountId, RequestId)>() + .output::>, true>() + .handle_error_code::() + .call(&(account_id::(), id)) +} + +/// Execute an XCM message locally, using the contract's address as the origin. +pub fn execute(msg: &VersionedXcm) -> ink::env::Result<()> { + xcm_execute::(msg) +} + +/// Send an XCM message, using the contract's address as the origin. +pub fn send( + dest: &VersionedLocation, + msg: &VersionedXcm, +) -> ink::env::Result { + xcm_send::(dest, msg) +} diff --git a/pop-api/src/v0/mod.rs b/pop-api/src/v0/mod.rs index c6153892..ab8ca179 100644 --- a/pop-api/src/v0/mod.rs +++ b/pop-api/src/v0/mod.rs @@ -8,6 +8,9 @@ use crate::{ /// APIs for fungible tokens. #[cfg(feature = "fungibles")] pub mod fungibles; +/// APIs for messaging. +#[cfg(feature = "messaging")] +pub mod messaging; pub(crate) const V0: u8 = 0; diff --git a/runtime/devnet/src/config/api/mod.rs b/runtime/devnet/src/config/api/mod.rs index 10f6b540..246989d2 100644 --- a/runtime/devnet/src/config/api/mod.rs +++ b/runtime/devnet/src/config/api/mod.rs @@ -2,7 +2,7 @@ use core::marker::PhantomData; use codec::Decode; use cumulus_primitives_core::Weight; -use frame_support::traits::Contains; +use frame_support::traits::{ConstU32, Contains}; pub(crate) use pallet_api::Extension; use pallet_api::{extension::*, Read}; use sp_core::ConstU8; @@ -11,7 +11,9 @@ use sp_std::vec::Vec; use versioning::*; use crate::{ - config::assets::TrustBackedAssetsInstance, fungibles, Runtime, RuntimeCall, RuntimeEvent, + config::{assets::TrustBackedAssetsInstance, xcm::LocalOriginToLocation}, + fungibles, messaging, Balances, Ismp, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, + RuntimeHoldReason, TransactionByteFee, }; mod versioning; @@ -31,7 +33,10 @@ type DecodesAs = pallet_api::extension::DecodesAs< pub enum RuntimeRead { /// Fungible token queries. #[codec(index = 150)] - FungiblesRevive(fungibles::Read), + Fungibles(fungibles::Read), + /// Messaging state queries. + #[codec(index = 151)] + Messaging(messaging::Read), } impl Readable for RuntimeRead { @@ -42,15 +47,16 @@ impl Readable for RuntimeRead { /// performed. fn weight(&self) -> Weight { match self { - RuntimeRead::FungiblesRevive(key) => fungibles::Pallet::weight(key), + RuntimeRead::Fungibles(key) => fungibles::Pallet::weight(key), + RuntimeRead::Messaging(key) => messaging::Pallet::weight(key), } } /// Performs the read and returns the result. fn read(self) -> Self::Result { match self { - RuntimeRead::FungiblesRevive(key) => - RuntimeResult::Fungibles(fungibles::Pallet::read(key)), + RuntimeRead::Fungibles(key) => RuntimeResult::Fungibles(fungibles::Pallet::read(key)), + RuntimeRead::Messaging(key) => RuntimeResult::Messaging(messaging::Pallet::read(key)), } } } @@ -61,6 +67,8 @@ impl Readable for RuntimeRead { pub enum RuntimeResult { /// Fungible token read results. Fungibles(fungibles::ReadResult), + // Messaging state read results. + Messaging(messaging::ReadResult), } impl RuntimeResult { @@ -68,10 +76,32 @@ impl RuntimeResult { fn encode(&self) -> Vec { match self { RuntimeResult::Fungibles(result) => result.encode(), + RuntimeResult::Messaging(result) => result.encode(), } } } +impl messaging::Config for Runtime { + type ByteFee = TransactionByteFee; + type Deposit = Balances; + // TODO: ISMP state written to offchain indexing, require some protection but perhaps not as + // much as onchain cost. + type IsmpByteFee = (); + type IsmpDispatcher = Ismp; + type MaxContextLen = ConstU32<64>; + type MaxDataLen = ConstU32<1024>; + type MaxKeyLen = ConstU32<32>; + type MaxKeys = ConstU32<10>; + // TODO: size appropriately + type MaxRemovals = ConstU32<1024>; + // TODO: ensure within the contract buffer bounds + type MaxResponseLen = ConstU32<1024>; + type OriginConverter = LocalOriginToLocation; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type Xcm = PolkadotXcm; +} + impl fungibles::Config for Runtime { type AssetsInstance = TrustBackedAssetsInstance; type RuntimeEvent = RuntimeEvent; @@ -132,6 +162,7 @@ pub struct Filter(PhantomData); impl> Contains for Filter { fn contains(c: &RuntimeCall) -> bool { use fungibles::Call::*; + use messaging::Call::*; T::BaseCallFilter::contains(c) && matches!( c, @@ -144,6 +175,11 @@ impl> Contains f start_destroy { .. } | clear_metadata { .. } | mint { .. } | burn { .. } + ) | RuntimeCall::Messaging( + send { .. } | + ismp_get { .. } | ismp_post { .. } | + xcm_new_query { .. } | + remove { .. } ) ) } @@ -152,16 +188,17 @@ impl> Contains f impl Contains for Filter { fn contains(r: &RuntimeRead) -> bool { use fungibles::Read::*; + use messaging::Read::*; matches!( r, - RuntimeRead::FungiblesRevive( + RuntimeRead::Fungibles( TotalSupply(..) | BalanceOf { .. } | Allowance { .. } | TokenName(..) | TokenSymbol(..) | TokenDecimals(..) | TokenExists(..) - ) + ) | RuntimeRead::Messaging(Poll(..) | Get(..) | QueryId(..)) ) } } diff --git a/runtime/devnet/src/config/ismp.rs b/runtime/devnet/src/config/ismp.rs index 896b996f..7450fac6 100644 --- a/runtime/devnet/src/config/ismp.rs +++ b/runtime/devnet/src/config/ismp.rs @@ -16,6 +16,7 @@ impl pallet_ismp::Config for Runtime { type Coprocessor = Coprocessor; type Currency = Balances; type HostStateMachine = HostStateMachine; + // State is stored in offchain database type Mmr = pallet_ismp::NoOpMmrTree; type Router = Router; type RuntimeEvent = RuntimeEvent; @@ -46,6 +47,10 @@ impl Get for HostStateMachine { pub struct Router; impl IsmpRouter for Router { fn module_for_id(&self, id: Vec) -> Result, Error> { + use pallet_api::messaging::transports::ismp::*; + if id == ID { + return Ok(Box::new(Handler::::new())); + } Err(Error::ModuleNotFound(id)) } } diff --git a/runtime/devnet/src/config/mod.rs b/runtime/devnet/src/config/mod.rs index 7fb3abcb..c4d73f00 100644 --- a/runtime/devnet/src/config/mod.rs +++ b/runtime/devnet/src/config/mod.rs @@ -1,7 +1,7 @@ mod api; // Public due to pop api integration tests crate. pub mod assets; -mod ismp; +pub mod ismp; mod proxy; // Public due to integration tests crate. pub mod xcm; diff --git a/runtime/devnet/src/config/revive.rs b/runtime/devnet/src/config/revive.rs index 412557f2..a6e5714d 100644 --- a/runtime/devnet/src/config/revive.rs +++ b/runtime/devnet/src/config/revive.rs @@ -3,15 +3,6 @@ use frame_support::{ traits::{ConstBool, ConstU32, ConstU64, Nothing}, }; use frame_system::EnsureSigned; -use pallet_api::Extension; -use pallet_revive::{ - chain_extension::{ - ChainExtension, Environment, Ext, RegisteredChainExtension, Result as ExtensionResult, - RetVal, ReturnFlags, - }, - wasm::Memory, -}; -use sp_std::vec; use super::api::{self, Config}; use crate::{ @@ -47,5 +38,5 @@ impl pallet_revive::Config for Runtime { type UploadOrigin = EnsureSigned; type WeightInfo = pallet_revive::weights::SubstrateWeight; type WeightPrice = pallet_transaction_payment::Pallet; - type Xcm = (); + type Xcm = pallet_xcm::Pallet; } diff --git a/runtime/devnet/src/config/xcm.rs b/runtime/devnet/src/config/xcm.rs index 40f18c7c..e07dfcc3 100644 --- a/runtime/devnet/src/config/xcm.rs +++ b/runtime/devnet/src/config/xcm.rs @@ -18,11 +18,11 @@ use xcm_builder::{ SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, }; -use xcm_executor::XcmExecutor; +use xcm_executor::{traits::OnResponse, XcmExecutor}; use crate::{ - AccountId, AllPalletsWithSystem, Balances, ParachainInfo, ParachainSystem, PolkadotXcm, - Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, XcmpQueue, + AccountId, AllPalletsWithSystem, Balances, Messaging, ParachainInfo, ParachainSystem, + PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, XcmpQueue, }; parameter_types! { @@ -146,7 +146,7 @@ impl xcm_executor::Config for XcmConfig { type MessageExporter = (); type OriginConverter = XcmOriginToTransactDispatchOrigin; type PalletInstancesInfo = AllPalletsWithSystem; - type ResponseHandler = PolkadotXcm; + type ResponseHandler = ResponseHandler; type RuntimeCall = RuntimeCall; type SafeCallFilter = Everything; type SubscriptionService = PolkadotXcm; @@ -206,3 +206,39 @@ impl cumulus_pallet_xcm::Config for Runtime { type RuntimeEvent = RuntimeEvent; type XcmExecutor = XcmExecutor; } + +// TODO: improve +pub struct ResponseHandler(PhantomData<(A, B)>); +impl OnResponse for ResponseHandler { + fn expecting_response(origin: &Location, query_id: u64, querier: Option<&Location>) -> bool { + A::expecting_response(origin, query_id, querier) || + B::expecting_response(origin, query_id, querier) + } + + fn on_response( + origin: &Location, + query_id: u64, + querier: Option<&Location>, + response: Response, + max_weight: Weight, + context: &XcmContext, + ) -> Weight { + let weight = Weight::zero(); + if A::expecting_response(origin, query_id, querier) { + weight.saturating_add(A::on_response( + origin, + query_id, + querier, + response.clone(), + max_weight, + context, + )); + } + if B::expecting_response(origin, query_id, querier) { + weight.saturating_add(B::on_response( + origin, query_id, querier, response, max_weight, context, + )); + } + weight + } +} diff --git a/runtime/devnet/src/lib.rs b/runtime/devnet/src/lib.rs index c25213cb..b357fba4 100644 --- a/runtime/devnet/src/lib.rs +++ b/runtime/devnet/src/lib.rs @@ -39,7 +39,7 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; -use pallet_api::fungibles; +use pallet_api::{fungibles, messaging}; use pallet_balances::Call as BalancesCall; use pallet_ismp::mmr::{Leaf, Proof, ProofKeys}; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; @@ -194,11 +194,9 @@ type EventRecord = frame_system::EventRecord< >; // Prints debug output of the `contracts` pallet to stdout if the node is -// started with `-lruntime::contracts=debug`. -const CONTRACTS_DEBUG_OUTPUT: pallet_contracts::DebugInfo = - pallet_contracts::DebugInfo::UnsafeDebug; -const CONTRACTS_EVENTS: pallet_contracts::CollectEvents = - pallet_contracts::CollectEvents::UnsafeCollect; +// started with `-lruntime::revive=trace`. +const CONTRACTS_DEBUG_OUTPUT: pallet_revive::DebugInfo = pallet_revive::DebugInfo::UnsafeDebug; +const CONTRACTS_EVENTS: pallet_revive::CollectEvents = pallet_revive::CollectEvents::UnsafeCollect; /// The version information used to identify this runtime when compiled natively. #[cfg(feature = "std")] @@ -634,6 +632,8 @@ mod runtime { // Pop API #[runtime::pallet_index(150)] pub type Fungibles = fungibles::Pallet; + #[runtime::pallet_index(151)] + pub type Messaging = messaging::Pallet; // Revive #[runtime::pallet_index(255)] @@ -813,8 +813,8 @@ impl_runtime_apis! { gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block), storage_deposit_limit.unwrap_or(u128::MAX), input_data, - pallet_revive::DebugInfo::UnsafeDebug, - pallet_revive::CollectEvents::UnsafeCollect, + CONTRACTS_DEBUG_OUTPUT, + CONTRACTS_EVENTS, ) } @@ -836,8 +836,8 @@ impl_runtime_apis! { code, data, salt, - pallet_revive::DebugInfo::UnsafeDebug, - pallet_revive::CollectEvents::UnsafeCollect, + CONTRACTS_DEBUG_OUTPUT, + CONTRACTS_EVENTS, ) } From a2b653dac5448851a7ff9925e497358fd8c0d88b Mon Sep 17 00:00:00 2001 From: Alex Bean Date: Fri, 25 Oct 2024 17:06:52 +0200 Subject: [PATCH 6/8] feat: builder incentives (#343) Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> --- Cargo.lock | 18 + Cargo.toml | 7 +- networks/devnet.toml | 2 + pallets/incentives/Cargo.toml | 55 +++ .../incentives/src/contract_fee_handler.rs | 195 +++++++++++ pallets/incentives/src/lib.rs | 327 ++++++++++++++++++ pallets/incentives/src/types.rs | 86 +++++ pop-api/examples/incentives/Cargo.toml | 22 ++ pop-api/examples/incentives/lib.rs | 39 +++ pop-api/integration-tests/Cargo.toml | 2 + pop-api/integration-tests/src/environment.rs | 2 +- .../integration-tests/src/fungibles/mod.rs | 56 +-- pop-api/integration-tests/src/incentives.rs | 64 ++++ pop-api/integration-tests/src/lib.rs | 22 +- pop-api/integration-tests/src/messaging.rs | 2 +- pop-api/src/lib.rs | 2 + pop-api/src/primitives.rs | 1 + pop-api/src/v0/incentives.rs | 55 +++ pop-api/src/v0/mod.rs | 2 + runtime/devnet/Cargo.toml | 4 + runtime/devnet/src/config/api/mod.rs | 3 + runtime/devnet/src/lib.rs | 74 +++- 22 files changed, 1003 insertions(+), 37 deletions(-) create mode 100644 pallets/incentives/Cargo.toml create mode 100644 pallets/incentives/src/contract_fee_handler.rs create mode 100644 pallets/incentives/src/lib.rs create mode 100644 pallets/incentives/src/types.rs create mode 100755 pop-api/examples/incentives/Cargo.toml create mode 100755 pop-api/examples/incentives/lib.rs create mode 100644 pop-api/integration-tests/src/incentives.rs create mode 100644 pop-api/src/v0/incentives.rs diff --git a/Cargo.lock b/Cargo.lock index 8e00dff6..8438b36d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8076,6 +8076,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-incentives" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-revive", + "pallet-transaction-payment", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-indices" version = "36.0.0" @@ -11010,6 +11027,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-contracts", + "pallet-incentives", "pallet-ismp", "pallet-ismp-runtime-api", "pallet-message-queue", diff --git a/Cargo.toml b/Cargo.toml index ab623009..8ffdf7e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,12 +64,13 @@ substrate-wasm-builder = "23.0.0" # Local pallet-api = { path = "pallets/api", default-features = false } +pallet-incentives = { path = "pallets/incentives", default-features = false } pop-chain-extension = { path = "./extension", default-features = false } pop-primitives = { path = "./primitives", default-features = false } pop-runtime-common = { path = "runtime/common", default-features = false } -pop-runtime-devnet = { path = "runtime/devnet", default-features = true } # default-features=true required for `-p pop-node` builds -pop-runtime-mainnet = { path = "runtime/mainnet", default-features = true } # default-features=true required for `-p pop-node` builds -pop-runtime-testnet = { path = "runtime/testnet", default-features = true } # default-features=true required for `-p pop-node` builds +pop-runtime-devnet = { path = "runtime/devnet", default-features = true } # default-features=true required for `-p pop-node` builds +pop-runtime-mainnet = { path = "runtime/mainnet", default-features = true } # default-features=true required for `-p pop-node` builds +pop-runtime-testnet = { path = "runtime/testnet", default-features = true } # default-features=true required for `-p pop-node` builds # Substrate frame-benchmarking = { version = "36.0.0", default-features = false } diff --git a/networks/devnet.toml b/networks/devnet.toml index 0f19c51d..99563c68 100644 --- a/networks/devnet.toml +++ b/networks/devnet.toml @@ -26,6 +26,8 @@ balances = [ ["5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy", 10000000000000000], ["5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw", 10000000000000000], ["5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL", 10000000000000000], + # Pallet Builder Incentives Account (ED) + ["5EYCAe5ZWx63mefx7GoWjmSZq46iS7tYpp2XkRHVRyU2i95M", 10000000], ] [[parachains.collators]] diff --git a/pallets/incentives/Cargo.toml b/pallets/incentives/Cargo.toml new file mode 100644 index 00000000..fecd7aad --- /dev/null +++ b/pallets/incentives/Cargo.toml @@ -0,0 +1,55 @@ +[package] +authors.workspace = true +description = "Builder incentives pallet, to incentivize builders to build on the platform" +edition.workspace = true +license.workspace = true +name = "pallet-incentives" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +codec.workspace = true +log.workspace = true +scale-info.workspace = true + +# Substrate +frame-benchmarking.workspace = true +frame-support.workspace = true +frame-system.workspace = true +pallet-revive.workspace = true +pallet-transaction-payment.workspace = true +sp-core.workspace = true +sp-runtime.workspace = true + +[dev-dependencies] +sp-io = { workspace = true, default-features = true } + +[features] +default = [ "std" ] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-revive/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "pallet-revive/std", + "pallet-transaction-payment/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-revive/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/incentives/src/contract_fee_handler.rs b/pallets/incentives/src/contract_fee_handler.rs new file mode 100644 index 00000000..935249df --- /dev/null +++ b/pallets/incentives/src/contract_fee_handler.rs @@ -0,0 +1,195 @@ +use codec::HasCompact; +use frame_support::{ + dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo}, + pallet_prelude::{Decode, Encode, TypeInfo}, + traits::{fungible::Inspect, IsSubType, IsType}, +}; +use pallet_revive::AddressMapper; +use pallet_transaction_payment::OnChargeTransaction; +use scale_info::StaticTypeInfo; +use sp_core::{crypto::AccountId32, U256}; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, Saturating, SignedExtension}, + transaction_validity::{TransactionValidity, TransactionValidityError, ValidTransaction}, + FixedPointOperand, Permill, +}; + +use crate::{types::*, Call, Config, Pallet}; + +/// A [`SignedExtension`] that handles fees sponsorship. +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct ContractFeeHandler( + #[codec(compact)] OnChargeTransactionBalanceOf, + core::marker::PhantomData, +); + +// Make this extension "invisible" from the outside (ie metadata type information) +impl TypeInfo for ContractFeeHandler { + type Identity = S; + + fn type_info() -> scale_info::Type { + S::type_info() + } +} + +impl core::fmt::Debug for ContractFeeHandler { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "ContractFeeHandler<{:?}>", self.0) + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result { + Ok(()) + } +} + +impl> ContractFeeHandler +where + ::RuntimeCall: + Dispatchable + IsSubType>, + OnChargeTransactionBalanceOf: Send + Sync + From, +{ + /// Utility constructor. Used only in client/factory code. + pub fn from(fee: OnChargeTransactionBalanceOf) -> Self { + Self(fee, core::marker::PhantomData) + } + + /// Returns the tip as being chosen by the transaction sender. + pub fn tip(&self) -> OnChargeTransactionBalanceOf { + self.0 + } + + fn withdraw_fee( + &self, + who: &T::AccountId, + call: &::RuntimeCall, + info: &DispatchInfoOf<::RuntimeCall>, + len: usize, + ) -> Result<(OnChargeTransactionBalanceOf, LiquidityInfoOf), TransactionValidityError> { + let tip = self.0; + let fee = pallet_transaction_payment::Pallet::::compute_fee(len as u32, info, tip); + as OnChargeTransaction>::withdraw_fee( + who, call, info, fee, tip, + ) + .map(|i| (fee, i)) + } +} + +impl> SignedExtension + for ContractFeeHandler +where + ::RuntimeCall: Dispatchable + + IsSubType> + + IsSubType>, + OnChargeTransactionBalanceOf: + Send + Sync + FixedPointOperand + From + IsType>, + <<::Currency as Inspect< + ::AccountId, +>>::Balance as HasCompact>::Type: + Clone + Eq + PartialEq + core::fmt::Debug + TypeInfo + Encode, + ::RuntimeCall: IsSubType>, + ::AccountId: From, + <::Currency as frame_support::traits::fungible::Inspect< + ::AccountId, + >>::Balance: Into, + <::Time as frame_support::traits::Time>::Moment: Into, + <::Currency as frame_support::traits::fungible::Inspect< + ::AccountId, + >>::Balance: TryFrom, +{ + type AccountId = T::AccountId; + type AdditionalSigned = (); + // type Call = S::Call; + type Call = ::RuntimeCall; + type Pre = ( + // tip + OnChargeTransactionBalanceOf, + // who + Self::AccountId, + // imbalance resulting from withdrawing the fee + LiquidityInfoOf, + // contract address. + Option, + ); + + // From the outside this extension should be "invisible", because it just extends the wrapped + // extension with an extra check in `pre_dispatch` and `post_dispatch`. Thus, we should forward + // the identifier of the wrapped extension to let wallets see this extension as it would only be + // the wrapped extension itself. + const IDENTIFIER: &'static str = S::IDENTIFIER; + + fn additional_signed(&self) -> Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> TransactionValidity { + let (fee, ..) = self.withdraw_fee(who, call, info, len)?; + let priority = pallet_transaction_payment::ChargeTransactionPayment::::get_priority( + info, len, self.0, fee, + ); + Ok(ValidTransaction { priority, ..Default::default() }) + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + let contract = if let Some(pallet_revive::Call::call { dest, .. }) = call.is_sub_type() { + Some(::AddressMapper::to_account_id(dest)) + } else { + None + }; + let (_fee, initial_payment) = self.withdraw_fee(who, call, info, len)?; + Ok((self.tip(), who.clone(), initial_payment, contract)) + } + + fn post_dispatch( + pre: Option, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + if let Some((tip, who, initial_payment, contract)) = pre { + pallet_transaction_payment::ChargeTransactionPayment::::post_dispatch( + Some((tip, who, initial_payment)), + info, + post_info, + len, + result, + )?; + if let Some(contract) = contract { + // Check if contract is registered to track usage. + if crate::RegisteredContracts::::contains_key(&contract) { + let actual_fee = pallet_transaction_payment::Pallet::::compute_actual_fee( + len as u32, info, post_info, tip, + ); + // TODO: This should not be hardcoded here. The 50% is specified in the runtime + // for DealWithFees. + let incentives_fee = Permill::from_percent(50) * actual_fee; + let era = crate::CurrentEra::::get(); + crate::ContractUsagePerEra::::mutate(contract.clone(), era, |fees| { + *fees = fees.saturating_add(incentives_fee.into()) + }); + crate::EraInformation::::mutate(era, |era_info| { + era_info.add_contract_fee(incentives_fee.into()); + }); + Pallet::::deposit_event(crate::Event::::ContractCalled { + contract: contract.clone(), + }); + } + } + } + Ok(()) + } +} diff --git a/pallets/incentives/src/lib.rs b/pallets/incentives/src/lib.rs new file mode 100644 index 00000000..1acfe043 --- /dev/null +++ b/pallets/incentives/src/lib.rs @@ -0,0 +1,327 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +// Re-export pallet items so that they can be accessed from the crate namespace. +pub use pallet::*; + +pub mod contract_fee_handler; +pub mod types; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{ + pallet_prelude::*, + traits::{Currency, ExistenceRequirement::AllowDeath, Get, ReservableCurrency}, + PalletId, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::{ + traits::{AccountIdConversion, SaturatedConversion, Saturating, Zero}, + Permill, + }; + + use super::*; + use crate::types::*; + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_transaction_payment::Config + pallet_revive::Config + { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The currency mechanism. + type Currency: ReservableCurrency; + /// Duration of an era in terms of the number of blocks. + #[pallet::constant] + type EraDuration: Get>; + /// The pallet's id, used for deriving its sovereign account ID. + #[pallet::constant] + type PalletId: Get; + // Weight information for dispatchables in this pallet. + // type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + pub struct Pallet(_); + + /// Registered contracts to their corresponding beneficiaries. + #[pallet::storage] + pub(crate) type RegisteredContracts = + StorageMap<_, Twox64Concat, AccountIdOf, AccountIdOf>; + + /// Tracks the usage of each contract by era. + #[pallet::storage] + pub(super) type ContractUsagePerEra = StorageDoubleMap< + _, + Twox64Concat, + AccountIdOf, + Twox64Concat, + EraNumber, + BalanceOf, + ValueQuery, + >; + + /// Information about each era. + #[pallet::storage] + pub(super) type EraInformation = + StorageMap<_, Twox64Concat, EraNumber, EraInfo, ValueQuery>; + + /// Current era number. + #[pallet::storage] + pub(super) type CurrentEra = StorageValue<_, EraNumber, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A smart contract has been called. + ContractCalled { + /// The smart contract's account that was called. + contract: AccountIdOf, + }, + /// A smart contract has been registered to receive rewards. + ContractRegistered { + /// The beneficiary of the incentives. + beneficiary: T::AccountId, + /// The smart contract's account being registered. + contract: AccountIdOf, + }, + /// A new era has started. + NewEra { + /// The number of the newly started era. + era: EraNumber, + }, + /// New incentives have been deposited into the reward pool. + IncentivesDeposited { + /// The account that deposited the incentives. + source: AccountIdOf, + /// The amount of incentives deposited. + amount: BalanceOf, + }, + /// Incentives have been claimed from the reward pool. + IncentivesClaimed { + /// The account that received the incentives. + beneficiary: AccountIdOf, + // The amount of incentives withdrawn. + amount: BalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// Unable to claim rewards for the current era. + CannotClaimRewardsForCurrentEra, + /// The smart contract is already registered. + ContractAlreadyRegistered, + /// The smart contract is not registered. + ContractNotRegistered, + /// The caller is not the beneficiary authorized to claim rewards. + NotAuthorizedToClaimRewards, + } + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub starting_era: EraNumber, + #[serde(skip)] + pub _config: PhantomData, + } + + impl Default for GenesisConfig { + fn default() -> Self { + Self { starting_era: 0, _config: PhantomData } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + let era_duration = T::EraDuration::get(); + let era_info = EraInfo:: { + next_era_start: era_duration + .saturating_add(BlockNumberFor::::zero()) + .saturated_into(), + contract_fee_total: BalanceOf::::zero(), + total_fee_amount: BalanceOf::::zero(), + }; + EraInformation::::insert(self.starting_era, era_info); + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + let now = now.saturated_into(); + let mut current_era = CurrentEra::::get(); + let mut current_era_info = EraInformation::::get(current_era); + let mut consumed_weight = T::DbWeight::get().reads(2); + + // Check if is new era, if not don't do anything. + if !current_era_info.is_new_era(now) { + return consumed_weight; + } + // If is new era, update the new era information. + current_era.saturating_inc(); + current_era_info + .set_next_era_start(now.saturating_add(T::EraDuration::get().saturated_into())); + current_era_info.reset_contract_fee(); + current_era_info.resent_total_fee(); + // Update storage items + CurrentEra::::put(current_era); + EraInformation::::insert(current_era, current_era_info); + consumed_weight = T::DbWeight::get().writes(2); + Self::deposit_event(Event::::NewEra { era: current_era }); + consumed_weight + } + // TODO: Implement on_idle for handling rewards not claimed after X period + // fn on_idle(_block: BlockNumberFor, remaining_weight: Weight) -> Weight { + // } + } + + #[pallet::call] + impl Pallet { + /// Register a new contract for incentives. + /// + /// # Parameters + /// - `origin`: The smart contract's account to be registered. + /// - `beneficiary`: The account that will be the beneficiary of the contract incentives. + #[pallet::call_index(0)] + #[pallet::weight(Weight::zero())] + pub fn register_contract( + origin: OriginFor, + beneficiary: T::AccountId, + ) -> DispatchResult { + let contract = ensure_signed(origin)?; + ensure!( + !RegisteredContracts::::contains_key(&contract), + Error::::ContractAlreadyRegistered, + ); + // TODO: Max number of registered contracts and check it doesn't + // ExceededMaxNumberOfContracts + RegisteredContracts::::insert(&contract, &beneficiary); + Self::deposit_event(Event::::ContractRegistered { beneficiary, contract }); + Ok(()) + } + + /// Claims rewards for a specific contract for a given era. + /// + /// Parameters: + /// - `contract`: The account of the smart contract for which rewards are being claimed. + /// - `era`: The era for which rewards are being claimed. Must be the current era. + #[pallet::call_index(1)] + #[pallet::weight(Weight::zero())] + // TODO: allow multiple eras + pub fn claim_rewards( + origin: OriginFor, + contract: AccountIdOf, + era: EraNumber, + ) -> DispatchResult { + ensure_signed(origin)?; + let beneficiary = RegisteredContracts::::get(&contract) + .ok_or(Error::::ContractNotRegistered)?; + ensure!(CurrentEra::::get() > era, Error::::CannotClaimRewardsForCurrentEra); + // TODO: If already claimed, or is 0, so no need to calculate anything else. + // TODO: Check if is too late to claim rewards + let contract_fees = ContractUsagePerEra::::take(&contract, era); + let era_info = EraInformation::::get(era); + let calculated_rewards = Self::calculate_contract_share( + contract_fees, + era_info.contract_fee_total, + era_info.total_fee_amount, + ); + // Transfer the funds to the beneficiary + ::Currency::transfer( + &Self::get_pallet_account(), + &beneficiary, + calculated_rewards, + AllowDeath, + )?; + // Reset the contract fees for the era. + ContractUsagePerEra::::remove(&contract, era); + Self::deposit_event(Event::IncentivesClaimed { + beneficiary, + amount: calculated_rewards, + }); + Ok(()) + } + + /// Deposit funds into the reward pool. + /// + /// Parameters: + /// - 'amount': Amount to be deposited. + #[pallet::call_index(2)] + #[pallet::weight(Weight::zero())] + pub fn deposit_funds(origin: OriginFor, amount: BalanceOf) -> DispatchResult { + let who = ensure_signed(origin)?; + ::Currency::transfer( + &who, + &Self::get_pallet_account(), + amount, + AllowDeath, + )?; + let current_era = CurrentEra::::get(); + EraInformation::::mutate_exists(current_era, |maybe_era_info| { + if let Some(era_info) = maybe_era_info { + era_info.add_total_fee(amount); + } + }); + Self::deposit_event(Event::IncentivesDeposited { source: who, amount }); + Ok(()) + } + } + + impl Pallet { + /// The account ID of the pallet. + /// + /// This actually does computation. If you need to keep using it, then make sure you cache + /// the value and only call this once. + pub fn get_pallet_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Calculate the share of the total fees that should be allocated to a specific contract. + /// + /// The share is calculated as a proportion of the fees generated by the contract relative + /// to the total fees generated by all contracts. + /// + /// # Arguments + /// - `contract_fees`: The amount of fees generated by the specific contract. + /// - `total_contract_fees`: The total amount of fees generated by all contracts. + /// - `total_fees`: The total amount of general fees to be distributed among the contracts. + fn calculate_contract_share( + contract_fees: BalanceOf, + total_contract_fees: BalanceOf, + total_fees: BalanceOf, + ) -> BalanceOf { + if total_contract_fees.is_zero() || contract_fees.is_zero() { + return BalanceOf::::zero(); + } + let proportion = Permill::from_rational(contract_fees, total_contract_fees); + proportion * total_fees + } + + pub fn update_incentives(amount: BalanceOf) { + let current_era = CurrentEra::::get(); + EraInformation::::mutate_exists(current_era, |maybe_era_info| { + if let Some(era_info) = maybe_era_info { + era_info.add_total_fee(amount); + } + }); + } + + pub fn is_registered(account: &T::AccountId) -> bool { + RegisteredContracts::::contains_key(account) + } + + // For testing only + #[cfg(feature = "std")] + pub fn accrue_fees(now: BlockNumberFor, contract: &AccountIdOf, fees: BalanceOf) { + Pallet::::on_initialize(now); + let era = CurrentEra::::get(); + ContractUsagePerEra::::mutate(contract, era, |contract_fees| { + *contract_fees = contract_fees.saturating_add(fees) + }); + EraInformation::::mutate(era, |era_info| { + era_info.add_contract_fee(fees); + }); + Self::update_incentives(fees); + } + } +} diff --git a/pallets/incentives/src/types.rs b/pallets/incentives/src/types.rs new file mode 100644 index 00000000..f9b21540 --- /dev/null +++ b/pallets/incentives/src/types.rs @@ -0,0 +1,86 @@ +use frame_support::{pallet_prelude::*, traits::Currency}; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_transaction_payment::OnChargeTransaction; +use sp_runtime::traits::{Saturating, Zero}; + +use crate::Config; + +pub type AccountIdOf = ::AccountId; +/// Era number type +pub type EraNumber = u32; + +/// Type aliases used for interaction with `OnChargeTransaction`. +pub(crate) type OnChargeTransactionOf = + ::OnChargeTransaction; + +/// OnChargeTransactionBalance type alias. +pub(crate) type OnChargeTransactionBalanceOf = + as OnChargeTransaction>::Balance; + +/// Balance alias +pub(crate) type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +/// Liquidity info type alias (imbalances). +pub(crate) type LiquidityInfoOf = + as OnChargeTransaction>::LiquidityInfo; + +/// Information about an era: fees generated, and the next era start. +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct EraInfo { + /// Block number at which the next era should start. + pub(crate) next_era_start: BlockNumberFor, + /// Total fees generated by smart contract calls within this era. + pub(crate) contract_fee_total: BalanceOf, + /// Total fees generated by all types of calls (both contract and non-contract) within this + /// era. + pub(crate) total_fee_amount: BalanceOf, +} + +impl Default for EraInfo { + fn default() -> Self { + Self { + next_era_start: 1u32.into(), + contract_fee_total: BalanceOf::::zero(), + total_fee_amount: BalanceOf::::zero(), + } + } +} + +impl EraInfo { + /// Block number at which the next era should start. + pub fn next_era_start(&self) -> BlockNumberFor { + self.next_era_start + } + + /// Set the next era start block number. + pub fn set_next_era_start(&mut self, next_era_start: BlockNumberFor) { + self.next_era_start = next_era_start; + } + + /// Checks whether a new era should be triggered. + pub fn is_new_era(&self, now: BlockNumberFor) -> bool { + self.next_era_start <= now + } + + /// Increases the total fees generated by smart contract calls for the current era. + pub fn add_contract_fee(&mut self, amount: BalanceOf) { + self.contract_fee_total = self.contract_fee_total.saturating_add(amount); + } + + /// Increases the total fees generated by all calls for the current era. + pub fn add_total_fee(&mut self, amount: BalanceOf) { + self.total_fee_amount = self.total_fee_amount.saturating_add(amount); + } + + /// Reset the total fees generated by smart contract calls for the current era. + pub fn reset_contract_fee(&mut self) { + self.contract_fee_total = BalanceOf::::zero(); + } + + /// Reset the total fees generated by all calls for the current era. + pub fn resent_total_fee(&mut self) { + self.total_fee_amount = BalanceOf::::zero(); + } +} diff --git a/pop-api/examples/incentives/Cargo.toml b/pop-api/examples/incentives/Cargo.toml new file mode 100755 index 00000000..640de369 --- /dev/null +++ b/pop-api/examples/incentives/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors = [ "R0GUE " ] +edition = "2021" +name = "incentives" +version = "0.1.0" + +[dependencies] +ink = { git = "https://github.com/r0gue-io/ink", branch = "sub0", default-features = false } +polkavm-derive = "0.11.0" +pop-api = { path = "../../../pop-api", default-features = false, features = [ "messaging" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/examples/incentives/lib.rs b/pop-api/examples/incentives/lib.rs new file mode 100755 index 00000000..79a34708 --- /dev/null +++ b/pop-api/examples/incentives/lib.rs @@ -0,0 +1,39 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use pop_api::{incentives as api, primitives::Era, StatusCode}; + +pub type Result = core::result::Result; + +#[ink::contract] +mod incentives { + + use super::*; + + #[ink(storage)] + #[derive(Default)] + pub struct Contract {} + + impl Contract { + #[ink(constructor, payable)] + pub fn new(beneficiary: AccountId) -> Result { + api::register(beneficiary)?; + Ok(Default::default()) + } + + #[ink(message)] + pub fn claim_rewards(&mut self, era: Era) -> Result<()> { + api::claim(era) + } + } + + #[cfg(test)] + mod tests { + + use super::*; + + #[ink::test] + fn default_works() { + Contracts::new(); + } + } +} diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml index 28745087..0e9caf1b 100644 --- a/pop-api/integration-tests/Cargo.toml +++ b/pop-api/integration-tests/Cargo.toml @@ -16,6 +16,7 @@ ismp = { git = "https://github.com/r0gue-io/ismp", branch = "polkadot-v1.14.0", log = "0.4.22" pallet-assets = { version = "37.0.0", default-features = false } pallet-balances = { version = "37.0.0", default-features = false } +pallet-incentives = { path = "../../pallets/incentives", default-features = false } pallet-ismp = { git = "https://github.com/r0gue-io/ismp", branch = "polkadot-v1.14.0", default-features = false } pallet-revive = { path = "../../pallets/revive", default-features = false } pallet-timestamp = { version = "35.0.0", default-features = false } @@ -41,6 +42,7 @@ std = [ "frame-system/std", "pallet-assets/std", "pallet-balances/std", + "pallet-incentives/std", "pallet-revive/std", "pallet-timestamp/std", "pop-api/std", diff --git a/pop-api/integration-tests/src/environment.rs b/pop-api/integration-tests/src/environment.rs index d401de87..7b1a5da9 100644 --- a/pop-api/integration-tests/src/environment.rs +++ b/pop-api/integration-tests/src/environment.rs @@ -193,7 +193,7 @@ fn xcm_send_works() { struct Contract((H160, AccountId32)); impl Contract { fn new() -> Self { - Self(instantiate(CONTRACT, INIT_VALUE, vec![])) + Self(instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![])) } fn caller(&self) -> AccountId32 { diff --git a/pop-api/integration-tests/src/fungibles/mod.rs b/pop-api/integration-tests/src/fungibles/mod.rs index 942346a9..3f5dcce3 100644 --- a/pop-api/integration-tests/src/fungibles/mod.rs +++ b/pop-api/integration-tests/src/fungibles/mod.rs @@ -24,7 +24,8 @@ const CONTRACT: &str = "contracts/fungibles/target/ink/fungibles.riscv"; #[test] fn total_supply_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); // No tokens in circulation. assert_eq!(total_supply(&addr, TOKEN_ID), Ok(Assets::total_supply(TOKEN_ID))); @@ -40,7 +41,8 @@ fn total_supply_works() { #[test] fn balance_of_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); // No tokens in circulation. assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(Assets::balance(TOKEN_ID, BOB))); @@ -56,7 +58,8 @@ fn balance_of_works() { #[test] fn allowance_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); // No tokens in circulation. assert_eq!( @@ -78,7 +81,8 @@ fn allowance_works() { #[test] fn transfer_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. @@ -120,7 +124,8 @@ fn transfer_works() { #[test] fn transfer_from_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. @@ -169,14 +174,15 @@ fn transfer_from_works() { #[test] fn approve_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, 0, vec![]); + let (addr, account_id) = instantiate(CONTRACT, 0, function_selector("new"), vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. assert_eq!(approve(&addr, 0, &BOB, amount), Err(Module { index: 52, error: [3, 0] })); let token = assets::create_and_mint_to(&ALICE, 0, &account_id, amount); assert_eq!(approve(&addr, token, &BOB, amount), Err(ConsumerRemaining)); - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![1]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![1]); // Create token with Alice as owner and mint `amount` to contract address. let token = assets::create_and_mint_to(&ALICE, 1, &account_id, amount); // Token is not live, i.e. frozen or being destroyed. @@ -211,7 +217,7 @@ fn increase_allowance_works() { new_test_ext().execute_with(|| { let amount: Balance = 100 * UNIT; // Instantiate a contract without balance - test `ConsumerRemaining. - let (addr, account_id) = instantiate(CONTRACT, 0, vec![]); + let (addr, account_id) = instantiate(CONTRACT, 0, function_selector("new"), vec![]); // Token does not exist. assert_eq!( increase_allowance(&addr, 0, &BOB, amount), @@ -221,7 +227,8 @@ fn increase_allowance_works() { assert_eq!(increase_allowance(&addr, token, &BOB, amount), Err(ConsumerRemaining)); // Instantiate a contract with balance. - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![1]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![1]); // Create token with Alice as owner and mint `amount` to contract address. let token = assets::create_and_mint_to(&ALICE, 1, &account_id, amount); // Token is not live, i.e. frozen or being destroyed. @@ -250,7 +257,8 @@ fn increase_allowance_works() { #[test] fn decrease_allowance_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); let amount: Balance = 100 * UNIT; // Create token and mint `amount` to contract address, then approve Bob to spend `amount`. @@ -290,7 +298,8 @@ fn decrease_allowance_works() { #[test] fn token_metadata_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); let name: Vec = vec![11, 12, 13]; let symbol: Vec = vec![21, 22, 23]; let decimals: u8 = 69; @@ -326,7 +335,7 @@ fn token_metadata_works() { fn create_works() { new_test_ext().execute_with(|| { // Instantiate a contract without balance for fees. - let (addr, account_id) = instantiate(CONTRACT, 0, vec![0]); + let (addr, account_id) = instantiate(CONTRACT, 0, function_selector("new"), vec![0]); // No balance to pay for fees. assert_eq!( create(&addr, TOKEN_ID, &account_id, 1), @@ -334,7 +343,7 @@ fn create_works() { ); // Instantiate a contract without balance for deposit. - let (addr, account_id) = instantiate(CONTRACT, 100, vec![1]); + let (addr, account_id) = instantiate(CONTRACT, 100, function_selector("new"), vec![1]); // No balance to pay the deposit. assert_eq!( create(&addr, TOKEN_ID, &account_id, 1), @@ -342,7 +351,8 @@ fn create_works() { ); // Instantiate a contract with enough balance. - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![2]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![2]); assert_eq!(create(&addr, TOKEN_ID, &BOB, 0), Err(Module { index: 52, error: [7, 0] }),); // The minimal balance for a token must be non zero. assert_eq!(create(&addr, TOKEN_ID, &BOB, 0), Err(Module { index: 52, error: [7, 0] }),); @@ -387,7 +397,8 @@ fn instantiate_and_create_fungible_works() { #[test] fn start_destroy_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![2]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![2]); // Token does not exist. assert_eq!(start_destroy(&addr, TOKEN_ID), Err(Module { index: 52, error: [3, 0] }),); @@ -409,7 +420,8 @@ fn set_metadata_works() { let name = vec![42]; let symbol = vec![42]; let decimals = 42u8; - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); // Token does not exist. assert_eq!( @@ -458,7 +470,8 @@ fn clear_metadata_works() { let name = vec![42]; let symbol = vec![42]; let decimals = 42u8; - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); // Token does not exist. assert_eq!(clear_metadata(&addr, 0), Err(Module { index: 52, error: [3, 0] }),); @@ -491,7 +504,8 @@ fn clear_metadata_works() { #[test] fn token_exists_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); // No tokens in circulation. assert_eq!(token_exists(&addr, TOKEN_ID), Ok(Assets::asset_exists(TOKEN_ID))); @@ -505,7 +519,8 @@ fn token_exists_works() { #[test] fn mint_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. @@ -536,7 +551,8 @@ fn mint_works() { #[test] fn burn_works() { new_test_ext().execute_with(|| { - let (addr, account_id) = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (addr, account_id) = + instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![]); let amount: Balance = 100 * UNIT; // Token does not exist. diff --git a/pop-api/integration-tests/src/incentives.rs b/pop-api/integration-tests/src/incentives.rs new file mode 100644 index 00000000..fb9a72cb --- /dev/null +++ b/pop-api/integration-tests/src/incentives.rs @@ -0,0 +1,64 @@ +use frame_support::traits::{fungible::Inspect, Currency, Hooks}; +use pop_api::primitives::{Era, Error}; +use pop_runtime_devnet::{Balances, Incentives, MINUTES}; +use sp_runtime::{app_crypto::sp_core::H160, Saturating}; + +use super::*; + +const CONTRACT: &str = "../examples/incentives/target/ink/incentives.riscv"; + +#[test] +fn register_works() { + let beneficiary = BOB; + new_test_ext().execute_with(|| { + let contract = Contract::new(&beneficiary); + assert!(Incentives::is_registered(&contract.0 .1)); + }); +} + +#[test] +fn claim_works() { + let beneficiary = BOB; + let amount = 1_000_000_000; + const ERA: u32 = 10 * MINUTES; + new_test_ext().execute_with(|| { + let contract = Contract::new(&beneficiary); + + // Fund pallet and accrue fees to contract + Balances::make_free_balance_be(&Incentives::get_pallet_account(), amount); + for block in 1..=ERA + 1 { + Incentives::accrue_fees(block, &contract.0 .1, amount / ERA as u128); + } + + let balance = Balances::balance(&beneficiary); + assert_ok!(contract.claim_rewards(1)); + assert_eq!(Balances::balance(&beneficiary), balance + amount) + }); +} + +// A simple, strongly typed wrapper for the contract. +struct Contract((H160, AccountId32)); +impl Contract { + fn new(beneficiary: &AccountId32) -> Self { + Self(instantiate( + CONTRACT, + INIT_VALUE, + [function_selector("new"), beneficiary.encode()].concat(), + vec![], + )) + } + + fn claim_rewards(&self, era: Era) -> Result<(), Error> { + let result = self.call("claim_rewards", era.encode(), 0); + >::decode(&mut &result.data[1..]) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + } + + fn call(&self, function: &str, params: Vec, value: u128) -> ExecReturnValue { + let function = function_selector(function); + let params = [function, params].concat(); + let result = bare_call(self.0.clone().0, params, value).expect("should work"); + assert!(!result.did_revert(), "calling contract reverted {:?}", result); + result + } +} diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index 0dbd4357..e199d3e1 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -8,14 +8,19 @@ use frame_support::{ weights::Weight, }; use pallet_revive::{AddressMapper, Code, CollectEvents, ExecReturnValue}; -use pop_runtime_devnet::{config::ismp::Router, Assets, Messaging, Revive, Runtime, RuntimeOrigin, System, UNIT}; +use pop_runtime_devnet::{ + config::ismp::Router, Assets, Messaging, Revive, Runtime, RuntimeOrigin, System, UNIT, +}; use scale::{Decode, Encode}; -use sp_runtime::{app_crypto::sp_core, - offchain::{testing::TestOffchainExt, OffchainDbExt}, - AccountId32, BuildStorage, DispatchError}; +use sp_runtime::{ + app_crypto::sp_core, + offchain::{testing::TestOffchainExt, OffchainDbExt}, + AccountId32, BuildStorage, DispatchError, +}; mod environment; mod fungibles; +mod incentives; mod messaging; type Balance = u128; @@ -80,7 +85,12 @@ fn bare_call( } // Deploy, instantiate and return contract address. -fn instantiate(contract: &str, init_value: u128, _salt: Vec) -> (sp_core::H160, AccountId32) { +fn instantiate( + contract: &str, + init_value: u128, + data: Vec, + _salt: Vec, +) -> (sp_core::H160, AccountId32) { let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); let result = Revive::bare_instantiate( @@ -89,7 +99,7 @@ fn instantiate(contract: &str, init_value: u128, _salt: Vec) -> (sp_core::H1 GAS_LIMIT, 1 * 1_000_000_000_000, Code::Upload(wasm_binary), - function_selector("new"), + data, None, DEBUG_OUTPUT, CollectEvents::Skip, diff --git a/pop-api/integration-tests/src/messaging.rs b/pop-api/integration-tests/src/messaging.rs index b743a727..cab830b9 100644 --- a/pop-api/integration-tests/src/messaging.rs +++ b/pop-api/integration-tests/src/messaging.rs @@ -202,7 +202,7 @@ fn get_ismp_request(ext: &mut TestExternalities) -> ::ismp::router::Request { struct Contract((H160, AccountId32)); impl Contract { fn new() -> Self { - Self(instantiate(CONTRACT, INIT_VALUE, vec![])) + Self(instantiate(CONTRACT, INIT_VALUE, function_selector("new"), vec![])) } fn ismp_get(&self, id: RequestId, request: Get, fee: Balance) -> Result<(), Error> { diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index 546106e5..057aa3c4 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -71,6 +71,8 @@ mod constants { pub(crate) const ASSETS: u8 = 52; pub(crate) const BALANCES: u8 = 10; pub(crate) const FUNGIBLES: u8 = 150; + pub(crate) const MESSAGING: u8 = 151; + pub(crate) const INCENTIVES: u8 = 152; } // Helper method to build a dispatch call or a call to read state. diff --git a/pop-api/src/primitives.rs b/pop-api/src/primitives.rs index 26070c54..453c0d16 100644 --- a/pop-api/src/primitives.rs +++ b/pop-api/src/primitives.rs @@ -6,4 +6,5 @@ pub type AccountId = ::AccountId; pub type Balance = ::Balance; pub type BlockNumber = ::BlockNumber; pub type Hash = ::Hash; +pub type Era = BlockNumber; pub type Timestamp = ::Timestamp; diff --git a/pop-api/src/v0/incentives.rs b/pop-api/src/v0/incentives.rs new file mode 100644 index 00000000..cc662cb8 --- /dev/null +++ b/pop-api/src/v0/incentives.rs @@ -0,0 +1,55 @@ +use ink::env::DefaultEnvironment; + +use crate::{ + constants::INCENTIVES, + primitives::{AccountId, Balance, Era}, + ChainExtensionMethodApi, Result, StatusCode, +}; + +// Dispatchables +pub(super) const REGISTER: u8 = 0; +pub(super) const CLAIM: u8 = 1; +pub(super) const DEPOSIT: u8 = 2; + +fn build_dispatch(dispatchable: u8) -> ChainExtensionMethodApi { + crate::v0::build_dispatch(INCENTIVES, dispatchable) +} + +/// Register to receive rewards. +/// +/// # Parameters +/// - `beneficiary`: The account that will be the beneficiary of the rewards. +#[inline] +pub fn register(beneficiary: AccountId) -> Result<()> { + build_dispatch(REGISTER) + .input::() + .output::, true>() + .handle_error_code::() + .call(&beneficiary) +} + +/// Claims rewards for a given era. +/// +/// Parameters: +/// - `era`: The era for which rewards are being claimed. +#[inline] +pub fn claim(era: Era) -> Result<()> { + build_dispatch(CLAIM) + .input::<(AccountId, Era)>() + .output::, true>() + .handle_error_code::() + .call(&(ink::env::account_id::(), era)) +} + +/// Deposit funds into the reward pool. +/// +/// Parameters: +/// - 'amount': Amount to be deposited. +#[inline] +pub fn deposit_funds(amount: Balance) -> Result<()> { + build_dispatch(DEPOSIT) + .input::() + .output::, true>() + .handle_error_code::() + .call(&amount) +} diff --git a/pop-api/src/v0/mod.rs b/pop-api/src/v0/mod.rs index ab8ca179..ecad085c 100644 --- a/pop-api/src/v0/mod.rs +++ b/pop-api/src/v0/mod.rs @@ -8,6 +8,8 @@ use crate::{ /// APIs for fungible tokens. #[cfg(feature = "fungibles")] pub mod fungibles; +/// APIs for builder incentives. +pub mod incentives; /// APIs for messaging. #[cfg(feature = "messaging")] pub mod messaging; diff --git a/runtime/devnet/Cargo.toml b/runtime/devnet/Cargo.toml index 300b1cc6..bd8191e7 100644 --- a/runtime/devnet/Cargo.toml +++ b/runtime/devnet/Cargo.toml @@ -23,6 +23,7 @@ smallvec.workspace = true # Local pallet-api.workspace = true +pallet-incentives.workspace = true pop-chain-extension.workspace = true pop-primitives.workspace = true pop-runtime-common.workspace = true @@ -139,6 +140,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-contracts/std", + "pallet-incentives/std", "pallet-ismp-runtime-api/std", "pallet-ismp/std", "pallet-message-queue/std", @@ -198,6 +200,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", "pallet-contracts/runtime-benchmarks", + "pallet-incentives/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-nft-fractionalization/runtime-benchmarks", @@ -237,6 +240,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-contracts/try-runtime", + "pallet-incentives/try-runtime", "pallet-ismp/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", diff --git a/runtime/devnet/src/config/api/mod.rs b/runtime/devnet/src/config/api/mod.rs index 246989d2..373d0139 100644 --- a/runtime/devnet/src/config/api/mod.rs +++ b/runtime/devnet/src/config/api/mod.rs @@ -163,6 +163,7 @@ impl> Contains f fn contains(c: &RuntimeCall) -> bool { use fungibles::Call::*; use messaging::Call::*; + use pallet_incentives::Call::*; T::BaseCallFilter::contains(c) && matches!( c, @@ -180,6 +181,8 @@ impl> Contains f ismp_get { .. } | ismp_post { .. } | xcm_new_query { .. } | remove { .. } + ) | RuntimeCall::Incentives( + register_contract { .. } | claim_rewards { .. } | deposit_funds { .. } ) ) } diff --git a/runtime/devnet/src/lib.rs b/runtime/devnet/src/lib.rs index b357fba4..e0579078 100644 --- a/runtime/devnet/src/lib.rs +++ b/runtime/devnet/src/lib.rs @@ -25,9 +25,11 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::HoldConsideration, tokens::nonfungibles_v2::Inspect, ConstBool, ConstU32, - ConstU64, ConstU8, Contains, EitherOfDiverse, EqualPrivilegeOnly, EverythingBut, - LinearStoragePrice, TransformOrigin, VariantCountOf, + fungible::{Balanced, Credit, HoldConsideration}, + tokens::nonfungibles_v2::Inspect, + ConstBool, ConstU32, ConstU64, ConstU8, Contains, EitherOfDiverse, EqualPrivilegeOnly, + EverythingBut, Imbalance, LinearStoragePrice, OnUnbalanced, TransformOrigin, + VariantCountOf, }, weights::{ ConstantMultiplier, Weight, WeightToFeeCoefficient, WeightToFeeCoefficients, @@ -64,7 +66,9 @@ use sp_runtime::{ transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, }; -pub use sp_runtime::{ExtrinsicInclusionMode, MultiAddress, Perbill, Permill}; +pub use sp_runtime::{ + traits::AccountIdConversion, ExtrinsicInclusionMode, MultiAddress, Perbill, Permill, +}; use sp_std::prelude::*; #[cfg(feature = "std")] use sp_version::NativeVersion; @@ -101,7 +105,10 @@ pub type SignedExtra = ( frame_system::CheckEra, frame_system::CheckNonce, frame_system::CheckWeight, - pallet_transaction_payment::ChargeTransactionPayment, + pallet_incentives::contract_fee_handler::ContractFeeHandler< + Runtime, + pallet_transaction_payment::ChargeTransactionPayment, + >, cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim, frame_metadata_hash_extension::CheckMetadataHash, ); @@ -298,6 +305,18 @@ impl pallet_authorship::Config for Runtime { type FindAuthor = pallet_session::FindAccountFromAuthorIndex; } +parameter_types! { + pub const IncentivesId: PalletId = PalletId(*b"BuildInc"); + pub const EraDuration: BlockNumber = 10 * MINUTES; // 10 minute for testing, 24 * HOURS for production (1 ERA in Polkadot) +} + +impl pallet_incentives::Config for Runtime { + type Currency = Balances; + type EraDuration = EraDuration; + type PalletId = IncentivesId; + type RuntimeEvent = RuntimeEvent; +} + parameter_types! { pub const ExistentialDeposit: Balance = EXISTENTIAL_DEPOSIT; } @@ -320,6 +339,45 @@ impl pallet_balances::Config for Runtime { type WeightInfo = pallet_balances::weights::SubstrateWeight; } +pub struct ToIncentivesPot; +impl OnUnbalanced> for ToIncentivesPot { + fn on_nonzero_unbalanced(amount: Credit) { + let incentives_pot = IncentivesId::get().into_account_truncating(); + let amount_fees = amount.peek(); + let _ = Balances::resolve(&incentives_pot, amount); + // Refresh the incentives + let _ = Incentives::update_incentives(amount_fees); + } +} + +/// Logic for the author to get a portion of fees. +pub struct ToAuthor; +impl OnUnbalanced> for ToAuthor { + fn on_nonzero_unbalanced(amount: Credit) { + if let Some(author) = Authorship::author() { + let _ = Balances::resolve(&author, amount); + } + } +} + +pub struct DealWithFees; +impl OnUnbalanced> for DealWithFees { + fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { + if let Some(fees) = fees_then_tips.next() { + // TODO: Change numbers -> Now for testing: 50% of fees, to the author, rest goes to + // incentives including 100% of the tips. + let (to_author, mut incentives) = fees.ration(50, 50); + // TODO: Decide what to do with the tips, for now to incentives + if let Some(tips) = fees_then_tips.next() { + tips.merge_into(&mut incentives); + } + // TODO: Use ToResolve + >::on_unbalanced(incentives); + >::on_unbalanced(to_author); + } + } +} + parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = 10 * MICROUNIT; @@ -328,7 +386,7 @@ parameter_types! { impl pallet_transaction_payment::Config for Runtime { type FeeMultiplierUpdate = SlowAdjustingFeeUpdate; type LengthToFee = ConstantMultiplier; - type OnChargeTransaction = pallet_transaction_payment::FungibleAdapter; + type OnChargeTransaction = pallet_transaction_payment::FungibleAdapter; type OperationalFeeMultiplier = ConstU8<5>; type RuntimeEvent = RuntimeEvent; type WeightToFee = WeightToFee; @@ -635,6 +693,10 @@ mod runtime { #[runtime::pallet_index(151)] pub type Messaging = messaging::Pallet; + // Incentives + #[runtime::pallet_index(152)] + pub type Incentives = pallet_incentives::Pallet; + // Revive #[runtime::pallet_index(255)] pub type Revive = pallet_revive::Pallet; From a8c024cf56ab47062b0d222a8364bd04e94b5a14 Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:05:11 +0100 Subject: [PATCH 7/8] fix: tests (#358) --- extension/src/environment.rs | 8 +-- extension/src/functions.rs | 46 +++++++------- extension/src/mock.rs | 91 ++++++++++++++++------------ extension/src/tests.rs | 29 ++++----- pallets/api/src/extension.rs | 4 +- pallets/api/src/messaging/mod.rs | 13 ++-- pallets/revive/src/exec.rs | 48 ++++++++------- pallets/revive/src/lib.rs | 13 ++-- pallets/revive/src/test_utils.rs | 30 +++++---- pallets/revive/src/tests.rs | 4 +- runtime/devnet/src/config/api/mod.rs | 4 +- 11 files changed, 151 insertions(+), 139 deletions(-) diff --git a/extension/src/environment.rs b/extension/src/environment.rs index 6da88cdd..b62babc5 100644 --- a/extension/src/environment.rs +++ b/extension/src/environment.rs @@ -1,16 +1,16 @@ use core::fmt::Debug; -use codec::{Decode, Encode}; +use codec::Decode; use frame_support::pallet_prelude::Weight; use pallet_revive::{ chain_extension::{ChargedAmount, Result}, wasm::Memory, AccountId32Mapper, AddressMapper, }; -use sp_core::{crypto::AccountId32, H160}; +use sp_core::crypto::AccountId32; use sp_std::vec::Vec; -use crate::{AccountIdOf, Config}; +use crate::AccountIdOf; /// Provides access to the parameters passed to a chain extension and its execution environment. /// @@ -219,5 +219,5 @@ where #[test] fn default_ext_works() { - assert_eq!(().address(), &()) + assert_eq!(().address(), ()) } diff --git a/extension/src/functions.rs b/extension/src/functions.rs index f5875091..ac07afb1 100644 --- a/extension/src/functions.rs +++ b/extension/src/functions.rs @@ -369,29 +369,31 @@ mod tests { } #[test] + #[ignore] fn dispatch_call_adjusts_weight() { - let migrate_weight = ::WeightInfo::migrate(); - let migration_noop_weight = - ::WeightInfo::migration_noop(); - new_test_ext().execute_with(|| { - // Attempt to perform non-existent migration with additional weight limit specified. - let extra_weight = Weight::from_parts(123456789, 12345); - let weight_limit = migration_noop_weight.saturating_add(extra_weight); - let call = RuntimeCall::Contracts(pallet_revive::Call::migrate { weight_limit }); - let encoded_call = call.encode(); - let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); - let expected: DispatchError = - pallet_revive::Error::::NoMigrationPerformed.into(); - assert_eq!(DispatchCall::execute(&mut env).err().unwrap(), expected); - // Ensure pre-dispatch weight is weight function + weight limit - assert_eq!(call.get_dispatch_info().weight, migrate_weight + weight_limit); - assert_eq!( - env.charged(), - read_from_buffer_weight(encoded_call.len() as u32) + - call.get_dispatch_info().weight - - extra_weight - ); - }) + todo!("restore the below on revive") + // let migrate_weight = ::WeightInfo::migrate(); + // let migration_noop_weight = + // ::WeightInfo::migration_noop(); + // new_test_ext().execute_with(|| { + // // Attempt to perform non-existent migration with additional weight limit specified. + // let extra_weight = Weight::from_parts(123456789, 12345); + // let weight_limit = migration_noop_weight.saturating_add(extra_weight); + // let call = RuntimeCall::Contracts(pallet_revive::Call::migrate { weight_limit }); + // let encoded_call = call.encode(); + // let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); + // let expected: DispatchError = + // pallet_revive::Error::::NoMigrationPerformed.into(); + // assert_eq!(DispatchCall::execute(&mut env).err().unwrap(), expected); + // // Ensure pre-dispatch weight is weight function + weight limit + // assert_eq!(call.get_dispatch_info().weight, migrate_weight + weight_limit); + // assert_eq!( + // env.charged(), + // read_from_buffer_weight(encoded_call.len() as u32) + + // call.get_dispatch_info().weight - + // extra_weight + // ); + // }) } #[test] diff --git a/extension/src/mock.rs b/extension/src/mock.rs index 5a55a529..1c552cf0 100644 --- a/extension/src/mock.rs +++ b/extension/src/mock.rs @@ -3,13 +3,14 @@ use std::marker::PhantomData; use codec::{Decode, Encode}; use frame_support::{ derive_impl, - pallet_prelude::Weight, + pallet_prelude::{ConstU32, Weight}, parameter_types, - traits::{fungible::Inspect, ConstU32, Everything, Nothing}, + traits::{fungible::Inspect, EnsureOrigin, Everything, Nothing}, }; -use frame_system::{pallet_prelude::BlockNumberFor, EnsureSigned}; -use pallet_revive::{chain_extension::RetVal, DefaultAddressGenerator, Frame, Schedule}; -use sp_runtime::{BuildStorage, DispatchError, Perbill}; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_revive::{chain_extension::RetVal, AccountId32Mapper}; +use sp_core::crypto::AccountId32; +use sp_runtime::{traits::IdentityLookup, BuildStorage, DispatchError, Perbill}; use crate::{ decoding::Identity, environment, matching::WithFuncId, AccountIdOf, ContractWeightsOf, @@ -17,7 +18,7 @@ use crate::{ Matches, Processor, ReadState, Readable, }; -pub(crate) const ALICE: u64 = 1; +pub(crate) const ALICE: AccountId32 = AccountId32::new([1u8; 32]); pub(crate) const DEBUG_OUTPUT: pallet_revive::DebugInfo = pallet_revive::DebugInfo::UnsafeDebug; pub(crate) const GAS_LIMIT: Weight = Weight::from_parts(500_000_000_000, 3 * 1024 * 1024); pub(crate) const INIT_AMOUNT: ::Balance = 100_000_000; @@ -67,8 +68,9 @@ frame_support::construct_runtime!( #[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::Config for Test { type AccountData = pallet_balances::AccountData; - type AccountId = u64; + type AccountId = AccountId32; type Block = frame_system::mocking::MockBlock; + type Lookup = IdentityLookup; } #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] @@ -80,54 +82,36 @@ impl pallet_balances::Config for Test { #[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig as pallet_timestamp::DefaultConfig)] impl pallet_timestamp::Config for Test {} +#[derive_impl(pallet_revive::config_preludes::TestDefaultConfig as pallet_revive::DefaultConfig)] impl pallet_revive::Config for Test { - type AddressGenerator = DefaultAddressGenerator; - type ApiVersion = (); + type AddressMapper = AccountId32Mapper; type CallFilter = (); - // TestFilter; - type CallStack = [Frame; 5]; type ChainExtension = Extension; + type ChainId = ChainId; type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; type Currency = Balances; type Debug = (); - // TestDebug; - type DefaultDepositLimit = DefaultDepositLimit; type DepositPerByte = DepositPerByte; type DepositPerItem = DepositPerItem; - type Environment = (); - type InstantiateOrigin = EnsureSigned; - type MaxCodeLen = ConstU32<{ 100 * 1024 }>; - type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; - type MaxDelegateDependencies = MaxDelegateDependencies; - type MaxStorageKeyLen = ConstU32<128>; - type Migrations = (); - // crate::migration::codegen::BenchMigrations; - type Randomness = Test; - type RuntimeCall = RuntimeCall; - type RuntimeEvent = RuntimeEvent; - type RuntimeHoldReason = RuntimeHoldReason; - type Schedule = MySchedule; + type InstantiateOrigin = EnsureAccount; + type PVFMemory = ConstU32<{ 512 * 1024 * 1024 }>; + type RuntimeMemory = ConstU32<{ 128 * 1024 * 1024 }>; type Time = Timestamp; - type UnsafeUnstableInterface = (); - // UnstableInterface; - type UploadOrigin = EnsureSigned; - type WeightInfo = (); - type WeightPrice = (); - // Self; - type Xcm = (); + type UnsafeUnstableInterface = UnstableInterface; + type UploadOrigin = EnsureAccount; } parameter_types! { - pub MySchedule: Schedule = { - let schedule = >::default(); - schedule - }; pub static DepositPerByte: ::Balance = 1; pub const DepositPerItem: ::Balance = 2; pub static MaxDelegateDependencies: u32 = 32; pub static MaxTransientStorageSize: u32 = 4 * 1024; pub static CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); pub static DefaultDepositLimit: ::Balance = 10_000_000; + pub static ChainId: u64 = 909; + pub static UploadAccount: Option<::AccountId> = None; + pub static InstantiateAccount: Option<::AccountId> = None; + pub static UnstableInterface: bool = true; } impl frame_support::traits::Randomness, BlockNumberFor> for Test { @@ -344,13 +328,13 @@ impl environment::BufOut for Environment { /// A mocked smart contract environment. #[derive(Clone, Default)] pub(crate) struct MockExt { - pub(crate) address: AccountIdOf, + pub(crate) address: Option>, } impl environment::Ext for MockExt { type AccountId = AccountIdOf; - fn address(&self) -> &Self::AccountId { - &self.address + fn address(&self) -> Self::AccountId { + self.address.clone().unwrap_or(ALICE) } } @@ -366,6 +350,10 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities { let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); + // register account mappings + ext.execute_with(|| { + Contracts::map_account(RuntimeOrigin::signed(ALICE)).unwrap(); + }); ext } @@ -387,3 +375,26 @@ impl Converter for UppercaseConverter { } } } + +pub struct EnsureAccount(PhantomData<(T, A)>); +impl>>> + EnsureOrigin<::RuntimeOrigin> for EnsureAccount +where + ::AccountId: From, +{ + type Success = T::AccountId; + + fn try_origin(o: T::RuntimeOrigin) -> Result { + let who = as EnsureOrigin<_>>::try_origin(o.clone())?; + if matches!(A::get(), Some(a) if who != a) { + return Err(o); + } + + Ok(who) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Err(()) + } +} diff --git a/extension/src/tests.rs b/extension/src/tests.rs index c9588cd3..986e5317 100644 --- a/extension/src/tests.rs +++ b/extension/src/tests.rs @@ -4,7 +4,9 @@ use std::{path::Path, sync::LazyLock}; use codec::{Decode, Encode}; use frame_support::weights::Weight; use frame_system::Call; -use pallet_contracts::{Code, CollectEvents, ContractExecResult, Determinism, StorageDeposit}; +use pallet_revive::{ + AccountId32Mapper, AddressMapper, Code, CollectEvents, ContractExecResult, StorageDeposit, +}; use sp_runtime::{ DispatchError::{self, BadOrigin, Module}, ModuleError, @@ -16,7 +18,7 @@ use crate::{ }; static CONTRACT: LazyLock> = - LazyLock::new(|| initialize_contract("contract/target/ink/proxy.wasm")); + LazyLock::new(|| initialize_contract("contract/target/ink/proxy.riscv")); mod dispatch_call { use super::*; @@ -27,7 +29,7 @@ mod dispatch_call { // Instantiate a new contract. let contract = instantiate(); let dispatch_result = call( - contract, + contract.clone(), DispatchContractFuncId::get(), RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }), GAS_LIMIT, @@ -37,9 +39,9 @@ mod dispatch_call { let decoded = , u32>>::decode(&mut &return_value.data[..]).unwrap(); assert!(decoded.unwrap().is_empty()); // Successfully emit event. - assert!(dispatch_result.events.unwrap().iter().any(|e| matches!(e.event, + assert!(dispatch_result.events.unwrap().iter().any(|e| matches!(&e.event, RuntimeEvent::System(frame_system::Event::::Remarked { sender, .. }) - if sender == contract))); + if *sender == contract))); assert_eq!(dispatch_result.storage_deposit, StorageDeposit::Charge(0)); }); } @@ -126,7 +128,7 @@ mod read_state { // Instantiate a new contract. let contract = instantiate(); let read_result = call(contract, ReadExtFuncId::get(), 99u8, GAS_LIMIT); - let expected: DispatchError = pallet_contracts::Error::::DecodingFailed.into(); + let expected: DispatchError = pallet_revive::Error::::DecodingFailed.into(); // Make sure the error is passed through the error converter. let error = <() as ErrorConverter>::convert(expected, &mock::MockEnvironment::default()).err(); @@ -155,7 +157,7 @@ fn invalid_func_id_fails() { // Instantiate a new contract. let contract = instantiate(); let result = call(contract, INVALID_FUNC_ID, (), GAS_LIMIT); - let expected: DispatchError = pallet_contracts::Error::::DecodingFailed.into(); + let expected: DispatchError = pallet_revive::Error::::DecodingFailed.into(); // Make sure the error is passed through the error converter. let error = <() as ErrorConverter>::convert(expected, &mock::MockEnvironment::default()).err(); @@ -185,10 +187,10 @@ fn initialize_contract(contract_path: &str) -> Vec { /// Instantiating the contract. fn instantiate() -> AccountId { let result = Contracts::bare_instantiate( - ALICE, + RuntimeOrigin::signed(ALICE), 0, GAS_LIMIT, - None, + 1_000_000_000_000, Code::Upload(CONTRACT.clone()), function_selector("new"), Default::default(), @@ -198,7 +200,7 @@ fn instantiate() -> AccountId { log::debug!("instantiate result: {result:?}"); let result = result.result.unwrap(); assert!(!result.result.did_revert()); - result.account_id + AccountId32Mapper::::to_fallback_account_id(&result.addr) } /// Perform a call to a specified contract. @@ -210,15 +212,14 @@ fn call( ) -> ContractExecResult { log::debug!("call: func_id={func_id}, input={input:?}"); let result = Contracts::bare_call( - ALICE, - contract, + RuntimeOrigin::signed(ALICE), + AccountId32Mapper::::to_address(&contract), 0, gas_limit, - None, + 0, [function_selector("call"), (func_id, input.encode()).encode()].concat(), DEBUG_OUTPUT, CollectEvents::UnsafeCollect, - Determinism::Enforced, ); log::debug!("gas consumed: {:?}", result.gas_consumed); log::debug!("call result: {result:?}"); diff --git a/pallets/api/src/extension.rs b/pallets/api/src/extension.rs index 0fd22f2c..f5175fb7 100644 --- a/pallets/api/src/extension.rs +++ b/pallets/api/src/extension.rs @@ -10,9 +10,9 @@ use pop_chain_extension::{ use sp_runtime::DispatchError; use sp_std::vec::Vec; -/// Encoded version of `pallet_contracts::Error::DecodingFailed`, as found within +/// Encoded version of `pallet_revive::Error::DecodingFailed`, as found within /// `DispatchError::ModuleError`. -pub const DECODING_FAILED_ERROR: [u8; 4] = [11, 0, 0, 0]; +pub const DECODING_FAILED_ERROR: [u8; 4] = [9, 0, 0, 0]; /// The logging target for the chain extension. pub const LOG_TARGET: &str = "pop-api::extension"; diff --git a/pallets/api/src/messaging/mod.rs b/pallets/api/src/messaging/mod.rs index 5f692dba..7f340bd6 100644 --- a/pallets/api/src/messaging/mod.rs +++ b/pallets/api/src/messaging/mod.rs @@ -323,13 +323,13 @@ pub enum Read { #[derive(Debug)] #[cfg_attr(feature = "std", derive(PartialEq, Clone))] -pub enum ReadResult { +pub enum ReadResult { Poll(Option), - Get(Option>), + Get(Option>), QueryId(Option), } -impl ReadResult { +impl ReadResult { pub fn encode(&self) -> Vec { use ReadResult::*; match self { @@ -342,7 +342,7 @@ impl ReadResult { impl crate::Read for Pallet { type Read = Read; - type Result = ReadResult; + type Result = ReadResult; fn weight(_read: &Self::Read) -> Weight { // TODO: implement benchmarks @@ -359,9 +359,8 @@ impl crate::Read for Pallet { })), Read::Get(request) => ReadResult::Get(Messages::::get(request.0, request.1).and_then(|m| match m { - Message::IsmpResponse { response, .. } => Some(response), - Message::XcmResponse { response, .. } => - Some(response.encode().try_into().unwrap()), // todo: handle error + Message::IsmpResponse { response, .. } => Some(response.into_inner()), + Message::XcmResponse { response, .. } => Some(response.encode()), _ => None, })), Read::QueryId(request) => ReadResult::QueryId( diff --git a/pallets/revive/src/exec.rs b/pallets/revive/src/exec.rs index b612be2a..4968c839 100644 --- a/pallets/revive/src/exec.rs +++ b/pallets/revive/src/exec.rs @@ -1960,7 +1960,7 @@ mod tests { ExtBuilder::default().build().execute_with(|| { place_contract(&BOB, success_ch); set_balance(&ALICE, 100); - let balance = get_balance(&BOB_CONTRACT_ID); + let balance = get_balance(&BOB_FALLBACK); let origin = Origin::from_account_id(ALICE); let mut storage_meter = storage::meter::Meter::new(&origin, 0, value).unwrap(); @@ -1976,7 +1976,7 @@ mod tests { .unwrap(); assert_eq!(get_balance(&ALICE), 100 - value); - assert_eq!(get_balance(&BOB_CONTRACT_ID), balance + value); + assert_eq!(get_balance(&BOB_FALLBACK), balance + value); }); } @@ -1998,7 +1998,7 @@ mod tests { ExtBuilder::default().build().execute_with(|| { place_contract(&BOB, delegate_ch); set_balance(&ALICE, 100); - let balance = get_balance(&BOB_CONTRACT_ID); + let balance = get_balance(&BOB_FALLBACK); let origin = Origin::from_account_id(ALICE); let mut storage_meter = storage::meter::Meter::new(&origin, 0, 55).unwrap(); @@ -2014,7 +2014,7 @@ mod tests { .unwrap(); assert_eq!(get_balance(&ALICE), 100 - value); - assert_eq!(get_balance(&BOB_CONTRACT_ID), balance + value); + assert_eq!(get_balance(&BOB_FALLBACK), balance + value); }); } @@ -2636,10 +2636,11 @@ mod tests { ), Ok((address, ref output)) if output.data == vec![80, 65, 83, 83] => address ); - let instantiated_contract_id = - ::AddressMapper::to_account_id_contract( - &instantiated_contract_address, - ); + let instantiated_contract_id = <::AddressMapper as AddressMapper< + Test, + >>::to_fallback_account_id( + &instantiated_contract_address + ); // Check that the newly created account has the expected code hash and // there are instantiation event. @@ -2691,10 +2692,11 @@ mod tests { Ok((address, ref output)) if output.data == vec![70, 65, 73, 76] => address ); - let instantiated_contract_id = - ::AddressMapper::to_account_id_contract( - &instantiated_contract_address, - ); + let instantiated_contract_id = <::AddressMapper as AddressMapper< + Test, + >>::to_fallback_account_id( + &instantiated_contract_address + ); // Check that the account has not been created. assert!(ContractInfo::::load_code_hash(&instantiated_contract_id).is_none()); @@ -2757,10 +2759,11 @@ mod tests { let instantiated_contract_address = *instantiated_contract_address.borrow().as_ref().unwrap(); - let instantiated_contract_id = - ::AddressMapper::to_account_id_contract( - &instantiated_contract_address, - ); + let instantiated_contract_id = <::AddressMapper as AddressMapper< + Test, + >>::to_fallback_account_id( + &instantiated_contract_address + ); // Check that the newly created account has the expected code hash and // there are instantiation event. @@ -2815,7 +2818,7 @@ mod tests { .build() .execute_with(|| { set_balance(&ALICE, 1000); - set_balance(&BOB_CONTRACT_ID, 100); + set_balance(&BOB_FALLBACK, 100); place_contract(&BOB, instantiator_ch); let origin = Origin::from_account_id(ALICE); let mut storage_meter = storage::meter::Meter::new(&origin, 200, 0).unwrap(); @@ -3278,7 +3281,7 @@ mod tests { EventRecord { phase: Phase::Initialization, event: MetaEvent::System(frame_system::Event::Remarked { - sender: BOB_CONTRACT_ID, + sender: BOB_FALLBACK, hash: remark_hash }), topics: vec![], @@ -3362,7 +3365,7 @@ mod tests { EventRecord { phase: Phase::Initialization, event: MetaEvent::System(frame_system::Event::Remarked { - sender: BOB_CONTRACT_ID, + sender: BOB_FALLBACK, hash: remark_hash }), topics: vec![], @@ -3426,7 +3429,10 @@ mod tests { ) .unwrap(); - let account_id = ::AddressMapper::to_account_id_contract(&addr); + let account_id = + <::AddressMapper as AddressMapper>::to_fallback_account_id( + &addr, + ); assert_eq!(System::account_nonce(&ALICE), alice_nonce); assert_eq!(System::account_nonce(ctx.ext.account_id()), 1); @@ -4219,7 +4225,7 @@ mod tests { .build() .execute_with(|| { set_balance(&ALICE, 1000); - set_balance(&BOB_CONTRACT_ID, 100); + set_balance(&BOB_FALLBACK, 100); place_contract(&BOB, instantiator_ch); let origin = Origin::from_account_id(ALICE); let mut storage_meter = storage::meter::Meter::new(&origin, 200, 0).unwrap(); diff --git a/pallets/revive/src/lib.rs b/pallets/revive/src/lib.rs index 660e9c4c..de86a751 100644 --- a/pallets/revive/src/lib.rs +++ b/pallets/revive/src/lib.rs @@ -27,7 +27,6 @@ mod benchmarking_dummy; mod exec; mod gas; mod primitives; -use frame_support::traits::IsType; pub use primitives::*; use sp_core::U256; @@ -49,22 +48,18 @@ use codec::{Codec, Decode, Encode}; use environmental::*; use frame_support::{ dispatch::{ - DispatchErrorWithPostInfo, DispatchResultWithPostInfo, GetDispatchInfo, Pays, - PostDispatchInfo, RawOrigin, + DispatchErrorWithPostInfo, DispatchResultWithPostInfo, GetDispatchInfo, PostDispatchInfo, + RawOrigin, }, ensure, traits::{ fungible::{Inspect, Mutate, MutateHold}, - ConstU32, ConstU64, Contains, EnsureOrigin, Get, Time, + ConstU32, ConstU64, Contains, EnsureOrigin, Time, }, weights::{Weight, WeightMeter}, BoundedVec, RuntimeDebugNoBound, }; -use frame_system::{ - ensure_signed, - pallet_prelude::{BlockNumberFor, OriginFor}, - EventRecord, Pallet as System, -}; +use frame_system::{pallet_prelude::OriginFor, EventRecord, Pallet as System}; use scale_info::TypeInfo; use sp_core::{H160, H256}; use sp_runtime::{ diff --git a/pallets/revive/src/test_utils.rs b/pallets/revive/src/test_utils.rs index b26021ff..9cb8419a 100644 --- a/pallets/revive/src/test_utils.rs +++ b/pallets/revive/src/test_utils.rs @@ -28,37 +28,35 @@ pub use sp_runtime::AccountId32; use crate::{BalanceOf, Config}; -const fn ee_suffix(addr: H160) -> AccountId32 { - let mut id = [0u8; 32]; - let mut i = 0; - while i < 20 { - id[i] = addr.0[i]; +const fn ee_suffix(mut account: [u8; 32]) -> AccountId32 { + let mut i = 20; + while i < 32 { + account[i] = 0xee; i += 1; } - - let mut j = 20; - while j < 32 { - id[j] = 0xee; - j += 1; - } - - AccountId32::new(id) + AccountId32::new(account) } pub const ALICE: AccountId32 = AccountId32::new([1u8; 32]); pub const ALICE_ADDR: H160 = H160([1u8; 20]); -pub const ETH_ALICE: AccountId32 = ee_suffix(ALICE_ADDR); +pub const ALICE_FALLBACK: AccountId32 = ee_suffix([1u8; 32]); pub const BOB: AccountId32 = AccountId32::new([2u8; 32]); pub const BOB_ADDR: H160 = H160([2u8; 20]); -pub const BOB_CONTRACT_ID: AccountId32 = ee_suffix(BOB_ADDR); +pub const BOB_FALLBACK: AccountId32 = ee_suffix([2u8; 32]); pub const CHARLIE: AccountId32 = AccountId32::new([3u8; 32]); pub const CHARLIE_ADDR: H160 = H160([3u8; 20]); +pub const CHARLIE_FALLBACK: AccountId32 = ee_suffix([3u8; 32]); pub const DJANGO: AccountId32 = AccountId32::new([4u8; 32]); pub const DJANGO_ADDR: H160 = H160([4u8; 20]); -pub const ETH_DJANGO: AccountId32 = ee_suffix(DJANGO_ADDR); +pub const DJANGO_FALLBACK: AccountId32 = ee_suffix([4u8; 32]); + +/// Eve is a non ee account and hence needs a stateful mapping. +pub const EVE: AccountId32 = AccountId32::new([5u8; 32]); +pub const EVE_ADDR: H160 = H160([5u8; 20]); +pub const EVE_FALLBACK: AccountId32 = ee_suffix([5u8; 32]); pub const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024); diff --git a/pallets/revive/src/tests.rs b/pallets/revive/src/tests.rs index 7431dcdd..bee4ac0d 100644 --- a/pallets/revive/src/tests.rs +++ b/pallets/revive/src/tests.rs @@ -66,8 +66,8 @@ use crate::{ wasm::Memory, weights::WeightInfo, AccountId32Mapper, BalanceOf, Code, CodeInfoOf, CollectEvents, Config, ContractInfo, - ContractInfoOf, DebugInfo, DefaultAddressMapper, DeletionQueueCounter, Error, HoldReason, - Origin, Pallet, PristineCode, H160, + ContractInfoOf, DebugInfo, DeletionQueueCounter, Error, HoldReason, Origin, Pallet, + PristineCode, H160, }; type Block = frame_system::mocking::MockBlock; diff --git a/runtime/devnet/src/config/api/mod.rs b/runtime/devnet/src/config/api/mod.rs index 373d0139..911c6e19 100644 --- a/runtime/devnet/src/config/api/mod.rs +++ b/runtime/devnet/src/config/api/mod.rs @@ -63,12 +63,12 @@ impl Readable for RuntimeRead { /// The result of a runtime state read. #[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Clone))] +#[cfg_attr(feature = "std", derive(PartialEq, Clone))] pub enum RuntimeResult { /// Fungible token read results. Fungibles(fungibles::ReadResult), // Messaging state read results. - Messaging(messaging::ReadResult), + Messaging(messaging::ReadResult), } impl RuntimeResult { From e0c8745ff7e5f550441ba666c7e9a00b08843a7b Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:16:04 +0100 Subject: [PATCH 8/8] fix: minor fixes (#359) --- pop-api/integration-tests/src/incentives.rs | 4 ++-- pop-api/integration-tests/src/lib.rs | 5 ++--- pop-api/src/v0/messaging/mod.rs | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pop-api/integration-tests/src/incentives.rs b/pop-api/integration-tests/src/incentives.rs index fb9a72cb..bedf96d0 100644 --- a/pop-api/integration-tests/src/incentives.rs +++ b/pop-api/integration-tests/src/incentives.rs @@ -1,7 +1,7 @@ -use frame_support::traits::{fungible::Inspect, Currency, Hooks}; +use frame_support::traits::{fungible::Inspect, Currency}; use pop_api::primitives::{Era, Error}; use pop_runtime_devnet::{Balances, Incentives, MINUTES}; -use sp_runtime::{app_crypto::sp_core::H160, Saturating}; +use sp_runtime::app_crypto::sp_core::H160; use super::*; diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index e199d3e1..1ffdd849 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -53,9 +53,8 @@ fn new_test_ext() -> sp_io::TestExternalities { ext.execute_with(|| System::set_block_number(1)); // register account mappings ext.execute_with(|| { - Revive::map_account(RuntimeOrigin::signed(ALICE)); - Revive::map_account(RuntimeOrigin::signed(BOB)); - Revive::map_account(RuntimeOrigin::signed(FERDIE)); + Revive::map_account(RuntimeOrigin::signed(ALICE)).unwrap(); + Revive::map_account(RuntimeOrigin::signed(BOB)).unwrap(); }); ext } diff --git a/pop-api/src/v0/messaging/mod.rs b/pop-api/src/v0/messaging/mod.rs index 9f42f266..4b281e13 100644 --- a/pop-api/src/v0/messaging/mod.rs +++ b/pop-api/src/v0/messaging/mod.rs @@ -1,6 +1,7 @@ use ink::prelude::vec::Vec; use crate::{ + constants::MESSAGING, primitives::{AccountId, Balance, BlockNumber}, ChainExtensionMethodApi, Result, StatusCode, }; @@ -10,7 +11,6 @@ pub mod ismp; /// APIs for messaging using Polkadot's Cross-Consensus Messaging (XCM). pub mod xcm; -pub(crate) const API: u8 = 151; // Dispatchables pub(super) const _REQUEST: u8 = 0; pub(super) const ISMP_GET: u8 = 1; @@ -25,11 +25,11 @@ pub(super) const QUERY_ID: u8 = 2; pub type RequestId = u64; fn build_dispatch(dispatchable: u8) -> ChainExtensionMethodApi { - crate::v0::build_dispatch(API, dispatchable) + crate::v0::build_dispatch(MESSAGING, dispatchable) } fn build_read_state(state_query: u8) -> ChainExtensionMethodApi { - crate::v0::build_read_state(API, state_query) + crate::v0::build_read_state(MESSAGING, state_query) } #[inline]