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 <>
Society Module
Overview
The Society module is an economic game which incentivizes users to participate and maintain a membership society.
User Types
At any point, a user in the society can be one of a:
- Bidder - A user who has submitted intention of joining the society.
- Candidate - A user who will be voted on to join the society.
- Suspended Candidate - A user who failed to win a vote.
- Member - A user who is a member of the society.
- Suspended Member - A member of the society who has accumulated too many strikes or failed their membership challenge.
Of the non-suspended members, there is always a:
- Head - A member who is exempt from suspension.
- Defender - A member whose membership is under question and voted on again.
Of the non-suspended members of the society, a random set of them are chosen as "skeptics". The mechanics of skeptics is explained in the member phase below.
Mechanics
Rewards
Members are incentivized to participate in the society through rewards paid by the Society treasury. These payments have a maturity period that the user must wait before they are able to access the funds.
Punishments
Members can be punished by slashing the reward payouts that have not been collected. Additionally, members can accumulate "strikes", and when they reach a max strike limit, they become suspended.
Skeptics
During the voting period, a random set of members are selected as "skeptics". These skeptics are expected to vote on the current candidates. If they do not vote, their skeptic status is treated as a rejection vote, the member is deemed "lazy", and are given a strike per missing vote.
Membership Challenges
Every challenge rotation period, an existing member will be randomly selected to defend their membership into society. Then, other members can vote whether this defender should stay in society. A simple majority wins vote will determine the outcome of the user. Ties are treated as a failure of the challenge, but assuming no one else votes, the defender always get a free vote on their own challenge keeping them in the society. The Head member is exempt from the negative outcome of a membership challenge.
Society Treasury
The membership society is independently funded by a treasury managed by this module. Some subset of this treasury is placed in a Society Pot, which is used to determine the number of accepted bids.
Rate of Growth
The membership society can grow at a rate of 10 accepted candidates per rotation period up to the max membership threshold. Once this threshold is met, candidate selections are stalled until there is space for new members to join. This can be resolved by voting out existing members through the random challenges or by using governance to increase the maximum membership count.
User Life Cycle
A user can go through the following phases:
+-------> User <----------+
| + |
| | |
+----------------------------------------------+
| | | | |
| | v | |
| | Bidder <-----------+ |
| | + | |
| | | + |
| | v Suspended |
| | Candidate +----> Candidate |
| | + + |
| | | | |
| + | | |
| Suspended +------>| | |
| Member | | |
| ^ | | |
| | v | |
| +-------+ Member <----------+ |
| |
| |
+------------------Society---------------------+
Initialization
The society is initialized with a single member who is automatically chosen as the Head.
Bid Phase
New users must have a bid to join the society.
A user can make a bid by reserving a deposit. Alternatively, an already existing member can create a bid on a user's behalf by "vouching" for them.
A bid includes reward information that the user would like to receive for joining the society. A vouching bid can additionally request some portion of that reward as a tip to the voucher for vouching for the prospective candidate.
Every rotation period, Bids are ordered by reward amount, and the module selects as many bids the Society Pot can support for that period.
These selected bids become candidates and move on to the Candidate phase. Bids that were not selected stay in the bidder pool until they are selected or a user chooses to "unbid".
Candidate Phase
Once a bidder becomes a candidate, members vote whether to approve or reject that candidate into society. This voting process also happens during a rotation period.
The approval and rejection criteria for candidates are not set on chain, and may change for different societies.
At the end of the rotation period, we collect the votes for a candidate and randomly select a vote as the final outcome.
[ a-accept, r-reject, s-skeptic ]
+----------------------------------+
| |
| Member |0|1|2|3|4|5|6|7|8|9| |
| ----------------------------- |
| Vote |a|a|a|r|s|r|a|a|s|a| |
| ----------------------------- |
| Selected | | | |x| | | | | | | |
| |
+----------------------------------+
Result: Rejected
Each member that voted opposite to this randomly selected vote is punished by slashing their unclaimed payouts and increasing the number of strikes they have.
These slashed funds are given to a random user who voted the same as the selected vote as a reward for participating in the vote.
If the candidate wins the vote, they receive their bid reward as a future payout. If the bid was placed by a voucher, they will receive their portion of the reward, before the rest is paid to the winning candidate.
One winning candidate is selected as the Head of the members. This is randomly chosen, weighted by the number of approvals the winning candidates accumulated.
If the candidate loses the vote, they are suspended and it is up to the Suspension Judgement origin to determine if the candidate should go through the bidding process again, should be accepted into the membership society, or rejected and their deposit slashed.
Member Phase
Once a candidate becomes a member, their role is to participate in society.
Regular participation involves voting on candidates who want to join the membership society, and by voting in the right way, a member will accumulate future payouts. When a payout matures, members are able to claim those payouts.
Members can also vouch for users to join the society, and request a "tip" from the fees the new member would collect by joining the society. This vouching process is useful in situations where a user may not have enough balance to satisfy the bid deposit. A member can only vouch one user at a time.
During rotation periods, a random group of members are selected as "skeptics". These skeptics are expected to vote on the current candidates. If they do not vote, their skeptic status is treated as a rejection vote, the member is deemed "lazy", and are given a strike per missing vote.
There is a challenge period in parallel to the rotation period. During a challenge period, a random member is selected to defend their membership to the society. Other members make a traditional majority-wins vote to determine if the member should stay in the society. Ties are treated as a failure of the challenge.
If a member accumulates too many strikes or fails their membership challenge, they will become suspended. While a member is suspended, they are unable to claim matured payouts. It is up to the Suspension Judgement origin to determine if the member should re-enter society or be removed from society with all their future payouts slashed.
Interface
Dispatchable Functions
For General Users
bid- A user can make a bid to join the membership society by reserving a deposit.unbid- A user can withdraw their bid for entry, the deposit is returned.
For Members
vouch- A member can place a bid on behalf of a user to join the membership society.unvouch- A member can revoke their vouch for a user.vote- A member can vote to approve or reject a candidate's request to join the society.defender_vote- A member can vote to approve or reject a defender's continued membership to the society.payout- A member can claim their first matured payment.unfound- Allow the founder to unfound the society when they are the only member.
For Super Users
found- The founder origin can initiate this society. Useful for bootstrapping the Society pallet on an already running chain.judge_suspended_member- The suspension judgement origin is able to make judgement on a suspended member.judge_suspended_candidate- The suspension judgement origin is able to make judgement on a suspended candidate.set_max_membership- The ROOT origin can update the maximum member count for the society. The max membership count must be greater than 1.
License: Apache-2.0