Skip to content
Snippets Groups Projects
  • georgepisaltu's avatar
    FRAME: Reintroduce `TransactionExtension` as a replacement for `SignedExtension` (#3685) · b76e91ac
    georgepisaltu authored
    
    Original PR https://github.com/paritytech/polkadot-sdk/pull/2280
    reverted in https://github.com/paritytech/polkadot-sdk/pull/3665
    
    This PR reintroduces the reverted functionality with additional changes,
    related effort
    [here](https://github.com/paritytech/polkadot-sdk/pull/3623).
    Description is copied over from the original PR
    
    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) in extrinsic v4.
    - General transactions (without a hardcoded signature) in extrinsic v5.
    
    `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
    (RFC [here](https://github.com/polkadot-fellows/RFCs/pull/84)) in
    extrinsic version 5:
    - 0b00000100 or 0b00000101: 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.
    Available in both extrinsic versions 4 and 5.
    - 0b10000100: Old-school "Signed" Transaction: contains Signature, Extra
    (extension data) and an extension version byte, introduced as part of
    [RFC99](https://github.com/polkadot-fellows/RFCs/blob/main/text/0099-transaction-extension-version.md).
    Still available as part of extrinsic v4.
    - 0b01000101: New-school "General" Transaction: contains Extra
    (extension data) and an extension version byte, as per RFC99, but no
    Signature. Only available in extrinsic v5.
    
    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.
    
    `UncheckedExtrinsic` still maintains encode/decode backwards
    compatibility with extrinsic version 4, where the first byte was encoded
    as:
    - 0b00000100 - Unsigned transactions
    - 0b10000100 - Old-school Signed transactions, without the extension
    version byte
    
    Now, `UncheckedExtrinsic` contains a `Preamble` and the actual call. The
    `Preamble` describes the type of extrinsic as follows:
    ```rust
    /// A "header" for extrinsics leading up to the call itself. Determines the type of extrinsic and
    /// holds any necessary specialized data.
    #[derive(Eq, PartialEq, Clone)]
    pub enum Preamble<Address, Signature, Extension> {
    	/// An extrinsic without a signature or any extension. This means it's either an inherent or
    	/// an old-school "Unsigned" (we don't use that terminology any more since it's confusable with
    	/// the general transaction which is without a signature but does have an extension).
    	///
    	/// NOTE: In the future, once we remove `ValidateUnsigned`, this will only serve Inherent
    	/// extrinsics and thus can be renamed to `Inherent`.
    	Bare(ExtrinsicVersion),
    	/// An old-school transaction extrinsic which includes a signature of some hard-coded crypto.
    	/// Available only on extrinsic version 4.
    	Signed(Address, Signature, ExtensionVersion, Extension),
    	/// A new-school transaction extrinsic which does not include a signature by default. The
    	/// origin authorization, through signatures or other means, is performed by the transaction
    	/// extension in this extrinsic. Available starting with extrinsic version 5.
    	General(ExtensionVersion, Extension),
    }
    ```
    
    ## 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 a `Call` type parameter. `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.
    - 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.
    
    ---------
    
    Signed-off-by: default avatargeorgepisaltu <george.pisaltu@parity.io>
    Co-authored-by: default avatarGuillaume Thiolliere <gui.thiolliere@gmail.com>
    Co-authored-by: default avatarBranislav Kontur <bkontur@gmail.com>
    Unverified
    b76e91ac
Code owners
Assign users and groups as approvers for specific file changes. Learn more.