Closes #2160 First part of [Extrinsic Horizon](https://github.com/paritytech/polkadot-sdk/issues/2415) Introduces a new trait `TransactionExtension` to replace `SignedExtension`. Introduce the idea of transactions which obey the runtime's extensions and have according Extension data (né Extra data) yet do not have hard-coded signatures. Deprecate the terminology of "Unsigned" when used for transactions/extrinsics owing to there now being "proper" unsigned transactions which obey the extension framework and "old-style" unsigned which do not. Instead we have __*General*__ for the former and __*Bare*__ for the latter. (Ultimately, the latter will be phased out as a type of transaction, and Bare will only be used for Inherents.) Types of extrinsic are now therefore: - Bare (no hardcoded signature, no Extra data; used to be known as "Unsigned") - Bare transactions (deprecated): Gossiped, validated with `ValidateUnsigned` (deprecated) and the `_bare_compat` bits of `TransactionExtension` (deprecated). - Inherents: Not gossiped, validated with `ProvideInherent`. - Extended (Extra data): Gossiped, validated via `TransactionExtension`. - Signed transactions (with a hardcoded signature). - General transactions (without a hardcoded signature). `TransactionExtension` differs from `SignedExtension` because: - A signature on the underlying transaction may validly not be present. - It may alter the origin during validation. - `pre_dispatch` is renamed to `prepare` and need not contain the checks present in `validate`. - `validate` and `prepare` is passed an `Origin` rather than a `AccountId`. - `validate` may pass arbitrary information into `prepare` via a new user-specifiable type `Val`. - `AdditionalSigned`/`additional_signed` is renamed to `Implicit`/`implicit`. It is encoded *for the entire transaction* and passed in to each extension as a new argument to `validate`. This facilitates the ability of extensions to acts as underlying crypto. There is a new `DispatchTransaction` trait which contains only default function impls and is impl'ed for any `TransactionExtension` impler. It provides several utility functions which reduce some of the tedium from using `TransactionExtension` (indeed, none of its regular functions should now need to be called directly). Three transaction version discriminator ("versions") are now permissible: - 0b000000100: Bare (used to be called "Unsigned"): contains Signature or Extra (extension data). After bare transactions are no longer supported, this will strictly identify an Inherents only. - 0b100000100: Old-school "Signed" Transaction: contains Signature and Extra (extension data). - 0b010000100: New-school "General" Transaction: contains Extra (extension data), but no Signature. For the New-school General Transaction, it becomes trivial for authors to publish extensions to the mechanism for authorizing an Origin, e.g. through new kinds of key-signing schemes, ZK proofs, pallet state, mutations over pre-authenticated origins or any combination of the above. ## Code Migration ### NOW: Getting it to build Wrap your `SignedExtension`s in `AsTransactionExtension`. This should be accompanied by renaming your aggregate type in line with the new terminology. E.g. Before: ```rust /// The SignedExtension to the basic transaction logic. pub type SignedExtra = ( /* snip */ MySpecialSignedExtension, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, SignedExtra>; ``` After: ```rust /// The extension to the basic transaction logic. pub type TxExtension = ( /* snip */ AsTransactionExtension<MySpecialSignedExtension>, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, TxExtension>; ``` You'll also need to alter any transaction building logic to add a `.into()` to make the conversion happen. E.g. Before: ```rust fn construct_extrinsic( /* snip */ ) -> UncheckedExtrinsic { let extra: SignedExtra = ( /* snip */ MySpecialSignedExtension::new(/* snip */), ); let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap(); let signature = payload.using_encoded(|e| sender.sign(e)); UncheckedExtrinsic::new_signed( /* snip */ Signature::Sr25519(signature), extra, ) } ``` After: ```rust fn construct_extrinsic( /* snip */ ) -> UncheckedExtrinsic { let tx_ext: TxExtension = ( /* snip */ MySpecialSignedExtension::new(/* snip */).into(), ); let payload = SignedPayload::new(call.clone(), tx_ext.clone()).unwrap(); let signature = payload.using_encoded(|e| sender.sign(e)); UncheckedExtrinsic::new_signed( /* snip */ Signature::Sr25519(signature), tx_ext, ) } ``` ### SOON: Migrating to `TransactionExtension` Most `SignedExtension`s can be trivially converted to become a `TransactionExtension`. There are a few things to know. - Instead of a single trait like `SignedExtension`, you should now implement two traits individually: `TransactionExtensionBase` and `TransactionExtension`. - Weights are now a thing and must be provided via the new function `fn weight`. #### `TransactionExtensionBase` This trait takes care of anything which is not dependent on types specific to your runtime, most notably `Call`. - `AdditionalSigned`/`additional_signed` is renamed to `Implicit`/`implicit`. - Weight must be returned by implementing the `weight` function. If your extension is associated with a pallet, you'll probably want to do this via the pallet's existing benchmarking infrastructure. #### `TransactionExtension` Generally: - `pre_dispatch` is now `prepare` and you *should not reexecute the `validate` functionality in there*! - You don't get an account ID any more; you get an origin instead. If you need to presume an account ID, then you can use the trait function `AsSystemOriginSigner::as_system_origin_signer`. - You get an additional ticket, similar to `Pre`, called `Val`. This defines data which is passed from `validate` into `prepare`. This is important since you should not be duplicating logic from `validate` to `prepare`, you need a way of passing your working from the former into the latter. This is it. - This trait takes two type parameters: `Call` and `Context`. `Call` is the runtime call type which used to be an associated type; you can just move it to become a type parameter for your trait impl. `Context` is not currently used and you can safely implement over it as an unbounded type. - There's no `AccountId` associated type any more. Just remove it. Regarding `validate`: - You get three new parameters in `validate`; all can be ignored when migrating from `SignedExtension`. - `validate` returns a tuple on success; the second item in the tuple is the new ticket type `Self::Val` which gets passed in to `prepare`. If you use any information extracted during `validate` (off-chain and on-chain, non-mutating) in `prepare` (on-chain, mutating) then you can pass it through with this. For the tuple's last item, just return the `origin` argument. Regarding `prepare`: - This is renamed from `pre_dispatch`, but there is one change: - FUNCTIONALITY TO VALIDATE THE TRANSACTION NEED NOT BE DUPLICATED FROM `validate`!! - (This is different to `SignedExtension` which was required to run the same checks in `pre_dispatch` as in `validate`.) Regarding `post_dispatch`: - Since there are no unsigned transactions handled by `TransactionExtension`, `Pre` is always defined, so the first parameter is `Self::Pre` rather than `Option<Self::Pre>`. If you make use of `SignedExtension::validate_unsigned` or `SignedExtension::pre_dispatch_unsigned`, then: - Just use the regular versions of these functions instead. - Have your logic execute in the case that the `origin` is `None`. - Ensure your transaction creation logic creates a General Transaction rather than a Bare Transaction; this means having to include all `TransactionExtension`s' data. - `ValidateUnsigned` can still be used (for now) if you need to be able to construct transactions which contain none of the extension data, however these will be phased out in stage 2 of the Transactions Horizon, so you should consider moving to an extension-centric design. ## TODO - [x] Introduce `CheckSignature` impl of `TransactionExtension` to ensure it's possible to have crypto be done wholly in a `TransactionExtension`. - [x] Deprecate `SignedExtension` and move all uses in codebase to `TransactionExtension`. - [x] `ChargeTransactionPayment` - [x] `DummyExtension` - [x] `ChargeAssetTxPayment` (asset-tx-payment) - [x] `ChargeAssetTxPayment` (asset-conversion-tx-payment) - [x] `CheckWeight` - [x] `CheckTxVersion` - [x] `CheckSpecVersion` - [x] `CheckNonce` - [x] `CheckNonZeroSender` - [x] `CheckMortality` - [x] `CheckGenesis` - [x] `CheckOnlySudoAccount` - [x] `WatchDummy` - [x] `PrevalidateAttests` - [x] `GenericSignedExtension` - [x] `SignedExtension` (chain-polkadot-bulletin) - [x] `RefundSignedExtensionAdapter` - [x] Implement `fn weight` across the board. - [ ] Go through all pre-existing extensions which assume an account signer and explicitly handle the possibility of another kind of origin. - [x] `CheckNonce` should probably succeed in the case of a non-account origin. - [x] `CheckNonZeroSender` should succeed in the case of a non-account origin. - [x] `ChargeTransactionPayment` and family should fail in the case of a non-account origin. - [ ] - [x] Fix any broken tests. --------- Signed-off-by: georgepisaltu <george.pisaltu@parity.io> Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io> Signed-off-by: Andrei Sandu <andrei-mihail@parity.io> Co-authored-by: Nikhil Gupta <17176722+gupnik@users.noreply.github.com> Co-authored-by: georgepisaltu <52418509+georgepisaltu@users.noreply.github.com> Co-authored-by: Chevdor <chevdor@users.noreply.github.com> Co-authored-by: Bastian Köcher <git@kchr.de> Co-authored-by: Maciej <maciej.zyszkiewicz@parity.io> Co-authored-by: Javier Viola <javier@parity.io> Co-authored-by: Marcin S. <marcin@realemail.net> Co-authored-by: Tsvetomir Dimitrov <tsvetomir@parity.io> Co-authored-by: Javier Bullrich <javier@bullrich.dev> Co-authored-by: Koute <koute@users.noreply.github.com> Co-authored-by: Adrian Catangiu <adrian@parity.io> Co-authored-by: Vladimir Istyufeev <vladimir@parity.io> Co-authored-by: Ross Bulat <ross@parity.io> Co-authored-by: Gonçalo Pestana <g6pestana@gmail.com> Co-authored-by: Liam Aharon <liam.aharon@hotmail.com> Co-authored-by: Svyatoslav Nikolsky <svyatonik@gmail.com> Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: s0me0ne-unkn0wn <48632512+s0me0ne-unkn0wn@users.noreply.github.com> Co-authored-by: ordian <write@reusable.software> Co-authored-by: Sebastian Kunert <skunert49@gmail.com> Co-authored-by: Aaro Altonen <48052676+altonen@users.noreply.github.com> Co-authored-by: Dmitry Markin <dmitry@markin.tech> Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Co-authored-by: Alexander Samusev <41779041+alvicsam@users.noreply.github.com> Co-authored-by: Julian Eager <eagr@tutanota.com> Co-authored-by: Michal Kucharczyk <1728078+michalkucharczyk@users.noreply.github.com> Co-authored-by: Davide Galassi <davxy@datawok.net> Co-authored-by: Dónal Murray <donal.murray@parity.io> Co-authored-by: yjh <yjh465402634@gmail.com> Co-authored-by: Tom Mi <tommi@niemi.lol> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Will | Paradox | ParaNodes.io <79228812+paradox-tt@users.noreply.github.com> Co-authored-by: Bastian Köcher <info@kchr.de> Co-authored-by: Joshy Orndorff <JoshOrndorff@users.noreply.github.com> Co-authored-by: Joshy Orndorff <git-user-email.h0ly5@simplelogin.com> Co-authored-by: PG Herveou <pgherveou@gmail.com> Co-authored-by: Alexander Theißen <alex.theissen@me.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: Juan Girini <juangirini@gmail.com> Co-authored-by: bader y <ibnbassem@gmail.com> Co-authored-by: James Wilson <james@jsdw.me> Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: asynchronous rob <rphmeier@gmail.com> Co-authored-by: Parth <desaiparth08@gmail.com> Co-authored-by: Andrew Jones <ascjones@gmail.com> Co-authored-by: Jonathan Udd <jonathan@dwellir.com> Co-authored-by: Serban Iorga <serban@parity.io> Co-authored-by: Egor_P <egor@parity.io> Co-authored-by: Branislav Kontur <bkontur@gmail.com> Co-authored-by: Evgeny Snitko <evgeny@parity.io> Co-authored-by: Just van Stam <vstam1@users.noreply.github.com> Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com> Co-authored-by: gupnik <nikhilgupta.iitk@gmail.com> Co-authored-by: dzmitry-lahoda <dzmitry@lahoda.pro> Co-authored-by: zhiqiangxu <652732310@qq.com> Co-authored-by: Nazar Mokrynskyi <nazar@mokrynskyi.com> Co-authored-by: Anwesh <anweshknayak@gmail.com> Co-authored-by: cheme <emericchevalier.pro@gmail.com> Co-authored-by: Sam Johnson <sam@durosoft.com> Co-authored-by: kianenigma <kian@parity.io> Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com> Co-authored-by: Muharem <ismailov.m.h@gmail.com> Co-authored-by: joepetrowski <joe@parity.io> Co-authored-by: Alexandru Gheorghe <49718502+alexggh@users.noreply.github.com> Co-authored-by: Gabriel Facco de Arruda <arrudagates@gmail.com> Co-authored-by: Squirrel <gilescope@gmail.com> Co-authored-by: Andrei Sandu <54316454+sandreim@users.noreply.github.com> Co-authored-by: georgepisaltu <george.pisaltu@parity.io> Co-authored-by: command-bot <>
Substrate Runtime Benchmarking Framework
This crate contains a set of utilities that can be used to benchmark and weigh FRAME pallets that you develop for your Substrate Runtime.
Overview
Substrate's FRAME framework allows you to develop custom logic for your blockchain that can be included in your runtime. This flexibility is key to help you design complex and interactive pallets, but without accurate weights assigned to dispatchables, your blockchain may become vulnerable to denial of service (DoS) attacks by malicious actors.
The Substrate Runtime Benchmarking Framework is a tool you can use to mitigate DoS attacks against your blockchain
network by benchmarking the computational resources required to execute different functions in the runtime, for example
extrinsics, on_initialize, verify_unsigned, etc...
The general philosophy behind the benchmarking system is: If your node can know ahead of time how long it will take to execute an extrinsic, it can safely make decisions to include or exclude that extrinsic based on its available resources. By doing this, it can keep the block production and import process running smoothly.
To achieve this, we need to model how long it takes to run each function in the runtime by:
- Creating custom benchmarking logic that executes a specific code path of a function.
- Executing the benchmark in the Wasm execution environment, on a specific set of hardware, with a custom runtime configuration, etc...
- Executing the benchmark across controlled ranges of possible values that may affect the result of the benchmark (called "components").
- Executing the benchmark multiple times at each point in order to isolate and remove outliers.
- Using the results of the benchmark to create a linear model of the function across its components.
With this linear model, we are able to estimate ahead of time how long it takes to execute some logic, and thus make informed decisions without actually spending any significant resources at runtime.
Note that we assume that all extrinsics are assumed to be of linear complexity, which is why we are able to always fit them to a linear model. Quadratic or higher complexity functions are, in general, considered to be dangerous to the runtime as the weight of these functions may explode as the runtime state or input becomes too complex.
The benchmarking framework comes with the following tools:
- A set of macros (
benchmarks!,add_benchmark!, etc...) to make it easy to write, test, and add runtime benchmarks. - A set of linear regression analysis functions for processing benchmark data.
- A CLI extension to make it easy to execute benchmarks on your node.
The end-to-end benchmarking pipeline is disabled by default when compiling a node. If you want to run benchmarks, you
need to enable it by compiling with a Rust feature flag runtime-benchmarks. More details about this below.
Weight
Substrate represents computational resources using a generic unit of measurement called "Weight". It defines 10^12 Weight as 1 second of computation on the physical machine used for benchmarking. This means that the weight of a function may change based on the specific hardware used to benchmark the runtime functions.
By modeling the expected weight of each runtime function, the blockchain is able to calculate how many transactions or system level functions it will be able to execute within a certain period of time. Often, the limiting factor for a blockchain is the fixed block production time for the network.
Within FRAME, each dispatchable function must have a #[weight] annotation with a function that can return the expected
weight for the worst case scenario execution of that function given its inputs. This benchmarking framework will result
in a file that automatically generates those formulas for you, which you can then use in your pallet.
Writing Benchmarks
Writing a runtime benchmark is much like writing a unit test for your pallet. It needs to be carefully crafted to execute a certain logical path in your code. In tests you want to check for various success and failure conditions, but with benchmarks you specifically look for the most computationally heavy path, a.k.a the "worst case scenario".
This means that if there are certain storage items or runtime state that may affect the complexity of the function, for
example triggering more iterations in a for loop, to get an accurate result, you must set up your benchmark to trigger
this.
It may be that there are multiple paths your function can go down, and it is not clear which one is the heaviest. In
this case, you should just create a benchmark for each scenario! You may find that there are paths in your code where
complexity may become unbounded depending on user input. This may be a hint that you should enforce sane boundaries for
how a user can use your pallet. For example: limiting the number of elements in a vector, limiting the number of
iterations in a for loop, etc...
Examples of end-to-end benchmarks can be found in the pallets provided by Substrate, and the specific details on
how to use the benchmarks! macro can be found in its documentation.
Testing Benchmarks
You can test your benchmarks using the same test runtime that you created for your pallet's unit tests. By creating your
benchmarks in the benchmarks! macro, it automatically generates test functions for you:
fn test_benchmark_[benchmark_name]<T>::() -> Result<(), &'static str>
Simply add these functions to a unit test and ensure that the result of the function is Ok(()).
Note: If your test runtime and production runtime have different configurations, you may get different results when testing your benchmark and actually running it.
In general, benchmarks returning Ok(()) is all you need to check for since it signals the executed extrinsic has
completed successfully. However, you can optionally include a verify block with your benchmark, which can additionally
verify any final conditions, such as the final state of your runtime.
These additional verify blocks will not affect the results of your final benchmarking process.
To run the tests, you need to enable the runtime-benchmarks feature flag. This may also mean you need to move into
your node's binary folder. For example, with the Substrate repository, this is how you would test the Balances pallet's
benchmarks:
cargo test -p pallet-balances --features runtime-benchmarks
NOTE: Substrate uses a virtual workspace which does not allow you to compile with feature flags.
error: --features is not allowed in the root of a virtual workspace`To solve this, navigate to the folder of the node (
cd bin/node/cli) or pallet (cd frame/pallet) and run the command there.
This will instance each linear component with different values. The number of values per component is set to six and can
be changed with the VALUES_PER_COMPONENT environment variable.
Adding Benchmarks
The benchmarks included with each pallet are not automatically added to your node. To actually execute these benchmarks,
you need to implement the frame_benchmarking::Benchmark trait. You can see an example of how to do this in the
included Substrate node.
Assuming there are already some benchmarks set up on your node, you just need to add another instance of the
add_benchmark! macro:
/// configuration for running benchmarks
/// | name of your pallet's crate (as imported)
/// v v
add_benchmark!(params, batches, pallet_balances, Balances);
/// ^ ^
/// where all benchmark results are saved |
/// the `struct` created for your pallet by `construct_runtime!`
Once you have done this, you will need to compile your node binary with the runtime-benchmarks feature flag:
cd bin/node/cli
cargo build --profile=production --features runtime-benchmarks
The production profile applies various compiler optimizations.
These optimizations slow down the compilation process a lot.
If you are just testing things out and don't need final numbers, don't include --profile=production.
Running Benchmarks
Finally, once you have a node binary with benchmarks enabled, you need to execute your various benchmarks.
You can get a list of the available benchmarks by running:
./target/production/substrate benchmark pallet --chain dev --pallet "*" --extrinsic "*" --repeat 0
Then you can run a benchmark like so:
./target/production/substrate benchmark pallet \
--chain dev \ # Configurable Chain Spec
--wasm-execution=compiled \ # Always used `wasm-time`
--pallet pallet_balances \ # Select the pallet
--extrinsic transfer \ # Select the extrinsic
--steps 50 \ # Number of samples across component ranges
--repeat 20 \ # Number of times we repeat a benchmark
--output <path> \ # Output benchmark results into a folder or file
This will output a file pallet_name.rs which implements the WeightInfo trait you should include in your pallet.
Double colons :: will be replaced with a _ in the output name if you specify a directory. Each blockchain should
generate their own benchmark file with their custom implementation of the WeightInfo trait. This means that you will
be able to use these modular Substrate pallets while still keeping your network safe for your specific configuration and
requirements.
The benchmarking CLI uses a Handlebars template to format the final output file. You can optionally pass the flag
--template pointing to a custom template that can be used instead. Within the template, you have access to all the
data provided by the TemplateData struct in the benchmarking CLI
writer. You can find the default template used
here.
There are some custom Handlebars helpers included with our output generation:
underscore: Add an underscore to every 3rd character from the right of a string. Primarily to be used for delimiting large numbers.join: Join an array of strings into a space-separated string for the template. Primarily to be used for joining all the arguments passed to the CLI.
To get a full list of available options when running benchmarks, run:
./target/production/substrate benchmark --help
License: Apache-2.0